0%

【Kubernetes】ConfigMap

在应用开发部署过程中,经常会涉及到配置文件的变更,比如数据库连接的地址、报警级别等,为此很多公司专门开发了一套配置管理中心,比如百度的disconf等。在 k8s 中,专门提供了 ConfigMap 这一 API 用于实现对容器中应用的配置管理,本文将介绍 ConfigMap 的使用方法和实现原理。

ConfigMap概览

ConfigMap API给我们提供了向容器中注入配置信息的机制,通过 key/value 的形式,ConfigMap可以被用来保存单个属性,也可以用来保存整个配置文件或者JSON二进制大对象。虽然ConfigMap跟Secrets类似,但是ConfigMap更方便的处理不含敏感信息的字符串。 注意:ConfigMaps不是属性配置文件的替代品。ConfigMaps只是作为多个properties文件的引用。你可以把它理解为Linux系统中的/etc目录,专门用来存储配置文件的目录。下面举个例子,使用ConfigMap配置来创建Kubernetes Volumes,ConfigMap中的每个data项都会成为一个新文件。

1
2
3
4
5
6
7
8
9
10
11
12
kind: ConfigMap
apiVersion: v1
metadata:
name: example-config
namespace: default
data:
example.property.1: hello
example.property.2: world
example.property.file: |-
property.1=value-1
property.2=value-2
property.3=value-3

创建 ConfigMap

ConfigMap是用来存储配置文件的kubernetes资源对象,所有的配置内容都存储在etcd中。

创建ConfigMap的方式有4种:

  • 通过直接在命令行中指定 configmap 参数创建,即--from-literal
  • 通过指定文件创建,即将一个配置文件创建为一个ConfigMap,--from-file=<文件>
  • 通过一个文件内多个键值对--from-env-file=<文件>
  • 事先写好标准的configmap的yaml文件,然后kubectl create -f 创建。

使用文件创建

比如我们已经有了一些配置文件,其中包含了我们想要设置的ConfigMap的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ ls ./configmap/
game.properties
ui.properties

$ cat ./configmap//game.properties
enemies=aliens
lives=3
enemies.cheat=true
enemies.cheat.level=noGoodRotten
secret.code.passphrase=UUDDLRLRBABAS
secret.code.allowed=true
secret.code.lives=30

$ cat ./configmap//ui.properties
color.good=purple
color.bad=yellow
allow.textmode=true
how.nice.to.look=fairlyNice

使用下面的命令可以创建一个包含目录中所有文件的ConfigMap。

1
$ kubectl create configmap game-config --from-file=./configmap

—from-file指定在目录下的所有文件都会被用在ConfigMap里面创建一个键值对,键的名字就是文件名,值就是文件的内容。

让我们来看一下这个命令创建的ConfigMap:

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
$ kubectl describe configmaps game-config
Name: game-config
Namespace: default
Labels: <none>
Annotations: <none>

Data
====
game.properties:
----
enemies=aliens
lives=3
enemies.cheat=true
enemies.cheat.level=noGoodRotten
secret.code.passphrase=UUDDLRLRBABAS
secret.code.allowed=true
secret.code.lives=30

ui.properties:
----
color.good=purple
color.bad=yellow
allow.textmode=true
how.nice.to.look=fairlyNice

Events: <none>

我们可以看到那两个key是从指定的目录中的文件名。有些key的内容可能会很大,所以在kubectl describe的输出中,只能够看到键的名字和他们的大小。 如果想要看到键的值的话,可以使用kubectl get

1
$ kubectl get configmaps game-config -o yaml

我们以yaml格式输出配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1
data:
game.properties: |
enemies=aliens
lives=3
enemies.cheat=true
enemies.cheat.level=noGoodRotten
secret.code.passphrase=UUDDLRLRBABAS
secret.code.allowed=true
secret.code.lives=30
ui.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true
how.nice.to.look=fairlyNice
kind: ConfigMap
metadata:
creationTimestamp: 2021-03-06T05:01:06Z
name: game-config
namespace: default
resourceVersion: "7259926485"
selfLink: /api/v1/namespaces/default/configmaps/game-config
uid: f54025a7-7e38-11eb-bd4e-525400f991a8

刚才使用目录创建的时候我们—from-file指定的是一个目录,只要指定为一个文件就可以从单个文件中创建ConfigMap。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ kubectl create configmap game-config-2 --from-file=docs/user-guide/configmap/kubectl/game.properties 

