作者 青鸟

学习了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

参考文章: