环境变量虽然可以用来对应用程序进行配置,但是如果配置项特别多,这种方式很快就会显得难以为继,这种情况下,我们可以讲配置文件挂载到容器的文件系统中,今天我们来详细的聊聊这种方式。
具体来说,Kubernetes平台上的ConfigMap对象会包含大量应用程序的配置项,我们可以使用特殊的数据卷,将configMap对象投影(projected into)到容器中,给应用程序提供配置数据。
注:关于configMap对象中具体能放多大体量配置数据的问题经常会被企业客户或者架构师问到,对于Kubernetes而言,本质上API Server是无状态的,数据会被持久化到ETCD中,并且通过ETCD的Watch机制来触发(通知)后续的处理,因此这个限制在于ETCD到底允许Kubernetes对象有多大的体量,在写这篇文章的时候,ETCD提供的上限是1M(但是这个可能会变)。
configMap类型的数据卷将键值对以文件的形式挂载到容器中,因此进程就可以像读取普通文件那样,从configMap数据卷中获取配置信息,这是基于Kubernetes平台进行容器化部署的主流配置管理形式,给用户提供了一种更加容易“管理”的方式提供大量配置信息给运行在POD的容器进程应用程序。当然只有少量配置的应用程序也可以采用这种方式,这有助于企业建立规范的应用配置管理机制。接下来,我们来看看如何使用configMap来优化我们介绍的边车容器提供SSL能力的场景,把配置信息摘出来,而不是build到容器镜像中。
如果大家看过笔者关于容器的几篇文章,应该还记得我们有一个叫yunpan-ssl-proxy的镜像,当时我们为了提供SSL能力给k8ssample应用程序,我们使用边车容器的这种方式,在应用程序的旁边运行了另外一个基于Envoy镜像的边车容器,这个叫envoy的容器接受HTTPS流量,然后基于配置的证书和私钥信息,卸掉S后初始化一个新的http请求访问k8ssample应用,因此你可以看到envoy容器需要配置,配置中最少需要包括TLS证书,私钥等信息。
由于我们那个时候还没有介绍Kubernetes提供的配置管理的能力,因此envoy容器运行需要的配置信息都被直接拷贝到容器镜像中,因此envoy容器启动的时候,这些证书啊,私钥啊都直接可以从容器的文件系统获取。但是这有个问题,从安全的角度,证书需要定期更换,要不然会有安全风险。而更换证书就意味着我们需要从新打包镜像,以及重新发布新版本,虽然看起来很合理,但是给生产环境全量部署新的容器镜像,即便是有严谨的上线流程和验证机制,但总是危机重重,况且只是换个配置,因为我们需要有更加轻量级的方法。
如我们开篇介绍的内容,configMap可以像文件那样挂载到容器的文件系统,因此我们接下来使用configMap来给yunpan-ssl-proxy这个容器提供配置信息,解决更换安全证书遇到的痛点。首先我们准备创建configMap需要的YAML文件。当然我们可以直接在命令行来创建这个对象,但是使用YAML文件来创建有个巨大的优势:我们可以像管理源代码那样,管理我们的系统部署架构信息。笔者主导的所有容器化部署项目,不允许通过命令行直接创建任何Kubernetes对象,而是要先创建YAML文件,提交到配置管理仓库,触发review的流程,最后由devops团队来决定是否,以及什么时候将变更推到生产环境。
这样做的好处是显而易见的,技术人员没有人喜欢写文档,结果就是你拿到的系统部署架构图要不然不准确,要不然就根本没有,甚至有很多都是文不对题。但是又不能没有啊,出问题了,没有4+1架构图,很难快速定位具体是哪个组件导致整个系统出现问题,而通过YAML这种方式,系统的最新部署情况都在YAML文件中,并且从配置仓库中,我们总是有和线上1:1准确的部署描述信息,这就极大的减轻了架构师和开发人员写文档的压力。另外如果我们要快速回复生产环境,YAML这种方式也是大有裨益,直接从配置管理仓库中拿到所有的对象YAML配置信息,几句kubetl app就把系统启动或者复制出来了,笔者建议大家在实际的项目中,也采用这种模式。
好了,废话不多说,我们先把configMap对象的YAML文件创建出来。通常情况下我们需要手动来编写YAML文件,但是Kubernetes也提供kubectl命令,来帮助我们自动创建对象的YAML文件。在开发环境运行命令:kubectl create configmap yunpan-envoy-config --from-file=envoy.yaml --from-file=dummy.bin --dry-run=client -o yaml > yunpan-envoy-config.yaml,运行完成后就可以在自己机器上的对应目录看到生成的YAML文件了,如下图所示:
从输出的yunpan-envoy-config文件可以看出,envoy.yaml文件和叫dummpy.bin的任意二进制文件被保存到了configMap对象中。特别要强调的是,由于我们使用了--dry-run选项,因此kubectl并不会调用API Server来实际生成对象,而是为我们生成创建这个configMap对象需要的YAML文件,-o选项就是为了输出YAML文件这个目的。另外从上图可以看出,二进制文件被嵌入到一个叫binaryData的字段,而envoy配置文件被整体写到data字段。
如果我们的输入数据中有非UTF-8的数据,那么这些数据只能被定义在binaryData字段,并且kubectl工具会自动判断,输入的数据应该被放到哪里。二进制数据会被BASE64编码,这是在YAML文件中表示二进制数据的标准格式。
接下来我们可以moving forward在自己的集群中创建这个configMap对象,但是为了向读者展示POD在启动的时候找不到对应configMap的场景,因此我们先来创建POD,看看具体会发生什么。如我们前边的介绍,为了使用configMap提供的配置信息,我们需要在POD定义一个configMap类型的数据卷,然后将这个数据卷挂载到容器中,这样配置信息就会出现在容器的文件系统上。如下图所示的yunpan-ssl-configmap-valume.yaml文件:
如上图所示,高亮部分我们定义了configMap类型的数据卷,如果读者有认真读过笔者前边的系列文章,理解这个配置应该没有什么太大的问题。这里需要特别强调的是,这个叫envoy-config的数据卷指向叫yunpan-envoy-config的configMap对象,并且被挂载到envoy容器的目录/etc/enovy下,因此我们在configMap中引用的envoy.yaml和dummy.bin中的配置信息就对envoy容器实例可见了。由于我们还没有创建对应的configMap文件,先kubectl apply一下这个pod,看具体会发生什么情况:
在自己的环境上执行kubectl apply之后,你会发现这个pod的状态一直处于ContainerCreating阶段,这个阶段一般会由于拉取镜像出现问题造成,我们赶紧describe一下,看看是否和我们没有创建对应的配置信息有关,笔者机器上执行desribe的输出如下:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 51s default-scheduler Successfully assigned default/yunpan-ssl to minikube
Warning FailedMount 20s (x7 over 51s) kubelet MountVolume.SetUp failed for volume "envoy-config" : configmap "yunpan-envoy-config" not found
从POD的事件列表上我们可以清晰的看到,由于没有挂载到对应的configMap对象,因此容器启动失败造成一直处于ContainerCreating阶段。这里需要注意的是,由于我们的YAML文件中,并没有把POD的数据卷定义设置为option,因此这个volume的缺失,会阻塞所有POD中定义的容器启动,并不仅仅限于挂载了这个Volume的容器实例。
注:configMap可以显示的设置optional选项,只需要给数据卷的定义中增加一个optional字段,并设置为true。这样当容器启动,而对应的configMap对象不存在的时候,容器会正常启动。
看到预期结果后,我们赶紧把configMap对象创建出来,看看POD是不是可以恢复。在自己的机器上执行kubectl apply命令,把刚才通过kubectl生成的YAML文件部署到minikube集群中。当POD的状态ready后,我们可以使用命令:kubectl exec yunpan-ssl -c envoy -- ls /etc/envoy来验证配置文件是否已经被成功的挂载到容器实例中了,如下图是笔者本地环境的输出:
➜ Kubernetes配置管理 kubectl apply -f yunpan-envoy-config.yaml
configmap/yunpan-envoy-config created
NAME READY STATUS RESTARTS AGE
yunpan-ssl 2/2 Running 0 14m
➜ Kubernetes配置管理 kubectl exec yunpan-ssl -c envoy -- ls /etc/envoy
dummy.bin
envoy.yaml
基于上边的输出,我们就成功的验证了配置文件被完整的挂载到了容器中。但是我们又有另外一个问题了,对于envoy容器来说,其实dummuy.bin文件并不是必须的,而是提供给另外一个pod实例wanghan使用的,因此我们不能直接从configMap对象中将这部分配置删除。虽然说这个文件挂载到容器中也不会有什么大的问题,但是作为架构师,我们必须精确。
幸运的是,configMap对象允许我们“选择性”的将map中的配置项投影到容器对象的文件系统山,如下图所示:
如上图所示,我们通过items字段来指定哪些字段会被投影到数据卷中,在定义的时候,每个item必须指定key和文件名。大家需要注意的是,items是白名单机制(对应的黑名单机制指定哪些不被允许,其余都允许),没有被配置的key会被自动过滤。通过这种方式,我们就可以精确的控制配置项了。
通过configMap挂载到容器实例文件系统的文件默认的权限是644,代表着文件所有者对这些文件可读可写(可读4+可写2=6),文件所属用户组其他用户的权限是4代表仅可读,其他人的权限和所属组其他用户一样都是4。如果读者对Linux操作系统文件的权限不是很了解,可以自行学习,笔者不累述。
如果默认的644权限不满足我们的要求,可以在configMap中进行自动以,比如我们可以将所有挂载到容器进程中配置文件的权限修改为0740,具体的设置就是在key和路径后增加defaultMode: 0740的设置。
接下来我们就可以继续前进,在容器进程中读取这些配置数据了,但是具有强烈好奇心的同学会说且慢,前边的信息貌似有问题,我再自己的机器上看到的文件权限如下所示:
➜ Kubernetes配置管理 kubectl exec yunpan-ssl -c envoy -- ls -al /etc/envoy
total 12
drwxrwxrwx 3 root root 4096 Sep 11 02:06 .
drwxr-xr-x 1 root root 4096 Sep 11 02:06 ..
drwxr-xr-x 2 root root 4096 Sep 11 02:06 ..2021_09_11_02_06_42.685963020
lrwxrwxrwx 1 root root 31 Sep 11 02:06 ..data -> ..2021_09_11_02_06_42.685963020
lrwxrwxrwx 1 root root 16 Sep 11 02:06 dummy.bin -> ..data/dummy.bin
lrwxrwxrwx 1 root root 17 Sep 11 02:06 envoy.yaml -> ..data/envoy.yaml
从输出来看,这些文件的权限并不是文章中说的644啊。其实在投影到数据卷中的文件,只是个符号链接(symbolic links),符号链接的权限永远都是777(rwxrwxrwx),那么我们如何验证644呢?这就是我们接下来要介绍的内容,深入理解configMap类型的数据卷是如何工作的。
表面上看,当我们说将configMap类型的数据卷挂载到容器中意味着Kubernetes会为我们在容器的文件系统中创建对应的文件,然而事实并非如此。当我们把数据卷中的目录挂载到容器的文件系统中时,原来对应目录的文件就会被影响,容器就无法继续访问这些文件了。我们来举个例子,假设我们将configMap中的文件挂载到了容器实例的/etc目录,那么这个目录原来的数据就无法被访问到了,由于这目录保存的是操作系统配置文件,那么从容器的视角,看到的就是configMap中包含的文件和文件夹,这同时意味着应用可能运行不起来。
为了解决这个问题,Kubernetes在数据卷对象的结构中,提供了subPath字段,来挂载到一个子目录上。我们来看看具体如何操作。假设我们的configMap数据卷包含my-app.conf配置文件,并且我们希望这个叫my-app.conf的配置文件被拷贝到操作系统的/etc文件夹中,并且原来的文件还可以继续访问,那么我们就需要使用如下的配置方式:
spec:
containers:
- name: yunpan-container
volumeMounts:
- name: yunpan-volume
subPath: my-app.conf
mountPath: /etc/my-app.conf
如上所示的容器挂在配置就可以实现我们既可以保留原来目录的信息,又可以把追加的配置文件放进去的目标,整个过程的运行原理如下图所示:
如上图所示,我们可以使用subPath属性将单个文件挂载到容器进程的文件系统中,但是这里有个问题需要读者注意。如果我们不是将整个configMap挂载到文件系统,而是挂载单个文件,那么当我们更新了configMap对象中的数据,比如更新了新版本,那么容器中挂载的数据不会自动刷新。
为了解决这个问题,我们就必须先将整个configMap挂载到另外一个指定目录,然后在比如说/etc目录下创建符号链接指向目标文件,如果对类Unix操作系统的符号链接不是很了解,建议大家自行学习,笔者就不在这里累述了。
很多应用程序会监控配置文件的变更,如果检测到配置更新,应用程序会重新加载配置文件(热加载,无停机部署都是这种模式)。然而如果配置文件很大,或者有很多配置文件,应用程序可能出现检测失误,比如配置文件未更新完成,就重新加载,导致读取到了不完整的数据,不完整的数据可能导致应用挂掉。
为了防止这种情况出现,Kubernetes确保一个configMap对象中的所有数据具备原子操作,意味着同一个cofigMap中的配置信息,会作为一个整体被更新。背后的实现原理就是使用符号链接,顺带也会带我们前边看到的644的问题,如前边我们运行kubectl exec yunpan-ssl -c envoy -- ls -lA /etc/envoy命令输出所示,configMap的配置项以符号链接的形式被挂载到容器进程中,并且data目录也指向一个以时间和日期命名的文件夹,因此应用程序读取数据的时候,其实是通过两个符号链接来进行,通过data这个符号链接找到对应的以日期为文件名的文件夹,然后再通过具体文件件名定位到以日期为文件夹名称下的对应文件。
这种方式看起来异常的反人类和复杂,其实本质上就是为了解决更新的问题。每次我们更新configMap,Kubernetes会创建新的以时间和日期为名的文件夹,然后将数据写进去,当数据更新完了之后,就通过修改..data这个符号链接,来指向更新后的数据,这种模式可以确保原子性。大家如果对ElasticSearch有了解的话,应该知道ES提供的索引alias也是为了达到这个目的。
上边的信息也解释了我们通过subPath挂载单个文件不自动更新问题,单个文件挂载的话,会把文件直接写到容器的目标文件夹,不是通过符号链接的方式,读者可以自行验证。
好了,这篇文章的内容就这么多了,我们下一篇介绍Kubernetes提供的Secret对象,从安全的角度,看看如何将敏感数据提供给容器实例,敬请期待!