配置模板(Configuration Template)模式可以在应用程序启动期间创建和处理大型和复杂的配置。生成的配置对目标运行时环境具有特殊意义,这一点通过处理配置模板时使用的参数反映出来。

问题描述

在第 19 章 “配置资源” 中,你看到了如何使用 Kubernetes 原生资源对象 ConfigMap 和 Secret 来配置应用程序。但有时配置文件会变得很大很复杂。将配置文件直接放入 ConfigMap 中可能会有问题,因为它们必须正确地嵌入资源定义中。我们需要小心,避免使用特殊字符,如引号和破坏 Kubernetes 资源语法。配置的大小是另一个考虑因素,因为 ConfigMap 或 Secret 的所有值的总和是有限制的,即 1 MB(这是底层后端存储 etcd 施加的限制)。

大的配置文件通常只在不同的执行环境下略有不同。这种相似性导致了 ConfigMap 中的大量重复和冗余,因为每个环境的数据大多相同。我们在本章中探讨的配置模板模式解决了这些具体的用例问题。

解决方案

为了减少重复,只将不同的配置值(如数据库连接参数)存储在 ConfigMap 中,甚至直接存储在环境变量中是有意义的。在容器的启动过程中,这些值会被配置模板处理,以创建完整的配置文件(就像 JBoss WildFly 的一个配置文件 standalone.xml)。 有许多工具,如 Tiller(Ruby)或Gomplate(Go),用于在应用程序初始化期间处理模板。图 21-1 是一个配置模板的例子,其中的数据来自环境变量或挂载卷,可能由 ConfigMap 支持。

在应用程序启动之前,经过充分处理的配置文件会被放到一个可以像其他配置文件一样直接使用的位置。对于如何在运行时进行这种实时处理,有两种技术。

  • 我们可以将模板处理器作为 ENTRYPOINT 的一部分添加到 Dockerfile 中,这样模板处理就直接成为容器镜像的一部分。这里的入口点通常是一个脚本,首先执行模板处理,然后启动应用程序。模板的参数来自于环境变量。
  • 对于 Kubernetes,更好的方式是通过 Pod 的 Init 容器进行初始化,模板处理程序在其中运行,并为 Pod 中的应用容器创建配置。Init 容器在第 14 章有详细的介绍。

对于 Kubernetes 来说,Init 容器的方式是最吸引人的,因为我们可以直接使用 ConfigMap 来获取模板参数。图 21-1 说明了这种模式的工作原理。
image.png
图 21-1 配置模板

应用程序的 Pod 定义至少由两个容器组成:一个是模板处理的 Init 容器,一个是应用程序容器。Init 容器不仅包含模板处理器,还包含配置模板本身。除了容器之外,这个 Pod 还定义了两个卷:一个是用于模板参数的卷,由 ConfigMap 支持,另一个是用于在 Init 容器和应用程序容器之间共享处理后的模板的 emptyDir 卷。

有了这个设置,在启动这个 Pod 的过程中,会执行以下步骤:

  1. 启动 Init 容器并运行模板处理器。该处理器从其镜像中获取模板,从挂载的 ConfigMap 卷中获取模板参数,并将结果存储在 emptyDir 卷中。
  2. Init 容器完成后,应用容器启动并从 emptyDir 卷中加载配置文件。

下面的例子使用 Init 容器来管理两个环境的全套 WildFly 配置文件:一个开发环境和一个生产环境。两者之间非常相似,只有轻微的不同。事实上,在我们的例子中,它们只是在日志记录的方式上有所不同:每条日志行分别用 DEVELOPMENT:PRODUCTION: 预先固定。

你可以在我们的 GitHub 示例回帖中找到完整的示例以及完整的安装说明。(我们在这里只展示了主要概念,技术细节请参考源码。)

例子 21-1 中的日志模式存储在 standalone.xml 中,我们通过使用 Go 模板语法对其进行参数化。

  1. # 例 21-1 日志配置模板
  2. ....
  3. <formatter name="COLOR-PATTERN">
  4. <pattern-formatter pattern="{{(datasource "config").logFormat}}"/>
  5. </formatter>
  6. ....

在这里,我们使用 Gomplate 作为模板处理器,它使用数据源的概念来引用要填写的模板参数。在我们的案例中,这个数据源来自于一个挂载在 Init 容器上的 ConfigMap 支持的卷。在这里,ConfigMap 包含一个带有键 logFormat 的单一条目,实际格式就是从这里提取的。

有了这个模板,我们现在可以为 Init 容器创建 Docker 镜像。镜像 k8spatterns/example-configuration-template-init 的 Dockerfile 非常简单(例 21-2):

# 例 21-2 简单的模板镜像的 Dockerfile

FROM k8spatterns/gomplate

COPY in /in

基础镜像 k8spatterns/gomplate 包含模板处理器和一个入口点脚本,默认使用以下目录:

  • /in 存放 WildFly 配置模板,包括参数化的 standalone.xml。这些都是直接添加到镜像中的。
  • /params 用于查找 Gomplate 数据源,这些数据源是 YAML 文件。这个目录是从 ConfigMap 支持的 Pod 卷挂载的。
  • /out 是存储处理文件的目录。这个目录安装在 WildFly 应用容器中,用于配置。

我们这个例子的第二个成分是存放参数的 ConfigMap。在例 21-3 中,我们只使用一个带有键值对的简单文件。

# 例 21-3 日志配置模板的值

logFormat: "DEVELOPMENT: %-5p %s%e%n"

一个名为 wildfly-parameters 的 ConfigMap 包含了这个 YAML 格式的数据引用。由一个关键的 config.yml 来执行,并由 Init 容器来接收。

最后,我们需要 WildFly 服务器的部署资源(例 21-4):

# 例 21-4 将模板处理器作为初始容器进行部署

---
apiVersion: extensions/v1beta1 
kind: Deployment
metadata:
    labels:
        example: cm-template
    name: wildfly-cm-template 
spec:
    replicas: 1 
  template:
        metadata: 
        labels:
                example: cm-template 
    spec:
            initContainers:
      # 保存配置模板的镜像
            - image: k8spatterns/example-config-cm-template-init 
          name: init
                volumeMounts:
        # 参数从 ConfigMap(wildfly-parameters)挂载
        - mountPath: "/params"
          name: wildfly-parameters 
        # 用于写出处理过的模板的目标目录,这是从一个空卷
        - mountPath: "/out"
          name: wildfly-config 
      containers:
            - image: jboss/wildfly:10.1.0.Final
                name: server
                command:
                - "/opt/jboss/wildfly/bin/standalone.sh" 
        - "-Djboss.server.config.dir=/config" 
        ports:
                - containerPort: 8080 
            name: http 
          protocol: TCP
        volumeMounts:
        # 生成的完整配置文件所在的目录被挂载到了 /config
        - mountPath: "/config"
                    name: wildfly-config 
      # 参数的 ConfigMap 和空目录的卷声明用于共享处理后的配置
      volumes:
            - name: wildfly-parameters 
          configMap:
                    name: wildfly-parameters
            - name: wildfly-config
                emptyDir: {}

这个声明挺拗口的,我们来钻研一下:部署规范包含一个 Pod,其中有我们的 Init 容器、应用容器和两个内部 Pod 卷。

  • 第一个卷,wildfly-parameters,包含同名的 ConfigMap(即包含一个名为 config.yml 的文件,里面有我们的参数值)。
  • 另一个卷最初是一个空目录,由 Init 容器和 WildFly 容器共享。

如果您启动此部署,将发生以下情况:

  • 会创建一个 Init 容器,并执行其命令。该容器从 ConfigMap 卷中获取 config.yml,从 Init 容器的 /in 目录中填写模板,并将处理后的文件存储在 /out 目录中。/out 目录是挂载 wildfly-config 卷的地方。
  • Init 容器完成后,WildFly 服务器启动时有一个选项,这样它就可以从 /config 目录中查找完整的配置。同样,/config 是包含处理过的模板文件的共享卷 wildfly-config

需要注意的是,当从开发环境转到生产环境时,我们不必改变这些部署资源描述符。只有带有模板参数的 ConfigMap 是不同的。

通过这种技术,可以轻松创建 DRY (Don’t Repeat Yourself)配置,而无需复制和维护重复的大型配置文件。例如,当所有环境的 WildFly 配置发生变化时,只需要更新 Init 容器中的一个模板文件。当然,这种方法在维护上有很大的优势,因为没有配置漂移的危险。

:::info 💡卷调试技巧:当以这种模式处理 Pod 和卷时,如果事情没有按照预期工作,如何调试并不明显。所以,如果你想检查处理过的模板,可以查看节点上的目录 /var/lib/kubelet/pods/{podid}/volumes/kubernetes.io~empty-dir/,因为它包含了一个 emptyDir 卷的内容。只要在 Pod 运行时,kubectl exec 进入 Pod,检查这个目录是否有创建的文件。 :::

一些讨论

配置模板模式建立在配置资源的基础上,当我们需要在不同的环境中用类似的复杂配置来操作应用程序时,配置模板模式特别适合。但是,使用配置模版的设置比较复杂,而且有更多可能出错的活动部件。只有当你的应用程序需要庞大的配置数据时才使用它。这类应用程序往往需要相当多的配置数据,其中只有一小部分依赖于环境。即使将整个配置直接复制到特定环境的 ConfigMap 中最初是可行的,它也会给该配置的维护带来负担,因为它注定会随着时间的推移而变化。对于这样的情况,模板的方法是完美的。

参考资料