存储体系解决了持久化数据问题。配置和密钥是另一类需要与 Pod 生命周期解耦的数据:应用配置(数据库地址、功能开关)和敏感信息(密码、API Key、TLS 证书)不应该打包到镜像里,因为不同环境配置不同,密钥也需要独立的访问控制。

十二因素应用(The Twelve-Factor App)把"配置"定义为在不同部署(开发、测试、生产)之间存在差异的东西,并要求配置必须与代码严格分离。Kubernetes 的 ConfigMap 和 Secret 是这个原则的原生实现。ConfigMap 和 Secret 如何把配置注入 Pod、外部密钥管理系统(Vault、AWS Secrets Manager)如何与 Kubernetes 集成,是本篇覆盖的内容。

配置注入全景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
          ┌──────────────────────────────────────────┐
│ Kubernetes API │
│ │
│ ConfigMap Secret
│ ────────── ────── │
│ key: value key: base64(value) │
└────────┬─────────────────┬───────────────┘
│ │
┌───────────┼─────────────────┼───────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌────────────┐ ┌────────┐ ┌────────────┐ ┌────────────┐
│ 环境变量 │ │命令行 │ │ Volume 挂载 │ │ ImagePull │
│ envFrom │ │参数 │ │ 文件形式 │ │ Secret
│ env.valueFrom│ │ │ 热更新支持 │ │ │
└────────────┘ └────────┘ └────────────┘ └────────────┘
│ │
│ Pod 启动时注入 │ kubelet 定期 sync
│ 不自动更新 │ 约 60 秒延迟
▼ ▼
┌──────────────────────────────────────────┐
│ 容器进程 │
│ env: DB_HOST=postgres │
│ file: /config/LOG_LEVEL = debug
└──────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
外部密钥管理集成方式

┌─────────────────┐ ┌──────────────────┐ ┌──────────────┐
│ HashiCorp │ │ AWS Secrets │ │ Azure Key
│ Vault │ │ Manager │ │ Vault │
└────────┬────────┘ └────────┬─────────┘ └──────┬───────┘
│ │ │
└──────────┬──────────┘──────────────────────┘

┌──────────▼──────────┐
External Secrets │
Operator (ESO) │ 或 Secrets Store CSI Driver
└──────────┬──────────┘
│ 同步/挂载

┌──────────────────────┐
│ Kubernetes Secret │ ESO 方式:同步到 etcd
│ 或 Volume Mount │ CSI 方式:绕过 etcd 直接挂载
└──────────────────────┘

核心对象

ConfigMap

