作者 青鸟

学习了k8s之后,就一直想上手体验一下。但是直接去部署一个k8s集群所需要的钱是非常昂贵的,于是就参考了k8s官方文档给出来的意见,使用了minikube在本地来部署一个简易的k8s来体验一下

在使用的过程中,遇到了无数的坑,能踩的都踩了个遍,于是乎就有了这篇博客。(强烈建议看官方文档,其他的全是坑)

在本篇博客的本地环境如下:

  • macos 14.2.1
  • minikube 1.31.2
  • kubenetes 1.28.2

首先我们创建一个namespace来隔离服务,将我们这篇博客的所有的服务都运行在隔离环境中。这里我们就创建了一个dev的namespace

1
kubectl create namespace dev

使用下面的命令就可以打开k8s的可视化界面

1
minikube dashboard

部署一个无状态的服务

我们使用golang简单的写一个无状态服务demo,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello World!")
	})

	log.Fatalln(http.ListenAndServe(":80", nil))
}

然后编写dockerfile

1
2
3
4
5
FROM scratch
MAINTAINER  cbluebird
ADD hello /app/hello
WORKDIR /app
ENTRYPOINT ["/app/hello"]

然后我们将这个打包成docker并上传到本地的image仓库里,具体的代码如下:

1
2
3
4
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GIN_MODE=release go build -o hello
docker build -t bluebird/hello .
docker image save bluebird/hello >hello.tar
minikube image load  hello.tar

然后编写一个pod或者deployment的配置文件即可,这里推荐使用deployment,pod是k8s中的基本单位,deployment对pod具有非常好的工作负载管理能力

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
  name: hello
  labels:
    name: hello
spec:
  containers:
  - name: hello
    image: bluebird/hello
    imagePullPolicy: Never
    ports:
      - containerPort: 80
        hostPort: 8080

我们使用apply命令将其运行,并检查运行情况

这里的imagePullPolicy: Never标签很重要,这让k8s可以从本地的镜像中拉取到image

1
2
3
4
kubectl apply -f hello.yaml -n dev #运行pod
kubectl describe pods hello -n dev # 查看pod的具体运行情况
minikube ssh --node minikube # 进入命令行工具,在里面curl对应的ip
curl ip:8080 #测试
  • hello-deployment.yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-deployment
  labels:
    app: hello
spec:
  replicas: 2  # 创建一个 ReplicaSet,其中创建Pod副本的个数
  selector: #selector字段定义所创建的ReplicaSet如何查找要管理的Pod,这里通过标签
    matchLabels:
      app: hello
  template: # 定义被创建的pod
    metadata: #元数据,这里给pod打上标签
      labels:
        app: hello
    spec: #Pod模板规约,定义容器如何在pod中运行
      containers:
      - name: hello
        image: bluebird/hello
        imagePullPolicy: Never
        ports:
        - containerPort: 80

接着运行deployment

1
2
kubectl apply -f hello-depolyment.yaml -n dev 
kubectl get deployments -n dev

更多有关于deployment的使用参考Deployments

至此一个简单的无状态服务便运行在了我们的minikube上

部署一个有状态的服务

有状态的服务部署相比于无状态就复杂了很多了

首先我们来改动一下代码,为golang程序增加一个yaml配置文件,以及数据库的连接

  • config.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package config

import (
	"github.com/spf13/viper"
	"log"
)

var Config = viper.New()

func init() {
	Config.SetConfigName("config")
	Config.SetConfigType("yaml")
	Config.AddConfigPath(".")
	Config.WatchConfig() // 自动将配置读入Config变量
	err := Config.ReadInConfig()
	if err != nil {
		log.Fatal("Config not find", err)
	}
}
  • database.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package database

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"log"
	"hello/config"
)

var DB *gorm.DB

func Init() { // 初始化数据库

	user := config.Config.GetString("database.user")
	pass := config.Config.GetString("database.pass")
	port := config.Config.GetString("database.port")
	host := config.Config.GetString("database.host")
	name := config.Config.GetString("database.name")

	dsn := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8&parseTime=True&loc=Local", user, pass, host, port, name)
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		DisableForeignKeyConstraintWhenMigrating: true, // 关闭外键约束 提升数据库速度
	})

	if err != nil {
		log.Fatal("DatabaseConnectFailed", err)
	}

	err = autoMigrate(db)
	if err != nil {
		log.Fatal("DatabaseMigrateFailed", err)
	}
	DB = db
}

func autoMigrate(db *gorm.DB) error {
	return db.AutoMigrate(
	)
}
  • config.yaml
