各种成熟软件的官方container image中,大多包含了默认的配置文件,在真正的生产部署中,一定需要灵活的方法管理这些配置文件,以满足下列需求:
helm 作为 container 的编排工具,可以满足以上的需求,虽然有些有些并不那么直接。我们以 helm 官方的 cassandra chart (incubator repo)为基础,看看如何实现以上三个需求。
配置文件可简单看做一堆配置项(Key: value) 的集合,在安装 chart 时作为参数传入并设置到 deployment 中。这是典型的给 helm chart 传参的过程,因此自然想到可以使用 chart values file – values.yaml
来存储你自定义的配置文件,通过下面的传递途径把值传入到container 中:
cassandra chart 使用的 cassandra image 默认包含了下面一系列必备配置文件。其中,像 cassandra.yaml
这样的核心配置文件是我们在部署过程中常需要修改的,而且,修改后需要重启 cassandra instance 才能生效。
> kubectl exec po/cassandra-01-1 -- ls /etc/cassandra/
cassandra-env.ps1 hotspot_compiler
cassandra-env.sh jvm.options
cassandra-jaas.config logback-tools.xml
cassandra-rackdc.properties logback.xml
cassandra-topology.properties metrics-reporter-config-sample.yaml
cassandra.yaml README.txt
commitlog_archiving.properties triggers
cqlshrc.sample
官方 cassandra chart 中也确实提供了这种实现方式,你可以通过修改 values.yaml
中的这个参数,来覆盖container中的默认配置文件。
parameter | Description | Default |
---|---|---|
configOverrides |
Overrides config files in /etc/cassandra dir | {} |
如果你想覆盖哪个 config file,你必须在 configOverrides
这个参数下面,以该 config file 的名字为 key 粘贴上整个 config file 的内容,例如,以 cassandra.yaml
为例,你需要在 values.yaml
中添加如下内容:
# values.yaml
...
## Cassandra config files overrides
configOverrides:
cassandra.yaml: |
# Cassandra storage config YAML
# NOTE:
# See http://wiki.apache.org/cassandra/StorageConfiguration for
# full explanations of configuration directives
# /NOTE
# The name of the cluster. This is mainly used to prevent machines in
# one logical cluster from joining another.
cluster_name: cassandra
...
(整个config file的内容比如全部出现在这里)
...
只是这种修改的方法比较笨拙,由于 default helm chart 默认覆盖了整个 /etc/cassandra
path,因此,你必须把所有 cassandra 启动必须的 config file 的所有内容都填入 values.yaml/configOverride
下,否则的话,该文件将不会出现在 container 中。首先,对于绝大多数我们不需要修改的配置文件,把默认内容贴在这完全是无意义的工作。其次,这会产生一个非常庞大的 values.yaml
file,非常不便于维护。
针对上面的实现,我希望对现有 chart 做两点改进
values.yaml
中。实现方法,把你自定义的配置文件存放在 chart 根目录的 ./configs
文件夹下。在 values.yaml/configOverride
下以数组形式列出所有你希望覆盖的配置文件。整个数据传递过程变为:
## Cassandra config files overrides
configOverrides:
- cassandra.yaml
- cassandra-rackdc.properties
values.yaml 中只记录我们要覆盖的config file的索引。
{{- if .Values.configOverrides }}
kind: ConfigMap
metadata:
name: cassandra
...
data:
{{- range .Values.configOverrides }}
{{ . }}: |
{{ $.Files.Get (printf "%s%s" "configs/" .) | indent 4 }}
{{- end }}
{{- end }}
自动生成一个包含所有自定义 config files 内容的 configMap manifest。每个自定义 config 的内容作为一个 data 的 sub-object 出现,key 是文件名,value 是 config file 的内容。
apiVersion: apps/v1
kind: StatefulSet
...
spec:
...
template:
...
spec:
...
{{- if or .Values.configOverrides (not .Values.persistence.enabled) }}
volumes:
{{- end }}
{{- if .Values.configOverrides }}
- configMap:
name: cassandra
name: cassandra-config-overrides
- name: cassandra-configs
emptyDir: {}
{{- end }}
...
首先,显然意见,我们需要把包含自定义配置文件的 configMap resource mount成一个volume – cassandra-config-overrides
。同时,我们还创建了一个 cassandra-configs
的临时 volume,它的作用下面介绍。
apiVersion: apps/v1
kind: StatefulSet
...
spec:
...
template:
...
spec:
{{- if .Values.configOverrides }}
initContainers:
- name: config-chown
image: busybox
command: [ 'sh', '-c', 'cp /cassandra-config-overrides/* /cassandra-configs/ && chown 999:999 /cassandra-configs/*']
volumeMounts:
- name: cassandra-config-overrides
mountPath: /cassandra-config-overrides
- name: cassandra-configs
mountPath: /cassandra-configs/
{{- end }}
最直接的方法,我们应该把包含 config files 的 cassandra-config-overrides
volume 直接 mount 到 container 的 /etc/cassandra
目录中,但是我们并没有这么做,而是创建了一个 initContainer 来把 cassandra-config-overrides 中的 config files 拷贝到刚才创建的 emptyDir cassandra-configs
volume 中,并修改文件的 owner 和 group。
之所以多此一举,是因为 kubernetes 的 configMap volume 现在还不支持在 mount 时自定义 user 和 group (参考 https://github.com/kubernetes/kubernetes/issues/81089),直接 mount configMap volume,不满足 cassandra 对配置文件 permission 的要求,container 启动时会遇到如下错误:
chown: changing ownership of '/etc/cassandra/cassandra.yaml': Read-only file system
暂时的解决方案就是增加如上的 initContainer,把配置文件从 Ready-only 的 configMap volume 拷贝到权限更大的 emptyDir volume,并修改文件的 owner:group 为当前 session 的 user:group。然后把 emptyDir volume mount 到 container 中。如下:
apiVersion: apps/v1
kind: StatefulSet
...
spec:
...
template:
...
spec:
containers:
- name: {{ template "cassandra.fullname" . }}
image: "{{ .Values.image.repo }}:{{ .Values.image.tag }}"
...
volumeMounts:
- name: data
mountPath: /var/lib/cassandra
{{- if .Values.configOverrides }}
{{- range .Values.configOverrides }}
- name: cassandra-configs
mountPath: /etc/cassandra/{{ . }}
subPath: {{ . }}
{{- end }}
{{- end }}
...
经过以上操作,用户自定义的配置文件的 user:group 被修改为 cassandra instance用户,statefulset 中的 database replica pod 可以正常启动。
> kubectl exec po/cassandra-01-2 -- ls -l /etc/cassandra/cassandra.yaml
-rw-r--r-- 1 cassandra cassandra 58588 May 30 11:49 /etc/cassandra/cassandra.yaml
通常,helm chart 都是存放在 VCS 系统中,例如 github,这种配置管理代码化,可以方便的实现软件部署的版本管理。另外, helm chart 本身也有 release revision的概念,例如,在部署了以上 chart 后,如果运维过程中需要修改软件配置文件,可以通过 helm upgrade
来启用新的配置,或者,如果升级出现问题,可通过 helm rollback
回滚到之前的配置。
如果我们用 configMap 导入配置文件,当修改了配置文件后,helm upgrade
会自动更新 kubernetes 中的 configMap resource。但是,即便是 configMap 被 mount 为 volume,它本身的修改并不会触发它所属的 deployment / statefulset 等上层资源的 rolling update。
对于像 cassandra 这样的应用程序来说,配置文件修改后,必须通过 restart 来使新的配置生效。也就是说,在你修改了 ./configs/
下的某个配置文件后,即便是通过 helm upgrade
发布了修改,它也不会立刻生效,必须手动 rolling update (restart) 所有的 replica pod 之后,新的配置才能真正生效。 如何能自动的触发 rolling update 呢?
参考 charts_tips_and_tricks/#automatically-roll-deployments,我们知道,只有当一个 deployment / statefulset 的 spec 发生修改时,rolling update 才会自动发生。因此,在 helm chart 中,一个巧妙的做法是将 configMap 的 hash 值作为 annotation 写入 deployment / statefulset 的 template.metadata.annotations 中,这样,当 configMap 的内容修改的时候,annotation 会响应发生变化,rolling update 被自动 trigger。
在 cassandra 的例子中,我们希望将范围扩大到:任何 ./configs 下的配置文件的内容发生变化,都会触发 rolling update。实现方法为:
apiVersion: apps/v1
kind: StatefulSet
...
spec:
...
template:
metadata:
{{- if .Values.configOverrides }}
annotations:
{{- $SUM := "" }}
{{- range .Values.configOverrides }}
{{- $SUM = $.Files.Get (print "configs/" .) | sha256sum | print $SUM }}
{{- end }}
checksum/config: {{ $SUM | sha256sum }}
{{- end }}
...
思路非常简单,先对每个配置文件的内容进行 hash,把所有 hash 值拼接成一个字符串,再对字符串进行二次 hash,生成的最终 hash 值作为 annotation 写入 statefulset.spec.template.metadata.annotations
。这样,你可以任意修改 config file,在 helm upgrade
后,rolling update 会自动进行,你修改也会随之生效。