k8s 中的资源对象太多了,所以分了三篇来说,今天是介绍的最后一篇,主要针对存储和包管理工具 Helm。

ConfigMap

许多应用经常会有从配置文件、命令行参数或者环境变量中读取一些配置信息,但这些配置信息肯定不能直接写死到应用程序中去的,因为这意味着会每个不同的配置需要多构建一个镜像,随着配置项的数量的增加,镜像量将成倍增加。所以正确的方法是在容器里面用某种方式读取这些配置信息,也就是 ConfigMap 做的事情。

ConfigMap 资源对象使用 key-value 形式的键值对来配置数据,下面是一个例子 (config_map.yaml):

apiVersion: v1
kind: ConfigMap
metadata:
  name: k8s-demo-configmap
data:
  mysql.host: 127.0.0.1
  mysql.port: "3308"
  mysql.passwd: "qazxsw"

这个文件中存了三个关于 MySQL 和 Redis 属性下的 3 个键值对,需要注意 2 点:

  • 端口 3308 使用的是字符串
  • 为了凸显配置的价值,MySQL 端口号没有用默认的 3306,而是 3308

创建它:

❯ kubectl create -f config_map.yaml
configmap/k8s-demo-configmap created
# 除此之外还可以在命令行创建,或者按目录创建

接着在一个 MySQL 的 Pod 例子中使用上述k8s-demo-configmap里的 mysql.port (mysql.yaml):

apiVersion: v1
kind: Pod
metadata:
  labels:
    name: mysql
  name: mysql-server
spec:
  containers:
    - name: mysql
      image: mysql:8.0.18
      env:
        - name: MYSQL_TCP_PORT
          valueFrom:
            configMapKeyRef:
              name: k8s-demo-configmap
              key: mysql.port
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            configMapKeyRef:
              name: k8s-demo-configmap
              key: mysql.passwd

在这个配置中,通过环境变量MYSQL_TCP_PORT设置端口号,用valueFrom.configMapKeyRef就可以引用对应的 configmap 中的键值对了。接着验收一下:

❯ kubectl apply -f mysql.yaml
❯ kubectl exec -it mysql-server -- /bin/bash  # 进入正在运行的容器中
root@mysql-server:/# env | grep MYSQL_SERVER_SERVICE_PORT
MYSQL_SERVER_SERVICE_PORT=3308
root@mysql-server:/# apt-get update && apt-get install procps iproute  # 安装对应命令
root@mysql-server:/# ps -ef |grep mysqld |grep -v grep
mysql        1     0  1 12:58 ?        00:01:23 mysqld
root@mysql-server:/# ss -tunlp |grep 330
tcp    LISTEN     0      70       :::33060                :::*
tcp    LISTEN     0      128      :::3308                 :::*

可以看到,这个容器运行着 mysqld 进程,端口使用了 3308 (而不是 3306),说明应用 ConfigMap 生效了。

Secret

细心的同学可能觉得前面的config_map.yaml中的这句的安全性产生疑虑:

...
  mysql.passwd: "qazxsw"

一般情况下 ConfigMap 只适合用来存储一些非安全的配置信息,这种 MySQL 账号密码如果也是这种明文的方式存储很不安全。就需要用到 Secret 这种资源对象了:Secret 用来保存敏感信息,例如密码、密钥、各种 Token 等等。

Secret 配置如下 (secret.yaml):

apiVersion: v1
kind: Secret
metadata:
  name: k8s-demo-secret
data:
  passwd: cWF6eHN3

其中只有 passwd 一项,cWF6eHN3是如下方法获取的。然后创建它:

echo -n 'qazxsw' | base64  # base64编码
cWF6eHN3

❯ kubectl apply -f secret.yaml
secret/k8s-demo-secret created

❯ kubectl get secret k8s-demo-secret --output=jsonpath='{.data}'
map[passwd:cWF6eHN3]

虽然可以获取到编码后的变量,但是和 ConfigMap 不同的是,在对应容器中是看不到密码的任何信息的。

