通过前面几节课的学习,我们已经对 Kubernetes 中的 Pod 以及一些业务负载有所了解。你可以根据课程中提供的示例,自己动手尝试在集群中实践起来。
在使用过程中,我们常常需要对 Pod 进行一些配置管理,比如参数配置文件怎么使用,敏感数据怎么保存传递,等等。有些人可能会觉得,为什么不把这些配置(不限于参数、配置文件、密钥等)打包到镜像中去啊?乍一听,好像有点可行,但是这种做法“硬伤”太多。
有些不变的配置是可以打包到镜像中的,那可变的配置呢?
信息泄漏,很容易引发安全风险,尤其是一些敏感信息,比如密码、密钥等。
每次配置更新后,都要重新打包一次,升级应用。镜像版本过多,也给镜像管理和镜像中心存储带来很大的负担。
定制化太严重,可扩展能力差,且不容易复用。
所以这里的一个最佳实践就是将配置信息和容器镜像进行解耦,以“不变应万变”。在 Kubernetes 中,一般有 ConfigMap 和 Secret 两种对象,可以用来做配置管理。
ConfigMap
首先我们来讲一下 ConfigMap 这个对象,它主要用来保存一些非敏感数据,可以用作环境变量、命令行参数或者挂载到存储卷中。
(https://matthewpalmer.net/kubernetes-app-developer/articles/configmap-diagram.gif)
ConfigMap 通过键值对来存储信息,是个 namespace 级别的资源。在 kubectl 使用时,我们可以简写成 cm。
我们来看一下两个 ConfigMap 的 API 定义:
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
|
$ cat cm-demo-mix.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: cm-demo-mix # 对象名字
namespace: demo # 所在的命名空间
data: # 这是跟其他对象不太一样的地方,其他对象这里都是spec
# 每一个键都映射到一个简单的值
player_initial_lives: "3" # 注意这里的值如果数字的话,必须用字符串来表示
ui_properties_file_name: "user-interface.properties"
# 也可以来保存多行的文本
game.properties: |
enemy.types=aliens,monsters
player.maximum-lives=5
user-interface.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true
$ cat cm-demo-all-env.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: cm-demo-all-env
namespace: demo
data:
SPECIAL_LEVEL: very
SPECIAL_TYPE: charm
|
可见,我们通过 ConfigMap 既可以存储简单的键值对,也能存储多行的文本。
现在我们来创建这两个 ConfigMap:
1
2
3
4
|
$ kubectl create -f cm-demo-mix.yaml
configmap/cm-demo-mix created
$ kubectl create -f cm-demo-all-env.yaml
configmap/cm-demo-all-env created
|
创建 ConfigMap,你也可以通过
kubectl create cm
基于目录、文件或者字面值来创建,详细可参考这个官方文档。
创建成功后,我们可以通过如下方式来查看创建出来的对象。
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
|
$ kubectl get cm -n demo
NAME DATA AGE
cm-demo-all-env 2 30s
cm-demo-mix 4 2s
$ kubectl describe cm cm-demo-all-env -n demo
Name: cm-demo-all-env
Namespace: demo
Labels: <none>
Annotations: <none>
Data
====
SPECIAL_LEVEL:
----
very
SPECIAL_TYPE:
----
charm
Events: <none>
$ kubectl describe cm cm-demo-mix -n demo
Name: cm-demo-mix
Namespace: demo
Labels: <none>
Annotations: <none>
Data
====
user-interface.properties:
----
color.good=purple
color.bad=yellow
allow.textmode=true
game.properties:
----
enemy.types=aliens,monsters
player.maximum-lives=5
player_initial_lives:
----
3
ui_properties_file_name:
----
user-interface.properties
Events: <none>
|
下面我们看看怎么和 Pod 结合起来使用。在使用的时候,有几个地方需要特别注意:
Pod 必须和 ConfigMap 在同一个 namespace 下面;
在创建 Pod 之前,请务必保证 ConfigMap 已经存在,否则 Pod 创建会报错。
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
|
$ cat cm-demo-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: cm-demo-pod
namespace: demo
spec:
containers:
- name: demo
image: busybox:1.28
command:
- "bin/sh"
- "-c"
- "echo PLAYER_INITIAL_LIVES=$PLAYER_INITIAL_LIVES && sleep 10000"
env:
# 定义环境变量
- name: PLAYER_INITIAL_LIVES # 请注意这里和 ConfigMap 中的键名是不一样的
valueFrom:
configMapKeyRef:
name: cm-demo-mix # 这个值来自 ConfigMap
key: player_initial_lives # 需要取值的键
- name: UI_PROPERTIES_FILE_NAME
valueFrom:
configMapKeyRef:
name: cm-demo-mix
key: ui_properties_file_name
envFrom: # 可以将 configmap 中的所有键值对都通过环境变量注入容器中
- configMapRef:
name: cm-demo-all-env
volumeMounts:
- name: full-config # 这里是下面定义的 volume 名字
mountPath: "/config" # 挂载的目标路径
readOnly: true
- name: part-config
mountPath: /etc/game/
readOnly: true
volumes: # 您可以在 Pod 级别设置卷,然后将其挂载到 Pod 内的容器中
- name: full-config # 这是 volume 的名字
configMap:
name: cm-demo-mix # 提供你想要挂载的 ConfigMap 的名字
- name: part-config
configMap:
name: cm-demo-mix
items: # 我们也可以只挂载部分的配置
- key: game.properties
path: properties
|
在上面的这个例子中,几乎囊括了 ConfigMap 的几大使用场景:
命令行参数;
环境变量,可以只注入部分变量,也可以全部注入;
挂载文件,可以是单个文件,也可以是所有键值对,用每个键值作为文件名。
我们接着来创建:
1
2
|
$ kubectl create -f cm-demo-pod.yaml
pod/cm-demo-pod created
|
创建成功后,我们 exec 到容器中看看:
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
|
$ kubectl exec -it cm-demo-pod -n demo sh
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl kubectl exec [POD] -- [COMMAND] instead.
/ # env
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT=tcp://10.96.0.1:443
UI_PROPERTIES_FILE_NAME=user-interface.properties
HOSTNAME=cm-demo-pod
SHLVL=1
HOME=/root
SPECIAL_LEVEL=very
TERM=xterm
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
PLAYER_INITIAL_LIVES=3
KUBERNETES_SERVICE_HOST=10.96.0.1
PWD=/
SPECIAL_TYPE=charm
/ # ls /config/
game.properties ui_properties_file_name
player_initial_lives user-interface.properties
/ # ls -alh /config/
total 12
drwxrwxrwx 3 root root 4.0K Aug 27 09:54 .
drwxr-xr-x 1 root root 4.0K Aug 27 09:54 ..
drwxr-xr-x 2 root root 4.0K Aug 27 09:54 ..2020_08_27_09_54_31.007551221
lrwxrwxrwx 1 root root 31 Aug 27 09:54 ..data -> ..2020_08_27_09_54_31.007551221
lrwxrwxrwx 1 root root 22 Aug 27 09:54 game.properties -> ..data/game.properties
lrwxrwxrwx 1 root root 27 Aug 27 09:54 player_initial_lives -> ..data/player_initial_lives
lrwxrwxrwx 1 root root 30 Aug 27 09:54 ui_properties_file_name -> ..data/ui_properties_file_name
lrwxrwxrwx 1 root root 32 Aug 27 09:54 user-interface.properties -> ..data/user-interface.properties
/ # cat /config/game.properties
enemy.types=aliens,monsters
player.maximum-lives=5
/ # cat /etc/game/properties
enemy.types=aliens,monsters
player.maximum-lives=5
|
可以看到,环境变量都已经正确注入,对应的文件和目录也都挂载进来了。在上面
ls -alh /config/
后,我们看到挂载的文件中存在软链接,都指向了
..data
目录下的文件。这样做的好处,是 kubelet 会定期同步检查已经挂载的 ConfigMap 是否是最新的,如果更新了,就是创建一个新的文件夹存放最新的内容,并同步修改
..data
指向的软链接。
一般我们只把一些非敏感的数据保存到 ConfigMap 中,敏感的数据就要保存到 Secret 中了。
Secret
我们可以用 Secret 来保存一些敏感的数据信息,比如密码、密钥、token 等。在使用的时候, 跟 ConfigMap 的用法基本保持一致,都可以用来作为环境变量或者文件挂载。
Kubernetes 自身也有一些内置的 Secret,主要用来保存访问 APIServer 的 service account token,我们放到后面权限部分一起讲解,在此先略过。
除此之外,还可以用来保存私有镜像中心的身份信息,这样 kubelet 可以拉取到镜像。
注: 如果你使用的是 Docker,也可以提前在目标机器上运行
docker login yourprivateregistry.com
来保存你的有效登录信息。Docker 一般会将私有仓库的密钥保存在
$HOME/.docker/config.json
文件中,将该文件分发到所有节点即可。
我们看看如何通过 kubectl 来创建 secret,通过命令行 help 可以看到 kubectl 能够创建多种类型的 Secret。
1
2
3
4
5
6
7
8
9
10
|
$ kubectl create secret -h
Create a secret using specified subcommand.
Available Commands:
docker-registry Create a secret for use with a Docker registry
generic Create a secret from a local file, directory or literal value
tls Create a TLS secret
Usage:
kubectl create secret [flags] [options]
Use "kubectl --help" for more information about a given command.
Use "kubectl options" for a list of global command-line options (applies to all commands).
|
我们先来创建一个 Secret 来保存访问私有容器仓库的身份信息:
1
2
3
4
5
6
7
8
9
|
$ kubectl create secret -n demo docker-registry regcred \
--docker-server=yourprivateregistry.com \
--docker-username=allen \
--docker-password=mypassw0rd \
--docker-email=allen@example.com
secret/regcred created
$ kubectl get secret -n demo regcred
NAME TYPE DATA AGE
regcred kubernetes.io/dockerconfigjson 1 28s
|
这里我们可以看到,创建出来的 Secret 类型是
kubernetes.io/dockerconfigjson
:
1
2
3
4
5
6
7
8
9
|
$ kubectl describe secret -n demo regcred
Name: regcred
Namespace: demo
Labels: <none>
Annotations: <none>
Type: kubernetes.io/dockerconfigjson
Data
====
.dockerconfigjson: 144 bytes
|
为了防止 Secret 中的内容被泄漏,
kubectl get
和
kubectl describe
会避免直接显示密码的内容。但是我们可以通过拿到完整的 Secret 对象来进一步查看其数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
$ kubectl get secret -n demo regcred -o yaml
apiVersion: v1
data: # 跟 configmap 一样,这块用于保存数据信息
.dockerconfigjson: eyJhdXRocyI6eyJ5b3VycHJpdmF0ZXJlZ2lzdHJ5LmNvbSI6eyJ1c2VybmFtZSI6ImFsbGVuIiwicGFzc3dvcmQiOiJteXBhc3N3MHJkIiwiZW1haWwiOiJhbGxlbkBleGFtcGxlLmNvbSIsImF1dGgiOiJZV3hzWlc0NmJYbHdZWE56ZHpCeVpBPT0ifX19
kind: Secret
metadata:
creationTimestamp: "2020-08-27T12:18:35Z"
managedFields:
- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:data:
.: {}
f:.dockerconfigjson: {}
f:type: {}
manager: kubectl
operation: Update
time: "2020-08-27T12:18:35Z"
name: regcred
namespace: demo
resourceVersion: "1419452"
selfLink: /api/v1/namespaces/demo/secrets/regcred
uid: 6d34123e-4d79-406b-9556-409cfb4db2e7
type: kubernetes.io/dockerconfigjson
|
这里我们发现
.dockerconfigjson
是一段乱码,我们用 base64 解压试试看:
1
2
|
$ kubectl get secret regcred -n demo --output="jsonpath={.data.\.dockerconfigjson}" | base64 --decode
{"auths":{"yourprivateregistry.com":{"username":"allen","password":"mypassw0rd","email":"allen@example.com","auth":"YWxsZW46bXlwYXNzdzByZA=="}}}
|
这实际上跟我们通过 docker login 后的
~/.docker/config.json
中的内容一样。至此,我们发现 Secret 和 ConfigMap 在数据保存上的最大不同。Secret 保存的数据都是通过 base64 加密后的数据。
我们平时使用较为广泛的还有另外一种
Opaque
类型的 Secret:
1
2
3
4
5
6
7
8
9
10
|
$ cat secret-demo.yaml
apiVersion: v1
kind: Secret
metadata:
name: dev-db-secret
namespace: demo
type: Opaque
data: # 这里的值都是 base64 加密后的
password: UyFCXCpkJHpEc2I9
username: ZGV2dXNlcg==
|
或者我们也可以通过如下等价的 kubectl 命令来创建出来:
1
2
3
|
$ kubectl create secret generic dev-db-secret -n demo \
--from-literal=username=devuser \
--from-literal=password='S!B\*d$zDsb='
|
或通过文件来创建对象,比如:
1
2
3
4
|
$ echo -n 'username=devuser' > ./db_secret.txt
$ echo -n 'password=S!B\*d$zDsb=' >> ./db_secret.txt
$ kubectl create secret generic dev-db-secret -n demo \
--from-file=./db_secret.txt
|
有时候为了方便,你也可以使用
stringData
,这样可以避免自己事先手动用 base64 进行加密。
1
2
3
4
5
6
7
8
9
10
|
$ cat secret-demo-stringdata.yaml
apiVersion: v1
kind: Secret
metadata:
name: dev-db-secret
namespace: demo
type: Opaque
stringData:
password: devuser
username: S!B\*d$zDsb=
|
下面我们在 Pod 中使用 Secret:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
$ cat pod-secret.yaml
apiVersion: v1
kind: Pod
metadata:
name: secret-test-pod
namespace: demo
spec:
containers:
- name: demo-container
image: busybox:1.28
command: [ "/bin/sh", "-c", "env" ]
envFrom:
- secretRef:
name: dev-db-secret
restartPolicy: Never
$ kubectl create -f pod-secret.yaml
pod/secret-test-pod created
|
创建成功后,我们来查看下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
$ kubectl get pod -n demo secret-test-pod
NAME READY STATUS RESTARTS AGE
secret-test-pod 0/1 Completed 0 14s
$ kubectl logs -f -n demo secret-test-pod
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT=tcp://10.96.0.1:443
HOSTNAME=secret-test-pod
SHLVL=1
username=devuser
HOME=/root
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
KUBERNETES_PORT_443_TCP_PORT=443
password=S!B\*d$zDsb=
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_SERVICE_HOST=10.96.0.1
PWD=/
|
我们可以在日志中看到命令
env
的输出,看到环境变量
username
和
password
已经正确注入。类似地,我们也可以将 Secret 作为 Volume 挂载到 Pod 内,你大家可以课后实践一下。
写在最后
ConfigMap 和 Secret 是 Kubernetes 常用的保存配置数据的对象,你可以根据需要选择合适的对象存储数据。通过 Volume 方式挂载到 Pod 内的,kubelet 都会定期进行更新。但是通过环境变量注入到容器中,这样无法感知到 ConfigMap 或 Secret 的内容更新。
目前如何让 Pod 内的业务感知到 ConfigMap 或 Secret 的变化,还是一个待解决的问题。但是我们还是有一些 Workaround 的。
如果业务自身支持 reload 配置的话,比如
nginx -s reload
,可以通过 inotify 感知到文件更新,或者直接定期进行 reload(这里可以配合我们的 readinessProbe 一起使用)。
如果我们的业务没有这个能力,考虑到不可变基础设施的思想,我们是不是可以采用滚动升级的方式进行?没错,这是一个非常好的方法。目前有个开源工具Reloader,它就是采用这种方式,通过 watch ConfigMap 和 Secret,一旦发现对象更新,就自动触发对 Deployment 或 StatefulSet 等工作负载对象进行滚动升级。具体使用方法,可以参考项目的文档说明。
对于这个问题,社区其实也一直在讨论比较好的解法,我们可以拭目以待。
好的,如果你对本节课有什么想法或者疑问,欢迎你在留言区留言,我们一起讨论。
-– ### 精选评论 ##### **某: > 对于 ConfigMap 或者 Secret 的“热更新”,我目前的方法比较原始。在 Deployment 对象中增加一个ConfigMap 或 Secret 发生变化,则修改这个注解的值,从而触发工作负载对象的滚动升级。 ##### TUTU: > 所以这里的一个最佳实践就是将配置信息和容器镜像进行解耦,以“不变应万变”,这句话印象深刻,对于一个不对容器不怎么精通的入门者,get到了。