Repository Reading Site
第七课:ConfigMap 与 Secret 注入模型、更新机制与安全边界
上一课我们解决的是: 这一课继续往下,解决的是另外一个在生产中几乎天天会碰到的问题: 你以后做项目架构、平台治理、故障排查,绕不开三类对象: 而 Kubernetes 对这三类问题的答案,核心就是: 如果上一课讲的是: 这一课讲的就是: --- 很多人学习 ConfigMap / Secret 时最大的问题,是把“对象更新”和“应用生效”混成一件事。 其实至
第七课:ConfigMap 与 Secret 注入模型、更新机制与安全边界
为什么这一课必须接在身份与权限后面
上一课我们解决的是:
- Pod 代表谁
- ServiceAccount 是什么
- RBAC 怎么授权
- API 请求为什么会被拒绝
这一课继续往下,解决的是另外一个在生产中几乎天天会碰到的问题:
配置和凭据到底怎么进入 Pod,为什么改了之后有的会生效,有的不会生效?
你以后做项目架构、平台治理、故障排查,绕不开三类对象:
- 业务配置
- 敏感凭据
- 配置更新后的传播路径
而 Kubernetes 对这三类问题的答案,核心就是:
ConfigMapSecret- 多种注入方式
如果上一课讲的是:
“谁有资格访问系统”
这一课讲的就是:
“系统运行时到底吃的是什么配置,以及这些配置什么时候会变”
先给你一个专家视角:配置传播其实有三层时钟
很多人学习 ConfigMap / Secret 时最大的问题,是把“对象更新”和“应用生效”混成一件事。
其实至少有三层时钟:
第一层:API 对象时钟
也就是:
- 你执行
kubectl apply - 或
kubectl patch - API Server 里的对象内容立刻改变
这一层很快。
第二层:kubelet 投影时钟
如果你把 ConfigMap / Secret 挂载成 volume:
- kubelet 需要把新内容同步到节点
- 再通过原子切换方式映射进容器
这不是实时推送,往往有同步延迟。
第三层:应用进程时钟
即使文件已经更新了,应用也不一定马上“感知”。
原因可能是:
- 应用只在启动时读一次
- 应用把文件内容缓存到内存里
- 应用一直拿着旧文件描述符
- 应用根本没有 reload 机制
所以真正的专家视角不是:
“我改了 ConfigMap,为什么 Pod 还没变?”
而是:
“到底是 API 对象没变、kubelet 还没同步、还是应用进程没重新读取?”
这三层一旦分清,很多线上疑难杂症都会变得非常直接。
ConfigMap 和 Secret 的本质区别
ConfigMap:非敏感配置
用于保存:
- 业务开关
- 日志级别
- 普通配置文件
- 服务地址
核心理念是:
镜像不变,配置外置。
Secret:敏感信息
用于保存:
- 密码
- Token
- Access Key
- 证书与私钥
但你必须牢记:
Secret 默认不是“加密保险箱”,而是“受控暴露的敏感对象”。
它的安全性主要依赖:
- RBAC
- TLS 传输
- etcd 加密配置
- 节点上的挂载方式
不是依赖:
- base64
base64 只是编码,不是加密。
这次我不是只讲语法,而是做了四种注入方式并行对照
本轮实验全部放在:
config-lab
实验文件在:
我创建了:
ConfigMap/app-configSecret/app-secretConfigMap/immutable-configPod/env-demoPod/volume-demoPod/subpath-demoPod/projected-demo
之所以这样设计,是因为我想把四种常见注入模型一次讲透:
env-demo- 通过环境变量注入 ConfigMap / Secret
volume-demo- 分别把 ConfigMap / Secret 挂载成目录
subpath-demo- 把 ConfigMap 的单个 key 用
subPath挂成单文件
- 把 ConfigMap 的单个 key 用
projected-demo- 把 ConfigMap 和 Secret 合并成一个 projected volume
再加一个:
immutable-config- 演示不可变 ConfigMap
这不是为了花哨,而是为了让你彻底建立:
注入方式决定运行时行为。
先看我们注入了什么
app-config
初始内容是:
APP_MODE=blueLOG_LEVEL=infoFEATURE_FLAG=falseapp.yaml
app-secret
初始内容是:
USERNAME=lab-userPASSWORD=InitialPass123API_TOKEN=token-v1
immutable-config
内容不重要,关键在于:
immutable: true
它的作用不是“更灵活”,而是明确告诉系统:
这份配置创建后就不允许修改。
实验一:环境变量注入,本质是“启动时快照”
env-demo 把 ConfigMap 和 Secret 以环境变量形式注入:
APP_MODELOG_LEVELPASSWORDAPI_TOKEN
初始结果
Pod 启动后我在容器里看到:
APP_MODE=blueLOG_LEVEL=infoPASSWORD=InitialPass123API_TOKEN=token-v1
然后我修改了对象
把 ConfigMap/app-config 改成:
APP_MODE=greenLOG_LEVEL=debug
把 Secret/app-secret 改成:
PASSWORD=RotatedPass456API_TOKEN=token-v2
真实结果
对象在 API Server 里已经变了,但 env-demo 中环境变量还是旧值:
APP_MODE=blueLOG_LEVEL=infoPASSWORD=InitialPass123API_TOKEN=token-v1
为什么会这样
因为环境变量的注入时机是:
- 容器进程启动之前
容器一旦启动,环境变量就成了进程内存的一部分。
后续即使你修改:
- ConfigMap
- Secret
已经启动的进程也不会自动重读这部分值。
这意味着什么
如果你的应用配置是通过环境变量注入的,那么改配置后要想生效,通常需要:
- 重启 Pod
- 或者做滚动更新
这不是 Kubernetes “不智能”,而是环境变量这个注入模型天然如此。
实验二:ConfigMap volume 挂载,文件会更新,但不是实时秒变
volume-demo 同时挂载了:
/etc/config←ConfigMap/app-config/etc/secret←Secret/app-secret
初始结果
我看到目录结构是:
/etc/config/APP_MODE/etc/config/LOG_LEVEL/etc/config/FEATURE_FLAG/etc/config/app.yaml/etc/secret/USERNAME/etc/secret/PASSWORD/etc/secret/API_TOKEN
而且这些文件并不是普通静态文件,它们都指向:
..data/<key>
例如:
APP_MODE -> ..data/APP_MODEPASSWORD -> ..data/PASSWORD
这就是 Kubernetes 做原子更新的关键实现。
修改 ConfigMap 后发生了什么
我把 APP_MODE 从 blue 改成 green 后,开始持续观察 /etc/config/APP_MODE。
真实现象不是秒变,而是:
- 连续一段时间仍然是
blue - 大约 65 秒后变成
green
这说明什么
说明 kubelet 不会在 API 对象变化后立刻把新文件推到容器里。
它有自己的:
- 同步周期
- 缓存窗口
所以你在生产中看到:
kubectl patch成功- 容器内文件 30 秒、60 秒甚至更久后才变化
这是正常现象。
但有一个更深的点
文件更新不等于应用已经生效。
因为很多进程会:
- 启动时读一次
- 之后只用内存缓存
或者:
- 持续持有旧文件描述符
这就是为什么某些应用明明挂载的是 ConfigMap volume,文件已经变了,但业务行为还是旧的。
根因不一定是 Kubernetes,可能是应用自己没有 reload。
原理深挖:Kubernetes 是怎么做到“原子更新”的
这一部分很重要,是专家和普通使用者的分水岭。
我在 projected volume 里看到了什么
projected-demo 目录长这样:
..2026_04_09_18_31_36.xxxxx..data -> ..2026_04_09_18_31_36.xxxxxconfig -> ..data/configsecret -> ..data/secret
ConfigMap 更新后,又变成:
..2026_04_09_18_34_39.xxxxx..data -> ..2026_04_09_18_34_39.xxxxx
这说明 Kubernetes 的更新方式不是“原地改文件”
而是:
- 先写出一套全新的版本目录
- 再把
..data这个符号链接原子切换过去 - 用户看到的文件路径通过符号链接自然跟着切换
这就是为什么它可以做到:
- 更新时尽量避免半写入状态
- 目录整体一致性更好
这又带来一个实战含义
如果应用每次都是重新打开文件读取,那么它更容易看到新版本。
如果应用长期持有旧文件描述符,可能仍然盯着旧 inode。
这就是很多“文件明明更新了,程序却没更新”的真正底层原因。
实验三:subPath 单文件挂载,是最容易踩坑的地方
subpath-demo 把:
ConfigMap/app-config中的app.yaml
通过:
subPath: app.yaml
挂载到了:
/etc/app/app.yaml
初始结果
文件内容是:
mode: bluelogLevel: infofeatureFlag: false
ConfigMap 更新后呢
即使 app-config 已经改成:
greendebugtrue
subpath-demo 里的 /etc/app/app.yaml 仍然保持旧内容:
blueinfofalse
为什么 subPath 不会热更新
因为 subPath 不是把整个 ConfigMap 目录暴露给容器,而是把某个具体文件路径绑定进容器。
换句话说,它失去了上面那套:
..data- 符号链接整体切换
- 原子目录版本切换
的能力。
它更接近:
“把某个具体文件实体塞到这个路径上。”
所以后续 ConfigMap 新版本出来时,这个文件不会自动跟着换。
这在生产里为什么危险
因为很多人喜欢这样写:
volumeMounts:
- mountPath: /app/config.yaml
name: config
subPath: config.yaml
看起来非常方便。
但代价就是:
改 ConfigMap 后,运行中的容器很可能不会热更新。
这就是一个非常经典的“看起来优雅,实际埋坑”的设计。
实验四:projected volume 不是只给 ServiceAccount 用的
很多人因为上一课看到 ServiceAccount token 是 projected volume,就以为 projected 只和身份有关。
其实不是。
projected-demo 证明了:
- ConfigMap
- Secret
也可以被组合进同一个 projected volume。
我在这个实验里做了:
/etc/projected/config/APP_MODE/etc/projected/config/app.yaml/etc/projected/secret/USERNAME/etc/projected/secret/PASSWORD
这有什么价值
它适合那种场景:
- 应用希望从一个根目录拿到完整配置包
- 其中既有普通配置
- 又有敏感凭据
它的更新行为如何
和普通 volume 一样,它也会随 kubelet 同步而更新,但同样不是秒级立即生效。
这次实验里我们观察到:
- ConfigMap 部分大约 70 秒左右切换
- Secret 部分大约几十秒后切换
不要把这个时间当成固定常数,但要记住:
projected volume 也遵循 kubelet 同步与原子切换模型。
Secret 的两个关键认知:base64 不是加密,挂载到容器里是明文文件
认知一:base64 不是加密
我直接从 API 里拿到了:
PASSWORDAPI_TOKEN
的 base64 值,然后用:
base64 -d
就解回了明文。
这说明:
Secret 在 API 表示层的 base64,只是为了序列化与传输,不是为了保密。
所以你不能说:
- “放在 Secret 里就加密了”
这是错的。
认知二:挂载进容器后,本质就是可读文件
在 volume-demo 里,我直接:
cat /etc/secret/PASSWORD
就看到了明文。
这也非常正常。
因为应用最终总得能读到它要用的密码。
真正的安全边界不是“运行中的容器永远看不到明文”,而是:
- 谁能读这个 Secret 对象
- 谁能 exec 进这个 Pod
- 节点如何承载 Secret 挂载
- 凭据是否最小化
- 是否有轮换机制
Secret volume 为什么比 ConfigMap volume 更敏感
这次实验里,我专门看了 volume-demo 的挂载信息:
/etc/config挂载显示为ext4/etc/secret挂载显示为tmpfs
这说明什么
说明在这套集群里:
- Secret volume 是内存文件系统
- ConfigMap volume 则不是同样的表现
这与 Kubernetes 对 Secret 的安全处理预期是一致的:
Secret 应尽量避免落到普通磁盘路径。
但这不意味着 Secret 就绝对安全
因为:
- 只要容器里能读
- 进入容器的人也能读
- 应用日志打出来也会泄漏
- 崩溃转储也可能带出
所以 Secret 的安全问题,本质还是:
- 最小权限
- 最小暴露
- 最小生命周期
实验五:immutable ConfigMap 会直接拒绝修改
我对:
immutable-config
执行 patch,真实返回是:
data: Forbidden: field is immutable when `immutable` is set
这说明什么
一旦设置:
immutable: true
这个 ConfigMap 的数据字段就不允许再变。
为什么生产里有人会用它
因为在大规模集群中,不可变配置有几个好处:
- 减少 watch / 同步开销
- 明确配置生命周期
- 防止误修改
代价是什么
代价是:
- 不能原地改
- 需要新建新对象或换版本名
这很像镜像版本管理的思维:
不修改旧版本,只发布新版本。
实验六:环境变量要拿到新值,必须重建 Pod
为了把“环境变量是启动时快照”讲透,我做了最后一个闭环:
- 先修改
ConfigMap和Secret - 观察
env-demo里的变量仍然是旧值 - 删除
env-demo - 重新 apply 同一个 Pod 清单
- 再次读取环境变量
真实结果
重建后的 env-demo 终于看到新值:
APP_MODE=greenLOG_LEVEL=debugPASSWORD=RotatedPass456API_TOKEN=token-v2
你必须把这件事和生产模式连起来
真实生产里很少直接跑裸 Pod,更常见的是:
- Deployment
- StatefulSet
所以常见做法是:
- 改 ConfigMap / Secret
- 再做
rollout restart
如果是 Helm Chart,常常会用:
- checksum 注解
让模板变化自动触发滚动更新。
真实集群里的两个案例,比实验更重要
案例一:aiforge-gateway 使用 ConfigMap + subPath
我检查了真实部署,看到:
aiforge-gateway把aiforge-config挂载到/app/config.yaml- 使用的是:
subPath: config.yaml
这意味着:
它天然不具备普通 ConfigMap 目录挂载那种热更新语义。
如果你以后改这个 ConfigMap,运行中的网关进程不应被想当然地认为会自动读到新内容。
更严重的点:它还是一个真实反例
aiforge-config 这个 ConfigMap 中,当前包含了敏感字段类别,例如:
database.passwordstorage.access_keystorage.secret_keyauth.jwt_secret
这在设计上是不合适的。
因为这些内容更应该放到:
- Secret
而不是:
- ConfigMap
这正是你以后做架构审查时要有的敏感度:
不是所有“配置”都应该进 ConfigMap,带凭据属性的配置应该进入 Secret。
案例二:monitoring-grafana 的工程化做法更成熟
我检查 Grafana Deployment 时看到三件很关键的事:
- 管理员账号密码通过
secretKeyRef注入环境变量 grafana.ini和某些 provider 文件通过subPath挂载- Pod 模板带有:
checksum/configchecksum/secret
这三件事合在一起说明什么?
说明它的作者已经知道:
subPath和 env 注入都不适合指望运行中热更新- 所以用模板 checksum 变化来强制触发滚动更新
这就是生产工程上非常成熟的一种处理方式。
你以后做 Helm / GitOps 平台时,这个模式几乎是必修课。
这节课最重要的调试命令
看对象本身有没有变
kubectl -n config-lab get configmap app-config -o yaml
kubectl -n config-lab get secret app-secret -o yaml
解码 Secret 某个字段
kubectl -n config-lab get secret app-secret -o jsonpath='{.data.PASSWORD}' | base64 -d
看环境变量注入结果
kubectl -n config-lab exec env-demo -- sh -c 'printenv APP_MODE LOG_LEVEL PASSWORD API_TOKEN'
看 volume 中真实文件内容
kubectl -n config-lab exec volume-demo -- cat /etc/config/app.yaml
kubectl -n config-lab exec volume-demo -- cat /etc/secret/PASSWORD
看是不是 ..data 原子切换模型
kubectl -n config-lab exec projected-demo -- ls -la /etc/projected
看挂载类型
kubectl -n config-lab exec volume-demo -- cat /proc/mounts | grep '/etc/config\|/etc/secret'
检查 subPath 文件是否仍停留在旧值
kubectl -n config-lab exec subpath-demo -- cat /etc/app/app.yaml
修改对象做实验
kubectl -n config-lab patch configmap app-config --type merge -p '...'
kubectl -n config-lab patch secret app-secret --type merge -p '...'
观察 env 是否需要重建
kubectl -n config-lab delete pod env-demo
kubectl apply -f manifests/07-config-secret/20-env-demo.yaml
这节课你必须真正记住的结论
结论 1
ConfigMap 和 Secret 的区别,不只是“一个放普通配置,一个放敏感配置”。
更重要的是:
- 你要知道它们如何进入 Pod
- 以及更新后如何传播
结论 2
环境变量注入是启动时快照。
改对象后:
- 已运行进程不会自动变
结论 3
普通 volume / projected volume 能热更新文件,但依赖 kubelet 同步周期,不是秒变。
结论 4
subPath 是最常见的热更新陷阱。
把单文件挂进去很方便,但通常失去动态更新能力。
结论 5
Secret 的 base64 不是加密。
真正的安全边界在:
- RBAC
- 传输加密
- etcd 加密
- 最小暴露
结论 6
Secret 挂载到容器里后,本质就是应用可读的明文文件,所以应用侧和运维侧都要有最小暴露意识。
结论 7
工程上如果配置通过 env 或 subPath 注入,常见的正确方案不是“等它自动变”,而是:
- 明确重启
- 滚动更新
- checksum 注解触发 rollout
你现在应该能回答的高级问题
学完这一课,你至少应该能独立回答下面这些问题:
- 为什么
kubectl patch configmap成功了,但应用行为还没变? - 为什么 volume 里的文件会更新,但环境变量不更新?
- 为什么
subPath是配置热更新里的高频坑? - 为什么 Secret 不能因为用了 base64 就被当成“已经加密”?
- 为什么很多 Helm Chart 要用
checksum/config和checksum/secret? - 什么时候该用 ConfigMap,什么时候必须用 Secret?
- 遇到“改了配置不生效”,你应该先查 API 对象、再查 kubelet 投影、最后查应用 reload 机制,而不是一上来瞎重启。
如果这些问题你能讲清楚,那么你对 Kubernetes 配置体系的理解,就已经从“会写 YAML”提升到“能解释运行时系统”。
下一轮最自然的延伸会是:
- 存储与数据持久化
- 为什么容器重建后数据会不会丢
- PVC / PV / StorageClass / NFS / 本地盘
- 配置、凭据、数据三者在架构上怎么分层