$ kubectl get configmaps game-config-2 -o yaml
apiVersion: v1
data:
game.properties: |
enemies=aliens
lives=3
enemies.cheat=true
enemies.cheat.level=noGoodRotten
secret.code.passphrase=UUDDLRLRBABAS
secret.code.allowed=true
secret.code.lives=30
kind: ConfigMap
metadata:
creationTimestamp: 2021-03-06T05:03:34Z
name: game-config-2
namespace: default
resourceVersion: "7259941723"
selfLink: /api/v1/namespaces/default/configmaps/game-config-2
uid: 4dbc59b1-7e39-11eb-bd4e-525400f991a8

—from-file这个参数可以使用多次,你可以使用两次分别指定上个实例中的那两个配置文件,效果就跟指定整个目录是一样的。

1
2
# Create a new configmap named my-config with specified keys instead of file basenames on disk
kubectl create configmap my-config --from-file=key1=/path/to/bar/file1.txt --from-file=key2=/path/to/bar/file2.txt

使用字面值创建

使用文字值创建,利用—from-literal参数传递配置信息,该参数可以使用多次,格式如下;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ kubectl create configmap special-config --from-literal=special.how=very --from-literal=special.type=charm

$ kubectl get configmaps special-config -o yaml
apiVersion: v1
data:
special.how: very
special.type: charm
kind: ConfigMap
metadata:
creationTimestamp: 2021-03-06T05:04:34Z
name: special-config
namespace: default
resourceVersion: "7259947895"
selfLink: /api/v1/namespaces/default/configmaps/special-config
uid: 7186dc15-7e39-11eb-bd4e-525400f991a8

使用环境变量文件创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ cat << EOF > env.txt
db.host=172.18.8.200
db.port=3306
EOF
$ kubectl create cm test-config3 --from-env-file=env.txt

$ kubectl get cm test-config3 -o yaml
apiVersion: v1
data:
db.host: 172.18.8.200
db.port: "3306"
kind: ConfigMap
metadata:
creationTimestamp: 2021-03-06T05:06:00Z
name: test-config3
namespace: default
resourceVersion: "7259956721"
selfLink: /api/v1/namespaces/default/configmaps/test-config3
uid: a4c748a2-7e39-11eb-bd4e-525400f991a8

使用 ConfigMap

通过环境变量使用

ConfigMap可以被用来填入环境变量。看下下面的ConfigMap。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: ConfigMap
metadata:
name: special-config
namespace: default
data:
special.how: very
special.type: charm
---
apiVersion: v1
kind: ConfigMap
metadata:
name: env-config
namespace: default
data:
log_level: INFO

我们可以在Pod中这样使用ConfigMap:

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: cm-pod
spec:
containers:
- name: test-container
image: busybox
command: [ "/bin/sh", "-c", "env" ]
env:
- name: SPECIAL_LEVEL_KEY
valueFrom:
configMapKeyRef:
name: special-config
key: special.how
- name: SPECIAL_TYPE_KEY
valueFrom:
configMapKeyRef:
name: special-config
key: special.type
envFrom:
- configMapRef:
name: env-config
restartPolicy: Never

这个Pod运行后会输出如下几行:

1
2
3
SPECIAL_LEVEL_KEY=very
SPECIAL_TYPE_KEY=charm
log_level=INFO

通过volume挂载使用

ConfigMap也可以在数据卷里面被使用,还是这个ConfigMap。

1
2
3
4
5
6
7
8
apiVersion: v1
kind: ConfigMap
metadata:
name: special-config
namespace: default
data:
special.how: very
special.type: charm

在数据卷里面使用这个ConfigMap,有不同的选项。最基本的就是将文件填入数据卷,在这个文件中,键就是文件名,键值就是文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Pod
metadata:
name: dapi-test-pod
spec:
containers:
- name: test-container
image: busybox
command: [ "/bin/sh", "-c", "cat /etc/config/special.how" ]
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: special-config
restartPolicy: Never

运行这个Pod的输出是very,也就是说,ConfigMap 中的每一个 key,作为挂载到 Volume 里面的文件名。

ConfigMap 热更新

业务场景里经常会碰到配置更新的问题,在 “GitOps“模式下,Kubernetes 的 ConfigMapSecret 是非常好的配置管理机制。但是,Kubernetes 到目前为止(1.13版本)还没有提供完善的 ConfigMap 管理机制,当我们更新 ConfigMapSecret 时,引用了这些对象的 DeploymentStatefulSet 并不会发生滚动更新。因此,我们需要自己想办法解决配置更新问题,让整个流程完全自动化起来。

