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项都会成为一个新文件。
|
|
创建 ConfigMap
ConfigMap是用来存储配置文件的kubernetes资源对象,所有的配置内容都存储在etcd中。
创建ConfigMap的方式有4种:
- 通过直接在命令行中指定 configmap 参数创建,即
--from-literal; - 通过指定文件创建,即将一个配置文件创建为一个ConfigMap,
--from-file=<文件>; - 通过一个文件内多个键值对,
--from-env-file=<文件>; - 事先写好标准的configmap的yaml文件,然后
kubectl create -f创建。
使用文件创建
比如我们已经有了一些配置文件,其中包含了我们想要设置的ConfigMap的值:
|
|
使用下面的命令可以创建一个包含目录中所有文件的ConfigMap。
|
|
—from-file指定在目录下的所有文件都会被用在ConfigMap里面创建一个键值对,键的名字就是文件名,值就是文件的内容。
让我们来看一下这个命令创建的ConfigMap:
|
|
我们可以看到那两个key是从指定的目录中的文件名。有些key的内容可能会很大,所以在kubectl describe的输出中,只能够看到键的名字和他们的大小。 如果想要看到键的值的话,可以使用kubectl get:
|
|
我们以yaml格式输出配置。
|
|
刚才使用目录创建的时候我们—from-file指定的是一个目录,只要指定为一个文件就可以从单个文件中创建ConfigMap。
|
|
—from-file这个参数可以使用多次,你可以使用两次分别指定上个实例中的那两个配置文件,效果就跟指定整个目录是一样的。
|
|
使用字面值创建
使用文字值创建,利用—from-literal参数传递配置信息,该参数可以使用多次,格式如下;
|
|
使用环境变量文件创建
|
|
使用 ConfigMap
通过环境变量使用
ConfigMap可以被用来填入环境变量。看下下面的ConfigMap。
|
|
我们可以在Pod中这样使用ConfigMap:
|
|
这个Pod运行后会输出如下几行:
SPECIAL_LEVEL_KEY=very
SPECIAL_TYPE_KEY=charm
log_level=INFO
通过volume挂载使用
ConfigMap也可以在数据卷里面被使用,还是这个ConfigMap。
|
|
在数据卷里面使用这个ConfigMap,有不同的选项。最基本的就是将文件填入数据卷,在这个文件中,键就是文件名,键值就是文件内容:
|
|
运行这个Pod的输出是very,也就是说,ConfigMap 中的每一个 key,作为挂载到 Volume 里面的文件名。
ConfigMap 热更新
业务场景里经常会碰到配置更新的问题,在 “GitOps“模式下,Kubernetes 的 ConfigMap 或 Secret 是非常好的配置管理机制。但是,Kubernetes 到目前为止(1.13版本)还没有提供完善的 ConfigMap 管理机制,当我们更新 ConfigMap 或 Secret 时,引用了这些对象的 Deployment 或 StatefulSet 并不会发生滚动更新。因此,我们需要自己想办法解决配置更新问题,让整个流程完全自动化起来。
首先,我们先给定一个背景,假设我们定义了如下的 ConfigMap:
|
|
这个 ConfigMap 的 data 字段中声明了两个配置文件,config.yml 和 bootstrap.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 值替换进去:
|
|
这时,假如 ConfigMap 变化了,那 Deployment 中的 Pod 模板自然也会发生变化,k8s 自己就会帮助我们做滚动更新了。另外,如何 ConfigMap 不大,直接把 ConfigMap 转化为 JSON 放到 Pod 模板中都可以,这样做还有一个额外的好处,那就是在排查故障时,我们一眼就能看到这个 Pod 现在关联的 ConfigMap 内容是什么。
Pod 滚动更新二:Controller
还有一个办法就是写一个 Controller 来监听 ConfigMap 变更并触发滚动更新。在自己动手写之前,推荐先看看一下社区的这些 Controller 能否能满足需求:
热更新总结
上面就是我针对 ConfigMap 和 Secret 热更新总结的一些方案。最后我们选择的是使用 sidecar 进行热更新,因为这种方式更新配置带来的开销最小,我们也为此主动避免掉了”热更新环境变量这种场景”。
当然了,配置热更新也完全可以不依赖 ConfigMap,Etcd + Confd, 阿里的 Nacos, 携程的 Apollo 包括不那么好用的 Spring-Cloud-Config 都是可选的办法。但它们各自也都有需要考虑的东西,比如 Etcd + Confd 就要考虑 Etcd 里的配置项变更怎么管理;Nacos, Apollo 这种则需要自己在 client 端进行代码集成。相比之下,对于刚起步的架构,用 k8s 本身的 ConfigMap 和 Secret 可以算是一种最快最通用的选择了。
更新 ConfigMap 目前并不会触发相关 Pod 的滚动更新,可以通过修改 pod annotations 的方式强制触发滚动更新。
|
|
这个例子里我们在 .spec.template.metadata.annotations 中添加 version/config,每次通过修改 version/config 来触发滚动更新。
参考资料
-
No backlinks found.