K8s Lab 把当前仓库文档整理成一个可阅读的网页站点

Repository Reading Site

第七课:ConfigMap 与 Secret 注入模型、更新机制与安全边界

上一课我们解决的是: 这一课继续往下,解决的是另外一个在生产中几乎天天会碰到的问题: 你以后做项目架构、平台治理、故障排查,绕不开三类对象: 而 Kubernetes 对这三类问题的答案,核心就是: 如果上一课讲的是: 这一课讲的就是: --- 很多人学习 ConfigMap / Secret 时最大的问题,是把“对象更新”和“应用生效”混成一件事。 其实至

Markdown07-第七课-ConfigMap与Secret注入模型更新机制与安全边界.md2026年4月9日 18:43

第七课:ConfigMap 与 Secret 注入模型、更新机制与安全边界

为什么这一课必须接在身份与权限后面

上一课我们解决的是:

  • Pod 代表谁
  • ServiceAccount 是什么
  • RBAC 怎么授权
  • API 请求为什么会被拒绝

这一课继续往下,解决的是另外一个在生产中几乎天天会碰到的问题:

配置和凭据到底怎么进入 Pod,为什么改了之后有的会生效,有的不会生效?

你以后做项目架构、平台治理、故障排查,绕不开三类对象:

  • 业务配置
  • 敏感凭据
  • 配置更新后的传播路径

而 Kubernetes 对这三类问题的答案,核心就是:

  • ConfigMap
  • Secret
  • 多种注入方式

如果上一课讲的是:

“谁有资格访问系统”

这一课讲的就是:

“系统运行时到底吃的是什么配置,以及这些配置什么时候会变”


先给你一个专家视角:配置传播其实有三层时钟

很多人学习 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-config
  • Secret/app-secret
  • ConfigMap/immutable-config
  • Pod/env-demo
  • Pod/volume-demo
  • Pod/subpath-demo
  • Pod/projected-demo

之所以这样设计,是因为我想把四种常见注入模型一次讲透:

  1. env-demo
    • 通过环境变量注入 ConfigMap / Secret
  2. volume-demo
    • 分别把 ConfigMap / Secret 挂载成目录
  3. subpath-demo
    • 把 ConfigMap 的单个 key 用 subPath 挂成单文件
  4. projected-demo
    • 把 ConfigMap 和 Secret 合并成一个 projected volume

再加一个:

  1. immutable-config
    • 演示不可变 ConfigMap

这不是为了花哨,而是为了让你彻底建立:

注入方式决定运行时行为。


先看我们注入了什么

app-config

初始内容是:

  • APP_MODE=blue
  • LOG_LEVEL=info
  • FEATURE_FLAG=false
  • app.yaml

app-secret

初始内容是:

  • USERNAME=lab-user
  • PASSWORD=InitialPass123
  • API_TOKEN=token-v1

immutable-config

内容不重要,关键在于:

  • immutable: true

它的作用不是“更灵活”,而是明确告诉系统:

这份配置创建后就不允许修改。


实验一:环境变量注入,本质是“启动时快照”

env-demo 把 ConfigMap 和 Secret 以环境变量形式注入:

  • APP_MODE
  • LOG_LEVEL
  • PASSWORD
  • API_TOKEN

初始结果

Pod 启动后我在容器里看到:

  • APP_MODE=blue
  • LOG_LEVEL=info
  • PASSWORD=InitialPass123
  • API_TOKEN=token-v1

然后我修改了对象

ConfigMap/app-config 改成:

  • APP_MODE=green
  • LOG_LEVEL=debug

Secret/app-secret 改成:

  • PASSWORD=RotatedPass456
  • API_TOKEN=token-v2

真实结果

对象在 API Server 里已经变了,但 env-demo 中环境变量还是旧值:

  • APP_MODE=blue
  • LOG_LEVEL=info
  • PASSWORD=InitialPass123
  • API_TOKEN=token-v1

为什么会这样

因为环境变量的注入时机是:

  • 容器进程启动之前

容器一旦启动,环境变量就成了进程内存的一部分。

后续即使你修改:

  • ConfigMap
  • Secret

已经启动的进程也不会自动重读这部分值。

这意味着什么

如果你的应用配置是通过环境变量注入的,那么改配置后要想生效,通常需要:

  • 重启 Pod
  • 或者做滚动更新

这不是 Kubernetes “不智能”,而是环境变量这个注入模型天然如此。


实验二:ConfigMap volume 挂载,文件会更新,但不是实时秒变

volume-demo 同时挂载了:

  • /etc/configConfigMap/app-config
  • /etc/secretSecret/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_MODE
  • PASSWORD -> ..data/PASSWORD

这就是 Kubernetes 做原子更新的关键实现。

修改 ConfigMap 后发生了什么

我把 APP_MODEblue 改成 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.xxxxx
  • config -> ..data/config
  • secret -> ..data/secret

ConfigMap 更新后,又变成:

  • ..2026_04_09_18_34_39.xxxxx
  • ..data -> ..2026_04_09_18_34_39.xxxxx

这说明 Kubernetes 的更新方式不是“原地改文件”

而是:

  1. 先写出一套全新的版本目录
  2. 再把 ..data 这个符号链接原子切换过去
  3. 用户看到的文件路径通过符号链接自然跟着切换

