在这个环境变量(EnvVar)配置模式中,我们研究了配置应用程序的最简单方法。对于小的配置值集,最简单的外部化配置方式是将它们放到普遍支持的环境变量中。我们看到了在 Kubernetes 中声明环境变量的不同方式,但也看到了在复杂配置中使用环境变量的局限性。

问题描述

每个非平凡的应用程序都需要一些配置来访问数据源、外部服务或生产级调优。而在《十二因素应用宣言》之前,我们就清楚地知道,在应用中硬编码配置是一件坏事。相反,配置应该是外部化的,这样即使在应用构建完成后,我们也可以改变它。这为实现和促进不可改变的应用程序的共享的 容器化应用程序提供了更多的价值。但是,在容器化的世界里,如何才能做到最好呢?

解决方案

说明这种模式的最好方法是通过一个例子。成功运行和支持分布式系统的一个主要前提是提供详细的监控和警报。此外,如果我们有一个由多个服务组成的分布式系统,我们想要监控,我们可能会使用一个外部监控工具来轮询每个服务的指标并记录它们。

然而,用不同语言编写的服务可能具有不同的功能,并且可能不会以监控工具所期望的相同格式暴露度量。这种多样性给从单一的监控解决方案中监控这样一个异构的应用程序带来了挑战,因为该解决方案期望对整个系统有一个统一的视图。通过适配器模式,可以通过导出以下内容来提供统一的监控界面。

将来自不同应用容器的度量信息转换为一种标准格式和协议。在图16-1 中,一个适配器容器将本地存储的度量信息翻译成监控服务器理解的外部格式。

  1. # 例 18-1 带有环境变量的 Docker 文件示例
  2. FROM openjdk:11
  3. ENV PATTERN "EnvVar Configuration"
  4. ENV LOG_FILE "/tmp/random.log"
  5. ENV SEED "1349093094"
  6. # Alternatively:
  7. ENV PATTERN="EnvVar Configuration" LOG_FILE=/tmp/random.log SEED=1349093094
  8. ...

那么在这样的容器中运行的 Java 应用程序就可以通过调用 Java 标准库轻松地访问变量,如例 18-2 所示:

// 例 18-2 在 Java 中读取环境变量

public Random initRandom() {
    // 用环境变量的种子初始化一个随机数发生器
    long seed = Long.parseLong(System.getenv("SEED")); 
    return new Random(seed);
}

直接运行这样的映像将使用默认的硬编码值。但在大多数情况下,你想从镜像外部重写这些参数。

当直接使用 Docker 运行这样的镜像时,可以通过调用 Docker 从命令行设置环境变量,如例 18-3:

# 例 18-3 启动 Docker 容器时设置环境变量

docker run -e PATTERN="EnvVarConfiguration" \ 
                     -e LOG_FILE="/tmp/random.log" \
                     -e SEED="147110834325" \ 
           k8spatterns/random-generator:1.0

对于 Kubernetes 来说,这些类型的环境变量可以直接在 Deployment 或 ReplicaSet 等控制器的 Pod 规范中设置(如例 18-4)。

# 例 18-4 部署时设置了环境变量

---
apiVersion: v1 
kind: Pod
metadata:
    name: random-generator
spec:
    containers:
    - image: k8spatterns/random-generator:1.0
        name: random-generator 
    env:
        - name: LOG_FILE
        # 环境变量的一个字面值
            value: /tmp/random.log
        - name: PATTERN
            valueFrom: 
          # 来自 ConfigMap 的环境变量
          configMapKeyRef:
            # ConfigMap 的名称
                    name: random-generator-config
          # 在 ConfigMap 中寻找环境变量值的键值
                    key: pattern
        - name: SEED
            valueFrom: 
          # 来自 Secret 的环境变量(查询语义与 ConfigMap 相同)
          secretKeyRef:
                    name: random-generator-secret 
          key: seed

在这样的 Pod 模板中,你不仅可以直接给环境变量附加值(比如 LOG_FILE),还可以使用委托给 Kubernetes Secret(用于敏感数据)和 ConfigMap(用于非敏感配置)。ConfigMap 和 Secret 间接的好处是,环境变量可以从 Pod 定义中独立管理。Secret 和 ConfigMap 及其优缺点在第 19 章 “配置资源” 中详细解释。

:::warning 在前面的例子中,SEED 变量来自于 Secret 资源。虽然这是 Secret 的一个完全有效的用法,但同样重要的是要指出,环境变量并不安全。把敏感的、可读的信息放到环境变量中,会使这些信息很容易被读取,甚至可能泄露到日志中。 :::

关于默认值

默认值使生活变得更轻松,因为它们消除了为一个你可能根本不知道存在的配置参数选择值的负担。它们在约定俗成的配置范式中也发挥了重要作用。然而,默认值并不总是一个好主意。有时,它们甚至可能是一个不断发展的应用程序的反模式。

这是因为追溯性地改变默认值是一项困难的任务。首先,改变默认值意味着在代码中替换它们,这需要重新构建。其次,当默认值发生变化时,依赖默认值的人(无论是习惯还是有意)总是会感到惊讶。我们必须传达这种变化,而这种应用程序的用户可能也要修改调用代码。

然而,默认值的变化往往是有意义的,因为很难从一开始就得到默认值。我们必须把对默认值的修改看作是一个主要的修改,如果使用了语义版本号,这样的修改就可以证明在主要版本号上有一个凸起。如果对给定的默认值不满意,通常最好是完全删除默认值,并在用户没有提供配置值时抛出一个错误。这样至少可以尽早地、突出地破坏应用程序,而不是它默默地做一些不同的、意想不到的事情。

:::tips 考虑到所有这些问题,如果你不能 90% 确定一个合理的默认值会长期存在,那么从一开始就避免使用默认值往往是最好的解决方案。密码或数据库连接参数是不提供默认值的很好的候选者,因为它们高度依赖于环境,而且往往无法可靠地预测。另外,如果我们不使用默认值,配置信息必须明确地提供,这也是文档的作用。 :::

一些讨论

环境变量很容易使用,而且每个人都知道它们。这个概念可以顺利地映射到容器上,而且每个运行时平台都支持环境变量。但是环境变量并不安全,它们只适用于相当数量的配置值。当有很多不同的参数需要配置时,对所有这些环境变量的管理就会变得很麻烦。

在这种情况下,许多人使用额外的间接层次,把配置放到不同的配置文件中,每个环境一个。然后用一个环境变量来选择这些文件中的一个。Spring Boot 中的配置文件就是这种方法的一个例子。由于这些配置文件通常存储在应用程序本身,也就是容器内,因此它将配置与应用程序紧密结合在一起。这通常会导致开发的配置和生产环境在同一个 Docker 镜像中并排结束,这就需要为任何一个环境中的每一个变化重建镜像。所有这些都告诉我们,环境变量只适合于小规模的配置集。

当遇到比较复杂的配置需求时,下面章节中介绍的配置资源、不可变配置和配置模板是不错的选择。

环境变量是普遍适用的,正因为如此,我们可以在不同的层次上设置它们。这种选择会导致配置定义的碎片化,对于一个给定的环境变量来说,很难追踪它的来源。当所有的环境变量没有被定义在一个中心位置时,就很难调试配置问题。

环境变量的另一个缺点是,它们只能在应用程序启动前设置,我们不能在之后更改它们。一方面,这是一个缺点,你不能在运行时 “热” 地改变配置来调整应用程序。然而,许多人认为这是一个优点,因为它促进了甚至对配置的不可变性。这里的不变性意味着你扔掉正在运行的应用容器,然后用修改过的配置启动一个新的副本,很可能采用滚动更新等平滑的 Deployment 策略。这样一来,你总是处于一个确定的、众所周知的配置状态。

环境变量使用起来很简单,但主要适用于简单的用例,对于复杂的配置需求有一定的局限性。接下来的模式将展示如何克服这些限制。

参考资料