7.4 配置与密钥 (ConfigMap / Secret)
一句话
ConfigMap / Secret 解决的是 “配置与镜像分离”,不解决 “配置改了怎么生效”:环境变量注入永不更新,卷挂载会最终更新但进程不会自己重读,
subPath挂载干脆不更新;Secret 默认只是 base64,不是加密。
三种注入方式,三种更新语义
| 方式 | 写法 | 改了配置会怎样 | 适合 |
|---|---|---|---|
| 单个环境变量 | env.valueFrom.configMapKeyRef | 永不更新(Pod 不重建就一直是旧值) | 启动参数类、不常变的配置 |
| 批量环境变量 | envFrom.configMapRef | 同上,永不更新 | 大量键、且可接受重建 |
| 卷挂载 | volumes.configMap | kubelet 周期性同步(通常 ≤ 1 分钟)后文件更新;进程不会自动重读 | 需要热加载的配置 |
卷挂载 + subPath | volumeMounts.subPath | 不更新(已知行为) | 只挂单个文件且不打算热更新 |
⚠️
subPath是最容易踩的坑:团队为了 “只挂一个 application.yaml” 用了 subPath,然后改了 ConfigMap 怎么都不生效——因为它绕过了 kubelet 的原子替换机制。
为什么环境变量永不更新
容器的环境变量在进程启动时就固定了,内核层面没有 “改环境变量” 的接口。所以任何通过 env 注入的配置,只有重建 Pod 才能生效。由此得到两条实践:
- 想热更新 → 必须走文件(或配置中心 + watch)。
- 想 “改了就滚动更新” → 把配置指纹写进 Pod 模板,主动触发滚动:
spec:
template:
metadata:
annotations:
checksum/config: "{{ include (print $.Template.BasePath \"/configmap.yaml\") . | sha256sum }}"
这行是 Helm 常用手法:ConfigMap 内容一变,注解变,Pod 模板变,Deployment 自动滚动。不引入 Helm 也能做——CI 里把配置的哈希写进 kubectl set env deploy/app CONFIG_HASH=<sha>。
Secret:三个必须知道的真相
| 真相 | 含义 | 应对 |
|---|---|---|
| base64 不是加密 | kubectl get secret -o yaml 直接可解 | 把 Secret 当敏感数据管理:RBAC 限制 get/list、不提交进 git |
| 默认明文存 etcd | 能读 etcd 快照/备份的人就能读到全部密钥 | 开 EncryptionConfiguration(aescbc 或 KMS),至少对 secrets 资源加密 |
| env 注入会泄露到进程环境 | /proc/1/environ、崩溃转储、子进程都能看到 | 优先用卷挂载成文件(权限 0400),只有确实需要环境变量时才用 env |
更进一步的方案按成熟度排:
| 方案 | 做法 | 代价 |
|---|---|---|
| 原生 Secret + 加密存储 | 开 etcd 加密 + 严 RBAC | 最低,但密钥仍随集群走 |
| Sealed Secrets / SOPS | 加密后进 git,集群内解密 | 需要管理解密密钥 |
| External Secrets Operator | 从 Vault/KMS/云密钥服务同步 | 多一个组件,但审计与轮换最规范 |
| CSI Secret Store | 把密钥以卷的形式直接挂进 Pod | 配置复杂,适合强合规场景 |
密钥管理的通用原则(轮换、最小权限、审计)见 13.4 密钥与凭证管理。
命名与版本化:用 “新名字” 换 “可回滚”
| 做法 | 效果 |
|---|---|
app-config-v3(版本写进名字) | 改配置 = 换引用 = 触发滚动 = 可回滚;旧版本留着便于回退 |
app-config(固定名字,原地改) | 改配置不触发滚动(env 场景完全不生效),且不可回滚 |
immutable: true | 禁止原地修改,强制走 “换名字” 流程;还能减轻 apiserver 与 kubelet 的 watch 压力 |
对于 “必须原地改” 的共享配置(如全局开关),正确姿势是配置中心(48.1 配置中心)+ 客户端 watch,而不是靠 ConfigMap 卷同步。配置注入的制品侧做法见 54.5 配置注入。
反模式
| 反模式 | 后果 |
|---|---|
| 把 Secret 的 YAML(base64)提交进 git | 等于明文泄露,且历史里永久留存 |
用 subPath 挂配置却期待热更新 | 改了永远不生效,排查耗时以天计 |
| 改 ConfigMap 后等应用自动生效(env 注入) | 永不生效,误判为 “应用有 bug” |
| 所有环境共用一个 ConfigMap | 改测试环境把生产带崩 |
| 不给 Secret 配 RBAC 最小权限 | 任何有 get secrets 的人可读全集群密钥 |
| 用 env 注入大段配置 | 启动慢、/proc 暴露、改一次要重启 |
| 配置格式错误直接上线 | 应用启动即崩;应加启动校验(schema/必填项) |
三条铁律
🔑 env 注入的配置永不更新;要热更新只能走文件或配置中心。
🔑
subPath不更新;需要热加载就别用 subPath。
🔑 Secret 是 “访问受控的明文”,不是加密;加密要在 etcd 层或外部密钥系统做。
自检
Q1 改了 ConfigMap,Pod 里的 /etc/app/conf.yaml 一分钟内变了,但应用行为没变。为什么?
▸ 答案:因为进程不会自动重读配置文件。kubelet 只负责把文件更新到卷里(原子替换 symlink),是否生效取决于应用:它可能只在启动时读一次。解法三选一:应用监听文件变化(inotify/SIGHUP)、加 sidecar 在变更时发信号、或用配置中心 + watch 主动推送。
Q2 为什么用 subPath 挂载的 ConfigMap 不会更新?
▸ 答案:因为 subPath 是把单个文件bind-mount 进容器,kubelet 的 “原子替换整个目录 symlink” 机制对它无效。这是实现上的取舍(不是 bug)。需要热更新时去掉 subPath,改为挂整个目录;只想挂一个文件又必须热更新,就用配置中心或 sidecar。
Q3 团队说 “我们 Secret 用了 base64,是加密的”。错在哪?
▸ 答案:base64 只是编码,任何人 base64 -d 就能还原。真实风险点有三处:etcd 里默认明文(未开 EncryptionConfiguration)、kubectl get secret -o yaml 有权限就能读、以及 env 注入后出现在 /proc/1/environ。正确做法是 “加密存储 + 最小 RBAC + 优先文件挂载”,并考虑外部密钥系统。
Q4 为什么说 “改配置触发滚动更新” 要靠注解哈希,而不是靠 kubectl apply?
▸ 答案:因为 kubectl apply 只更新 ConfigMap 对象,不会改 Deployment 的 Pod 模板——模板没变,ReplicaSet 不变,Pod 不重建,env 注入的配置自然不生效。把配置内容的哈希写进 spec.template.metadata.annotations,配置一变哈希变,模板变,才触发滚动;同时这个哈希让 “这次滚动对应哪版配置” 可追溯。
Q5 一个 Secret 需要被 40 个服务读取,怎么给权限?
▸ 答案:不要给 40 个 ServiceAccount 都授 get secrets(等于全集群可读)。三种做法:① 把 Secret 复制到各命名空间(分散风险,但要管轮换);② 用 External Secrets Operator,让每个命名空间只同步自己那份,源在 Vault/KMS 并留审计;③ 用 CSI Secret Store 以卷挂载,权限按 Pod 粒度收窄。选择依据是 “轮换频率 × 合规要求”。
跨篇相关:配置中心的 watch 与推送见 48.1 配置中心;制品侧的环境一致性与注入见 54.4 环境一致性、54.5 配置注入;滚动更新的触发与回滚见 7.6 自愈、滚动更新与回滚;密钥的轮换与审计见 13.4 密钥与凭证管理;Secret 的 etcd 加密属于集群侧安全基线,见 13.5 系统基线加固。