创建好 Secret 对象后,有两种方式来使用它:

  • 以环境变量的形式。就像前面的 ConfigMap 例子
  • 以 Volume 的形式挂载。在容器中密码被保存在文件中可以用应用读取

我这里还是用环境变量的形式,修改mysql.yaml中的MYSQL_ROOT_PASSWORD部分:

...
- name: MYSQL_ROOT_PASSWORD
  valueFrom:
    secretKeyRef:
      name: k8s-demo-secret
      key: passwd

原来是configMapKeyRef,现在用secretKeyRef。这次改动内容大,需要先删除再创建 MySQL:

❯ kubectl delete -f mysql.yaml && kubectl apply -f mysql.yaml
pod "mysql-server" deleted
pod/mysql-server created

❯ kubectl exec -it mysql-server -- /bin/bash
root@mysql-server:/# env|grep PASS
MYSQL_ROOT_PASSWORD=qazxsw

变量值是解码后的密码

存储卷

容器中的文件在磁盘上是临时存放的,这给容器中运行的特殊应用程序 (如 Redis、MySQL) 带来一些问题:

  • 当容器崩溃时,kubelet 将重新启动容器,容器中的文件将会丢失 —— 因为容器会以干净的状态重建
  • 当在一个 Pod 中同时运行多个容器时,常常需要在这些容器之间共享文件

k8s 抽象出 Volume 对象来解决容器数据持久化和容器间共享数据问题,k8s 支持的 Volume 类型很多,如 configMap、emptyDir、NFS、hostPath、persistentVolumeClaim (简称 PVC) 等,下面介绍下几种常见的 Volume 类型和其用法。

hostPath

hostPath 允许挂载 Node 上的文件系统到 Pod 里面去,看一下对pod.yaml的修改:

apiVersion: v1
kind: Pod
metadata:
  name: k8s-demo
  labels:
    app: k8s
spec:
  containers:
  - name: k8s-demo
    image: k8s-demo:0.1
    ports:
    - containerPort: 80
    volumeMounts:
    - mountPath: /data
      name: host-volume
  volumes:
    - name: host-volume
      hostPath:
        path: /tmp/k8s-demo

也就是说把本机的/tmp/k8s-demo目录挂在了容器的/data,试一下:

❯ mkdir /tmp/k8s-demo
❯ kubectl delete -f pod.yaml && kubectl create -f pod.yaml
pod "k8s-demo" deleted
pod/k8s-demo created
❯ kubectl exec -it k8s-demo sh
/ # echo Hello! > /data/1.txt
# 退出容器
❯ cat /tmp/k8s-demo/1.txt
Hello!

看到了吧,容器使用了宿主机的文件系统。

PV/PVC

在说 PVC (PersistentVolumeClaim,持久化卷声明) 之前需要先说 PV (PersistentVolume,持久化卷)。PV 和 Node 一样,它也是集群的资源,提供网络存储资源,PV 跟 Volume 类似,不过会有独立于 Pod 的生命周期。通过一个 hostPath 类型的 PV 例子来理解 (pv-volume.yaml):

kind: PersistentVolume
apiVersion: v1
metadata:
  name: k8s-pv-volume
  labels:
    type: local
spec:
  storageClassName: k8s-storage
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /tmp/k8s-demo

配置文件指定了该卷位于集群节点上的/tmp/k8s-demo目录,卷大小为 10G 访问模式为 ReadWriteOnce (PV 支持三种访问模式,这是最基本的模式:可读可写,但只支持被单个节点挂载)。 StorageClass 是给 PVC 用的,一会儿再说。先创建它:

❯ kubectl create -f pv-volume.yaml
persistentvolume/k8s-pv-volume created

❯ kubectl get pv k8s-pv-volume
NAME            CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
k8s-pv-volume   10Gi       RWO            Retain           Available           k8s-storage             21s

