不可变配置(Immutable Configuration)模式将配置数据打包成不可变的容器映像,并在运行时将配置容器链接到应用程序。通过这种模式,我们不仅能够使用不可变和版本化的配置数据,还能克服存储在环境变量或 ConfigMap 中的配置数据的大小限制。

问题描述

正如你在第 18 章 “环境变量配置” 中所看到的,环境变量提供了一种配置基于容器的应用程序的简单方法。虽然它们易于使用并且普遍支持,但一旦环境变量的数量超过一定的阈值,管理它们就会变得很困难。

通过使用配置资源可以在一定程度上处理这种复杂性。但是,所有这些模式都不能强制要求配置数据本身的不可变性。这里的不可变性意味着我们在应用开始后不能改变配置,以确保我们的配置数据始终有一个定义良好的状态。此外,不可变配置可以被置于版本控制之下,并遵循变更控制流程。

解决方案

为了解决前面的问题,我们可以把所有特定环境的配置数据放到一个单一的、被动的数据镜像中,我们可以把它作为一个普通的容器镜像来发布。在运行时,应用程序和数据镜像链接在一起,这样应用程序就可以从数据映像中提取配置。通过这种方法,我们可以很容易地针对不同的环境制作不同的配置数据映像。然后,这些映像结合了特定环境的所有配置信息,并且可以像任何其他容器映像一样进行版本调整。

创建这样的数据镜像是很简单的,因为它是一个简单的容器镜像,只包含数据。挑战在于启动过程中的链接步骤。我们可以根据平台的不同,使用不同的方法。

Docker 卷

在看 Kubernetes 之前,我们先退一步,单纯考虑一下 Docker 的情况。在 Docker 中,可以让容器暴露出一个带有容器数据的卷。通过 Dockerfile 中的 VOLUME 指令,可以指定一个以后可以共享的目录。在启动过程中,容器内这个目录的内容会被复制到这个共享目录中。如图 20-1 所示,这种存储卷链接是与另一个应用容器共享专用配置容器中的配置信息的绝佳方式。
image.png
图 20-1 通过 Docker 卷实现不可变配置

我们来看一个例子。对于开发环境,我们创建一个存放开发者配置的 Docker 镜像,并创建一个卷 /config。我们可以用 Dockerfile-config 创建这样一个镜像,如例 20-1:

  1. # 例 20-1 配置镜像的 Dockerfile
  2. FROM scratch
  3. # 添加指定的属性
  4. ADD app-dev.properties /config/app.properties
  5. # 创建卷并将属性复制到其中
  6. VOLUME /config

现在,我们用 Docker CLI 创建镜像本身和 Docker 容器,如例 20-2:

# 例 20-2 构建配置 Docker 镜像

$ docker build -t k8spatterns/config-dev-image:1.0.1 -f Dockerfile-config 
$ docker create --name config-dev k8spatterns/config-dev-image:1.0.1 .

最后一步是启动应用程序容器,并将其连接到这个配置容器(例 20-3):

# 例 20-3 启动应用程序容器,并链接配置容器

$ docker run --volumes-from config-dev k8spatterns/welcome-servlet:1.0

应用程序镜像期望其配置文件在一个目录 /config 内,即配置容器所暴露的卷。当你把这个应用程序从开发环境转移到生产环境时,你所要做的就是改变启动命令。没有必要改变应用程序映像本身。取而代之的是,你只需将应用程序容器与生产配置容器进行卷链接,如例 20-4 所示:

# 例 20-4 在生产环境中使用不同的配置

$ docker build -t k8spatterns/config-prod-image:1.0.1 -f Dockerfile-config 
$ docker create --name config-prod k8spatterns/config-prod-image:1.0.1 . 
$ docker run --volumes-from config-prod k8spatterns/welcome-servlet:1.0

Kubernetes 初始化容器

在 Kubernetes 中,Pod 内的卷共享完全适合这种配置和应用容器的链接。然而,如果我们想把 Docker 卷链接的这种技术移植到 Kubernetes 世界中,我们会发现目前 Kubernetes 中还没有对容器卷的支持。考虑到讨论的年代和实现这一功能的复杂性与其有限的好处,容器卷很可能不会在短期内到来。

所以容器可以共享(外部)卷,但还不能直接共享位于容器内的目标。为了在 Kubernetes 中使用不可变配置容器,我们可以使用第 14 章中的 Init 容器模式,它可以在启动过程中初始化一个空的共享卷。

在 Docker 的例子中,我们基于从头开始的配置 Docker 镜像,一个没有任何操作系统文件的空 Docker 镜像。我们不需要更多的东西在那里,因为我们想要的是通过 Docker 卷共享的配置数据。然而,对于 Kubernetes Init 容器,我们需要一些基础镜像的帮助,将配置数据复制到共享的 Pod 卷上,busybox 是一个不错的基础镜像的选择,虽然它还是很小,但允许我们使用普通的 Unix cp 命令来完成这个任务。

那么,共享卷的初始化与配置是如何进行的呢?我们来看一个例子。首先,我们需要用 Docker 文件再次创建一个配置镜像,如例 20-5:

# 例 20-5 开发环境的配置镜像

FROM busybox

ADD dev.properties /config-src/demo.properties 

# 在这里使用 Shell 来解析通配符
ENTRYPOINT [ "sh", "-c", "cp /config-src/* $1", "--" ]

与例 20-1 中的普通 Docker 案例的唯一区别是,我们有一个不同的基础镜像,并且我们添加了一个 ENTRYPOINT,将属性文件复制到 Docker 镜像启动时作为参数给出的目录中。现在可以在 Deployment 的 .template.spec 中的 Init 容器中引用这个镜像(见例 20-6)。

# 例 20-6 将配置复制到 Init 容器中的目的地的部署

---
initContainers:
- image: k8spatterns/config-dev:1
    name: init
    args:
    - "/config" 
  volumeMounts:
    - mountPath: "/config"
        name: config-directory 
containers:
- image: k8spatterns/demo:1 
    name: demo
    ports:
    - containerPort: 8080
        name: http
        protocol: TCP 
  volumeMounts:
    - mountPath: "/var/config"
        name: config-directory 
volumes:
- name: config-directory 
    emptyDir: {}

部署的 Pod 模板规范包含一个卷和两个容器:

  • 卷的 config-directory 是空目录类型 emptyDir,所以它被创建为一个空目录在节点托管这个 Pod。
  • Kubernetes 在启动过程中调用的 Init 容器是由我们刚刚创建的镜像构建的,我们设置了一个单一参数 /config,由镜像的 ENTRYPOINT 使用。这个参数指示 Init 容器将其内容复制到指定的目录中。目录 /var/config 是从卷 config-directory 中挂载的。
  • 应用程序容器挂载卷 config-directory 来访问被 Init 容器复制过来的配置。

图 20-2 说明了应用容器如何在共享卷上访问由 Init 容器创建的配置数据。
image.png
图 20-2 使用 Init 容器实现不可更改的配置

现在,要改变从开发环境到生产环境的配置,我们需要做的就是交换 Init 容器的镜像,我们可以通过改变 YAML 定义或者通过更新 kubectl 来实现。然而,为每个环境编辑资源描述符并不理想。如果你使用的是 Kubernetes 的企业发行版 Red Hat OpenShift,OpenShift 模板(Templates)可以帮助解决这个问题。OpenShift 模板可以从一个模板中为不同环境创建不同的资源描述符。

OpenShift 模板

模板是常规的资源描述符,是参数化的。如例 20-7 所示,我们可以很容易地使用配置图像镜像作为参数。

# 例 20-7 用于配置镜像参数化的 OpenShift 模板

---
apiVersion: v1 
kind: Template 
metadata:
    name: demo 
parameters:
# 模板参数 CONFIG_IMAGE 声明
- name: CONFIG_IMAGE
    description: Name of configuration image 
  value: k8spatterns/config-dev:1
objects:
- apiVersion: v1
    kind: DeploymentConfig 
      // ....
        spec: 
        template:
                metadata: 
            // ....
                    spec: 
              initContainers: 
            - name: init
                # 模板参数的使用
                            image: ${CONFIG_IMAGE} 
              args: [ "/config" ] 
              volumeMounts:
                            - mountPath: /config
                                name: config-directory 
            containers:
                        - image: k8spatterns/demo:1 
                // ...
                            volumeMounts:
                            - mountPath: /config
                                name: config-directory 
            volumes:
                        - name: config-directory 
                emptyDir: {}

我们在这里只展示了完整描述符的一个片段,但我们可以快速识别出我们在 Init 容器声明中引用的参数 CONFIG_IMAGE。如果我们在 OpenShift 集群上创建这个模板,我们可以通过调用 oc 来实例化它,就像例 20-8 一样:

# 例 20-8 应用 OpenShift 模板创建新的应用 

$ oc new-app demo -p CONFIG_IMAGE=k8spatterns/config-prod:1

运行此示例的详细说明,以及完整的 Deployment 描述符,可以像往常一样在我们的 Git 仓库中找到。

一些讨论

使用数据容器进行不可变配置模式诚然有点复杂。但是,这种模式有一些独特的优势。

  • 特定环境的配置被封存在一个容器中。因此,它可以像任何其他容器镜像一样被版本化。
  • 以这种方式创建的配置可以分布在容器注册表上。即使不访问集群,也可以检查配置。
  • 配置与持有配置的容器镜像一样是不可改变的:配置的更改需要版本更新和新的容器镜像。
  • 当配置数据太复杂而无法放入环境变量或 ConfigMap 中时,配置数据镜像很有用,因为它可以容纳任意大的配置数据。

正如预期的那样,这种模式也有一些缺点。

  • 它具有较高的复杂性,因为需要建立额外的容器镜像并通过注册表进行分配。
  • 它没有解决任何围绕敏感配置数据的安全问题。
  • 在 Kubernetes 的情况下,需要额外的 Init 容器处理,因此我们需要为不同的环境管理不同的 Deployment 对象。

总而言之,我们应该仔细评估是否真的需要这种涉及到的方法。如果不需要不可更改性,也许像第 19 章 “配置资源” 中描述的简单的 ConfigMap 就完全足够了。

另一种处理大的配置文件的方法是用配置模板模式来描述的,这是下一章的主题。

参考资料