1
2
3
4
5
6
database:
  name: hello
  host: hello-mysql
  port: 3306
  user: root
  pass: "123456"
  • main.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
	"fmt"
	"hello/config/database"
	"log"
	"net/http"
)

func main() {
	database.Init()
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello World!")
	})

	log.Fatalln(http.ListenAndServe(":80", nil))
}

然后我们和之前无状态的服务一样,打包成一个image上传到本地仓库

ConfigMap

我们先处理这个config.yaml,我们将这个配置文件处理到configmap中,从而将配置文件由k8s集群来管理,对之后的服务拓展就很方便了。同时我们还可以利用configmap来实现热重载与更新的操作

1
kubectl create configmap config --from-file=config.yaml -n dev

创建一个configmap的方法有四种,我们这里选取一个最为简单与方便的,就是从一个文件中创建一个configmap,然后使用get命令查看创建的configmap

1
kubectl get cm -n dev

假如后续的时候,我们需要修改这个configmap,我们使用下面命令即可

1
kubectl edit configmap config -n dev

需要查看详细配置的话

1
 kubectl describe cm config -n dev

Secret

我们将初始化的数据库密码写入到Secret中,从而保证其安全性与便捷性

1
kubectl create secret generic mysql-root-password \ --from-literal=password=123456 -n dev

我们就可以在之后的初始化数据库的时候使用它了

部署mysql

这里我们只给一个单实例有状态应用,即部署一个mysql的pod,假如我们想要部署数据库集群,可以请参考 StatefulSet文档,StatefulSet更适合与集群的部署,这里就简单看一下单实例的mysql,并使用Service来调用这个mysql的服务,配置文件如下

  • mysql-deployment.yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
apiVersion: v1
kind: Service
metadata:
  name: hello-mysql
  labels:
    app: hello
spec:
  ports:
    - port: 3306
  selector:
    app: hello
    tier: mysql
  clusterIP: None
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pv-claim
  labels:
    app: hello
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-mysql
  labels:
    app: hello
spec:
  selector:
    matchLabels:
      app: hello
      tier: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: hello
        tier: mysql
    spec:
      containers:
      - image: mysql:8.0
        name: mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-root-password
              key: password
        - name: MYSQL_DATABASE
          value: hello
        - name: MYSQL_USER
          value: hello
        - name: MYSQL_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-root-password
              key: password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-persistent-storage
          mountPath: /root/mysql/data #挂载在服务器上的物理位置
      volumes:
      - name: mysql-persistent-storage
        persistentVolumeClaim:
          claimName: mysql-pv-claim

然后使用命令

1
kubectl apply -f mysql-deployment.yaml -n dev

就可以创建成功了

验证一下

1
kubectl describe hello-mysql -n dev

我们这里使用了动态制卷,在创建 PersistentVolumeClaim 时,将根据 StorageClass 配置动态制备一个 PersistentVolume并挂载在/root/mysql/data,相比于静态,可以更加灵活地释放空间。

注意:不要对应用进行规模扩缩。这里的设置仅适用于单实例应用。下层的 PersistentVolume 仅只能挂载到一个 Pod 上。对于集群级有状态应用,请参考 StatefulSet文档

这里要注意一个非常坑的点,在minikube中,如果我们想使用数据库可视化工具来访问这个数据库,我们需要部署一个nginx来转发流量。

原因在于,minikube是在部署机器上构建一套虚拟机部署k8s,这里我们有三台主机,第一层是我们本地的主机,第二层是vmware虚拟机,第三层是minikube虚拟机。第一层本机可以直接访问第二层虚拟机,第二层虚拟机可以访问第三层的minikube虚拟机,但是第一层的本机无法直接访问第三层的minikue虚拟机。因此,在构建本地环境时,如果我们是在虚拟机环境里面部署minikube,那么,我们本机是无法直接访问k8s里面的服务的。例如,我们上述部署的k8s MySQL服务,获取的隧道服务连接地址是192.168.49.2:3306,这个地址,我们在虚拟机里面是可以访问的,本机则无法访问。

那么,我们就需要解决本机如何访问k8s服务的问题,我们可以在第二层的虚拟机里面部署一个nginx,然后配置代理端口转发即可。当然k8s也给出了对应的方法我们使用kubectl port-forward命令也是可以将一个pod的端口映射到一个本地端口上去。

最后我们需要验证mysql服务是否成功部署,我们有两种办法

第一种:直接进入容器的bash,验证即可