现在它的状态还是 Available,另外注意RECLAIM POLICY这项,他表示 PV 的回收策略,Retain 是默认的 (可以通过persistentVolumeReclaimPolicy指定),表示不清理,保留 Volume。接着把 PV 绑定给 PersistentVolumeClaim (pv-claim.yaml):

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: k8s-pv-claim
spec:
  storageClassName: k8s-storage
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 3Gi

注意 StorageClass 这个键和对应值,如果 k8s 找到具有相同 StorageClass 的适当的 PV,且 PV 的 capacity.storage 剩余空间也满足要求 (这里要求 3G,而目前有 10G,满足条件),就会将 PVC 绑定到这个 PV 上。所以PVC 用作请求 PV 定义的存储资源

❯ kubectl create -f pv-claim.yaml
persistentvolumeclaim/k8s-pv-claim created

❯ kubectl get pv  # 注意状态已经成了Bound,表示已经将PV分配给PVC
NAME            CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                  STORAGECLASS   REASON   AGE
k8s-pv-volume   10Gi       RWO            Retain           Bound    default/k8s-pv-claim   k8s-storage             10s

❯ kubectl get pvc k8s-pv-claim
NAME           STATUS   VOLUME          CAPACITY   ACCESS MODES   STORAGECLASS   AGE
k8s-pv-claim   Bound    k8s-pv-volume   10Gi       RWO            k8s-storage    61s

接着修改 Pod 使用 PVC (pod.yaml):

apiVersion: v1
kind: Pod
metadata:
  name: k8s-demo
  labels:
    app: k8s
spec:
  containers:
  - name: k8s-demo
    image: k8s-demo:0.1
    ports:
    - containerPort: 80
    volumeMounts:
    - mountPath: /data
      name: k8s-pv-storage
  volumes:
    - name: k8s-pv-storage
      persistentVolumeClaim:
       claimName: k8s-pv-claim

还是把 PVC 挂载到/data,重新创建 Pod 并测试:

❯ kubectl delete -f pod.yaml && kubectl create -f pod.yaml
pod "k8s-demo" deleted
pod/k8s-demo created

❯ kubectl exec -it k8s-demo sh
/ # echo Bye! > /data/1.txt
# 退出容器
❯ cat /tmp/k8s-demo/1.txt
Bye!

可以这么理解 PV 和 PVC 的关系:

Pod 消费 Node 资源,而 PVC 消费 PV 资源;Pod 能够请求 CPU 和内存资源,而 PVC 请求特定大小和访问模式的数据卷。

Helm

k8s 内置了多种控制器来帮助我们进行容器编排,如 Deployment、StatefulSet、DeamonSet、Job、CronJob 等,还有多种基础资源对象如 ConfigMap、Serivce、PersistentVolume 等等,而一个真实的应用往往涉及多种资源对象。拿 lyanna 来说,至少需要如下资源对象:

  • Service。MySQL、Nginx、Redis、Memcached 等,每个都是独立的服务
  • Deployment。lyanna 主应用
  • Volume。存放 Redis、MySQL 数据的数据卷
  • DeamonSet。运行 arq,异步执行任务

可见 lyanna 应用需要持久化存储,且资源对象之间还有依赖关系,如果用 YAML 文件的方式配置应用比较繁琐而且还容易出错,这时可以用应用管理工具 - Helm

Helm 主要功能如下:

  • 创建新的 Chart
  • 把 Chart 打包成 tgz 格式
  • 上传 Chart 到 Chart 仓库 (Repositories) 或从仓库中下载 Chart
  • 在 k8s 集群中安装或卸载 Chart
  • 管理用 Helm 安装的 Chart 的周期

一个 Chart 就是一个包 (Package),它包含了一个 k8s 应用需要的全部内容 (包含配置文件、模板、解析模板需要的 Values、依赖关系等)。初学者可以简单的理解 Helm 操作系统的包管理器,就像 Ubuntu/Debian 的 apt、CentOS 的 yum,Chart 就是 Ubuntu/Debian 的 deb、CentOS 的 rpm 文件。