首先,我们先给定一个背景,假设我们定义了如下的 ConfigMap

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: ConfigMap
metadata:
name: test-config
data:
config.yml: |-
start-message: 'Hello, World!'
log-level: INFO
bootstrap.yml:
listen-address: '127.0.0.1:8080'

这个 ConfigMapdata 字段中声明了两个配置文件,config.ymlbootstrap.yml,各自有一些内容。当我们要引用里面的配置信息时,Kubernetes 提供了两种方式:

  • 使用 configMapKeyRef 引用 ConfigMap 中某个文件的内容作为 Pod 中容器的环境变量;
  • 将所有 ConfigMap 中的文件写到一个临时目录中,将临时目录作为 volume 挂载到容器里,也就是 configmap 类型的 volume;

在 k8s 的 ConfigMap 机制中,ConfigMap 当前更新情况如下:

  • 使用该 ConfigMap 挂载的 Env 不会同步更新
  • 使用该 ConfigMap 挂载的 Volume 中的数据需要一段时间(实测大概10秒)才能同步更新

好了,假设我们有一个 Deployment,它的 Pod 模板中以引用了这个 ConfigMap。现在的问题是,我们希望当 ConfigMap 更新时,这个 Deployment 的业务逻辑也能随之更新,有哪些方案?

  • 最好是在当 ConfigMap 发生变更时,直接进行热更新,从而做到不影响 Pod 的正常运行
  • 假如无法热更新或热更新完成不了需求,就需要触发对应的 Deployment 做一次滚动更新

接下来,我们就探究一下不同场景下的几种应对方案

场景一:针对可以做热更新的容器,进行配置热更新

ConfigMap 作为 volume 进行挂载时,它的内容是会更新的。为了更好地理解何时可以做热更新,我们要先简单分析 ConfigMap volume 的更新机制:

更新操作由 kubelet 的 Pod 同步循环触发。每次进行 Pod 同步时(默认每 10 秒一次),Kubelet 都会将 Pod 的所有 ConfigMap volume 标记为”需要重新挂载(RequireRemount)“,而 kubelet 中的 volume 控制循环会发现这些需要重新挂载的 volume,去执行一次挂载操作。

ConfigMap 的重新挂载过程中,kubelet 会先比较远端的 ConfigMap 与 volume 中的 ConfigMap 是否一致,再做更新。要注意,”拿远端的 ConfigMap” 这个操作可能是有缓存的,因此拿到的并不一定是最新版本。

由此,我们可以知道,ConfigMap 作为 volume 确实是会自动更新的,但是它的更新存在延时,最多的可能延迟时间是:

Pod 同步间隔(默认10秒) + ConfigMap 本地缓存的 TTL

kubelet 上 ConfigMap 的获取是否带缓存由配置中的 ConfigMapAndSecretChangeDetectionStrategy 决定

注意,假如使用了 subPath 将 ConfigMap 中的某个文件单独挂载到其它目录下,那这个文件是无法热更新的(这是 ConfigMap 的挂载逻辑决定的)

有了这个底,我们就明确了:

  • 假如应用对配置热更新有实时性要求,那么就需要在业务逻辑里自己到 ApiServer 上去 watch 对应的 ConfigMap 来做更新。或者,干脆不要用 ConfigMap,换成 etcd 这样的一致性 kv 存储来管理配置;
  • 假如没有实时性要求,那我们其实可以依赖 ConfigMap 本身的更新逻辑来完成配置热更新;

当然,配置文件更新完不代表业务逻辑就更新了,我们还需要通知应用重新读取配置进行业务逻辑上的更新。比如对于 Nginx,就需要发送一个 SIGHUP 信号量。这里有几种落地的办法。

热更新一:应用本身监听本地配置文件

假如是我们自己写的应用,我们完成可以在应用代码里去监听本地文件的变化,在文件变化时触发一次配置热更新。甚至有一些配置相关的第三方库本身就包装了这样的逻辑,比如说 viper

热更新二:使用 sidecar 来监听本地配置文件变更

Prometheus 的 Helm Chart 中使用的就是这种方式。这里有一个很实用的镜像叫做 configmap-reload,它会去 watch 本地文件的变更,并在发生变更时通过 HTTP 调用通知应用进行热更新。

但这种方式存在一个问题:Sidecar 发送信号(Signal)的限制比较多,而很多开源组件比如 Fluentd,Nginx 都是依赖 SIGHUP 信号来进行热更新的。主要的限制在于,kubernetes 1.10 之前,并不支持 pod 中的容器共享同一个 pid namespace,因此 sidecar 也就无法向业务容器发送信号了。而在 1.10 之后,虽然支持了 pid 共享,但在共享之后 pid namespace 中的 1 号进程会变成基础的 /pause 进程,我们也就无法轻松定位到目标进程的 pid 了。

