我们学习了 Kubernetes 里的三种 API 对象:Pod、Job 和 CronJob,虽然还没有讲到更高级的其他对象,但使用它们也可以在集群里编排运行一些实际的业务了。
不过想让业务更顺利地运行,有一个问题不容忽视,那就是应用的配置管理
配置文件,你应该有所了解吧,通常来说应用程序都会有一个,它把运行时需要的一些参数从代码中分离出来,让我们在实际运行的时候能更方便地调整优化,比如说 Nginx 有 nginx.conf、Redis 有 redis.conf、MySQL 有 my.cnf 等等。
我们在“入门篇”里学习容器技术的时候讲过,可以选择两种管理配置文件的方式。第一种是编写 Dockerfile,用 COPY 指令把配置文件打包到镜像里;第二种是在运行时使用 docker cp 或者 docker run -v,把本机的文件拷贝进容器。
但这两种方式都存在缺陷。第一种方法相当于是在镜像里固定了配置文件,不好修改,不灵活,第二种方法则显得有点“笨拙”,不适合在集群中自动化运维管理。
对于这个问题 Kubernetes 有它自己的解决方案,你也应该能够猜得到,当然还是使用 YAML 语言来定义 API 对象,再组合起来实现动态配置。
Kubernetes 里专门用来管理配置信息的两种对象:ConfigMap 和 Secret,使用它们可以灵活地配置、定制我们的应用。
首先你要知道,应用程序有很多类别的配置信息,但从数据安全的角度来看可以分成两类:
这两类配置信息本质上都是字符串,只是由于安全性的原因,在存放和使用方面有些差异,所以 Kubernetes 也就定义了两个 API 对象,ConfigMap 用来保存明文配置,Secret 用来保存秘密配置。
先来看 ConfigMap,我们仍然可以用命令 kubectl create 来创建一个它的 YAML 样板。注意,它有简写名字“cm”,所以命令行里没必要写出它的全称:
export out="--dry-run=client -o yaml" # 定义Shell变量
kubectl create cm info $out
得到的样板文件大概是这个样子:
apiVersion: v1
kind: ConfigMap
metadata:name: info
你可能会有点惊讶,ConfigMap 的 YAML 和之前我们学过的 Pod、Job 不一样,除了熟悉的“apiVersion”“kind”“metadata”,居然就没有其他的了,最重要的字段“spec”哪里去了?这是因为 ConfigMap 存储的是配置数据,是静态的字符串,并不是容器,所以它们就不需要用“spec”字段来说明运行时的“规格”。
既然 ConfigMap 要存储数据,我们就需要用另一个含义更明确的字段“data”。
你需要在 kubectl create 后面多加一个参数 --from-literal ,表示从字面值生成一些数据:
kubectl create cm info --from-literal=k=v $out#会输出以下信息
apiVersion: v1
data:k: v
kind: ConfigMap
metadata:creationTimestamp: nullname: info
注意,因为在 ConfigMap 里的数据都是 Key-Value 结构,所以 --from-literal 参数需要使用 k=v 的形式。
把 YAML 样板文件修改一下,再多增添一些 Key-Value,就得到了一个比较完整的 ConfigMap 对象:
apiVersion: v1
kind: ConfigMap
metadata:name: infodata:count: '10'debug: 'on'path: '/etc/systemd'greeting: |say hello to kubernetes.
kubectl apply -f cm.yml
kubectl get cm
kubectl describe cm info #查看状态
你可以看到,现在 ConfigMap 的 Key-Value 信息就已经存入了 etcd 数据库,后续就可以被其他 API 对象使用。
了解了 ConfigMap 对象,我们再来看 Secret 对象就会容易很多,它和 ConfigMap 的结构和用法很类似,不过在 Kubernetes 里 Secret 对象又细分出很多类,比如:
前几种我们现在暂时用不到,所以就只使用最后一种。
创建 YAML 样板的命令是 kubectl create secret generic ,同样,也要使用参数 --from-literal 给出 Key-Value 值:
kubectl create secret generic user --from-literal=name=root $out
得到的 Secret 对象大概是这个样子:
apiVersion: v1
data:name: cm9vdA==
kind: Secret
metadata:creationTimestamp: nullname: user
Secret 对象第一眼的感觉和 ConfigMap 非常相似,只是“kind”字段由“ConfigMap”变成了“Secret”,后面同样也是“data”字段,里面也是 Key-Value 的数据。
不过,既然它的名字是 Secret,我们就不能像 ConfigMap 那样直接保存明文了,需要对数据“做点手脚”。你会发现,这里的“name”值是一串“乱码”,而不是刚才在命令行里写的明文“root”。
这串“乱码”就是 Secret 与 ConfigMap 的不同之处,不让用户直接看到原始数据,起到一定的保密作用。不过它的手法非常简单,只是做了 Base64 编码,根本算不上真正的加密,所以我们完全可以绕开 kubectl,自己用 Linux 小工具“base64”来对数据编码,然后写入 YAML 文件,比如
echo -n "123456" | base64
MTIzNDU2
我们再来重新编辑 Secret 的 YAML,为它添加两个新的数据,方式可以是参数 --from-literal 自动编码,也可以是自己手动编码:
apiVersion: v1
kind: Secret
metadata:name: userdata:name: cm9vdA== # rootpwd: MTIzNDU2 # 123456db: bXlzcWw= # mysql
接下来的创建和查看对象操作和 ConfigMap 是一样的,使用 kubectl apply、kubectl get、kubectl describe:
kubectl apply -f secret.yml
kubectl get secret
kubectl describe secret user
这样一个存储敏感信息的 Secret 对象也就创建好了,而且因为它是保密的,使用 kubectl describe 不能直接看到内容,只能看到数据的大小,你可以和 ConfigMap 对比一下。
在前面讲 Pod 的时候,说过描述容器的字段“containers”里有一个“env”,它定义了 Pod 里容器能够看到的环境变量。
当时我们只使用了简单的“value”,把环境变量的值写“死”在了 YAML 里,实际上它还可以使用另一个“valueFrom”字段,从 ConfigMap 或者 Secret 对象里获取值,这样就实现了把配置信息以环境变量的形式注入进 Pod,也就是配置与应用的解耦。
由于“valueFrom”字段在 YAML 里的嵌套层次比较深,初次使用最好看一下 kubectl explain 对它的说明:
“valueFrom”字段指定了环境变量值的来源,可以是“configMapKeyRef”或者“secretKeyRef”,然后你要再进一步指定应用的 ConfigMap/Secret 的“name”和它里面的“key”,要当心的是这个“name”字段是 API 对象的名字,而不是 Key-Value 的名字。
下面我就把引用了 ConfigMap 和 Secret 对象的 Pod 列出来,给你做个示范,为了提醒你注意,我把“env”字段提到了前面:
这个 Pod 的名字是“env-pod”,镜像是“busybox”,执行命令 sleep 睡眠 300 秒,我们可以在这段时间里使用命令 kubectl exec 进入 Pod 观察环境变量。
你需要重点关注的是它的“env”字段,里面定义了 4 个环境变量,COUNT、GREETING、USERNAME、PASSWORD。
对于明文配置数据, COUNT、GREETING 引用的是 ConfigMap 对象,所以使用字段“configMapKeyRef”,里面的“name”是 ConfigMap 对象的名字,也就是之前我们创建的“info”,而“key”字段分别是“info”对象里的 count 和 greeting。
同样的对于机密配置数据, USERNAME、PASSWORD 引用的是 Secret 对象,要使用字段“secretKeyRef”,再用“name”指定 Secret 对象的名字 user,用“key”字段应用它里面的 name 和 pwd 。
apiVersion: v1
kind: Pod
metadata:name: env-podspec:containers:- env:- name: COUNTvalueFrom:configMapKeyRef:name: infokey: count- name: GREETINGvalueFrom:configMapKeyRef:name: infokey: greeting- name: USERNAMEvalueFrom:secretKeyRef:name: userkey: name- name: PASSWORDvalueFrom:secretKeyRef:name: userkey: pwdimage: busyboxname: busyimagePullPolicy: IfNotPresentcommand: ["/bin/sleep", "300"]
这段解释确实是有点绕口令的感觉,因为 ConfigMap 和 Secret 在 Pod 里的组合关系不像 Job/CronJob 那么简单直接,所以我还是用画图来表示它们的引用关系:
我们用 kubectl apply 创建 Pod,再用 kubectl exec 进入 Pod,验证环境变量是否生效:
kubectl apply -f cm.yml
test@juzi:/k8s$ cat cm.yml
apiVersion: v1
kind: ConfigMap
metadata:name: infodata:count: '10'debug: 'on'path: '/etc/systemd'greeting: say hello to kubernetes.
kubectl apply -f secret.yml
test@juzi:/k8s$ cat secret.yml
apiVersion: v1
kind: Secret
metadata:name: userdata:name: cm9vdA== # rootpwd: MTIzNDU2 # 123456db: bXlzcWw= # mysql
kubectl apply -f env-pod.yml
test@juzi:/k8s$ cat env.yml
apiVersion: v1
kind: Pod
metadata:name: env-podspec:containers:- env:- name: COUNTvalueFrom:configMapKeyRef:name: infokey: count- name: USERNAMEvalueFrom:secretKeyRef:name: userkey: nameimage: busybox:latestname: busyimagePullPolicy: IfNotPresentcommand: ["/bin/sleep", "300"]
kubectl exec -it env-pod -- sh
echo $COUNT
echo $GREETING
echo $USERNAME $PASSWORD
这张截图就显示了 Pod 的运行结果,可以看到在 Pod 里使用 echo 命令确实输出了我们在两个 YAML 里定义的配置信息,也就证明 Pod 对象成功组合了 ConfigMap 和 Secret 对象。
以环境变量的方式使用 ConfigMap/Secret 还是比较简单的,下面来看第二种加载文件的方式。