Kubernetes 中的 ConfigMap 和 Secret:将配置从镜像中分离

1. 总览

当你将应用打包成容器镜像时,不应该将数据库 URL、API 密钥等值硬编码到镜像中。这篇文章帮助你理解 Kubernetes 中的 ConfigMap 和 Secret ——两个用于将配置从镜像中分离的对象,如何创建它们,如何通过环境变量或卷加载到 Pod 中,以及如何与 Spring Boot 应用程序集成。读完之后,你将能够自信地配置一个 Deployment 来动态读取配置,而无需每次更改值时重新构建镜像。

2. ConfigMap 是什么

ConfigMap 是一个存储键值对形式非加密配置数据的对象,用于非敏感数据:服务名称、功能开关、属性文件等。

ConfigMap 中的数据独立于 Pod 存在——你可以修改 ConfigMap 而无需重建镜像,只需重启 Pod(或者如果应用程序支持,可以使用重新加载机制)。

3. 创建 ConfigMap

3.1. 从 YAML 文件

apiVersion: v1
kind: ConfigMap
metadata:
  name: order-service-config
data:
  APP_ENV: "production"
  LOG_LEVEL: "INFO"
  application.properties: |
    server.port=8080
    spring.jpa.hibernate.ddl-auto=validate

这里 data 包含两种类型:简单的键值对(APP_ENV、LOG_LEVEL)和包含完整文件内容的键(application.properties)。通过以下方式应用:

kubectl apply -f order-service-config.yaml

3.2. 从 literal 或现有文件

kubectl create configmap order-service-config \
  --from-literal=APP_ENV=production \
  --from-file=application.properties=./config/application.properties

这个命令直接从命令行创建 ConfigMap,无需编写 YAML,在 CI/CD 脚本中很方便。

4. 在 Pod 中使用 ConfigMap

4.1. 作为环境变量加载

apiVersion: v1
kind: Pod
metadata:
  name: order-service
spec:
  containers:
    - name: order-service
      image: nonladev/order-service:1.0
      envFrom:
        - configMapRef:
            name: order-service-config

envFrom 将 ConfigMap 中的所有键加载为同名的环境变量。容器读取 APP_ENV、LOG_LEVEL 就像普通环境变量一样。

如果只想选择特定的键:

      env:
        - name: APP_ENVIRONMENT
          valueFrom:
            configMapKeyRef:
              name: order-service-config
              key: APP_ENV

这种方式允许你在容器中重命名环境变量(APP_ENVIRONMENT),使其与 ConfigMap 中的原始键(APP_ENV)不同。

4.2. 作为卷加载

      volumeMounts:
        - name: config-volume
          mountPath: /app/config
  volumes:
    - name: config-volume
      configMap:
        name: order-service-config

ConfigMap 中的每个键都成为 /app/config 中的一个文件(例如 /app/config/application.properties)。当应用程序需要读取整个配置文件而不是单个环境变量时,这种方式很合适。

注意: 当以卷形式挂载时,Kubernetes 会在 ConfigMap 更改时自动更新文件(延迟几十秒到几分钟,取决于 kubelet 的同步周期)。对于环境变量(env/envFrom),Pod 不会自动重新加载——你必须手动重启 Pod。

5. Secret 是什么以及与 ConfigMap 的区别

Secret 的结构类似 ConfigMap,但用于敏感数据:密码、令牌、私钥。主要区别:

标准 ConfigMap Secret
加密 否 Base64 编码(不是真正的加密)
存储在 etcd 中 纯文本 可以启用静态加密
通过 kubectl get 显示 显示值 隐藏值(仅显示 base64)
用途 常规配置 敏感数据

注意: Secret 只进行 base64 编码,默认不加密。任何有 namespace 中 get secret 权限的人都可以解码。要获得真正的安全性,你需要在 etcd 启用静态加密或使用与外部 Secret 管理器(如 Vault、AWS Secrets Manager)通过 External Secrets Operator 集成。

6. 创建 Secret

6.1. 从 YAML(必须手动 base64 编码)

echo -n "P@ssw0rd123" | base64
# UEBzc3cwcmQxMjM=
apiVersion: v1
kind: Secret
metadata:
  name: order-service-secret
type: Opaque
data:
  DB_PASSWORD: UEBzc3cwcmQxMjM=

data 字段要求值已经 base64 编码。如果不想自己编码,使用 stringData:

stringData:
  DB_PASSWORD: "P@ssw0rd123"

Kubernetes 会在保存时自动编码。在手动编写 manifest 时使用 stringData 更易阅读。

6.2. 从命令行

kubectl create secret generic order-service-secret \
  --from-literal=DB_PASSWORD=P@ssw0rd123 \
  --from-literal=DB_USERNAME=order_app

7. 在 Pod 中使用 Secret

加载方式完全类似 ConfigMap:

      envFrom:
        - secretRef:
            name: order-service-secret

或者挂载卷:

      volumeMounts:
        - name: secret-volume
          mountPath: /app/secret
          readOnly: true
  volumes:
    - name: secret-volume
      secret:
        secretName: order-service-secret

Secret 卷挂载应该始终设置 readOnly: true 并通过 defaultMode 限制文件权限(如果需要):

      secret:
        secretName: order-service-secret
        defaultMode: 0400

8. 与 Spring Boot 集成

Spring Boot 根据标准的宽松绑定规则读取环境变量,所以你不需要编写任何特殊代码——只需使用正确的变量名约定。

      envFrom:
        - configMapRef:
            name: order-service-config
        - secretRef:
            name: order-service-secret
      env:
        - name: SPRING_DATASOURCE_USERNAME
          valueFrom:
            secretKeyRef:
              name: order-service-secret
              key: DB_USERNAME
        - name: SPRING_DATASOURCE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: order-service-secret
              key: DB_PASSWORD

Spring Boot 自动将 SPRING_DATASOURCE_USERNAME 映射到 spring.datasource.username。在代码中,你只需要使用普通的 @ConfigurationProperties:

@ConfigurationProperties(prefix = "spring.datasource")
public record DataSourceProperties(String url, String username, String password) {
}
@Service
public class PaymentService {

    private final DataSourceProperties dataSourceProperties;

    public PaymentService(DataSourceProperties dataSourceProperties) {
        this.dataSourceProperties = dataSourceProperties;
    }

    public String describeConnection() {
        return "Connecting as %s".formatted(dataSourceProperties.username());
    }
}

PaymentService 对 Kubernetes 一无所知——它只接收由 ConfigMap/Secret 提供的已绑定的值,这些值由 Spring Boot 处理。

8.1. 独立于集群的测试

你可以通过 @TestPropertySource 设置模拟环境变量来测试逻辑,而不需要真实的 Kubernetes:

@SpringBootTest
@TestPropertySource(properties = {
    "spring.datasource.username=order_app",
    "spring.datasource.password=test-pass"
})
class PaymentServiceTest {

    @Autowired
    private PaymentService paymentService;

    @Test
    void shouldDescribeConnectionUsingBoundProperties() {
        assertThat(paymentService.describeConnection())
            .isEqualTo("Connecting as order_app");
    }
}

这个测试确认 DataSourceProperties 被正确绑定,PaymentService 使用正确的值——完全独立于该值是否来自集群上真实的 ConfigMap 或 Secret。

PaymentServiceTest > shouldDescribeConnectionUsingBoundProperties() PASSED

9. 无需重启 Pod 的动态更新

如果以卷形式挂载 ConfigMap/Secret,文件会自动更新,但 Spring Boot不会自动重新读取除非你:

  • 使用 Spring Cloud Kubernetes(spring-cloud-starter-kubernetes-client-config)来轮询 ConfigMap 并刷新 @RefreshScope bean。
  • 或者自己实现文件监控器来重新读取挂载的文件。
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-kubernetes-client-config</artifactId>
</dependency>
spring.cloud.kubernetes.config.enabled=true
spring.cloud.kubernetes.reload.enabled=true
spring.cloud.kubernetes.reload.mode=polling

此配置启用 Spring Cloud Kubernetes 特性,以定期轮询 ConfigMap 并刷新标记为 @RefreshScope 的 bean,无需手动重启 Pod。

注意: 持续轮询会消耗 API 服务器资源。对于大型集群,考虑使用 spring.cloud.kubernetes.reload.mode=event(watch API)代替轮询。

10. 常见错误和何时不应该使用

  • 把密码放在 ConfigMap 中:ConfigMap 没有加密,kubectl get configmap -o yaml 会清楚地显示值。始终使用 Secret 存储敏感数据。
  • 在 Secret YAML 的 data 中忘记 base64 编码:导致 illegal base64 data 错误。如果不想手动编码,使用 stringData。
  • 更改 ConfigMap 但 Pod 没有收到新值:因为通过 env/envFrom 加载的值不会自动重新加载——必须执行 kubectl rollout restart deployment。
  • 挂载 Secret 卷但忘记 readOnly: true:容器可能会覆盖文件,在调试时造成混淆。
  • 把 Secret 当作长期的秘密存储库:Kubernetes 中的 Secret 适合运行时配置,不能代替专门的 Secret 管理器(如 Vault、AWS Secrets Manager)在需要审计日志、自动轮换或严格的版本控制时。
  • ConfigMap/Secret 过大:Kubernetes 限制对象大小约为 1 MiB(由于 etcd 限制)。如果需要大型配置文件,考虑使用来自 PersistentVolume 的卷挂载或初始化容器从对象存储加载。

11. 总结

ConfigMap 和 Secret 帮助你将配置从镜像中分离,通过仅更改配置对象,就可以使用相同的镜像部署到多个环境(开发、测试、生产)。ConfigMap 用于公开数据,Secret 用于敏感数据——但记住 Secret 默认只是编码而不是真正的加密。对于 Spring Boot,你利用宽松绑定通过环境变量加载值而无需更改业务代码,如果需要在不重启 Pod 的情况下动态重新加载配置,可以启用 Spring Cloud Kubernetes。

kubernetesconfigmapsecretspring-bootdevops