安装和配置 Helm

在 macOS 下用 Homebrew 安装即可:

❯ brew install kubernetes-helm
❯ helm init --history-max 200  # 初始化Helm,history-max指定保存的历史记录长度最多200个
# 由于某些周知的原因,可以指定国内Tiller镜像地址: --tiller-image=registry.cn-hangzhou.aliyuncs.com/google_containers/tiller:v2.15.1

❯ kubectl get deployment -n kube-system |grep tiller  # 稍等2分钟
tiller-deploy    1/1     1            1           2m34s

初始化时会以 Deployment 资源的方式安装 Tiller。

概念和组件

除了 Chart,Helm 还有 3 个主要概念:

  • Config。程序的配置
  • Release。根据 Chart+Config 创建出来的运行实例,包含 Pod、Deployment、Service、Ingress 等。每个 Chart 可以部署一个或多个 Release,每次部署如果不指定 Release 的名字是随机的
  • Repository。用来负责存储和管理用户的 Chart, 并提供简单的版本管理

Helm 有以下两个主要组成部分:

  • Helm Client。用户命令行工具,可以做 Chart 管理 (如打包、上传、安装、卸载、升级、查询、本地开发等)、仓库管理、Release 管理、Tiller Sever 交互等
  • Tiller Server。部署在 k8s 集群内部的 Server,它接收来自 Helm Client 的请求,并把相关资源的操作发送到 k8s,负责管理 (安装、查询、升级、回滚、增量更新、删除等) 和跟踪 k8s 资源。另外 Tiller 把 Release 的相关信息保存在 k8s 的 ConfigMap 中。

Helm 的工作流是这样的:

基本使用

接着简单体验一下,初始化结束后默认有 2 个应用源

❯ helm repo update  # 保持仓库最新
❯ helm search memcached # 搜索Memcached相关的Chart
NAME                CHART VERSION   APP VERSION DESCRIPTION
stable/memcached    3.1.0           1.5.19      Free & open source, high-performance, distributed memory ...
stable/mcrouter     1.0.2           0.36.0      Mcrouter is a memcached protocol router for scaling memca...

❯ helm install stable/memcached
NAME:   altered-narwhal  # release的名字很重要,之后的管理都使用这个名字
LAST DEPLOYED: Mon Oct 28 22:11:45 2019
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Pod(related)
NAME                         READY  STATUS             RESTARTS  AGE
altered-narwhal-memcached-0  0/1    ContainerCreating  0         2s

==> v1/Service
NAME                       TYPE       CLUSTER-IP  EXTERNAL-IP  PORT(S)    AGE
altered-narwhal-memcached  ClusterIP  None        <none>       11211/TCP  2s

==> v1/StatefulSet
NAME                       READY  AGE
altered-narwhal-memcached  0/3    2s


NOTES:
Memcached can be accessed via port 11211 on the following DNS name from within your cluster:
altered-narwhal-memcached.default.svc.cluster.local

If you'd like to test your instance, forward the port locally:

  export POD_NAME=$(kubectl get pods --namespace default -l "app=altered-narwhal-memcached" -o jsonpath="{.items[0].metadata.name}")
  kubectl port-forward $POD_NAME 11211

In another tab, attempt to set a key:

  $ echo -e 'set mykey 0 60 5\r\nhello\r' | nc localhost 11211

You should see:

  STORED
# 👆安装的过程输出非常详细, 这些输出可以通过`helm status RELEASE名字`重新看到
❯ helm ls  # 已使用Helm安装的release
NAME            REVISION    UPDATED                     STATUS      CHART           APP VERSION NAMESPACE
altered-narwhal 1           Mon Oct 28 22:11:45 2019    DEPLOYED    memcached-3.1.0 1.5.19      default