这就是为什么它可以做到:

  • 更新时尽量避免半写入状态
  • 目录整体一致性更好

这又带来一个实战含义

如果应用每次都是重新打开文件读取,那么它更容易看到新版本。

如果应用长期持有旧文件描述符,可能仍然盯着旧 inode。

这就是很多“文件明明更新了,程序却没更新”的真正底层原因。


实验三:subPath 单文件挂载,是最容易踩坑的地方

subpath-demo 把:

  • ConfigMap/app-config 中的 app.yaml

通过:

  • subPath: app.yaml

挂载到了:

  • /etc/app/app.yaml

初始结果

文件内容是:

  • mode: blue
  • logLevel: info
  • featureFlag: false

ConfigMap 更新后呢

即使 app-config 已经改成:

  • green
  • debug
  • true

subpath-demo 里的 /etc/app/app.yaml 仍然保持旧内容:

  • blue
  • info
  • false

为什么 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 里拿到了:

  • PASSWORD
  • API_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

为了把“环境变量是启动时快照”讲透,我做了最后一个闭环:

  1. 先修改 ConfigMapSecret
  2. 观察 env-demo 里的变量仍然是旧值
  3. 删除 env-demo
  4. 重新 apply 同一个 Pod 清单
  5. 再次读取环境变量

真实结果

重建后的 env-demo 终于看到新值:

  • APP_MODE=green
  • LOG_LEVEL=debug
  • PASSWORD=RotatedPass456
  • API_TOKEN=token-v2

你必须把这件事和生产模式连起来

真实生产里很少直接跑裸 Pod,更常见的是:

  • Deployment
  • StatefulSet

所以常见做法是:

  • 改 ConfigMap / Secret
  • 再做 rollout restart

如果是 Helm Chart,常常会用:

  • checksum 注解

让模板变化自动触发滚动更新。


真实集群里的两个案例,比实验更重要

案例一:aiforge-gateway 使用 ConfigMap + subPath

我检查了真实部署,看到:

  • aiforge-gatewayaiforge-config 挂载到 /app/config.yaml
  • 使用的是:
    • subPath: config.yaml

这意味着:

它天然不具备普通 ConfigMap 目录挂载那种热更新语义。

如果你以后改这个 ConfigMap,运行中的网关进程不应被想当然地认为会自动读到新内容。

更严重的点:它还是一个真实反例

aiforge-config 这个 ConfigMap 中,当前包含了敏感字段类别,例如:

  • database.password
  • storage.access_key
  • storage.secret_key
  • auth.jwt_secret

这在设计上是不合适的。

因为这些内容更应该放到:

  • Secret

而不是:

  • ConfigMap

这正是你以后做架构审查时要有的敏感度:

不是所有“配置”都应该进 ConfigMap,带凭据属性的配置应该进入 Secret。

案例二:monitoring-grafana 的工程化做法更成熟

我检查 Grafana Deployment 时看到三件很关键的事:

  1. 管理员账号密码通过 secretKeyRef 注入环境变量
  2. grafana.ini 和某些 provider 文件通过 subPath 挂载
  3. Pod 模板带有:
    • checksum/config
    • checksum/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

ConfigMapSecret 的区别,不只是“一个放普通配置,一个放敏感配置”。

更重要的是:

  • 你要知道它们如何进入 Pod
  • 以及更新后如何传播

结论 2

环境变量注入是启动时快照。

改对象后:

  • 已运行进程不会自动变

结论 3

普通 volume / projected volume 能热更新文件,但依赖 kubelet 同步周期,不是秒变。

结论 4

subPath 是最常见的热更新陷阱。

把单文件挂进去很方便,但通常失去动态更新能力。

结论 5

Secret 的 base64 不是加密。

真正的安全边界在:

  • RBAC
  • 传输加密
  • etcd 加密
  • 最小暴露

结论 6

Secret 挂载到容器里后,本质就是应用可读的明文文件,所以应用侧和运维侧都要有最小暴露意识。

结论 7

工程上如果配置通过 env 或 subPath 注入,常见的正确方案不是“等它自动变”,而是:

  • 明确重启
  • 滚动更新
  • checksum 注解触发 rollout

你现在应该能回答的高级问题

学完这一课,你至少应该能独立回答下面这些问题:

  1. 为什么 kubectl patch configmap 成功了,但应用行为还没变?
  2. 为什么 volume 里的文件会更新,但环境变量不更新?
  3. 为什么 subPath 是配置热更新里的高频坑?
  4. 为什么 Secret 不能因为用了 base64 就被当成“已经加密”?
  5. 为什么很多 Helm Chart 要用 checksum/configchecksum/secret
  6. 什么时候该用 ConfigMap,什么时候必须用 Secret?
  7. 遇到“改了配置不生效”,你应该先查 API 对象、再查 kubelet 投影、最后查应用 reload 机制,而不是一上来瞎重启。

如果这些问题你能讲清楚,那么你对 Kubernetes 配置体系的理解,就已经从“会写 YAML”提升到“能解释运行时系统”。

下一轮最自然的延伸会是:

  • 存储与数据持久化
  • 为什么容器重建后数据会不会丢
  • PVC / PV / StorageClass / NFS / 本地盘
  • 配置、凭据、数据三者在架构上怎么分层