Python项目容器化实践(六) - Kubernetes基础篇三
/ / / 阅读数:6731k8s 中的资源对象太多了,所以分了三篇来说,今天是介绍的最后一篇,主要针对存储和包管理工具 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 找到