❯ kubectl exec -it k8s-demo -- sh  # 进入一个Pod连接Memcached
/ # echo -e 'set mykey 0 60 5\r\nhello\r' | nc altered-narwhal-memcached.default.svc.cluster.local 11211  # 没有安装memcached客户端,可以用nc在命令行set
STORED  # 看到它说明set成功了
/ # echo -e 'get mykey\r' | nc altered-narwhal-memcached.default.svc.cluster.local 11211  # 获取mykey的值
VALUE mykey 0 5
hello
END

通过上面的输出可以看到,altered-narwhal是本次 Release 的名字,Pod,Deployment、Service、Ingress 等资源对象的名字前缀都是它。

通常安装时使用 Chart 的默认配置选项。Helm 也支持自定义配置安装 Chart,下面实现启动 Memcached 时添加更多参数:

❯ helm delete altered-narwhal  # 首先先删除Release,再重新安装
release "altered-narwhal" deleted

❯ helm inspect values stable/memcached | less  # 查看Chart上可配置的选项
...  # 输出很长,其中包含下面一段:
memcached:
  ...
  ## Additional command line arguments to pass to memcached
  ## E.g. to specify a maximum value size
  ## extraArgs:
  ##   - -I 2m
  extraArgs: []  # 可以通过`extraArgs`指定额外的参数
...
❯ cat memcached_values.yaml
memcached:
  extraArgs: ["-I", "20m"]
❯ helm install -f memcached_values.yaml stable/memcached
NAME:   goodly-penguin
...
❯ kubectl get pods --namespace default -l "app=goodly-penguin-memcached" -o jsonpath="{.items[0].metadata.name}"
goodly-penguin-memcached-0

❯ kubectl exec -it goodly-penguin-memcached-0 sh
/ $ ps -ef|grep memcached |grep -v grep
    1 1001      0:00 memcached -m 64 -o modern -v -I 20m  # 可以看到启动Memcached时参数中多了`-I 20m`

自定义 Chart

安装 Helm 后有 2 个默认源,stable 源中的 Chart 其实并不多,具体的可以看延伸阅读链接。接着把 k8s-demo 自定义成为一个 Chart 并上传到 local 源:

❯ helm repo list
NAME    URL
stable  https://kubernetes-charts.storage.googleapis.com
local   http://127.0.0.1:8879/charts

❯ helm create k8s-demo
❯ tree k8s-demo
k8s-demo
├── Chart.yaml # Chart基本信息,包含版本、名字、描述等
├── charts  # 依赖的Chart,对我们这个例子用不上
├── templates  # 配置模板目录
│   ├── NOTES.txt  # 提示信息,编辑NOTES内容可以影响在helm install输出的内容
│   ├── _helpers.tpl  # 用于修改k8s objcet配置的模板
│   ├── deployment.yaml  # Deployment配置文件模板
│   ├── ingress.yaml  # Ingress配置文件模板
│   ├── service.yaml  # Service配置文件模板
│   └── tests
│       └── test-connection.yaml
└── values.yaml  # Chart配置,主要是修改它

3 directories, 8 files

templates 目录下是 yaml 文件的模板,用 Go template 语法。Helm 创建的这些文件已经非常详细和完善了,对 k8s 这个例子来说不需要修改 templates 模板下的配置文件,如果有必要无非就是把之前写好的配置文件对应配置项按 Go template 语法,再加上_helpers.tpl里面定义的对象。

我改了 values.yaml 和 NOTES.txt。对 values.yaml 的修改如下:

  • replicaCount。默认是 1,改成 3
  • image。默认是 nginx:stable,需要改成 k8s-demo:0.2,image 和标签也是在 values.yaml 里面设置,不需要直接修改模板
  • ingress。默认服务类型是 ClusterIP,再通过 port-forward 达到访问目的,这次我想用 Ingress,所以需要改成 ingress.enabled=true,另外是 ingress.hosts 指定成我自定义的域名

另外也改了 NOTES.txt,虽然 values.yaml 里面的设置项决定了怎么显示提示信息,但是我还想让它相信的更清晰,所以加了一行。

全部改动看一个 git diff:

diff --git a/values.yaml b/values.yaml
-replicaCount: 1
+replicaCount: 3

 image:
-  repository: nginx
-  tag: stable
+  repository: k8s-demo
+  tag: 0.2
@@ -36,13 +36,13 @@ service:
 ingress:
-  enabled: false
+  enabled: true
   annotations: {}
     # kubernetes.io/ingress.class: nginx
     # kubernetes.io/tls-acme: "true"
   hosts:
     - host: chart-example.local
-      paths: []
+      paths: ["/"]

diff --git a/k8s-demo/templates/NOTES.txt b/k8s-demo/templates/NOTES.txt
@@ -4,6 +4,7 @@
+  echo "$(minikube ip) {{ $host.host }}" | sudo tee -a /etc/hosts
   {{- range .paths }}
   http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }}
   {{- end }}

接着本地安装感受一下:

❯ helm install ./k8s-demo
NAME:   manageable-rat
LAST DEPLOYED: Tue Oct 29 18:40:45 2019
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Deployment
NAME                     READY  UP-TO-DATE  AVAILABLE  AGE
manageable-rat-k8s-demo  0/3    3           0          1s

==> v1/Pod(related)
NAME                                      READY  STATUS             RESTARTS  AGE
manageable-rat-k8s-demo-5964585b69-98l9s  0/1    ContainerCreating  0         1s
manageable-rat-k8s-demo-5964585b69-dph4t  0/1    ContainerCreating  0         1s
manageable-rat-k8s-demo-5964585b69-zrc94  0/1    ContainerCreating  0         1s

==> v1/Service
NAME                     TYPE       CLUSTER-IP    EXTERNAL-IP  PORT(S)  AGE
manageable-rat-k8s-demo  ClusterIP  10.110.67.45  <none>       80/TCP   1s

==> v1/ServiceAccount
NAME                     SECRETS  AGE
manageable-rat-k8s-demo  1        1s

==> v1beta1/Ingress
NAME                     AGE
manageable-rat-k8s-demo  1s


NOTES:
1. Get the application URL by running these commands:
  echo "$(minikube ip) chart-example.local" | sudo tee -a /etc/hosts
  http://chart-example.local/

❯ helm ls
NAME                REVISION    UPDATED                     STATUS      CHART           APP VERSION NAMESPACE
manageable-rat      1           Mon Oct 28 07:18:07 2019    DEPLOYED    k8s-demo-0.1.0  1.0         default

按提示在 HOSTS 文件中加上域名后,再用浏览器访问http://chart-example.local/就能看到熟悉的「Hello Kubernetes!」文本了。

接着打包和发布,最后再安装:

❯ helm package k8s-demo  # 把Chart打包成tgz文件
Successfully packaged chart and saved it to: /Users/dongwm/k8s-demo/k8s-demo-0.1.0.tgz
❯ helm repo index . --url http://127.0.0.1:8879/charts  # 索引当前目录下的tgz文件到local源
❯ helm search k8s-demo  # 现在就可以搜到k8s-demo这个应用了
NAME            CHART VERSION   APP VERSION DESCRIPTION
local/k8s-demo  0.1.0           1.0         A Helm chart for Kubernetes

❯ cat demo_values.yaml  # 还记得可以覆盖Values嘛?
ingress:
  hosts:
    - host: demo.local
      paths: ["/"]
❯ helm install k8s-demo -f demo_values.yaml
NAME:   nobby-opossum
...
NOTES:
1. Get the application URL by running these commands:
  echo "$(minikube ip) demo.local" | sudo tee -a /etc/hosts
  http://demo.local/

Ok 啦

项目源码

本文提到的全部源码可以在 mp 找到

延伸阅读

  1. https://kubernetes.io/docs/concepts/storage/persistent-volumes/
  2. https://whmzsu.github.io/helm-doc-zh-cn/quickstart/using_helm-zh_cn.html
  3. https://helm.sh/docs/architecture/
  4. https://github.com/helm/charts