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 系统基线加固。