ConfigMap 存储非敏感的键值对配置,可以存储单个值、多行文本、完整配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: production
data:
# 简单键值
LOG_LEVEL: "info"
DB_HOST: "postgres.production.svc.cluster.local"
# 多行配置文件
nginx.conf: |
worker_processes auto;
events { worker_connections 1024; }
http {
server {
listen 80;
location / { proxy_pass http://backend; }
}
}

单个 ConfigMap 最大 1MiB(etcd 限制)。超过这个大小应使用 ConfigMap 分片或外部配置服务。

Secret

Secret 的 data 字段存储 base64 编码的值,stringData 字段接受原文(写入时自动编码):

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: production
type: Opaque
stringData:
password: "s3cr3t-p@ssword" # 明文,写入时自动 base64
connection-string: "postgres://user:s3cr3t@host:5432/db"

Secret 的三种常用类型:

类型 用途
Opaque 通用密钥,任意键值
kubernetes.io/tls TLS 证书,固定字段 tls.crt 和 tls.key
kubernetes.io/dockerconfigjson 镜像拉取凭证,imagePullSecrets 引用

注入方式对比

环境变量方式(envFrom)

1
2
3
4
5
6
7
containers:
- name: app
envFrom:
- configMapRef:
name: app-config # ConfigMap 所有键注入为环境变量
- secretRef:
name: db-credentials # Secret 所有键注入为环境变量

也可以引用单个键:

1
2
3
4
5
6
7
8
9
10
11
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL

Volume 挂载方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
volumes:
- name: config-vol
configMap:
name: app-config
- name: secret-vol
secret:
secretName: db-credentials
defaultMode: 0400 # 只有 owner 可读,适合密钥文件

containers:
- name: app
volumeMounts:
- name: config-vol
mountPath: /etc/config
- name: secret-vol
mountPath: /etc/secrets
readOnly: true

挂载后,每个键成为目录下的一个文件:/etc/config/LOG_LEVEL/etc/config/nginx.conf

热更新机制

Volume 挂载的 ConfigMap/Secret 更新时,kubelet 的 syncLoop 定期检查(默认 configMapAndSecretChangeDetectionStrategy 为 Watch,变更后约 1-2 分钟内更新文件)。更新通过 atomic swap 实现:kubelet 写入临时目录,再用 symlink 替换,确保应用看到的始终是完整配置。

应用需要主动感知文件变化(inotify watch 或轮询)才能实现热更新。环境变量方式不支持热更新,变量在容器启动时注入,之后不再变化,除非重启容器。

Immutable ConfigMap/Secret

1.21 GA,设置 immutable: true 后配置不可修改(只能删除重建):

1
2
3
4
5
6
7
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-v2
immutable: true
data:
VERSION: "2.0"

优点:减少 API Server 压力(kubelet 不再 watch immutable 对象,节省 watch 连接);防止配置意外修改。适合不可变基础设施模式,每次变更创建新版本 ConfigMap。

Secret 的安全局限

etcd 默认存储中,Secret 的 data 只是 base64 编码,不是加密。任何能读取 etcd 数据的人都能获取原始值。

启用静态加密需要在 API Server 配置 EncryptionConfiguration:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {} # fallback:读取未加密的旧数据

配置后,新写入的 Secret 在 etcd 中加密存储,读取时在内存中解密。旧数据不自动加密,需要 kubectl get secrets -A -o json | kubectl replace -f - 批量重写。

实际上,Secret 的安全性主要依赖两层:RBAC(控制谁能通过 API 读取 Secret)和 etcd 访问控制(控制谁能直接读 etcd)。etcd 加密是额外的防御层。

外部密钥管理集成

External Secrets Operator(ESO)

ESO 把外部密钥系统(Vault、AWS SM、GCP SM、Azure KV 等)中的密钥同步到 Kubernetes Secret:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-secret
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: db-credentials # 目标 Kubernetes Secret 名称
creationPolicy: Owner
data:
- secretKey: password # Kubernetes Secret 的键
remoteRef:
key: production/db # Vault 路径
property: password # Vault 字段

ESO 定期(refreshInterval)从外部系统拉取最新值,更新 Kubernetes Secret。密钥仍然存储在 etcd 中,但生命周期由外部系统管理。

Secrets Store CSI Driver

CSI Driver 方式绕过 etcd,直接把外部密钥挂载为 Pod volume:

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: vault-db-creds
spec:
provider: vault
parameters:
vaultAddress: "https://vault.example.com"
roleName: "production-app"
objects: |
- objectName: "db-password"
secretPath: "production/db"
secretKey: "password"

Pod 挂载时,CSI Driver 的 Node Plugin 从 Vault 获取密钥,写入 tmpfs(内存文件系统),bind mount 到 Pod。密钥不经过 etcd,减少攻击面。

Vault Agent Injector

Vault Agent Injector 通过 MutatingWebhook 向 Pod 注入 sidecar 容器:

  • vault-agent-init(init container):启动时从 Vault 获取密钥,写入共享 volume
  • vault-agent(sidecar):持续监听 Vault,密钥轮转时自动更新文件

应用只需在 Pod 上添加 annotation,无需修改代码:

1
2
3
4
5
6
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "production-app"
vault.hashicorp.com/agent-inject-secret-db-password: "production/db"
vault.hashicorp.com/agent-inject-template-db-password: |
{{ with secret "production/db" }}{{ .Data.data.password }}{{ end }}

实验:ConfigMap 热更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 创建 ConfigMap 和 Secret
kubectl create configmap app-config \
--from-literal=DB_HOST=postgres \
--from-literal=LOG_LEVEL=info

kubectl create secret generic db-secret \
--from-literal=password=s3cr3t

# 创建以 Volume 方式挂载的 Pod
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: config-test
spec:
containers:
- name: app
image: busybox
command: [sh, -c, "while true; do echo \"LOG_LEVEL: \$(cat /config/LOG_LEVEL)\"; sleep 5; done"]
volumeMounts:
- name: config-vol
mountPath: /config
- name: secret-vol
mountPath: /secrets
readOnly: true
volumes:
- name: config-vol
configMap:
name: app-config
- name: secret-vol
secret:
secretName: db-secret
defaultMode: 0400
EOF

# 观察输出
kubectl logs config-test -f &
# LOG_LEVEL: info
# LOG_LEVEL: info

# 更新 ConfigMap
kubectl patch configmap app-config \
--patch '{"data":{"LOG_LEVEL":"debug"}}'

# 约 60 秒后观察变化(文件更新,输出变为 debug)
# LOG_LEVEL: debug

# 验证文件更新(通过 symlink 原子替换)
kubectl exec config-test -- ls -la /config/
# 可以看到 ..data 是 symlink,指向带时间戳的目录

# 对比:以环境变量方式注入的值不会更新
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: env-test
spec:
containers:
- name: app
image: busybox
command: [sh, -c, "while true; do echo \$LOG_LEVEL; sleep 5; done"]
envFrom:
- configMapRef:
name: app-config
EOF

kubectl logs env-test -f &
# 更新 ConfigMap 后,env-test 的输出不变,仍然是 info

映射到 Kubernetes 内部机制

ConfigMap 和 Secret 存储在 etcd 中,通过 API Server 提供 CRUD 接口。kubelet 通过两种方式获取最新值:

对于 Volume 挂载的 ConfigMap/Secret,kubelet 的 VolumeManager 注册了 Informer,监听 ConfigMap/Secret 变更事件。收到变更后,kubelet 调用 configmap/secret manager 的 GetObject 获取最新版本,然后写入节点上的缓存目录,再通过 atomic symlink swap 更新 Pod 的 volume 目录。整个过程完全在 kubelet 侧完成,不需要重启容器。

对于环境变量注入,环境变量在 container.Env 中被展开,在容器创建时(通过 CRI 的 CreateContainer 调用)传递给运行时,之后不再变化。

模式提炼

ConfigMap 和 Secret 是"外部化配置"模式的 Kubernetes 实现。Volume 挂载方式支持热更新,适合需要动态配置的场景;环境变量方式简单直接,适合启动时固定的配置。外部密钥系统通过 ESO 或 CSI Driver 把 Secret 的生命周期托管给专业工具,把 Kubernetes 从密钥管理责任中解脱出来。

工程迁移表

配置机制 工程类比
ConfigMap Spring Boot application.properties,Consul KV,12-Factor 环境变量
Secret HashiCorp Vault,AWS Secrets Manager,Spring Cloud Vault
Volume 热更新 inotify 文件监听,Spring Cloud Config refresh endpoint
ESO Spring Cloud Vault,AWS Parameter Store SDK 集成
Secrets Store CSI Driver 在内存中注入密钥,不落磁盘,类似 tmpfs
Immutable ConfigMap 不可变基础设施,blue/green 部署,版本化配置
Vault Agent Injector AOP 拦截器注入凭证,透明代理获取密钥

常见误解

误解一:Secret 比 ConfigMap 更安全,可以放心存储敏感数据。

默认情况下,Secret 在 etcd 中只是 base64 编码,不是加密。任何有权限访问 etcd 的人(或者拿到 etcd 备份文件的人)都能解码。Secret 的安全性主要依赖 RBAC——控制谁能通过 Kubernetes API 读取 Secret,而不是依赖加密。如果需要真正的加密存储,需要额外配置 EncryptionConfiguration,或者使用 ESO/CSI Driver 等不在 etcd 存储的方案。

误解二:环境变量方式和 Volume 方式都支持热更新。

只有 Volume 挂载方式支持热更新。环境变量在容器进程创建时注入,之后是进程内存的一部分,Kubernetes 无法修改运行中进程的环境变量。更新 ConfigMap/Secret 后,使用环境变量方式的 Pod 必须重启才能获取新值。这是为什么生产环境的动态配置通常选择 Volume 挂载方式,并让应用监听文件变化。

误解三:Secret 的 base64 是加密,数据是安全的。

base64 是编码格式,不是加密算法。echo -n 's3cr3t' | base64 得到 czNjcjN0echo -n 'czNjcjN0' | base64 -d 立即还原。不需要任何密钥,任何人都能解码。Secret 和 ConfigMap 在存储格式上的唯一区别是 Secret 的 data 字段用 base64,便于存储二进制内容(证书、密钥文件)。

练习

练习一:对比注入方式的差异。创建同一个 ConfigMap,分别以环境变量和 Volume 两种方式挂载到两个 Pod。更新 ConfigMap 后,观察两个 Pod 的行为差异,测量 Volume 方式的热更新延迟(从 kubectl patch 到文件更新的时间)。

练习二:Immutable ConfigMap 的版本化管理。实践不可变配置模式:创建 app-config-v1,部署应用引用 v1。更新配置时,创建 app-config-v2,修改 Deployment 引用 v2,利用 Deployment 滚动更新实现零停机配置变更。对比 mutable ConfigMap 直接修改的风险(多个 Pod 可能在更新窗口内看到不同配置)。

练习三:ESO 集成实验。在本地安装 External Secrets Operator,使用其 Fake 提供方(用于测试)创建 ExternalSecret,观察自动同步到 Kubernetes Secret 的过程。修改 ExternalSecret 的 refreshInterval,观察同步频率变化。

系列导航

参考资料