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 并刷新@RefreshScopebean。 - 或者自己实现文件监控器来重新读取挂载的文件。
<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。