当然了,只要是 k8s 版本在 1.10 及以上并且开启了 ShareProcessNamespace 特性,我们多写点代码,通过进程名去找 pid,总是能完成需求的。但是 1.10 之前就是完全没可能用 sidecar 来做这样的事情了。

热更新三:胖容器

既然 sidecar 限制重重,那我们只能回归有点”反模式”的胖容器了。还是和 sidecar 一样的思路,但这次我们通过把主进程和sidecar 进程打在同一个镜像里,这样就直接绕过了 pid namespace 隔离的问题。当然,假如允许的话,还是用上面的一号或二号方案更好,毕竟容器本身的优势就是轻量可预测,而复杂则是脆弱之源。

场景二:无法热更新时,滚动更新 Pod

无法热更新的场景有很多:

  • 应用本身没有实现热更新逻辑,而一般来说自己写的大部分应用都不会特意去设计这个逻辑;
  • 使用 subPath 进行 ConfigMap 的挂载,导致 ConfigMap 无法自动更新;
  • 在环境变量或 init-container 中依赖了 ConfigMap 的内容;

最后一点额外解释一下,当使用 configMapKeyRef 引用 ConfigMap 中的信息作为环境变量时,这个操作只会在 Pod 创建时执行一次,因此不会自动更新。而 init-container 也只会运行一次,因此假如 init-contianer 的逻辑依赖了 ConfigMap 的话,这个逻辑肯定也不可能按新的再来一遍了。

当碰到无法热更新的时候,我们就必须去滚动更新 Pod 了。相信你一定想到了,那我们写一个 controller 去 watch ConfigMap 的变更,watch 到之后就去给 Deployment 或其它资源做一次滚动更新不就可以了吗?没错,但就我个人而言,我更喜欢依赖简单的东西,因此我们还是从简单的方案讲起。

Pod 滚动更新一:修改 CI 流程

这种办法异常简单,只需要我们写一个简单的 CI 脚本:给 ConfigMap 算一个 Hash 值,然后作为一个环境变量或 Annotation 加入到 Deployment 的 Pod 模板当中。

举个例子,我们写这样的一个 Deployment yaml 然后在 CI 脚本中,计算 Hash 值替换进去:

1
2
3
4
5
6
7
...
spec:
template:
metadata:
annotations:
com.aylei.configmap/hash: ${CONFIGMAP_HASH}
...

这时,假如 ConfigMap 变化了,那 Deployment 中的 Pod 模板自然也会发生变化,k8s 自己就会帮助我们做滚动更新了。另外,如何 ConfigMap 不大,直接把 ConfigMap 转化为 JSON 放到 Pod 模板中都可以,这样做还有一个额外的好处,那就是在排查故障时,我们一眼就能看到这个 Pod 现在关联的 ConfigMap 内容是什么。

Pod 滚动更新二:Controller

还有一个办法就是写一个 Controller 来监听 ConfigMap 变更并触发滚动更新。在自己动手写之前,推荐先看看一下社区的这些 Controller 能否能满足需求:

热更新总结

上面就是我针对 ConfigMapSecret 热更新总结的一些方案。最后我们选择的是使用 sidecar 进行热更新,因为这种方式更新配置带来的开销最小,我们也为此主动避免掉了”热更新环境变量这种场景”。

当然了,配置热更新也完全可以不依赖 ConfigMap,Etcd + Confd, 阿里的 Nacos, 携程的 Apollo 包括不那么好用的 Spring-Cloud-Config 都是可选的办法。但它们各自也都有需要考虑的东西,比如 Etcd + Confd 就要考虑 Etcd 里的配置项变更怎么管理;Nacos, Apollo 这种则需要自己在 client 端进行代码集成。相比之下,对于刚起步的架构,用 k8s 本身的 ConfigMapSecret 可以算是一种最快最通用的选择了。

更新 ConfigMap 目前并不会触发相关 Pod 的滚动更新,可以通过修改 pod annotations 的方式强制触发滚动更新。

1
$ kubectl patch deployment my-nginx --patch '{"spec": {"template": {"metadata": {"annotations": {"version/config": "20180411" }}}}}'

这个例子里我们在 .spec.template.metadata.annotations 中添加 version/config,每次通过修改 version/config 来触发滚动更新。

参考资料