第二种:前面 YAML 文件中创建了一个允许集群内其他 Pod 访问的数据库 Service。该 Service 中选项 clusterIP: None 让 Service 的 DNS 名称直接解析为 Pod 的 IP 地址,于是在这个集群内,hello-mysql会被dns直接解析为mysql的地址,这样子更为灵活。 当在一个 Service 下只有一个 Pod 并且不打算增加 Pod 的数量,这是最好的办法。

我们运行 MySQL 客户端以连接到服务器来测试:

1
kubectl run -it --rm -n dev --image=mysql:8.0 --restart=Never mysql-client -- mysql -h hello-mysql -ppassword

这里需要注意这个dev的位置,放在后面的话就会导致在默认的namespace中去查找这个Service,同样的,我们在之后的golang服务中,也是可以直接使用hello-mysql来代替主机地址。

部署应用

然后编写一个pod或者deployment的配置文件即可,这里推荐使用deployment,pod是k8s中的基本单位,deployment对pod具有非常好的工作负载管理能力

这里有个非常非常坑的点,就是我们使用 volume 将 ConfigMap 作为文件直接挂载configMap的时候,这个时候这个configmap会直接覆盖掉你这个挂载文件夹下的所有东西,可能导致你的可执行文件也被直接覆盖了。

所以最好的方式就是使用subpath只是将configmap中的key作为文件挂载到目录下,而这个key就是你使用文件导入时候的文件名,这个时候就不会存在覆盖问题了

  • hello-pod.yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: v1
kind: Pod
metadata:
  name: hello
spec:
  containers:
  - name: hello
    image: bluebird/hello
    imagePullPolicy: Never
    volumeMounts:
    - name: config
      mountPath: /app/config.yaml ## 镜像里的挂载目录
      subPath: config.yaml ## 挂载的文件
      readOnly: true
    ports:
      - containerPort: 80
        hostPort: 8080
  volumes:
  - name: config
    configMap:
      name: config ## 挂载的configmap的name
      items:
      - key: config.yaml
        path: config.yaml

我们运行

1
2
3
kubectl apply -f hello-pod.yaml -n dev
#查看运行情况
kubectl get pod hello-n dev 

然后我们也给出使用deployment部署的配置文件

  • hello-deployment.yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-deployment
  labels:
    app: hello
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello
  template:
    metadata:
      labels:
        app: hello
    spec:
      containers:
      - name: hello
        image: crk/hello
        imagePullPolicy: Never
        volumeMounts:
        - name: config
          mountPath: /app/config.yaml ## 镜像里的挂载目录
          subPath: config.yaml ## 挂载的文件
          readOnly: true
        ports:
        - containerPort: 80
      volumes:
      - name: config
        configMap:
         name: config ## 挂载的configmap的name
         items:
         - key: configmap.yaml
           path: config.yaml
1
2
kubectl apply -f hello-depolyment.yaml -n dev 
kubectl get deployments -n dev

k8s的常见命令

kubectl里的很多命令都是相同的,只需要变换名次就可以对不同的对象操作,比如pod、deployment、confgmap、service等

大致归为:kubectl [command] [TYPE] [NAME] [flags]

  • command:指定在一个或多个资源上要执行的操作。例如:create、get、describe、delete、apply等
  • TYPE:指定资源类型(如:pod、node、services、deployments等)。资源类型大小写敏感,可以指定单数、复数或缩写形式
  • NAME:指定资源的名称。名称大小写敏感。如果省略名称空间,则显示默认名称空间资源的详细信息或者提示:No resources found in default namespace.。
  • flags:指定可选的标记。例如,可以使用 -s 或 –server标识来指定Kubernetes API服务器的地址和端口;-n指定名称空间;等等。注意:你从命令行指定的flags将覆盖默认值和任何相应的环境变量。优先级最高。 在多个资源上执行操作时,可以通过类型 TYPE 和名称 NAME 指定每个资源,也可以指定一个或多个文件。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#启动minikube
minikube start

# 运行可视化
minikube dashboard

#从yaml配置文件中运行,不论是pod、deployment等都可以使用,-f指的是文件
kubectl apply -f mysql-deployment.yaml 

#通过-n 指定命名空间
kubectl apply -f mysql-deployment.yaml -n dev

#通过yaml里的类型和名称删除
kubectl delete -f hello.yaml

#查看pod详细情况,对于service、deployment,换一下对象就行
kubectl describe pod hello # 查看pod的运行情况

#获取单个pod
kubectl get pod hello

#获取所有pod
kubectl get pods

#编辑configmap
kubectl edit configmap config

#进入容器里的bash
kubectl exec -it podName -n nsName /bin/bash  #进入容器

# 查看日志
kubectl logs hello -n dev

参考文章: