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

Repository Reading Site

本轮操作记录:ConfigMap 与 Secret 注入、更新与安全边界实验

这一轮的目标不是简单演示: 而是要把更关键的东西讲透: 1. ConfigMap / Secret 有哪些运行时注入方式 2. 为什么同样是“改配置”,有的容器会变,有的不会变 3. kubelet 到底是怎么把配置同步进容器的 4. Secret 为什么是敏感对象,但又为什么不能被神化成“天然加密保险箱” 5. 真实业务里常见的好模式和坏模式分别是什么 -

Markdown07-操作记录-ConfigMap与Secret实验.md2026年4月9日 18:43

本轮操作记录:ConfigMap 与 Secret 注入、更新与安全边界实验

本轮目标

这一轮的目标不是简单演示:

  • 怎么 kubectl create configmap
  • 怎么 kubectl create secret

而是要把更关键的东西讲透:

  1. ConfigMap / Secret 有哪些运行时注入方式
  2. 为什么同样是“改配置”,有的容器会变,有的不会变
  3. kubelet 到底是怎么把配置同步进容器的
  4. Secret 为什么是敏感对象,但又为什么不能被神化成“天然加密保险箱”
  5. 真实业务里常见的好模式和坏模式分别是什么

Step 1:先看现有资料与集群现状

实际命令

sed -n '1,260p' phase-1/02-configmap-secret.md

KUBECONFIG=~/.kube/config-k8s-lab kubectl get configmap,secret -A | sed -n '1,220p'

KUBECONFIG=~/.kube/config-k8s-lab kubectl api-resources | rg 'configmaps|secrets'

为什么先做这一步

因为我要先确认两件事:

  1. 仓库里现有材料讲到了什么程度
  2. 真实集群里已经有哪些 ConfigMap / Secret 在被使用

我看到的结果

集群里已经有大量真实对象,例如:

  • argocd-cm
  • harbor-core
  • monitoring-grafana
  • gitea-*
  • 各 namespace 的 kube-root-ca.crt

Secret 也同样很多,例如:

  • argocd-secret
  • monitoring-grafana
  • gitea-postgresql
  • harbor-database

原理解释

这说明:

  • ConfigMap / Secret 不是孤立知识点
  • 它们几乎遍布每个真实系统组件

因此这轮课程必须建立在真实运行环境上,而不是只讲假想例子。


Step 2:回看仓库里已有练习对象

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n dev get configmap app-config -o yaml | sed -n '1,200p'
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n dev get secret db-creds -o yaml | sed -n '1,200p'
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n dev get secret db-creds -o jsonpath='{.data.password}' | base64 -d

为什么要看 dev 里的对象

因为仓库早前已经创建过:

  • ConfigMap/app-config
  • Secret/db-creds

它们是本轮实验的一个历史基线。

我看到的结果

db-credspassword 字段在 YAML 里是:

  • base64 字符串

base64 -d 后直接得到:

  • SuperSecret123

原理解释

这一步是为了在课程一开始就打掉一个错误认知:

Secret 的 base64 不是加密,只是编码。


Step 3:设计本轮实验对象

我创建了哪些文件

路径:

对象包括:

  • Namespace/config-lab
  • ConfigMap/app-config
  • Secret/app-secret
  • ConfigMap/immutable-config
  • Pod/env-demo
  • Pod/volume-demo
  • Pod/subpath-demo
  • Pod/projected-demo

为什么这样设计

这不是多建几个 Pod 而已,而是为了做四种注入模型并行对照:

  1. 环境变量
  2. 普通 volume
  3. subPath
  4. projected volume

再加一个不可变 ConfigMap 做对照。

这一步体现的工程原则

做实验不能只看“功能能不能实现”,要把最容易混淆的模型并排摆出来。


Step 4:apply 实验清单

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl apply -f manifests/07-config-secret

为什么直接按目录 apply

因为这一轮对象之间有明确关联:

  • Pod 依赖 ConfigMap / Secret
  • 命名空间隔离实验环境

目录级清单更适合:

  • 复现
  • 审核
  • 后续回放

实际结果

成功创建:

  • config-lab
  • app-config
  • app-secret
  • immutable-config
  • 四个实验 Pod

Step 5:等待 Pod Ready,确认实验环境成立

实际命令

kubectl -n config-lab wait --for=condition=Ready pod/env-demo --timeout=180s
kubectl -n config-lab wait --for=condition=Ready pod/volume-demo --timeout=180s
kubectl -n config-lab wait --for=condition=Ready pod/subpath-demo --timeout=180s
kubectl -n config-lab wait --for=condition=Ready pod/projected-demo --timeout=180s
kubectl -n config-lab get pod -o wide

为什么这一步不能省

因为如果 Pod 没起来,后续任何“配置不更新”的结论都可能是伪结论。

你必须先确认:

  • 镜像正常
  • 挂载正常
  • Pod 真的在运行

我确认到的结果

四个 Pod 都成功进入 Running / Ready


Step 6:先采集初始基线

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab exec env-demo -- \
  sh -c 'echo APP_MODE=$APP_MODE; echo LOG_LEVEL=$LOG_LEVEL; echo PASSWORD=$PASSWORD; echo API_TOKEN=$API_TOKEN'

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab exec volume-demo -- \
  sh -c 'echo "[config dir]"; ls -l /etc/config; echo; echo "[secret dir]"; ls -l /etc/secret; echo; echo "APP_MODE file:"; cat /etc/config/APP_MODE; echo; echo "app.yaml:"; cat /etc/config/app.yaml; echo; echo "PASSWORD file:"; cat /etc/secret/PASSWORD'

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab exec subpath-demo -- \
  sh -c 'ls -l /etc/app && echo && cat /etc/app/app.yaml'

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab exec projected-demo -- \
  sh -c 'find /etc/projected -maxdepth 3 -type f | sort | xargs -I{} sh -c "echo --- {}; cat {}; echo"'

为什么先采集初始状态

因为实验必须先有基线。

否则你后面改配置后看到的任何现象,都无法判断是:

  • 本来就这样
  • 还是改动造成的

我看到的关键信息

env-demo 初始值是:

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

volume-demo 里,ConfigMap / Secret 文件都是:

  • 指向 ..data/<key> 的符号链接

subpath-demo 中的 /etc/app/app.yaml 是一个普通文件视图。

projected-demo 则能在一个目录树里同时看到:

  • config
  • secret

原理解释

这一步已经埋下了后面最重要的伏笔:

  • 普通 volume / projected volume 走的是目录版本切换模型
  • subPath 不是同一套机制

Step 7:看 Pod YAML,确认环境变量是怎么注入的

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab get pod env-demo -o yaml | sed -n '1,220p'

为什么看 Pod YAML

因为我不想只从容器里“看到结果”,还要从对象定义层确认:

  • 值是通过 configMapKeyRef 注入
  • 还是 secretKeyRef

我看到的结果

env-demoenv 数组里明确使用了:

  • configMapKeyRef
  • secretKeyRef

原理解释

这证明环境变量不是“容器自己知道去哪里拿”,而是 kubelet 在容器启动前,把外部对象解析为环境变量,再交给进程。


Step 8:看挂载类型,确认 Secret 的载体

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab exec volume-demo -- \
  sh -c 'echo "[mounts for config/secret]"; cat /proc/mounts | grep "/etc/config\|/etc/secret"'

为什么要看 /proc/mounts

因为我要确认:

  • ConfigMap 和 Secret 在容器眼里到底是怎样的挂载点

我看到的结果

  • /etc/config 显示为 ext4
  • /etc/secret 显示为 tmpfs

原理解释

这进一步说明:

  • Secret 的处理比普通配置更敏感
  • 它在这套集群里是通过内存文件系统挂给容器的

这不是抽象描述,而是你这套集群上的真实行为。


Step 9:看 projected volume 的目录实现

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab exec projected-demo -- \
  sh -c 'ls -la /etc/projected'

为什么专门看 projected 目录

因为 projected volume 把 Kubernetes 的原子更新机制暴露得最直观。

我看到的结果

目录里有:

  • ..2026_04_09_18_31_36.xxxxx
  • ..data -> ..2026_04_09_18_31_36.xxxxx
  • config -> ..data/config
  • secret -> ..data/secret

原理解释

这说明 kubelet 并不是原地修改现有目录,而是:

  1. 写新版本目录
  2. 切换 ..data
  3. 让上层路径自然指向新版本

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


Step 10:修改 ConfigMap,观察不同注入方式的差异

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab patch configmap app-config \
  --type merge \
  -p '{"data":{"APP_MODE":"green","LOG_LEVEL":"debug","FEATURE_FLAG":"true","app.yaml":"app:\n  mode: green\n  logLevel: debug\n  featureFlag: true\n"}}'

命令解释

  • patch
    • 对现有对象做局部更新
  • --type merge
    • 用 merge patch 方式合并字段
  • -p
    • 后面直接给 patch 内容

为什么用 patch 而不是全量 apply

因为我这一步只想改变:

  • data

不想引入额外变量。

我还做了什么

修改后,我立刻分别观察:

  • env-demo 的环境变量
  • subpath-demo 的单文件
  • volume-demo 的 ConfigMap 文件
  • projected-demo 的 ConfigMap 文件

Step 11:观察 ConfigMap 更新传播时间

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab exec volume-demo -- \
  sh -c 'i=0; while [ $i -lt 18 ]; do now=$(date +%H:%M:%S); val=$(cat /etc/config/APP_MODE); echo "$now volume APP_MODE=$val"; if [ "$val" = "green" ]; then break; fi; i=$((i+1)); sleep 5; done'

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab exec projected-demo -- \
  sh -c 'i=0; while [ $i -lt 18 ]; do now=$(date +%H:%M:%S); val=$(cat /etc/projected/config/APP_MODE); echo "$now projected APP_MODE=$val"; if [ "$val" = "green" ]; then break; fi; i=$((i+1)); sleep 5; done'

为什么要轮询,而不是只看一次

因为我要确认的不是:

  • “最终会不会变”

而是:

  • “多久变”
  • “是不是马上变”

我观察到的真实现象

volume-demo 中:

  • 连续很多次还是 blue
  • 18:34:37 才变成 green

projected-demo 中:

  • 也不是立刻变
  • 18:34:42 才看到 green

原理解释

这一步非常重要,它把一个常被忽略的事实变成了肉眼可见的证据:

ConfigMap volume 更新依赖 kubelet 的同步周期,不是 API 一变、文件就秒变。


Step 12:立刻检查 env 和 subPath,证明它们没变

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab exec env-demo -- \
  sh -c 'echo APP_MODE=$APP_MODE; echo LOG_LEVEL=$LOG_LEVEL'

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab exec subpath-demo -- \
  sh -c 'echo "subPath app.yaml:"; cat /etc/app/app.yaml'

我看到的结果

env-demo 仍然是:

  • APP_MODE=blue
  • LOG_LEVEL=info

subpath-demo 仍然是旧文件内容:

  • blue
  • info
  • false

原理解释

这一步把两类最容易混淆的行为一次分开了:

  • env 是启动时快照
  • subPath 是静态单文件绑定

它们都不会因为 ConfigMap 变化而自动拿到新值。


Step 13:再次看最终文件状态,证明 volume 已更新而 subPath 仍停留旧值

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab exec volume-demo -- \
  sh -c 'echo APP_MODE=$(cat /etc/config/APP_MODE); echo; cat /etc/config/app.yaml'

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab exec projected-demo -- \
  sh -c 'ls -la /etc/projected && echo; cat /etc/projected/config/APP_MODE && echo; cat /etc/projected/config/app.yaml'

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab exec subpath-demo -- \
  sh -c 'cat /etc/app/app.yaml'

我看到的结果

  • volume-demo 已经变成绿色新版本
  • projected-demo 也已经切换到新的 ..data 目录
  • subpath-demo 仍然是旧版本

原理解释

这里已经能完整说明:

更新是否生效,不取决于“对象是不是 ConfigMap”,而取决于“它以什么方式进入容器”。


Step 14:修改 Secret,重复同样的实验链

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab patch secret app-secret \
  --type merge \
  -p '{"stringData":{"PASSWORD":"RotatedPass456","API_TOKEN":"token-v2"}}'

为什么这里用了 stringData

因为手写 data 时,你需要自己先做 base64 编码。

stringData 的好处是:

  • 直接写明文
  • API Server 帮你转换成 data

这更适合教学和日常 patch。

我随后做了什么

我继续观察:

  • env-demo 的环境变量
  • volume-demo/etc/secret/PASSWORD
  • projected-demo/etc/projected/secret/PASSWORD

Step 15:观察 Secret 的更新传播

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab exec volume-demo -- \
  sh -c 'i=0; while [ $i -lt 18 ]; do now=$(date +%H:%M:%S); val=$(cat /etc/secret/PASSWORD); echo "$now secret-volume PASSWORD=$val"; if [ "$val" = "RotatedPass456" ]; then break; fi; i=$((i+1)); sleep 5; done'

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab exec projected-demo -- \
  sh -c 'for i in 1 2 3 4 5 6 7 8 9 10 11 12; do now=$(date +%H:%M:%S); val=$(cat /etc/projected/secret/PASSWORD); echo "$now projected-secret PASSWORD=$val"; [ "$val" = "RotatedPass456" ] && break; sleep 5; done'

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab exec env-demo -- \
  sh -c 'echo PASSWORD=$PASSWORD; echo API_TOKEN=$API_TOKEN'

我看到的结果

env-demo 仍然是旧值:

  • PASSWORD=InitialPass123
  • API_TOKEN=token-v1

volume-demo 的 Secret 文件在几十秒后变成了:

  • RotatedPass456

projected-demo 的 Secret 文件随后也切换到了新值。

原理解释

这再次证明:

  • Secret 通过 env 注入时,同样是启动时快照
  • Secret 通过 volume / projected volume 注入时,同样依赖 kubelet 同步

所以不要把 Secret 当成“特殊到完全不同的更新模型”,它和 ConfigMap 在注入机制层面有很多共性,只是安全属性更强。


Step 16:再次用 base64 解码 Secret,证明 API 表达层只是编码

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab get secret app-secret \
  -o jsonpath='{.data.PASSWORD}{"\n"}{.data.API_TOKEN}{"\n"}'

printf '%s' 'Um90YXRlZFBhc3M0NTY=' | base64 -d && echo
printf '%s' 'dG9rZW4tdjI=' | base64 -d

为什么还要做这一步

因为很多人还是会潜意识里把 Secret 的 base64 当成某种“轻加密”。

我这里直接把它做成肉眼可见的证据。

原理解释

这一步再次强调:

  • API 表示层:base64
  • 容器消费层:明文

真正的安全边界不在 base64。


Step 17:验证 immutable ConfigMap 的行为

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab patch configmap immutable-config \
  --type merge \
  -p '{"data":{"SAFE_DEFAULT":"disabled"}}'

我看到的结果

返回错误:

data: Forbidden: field is immutable when `immutable` is set

原理解释

这说明:

  • immutable: true 不是注释,也不是建议
  • 它会直接阻止数据字段被修改

这在大规模平台里是很实用的约束工具。


Step 18:验证 env 注入要靠重建 Pod 才能拿到新值

第一次尝试时踩到的真实坑

我一开始先:

  1. env-demo
  2. 立即 apply 同一个 Pod 清单

结果拿到了:

  • 资源仍在删除中
  • unchanged
  • Pod 处于 Terminating

这个坑为什么值得写进操作记录

因为它说明一个真实问题:

Kubernetes 对象是有生命周期的,你不能假设“删命令发出去”就等于对象已经完全消失。

这类竞态在自动化脚本里非常常见。

修正后的实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab delete pod env-demo --wait=true

KUBECONFIG=~/.kube/config-k8s-lab kubectl apply -f manifests/07-config-secret/20-env-demo.yaml

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab wait --for=condition=Ready pod/env-demo --timeout=180s

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab exec env-demo -- \
  sh -c 'printenv APP_MODE LOG_LEVEL PASSWORD API_TOKEN'

我看到的结果

新建后的 env-demo 拿到了最新值:

  • green
  • debug
  • RotatedPass456
  • token-v2

原理解释

这就是环境变量注入模型最关键的结论:

想让 env 模式拿到新值,本质上要重建进程。


Step 19:回到真实业务,找实际案例

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n aiforge get deploy aiforge-gateway -o yaml | \
  rg -n 'configMap|mountPath|subPath'

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n aiforge get configmap aiforge-config -o yaml | \
  sed -n '1,120p'

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n monitoring get deploy monitoring-grafana -o yaml | \
  rg -n 'configMap|secret|secretKeyRef|mountPath|subPath'

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n monitoring get deploy monitoring-grafana -o yaml | \
  sed -n '30,48p'

为什么做这一步

因为我要把实验结论和真实业务对象连起来。

我看到的两个关键案例

案例一:aiforge-gateway

它把 aiforge-config 挂载到:

  • /app/config.yaml

并使用:

  • subPath: config.yaml

这意味着:

  • 它天然不适合指望运行中热更新

更值得警惕的是,aiforge-config 当前还包含敏感字段类别:

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

这说明真实集群里已经出现了:

  • 把敏感值塞进 ConfigMap 的反模式

案例二:monitoring-grafana

它同时体现了三种典型工程实践:

  1. 敏感信息通过 secretKeyRef 注入
  2. 某些配置文件通过 subPath 挂载
  3. Pod 模板上有:
    • checksum/config
    • checksum/secret

原理解释

Grafana 这个案例说明:

成熟的生产系统不会盲目指望 env / subPath 自动热更新,而是通过模板 checksum 触发滚动更新。

这正是你以后在 Helm / GitOps 里应当掌握的模式。


Step 20:最后核对实验环境当前状态

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n config-lab get configmap,secret,pod -o wide

我确认到的当前状态

当前 config-lab 中:

  • app-config 已存在,内容是更新后的版本
  • immutable-config 仍保持原值
  • app-secret 已更新到新凭据
  • volume-demoprojected-demo 已体现新文件
  • subpath-demo 仍保留旧文件视图
  • env-demo 经重建后已拿到新环境变量

原理解释

这一步相当于给整轮实验做收口,确保:

  • API 对象状态
  • 挂载文件状态
  • Pod 进程看到的状态

三者都被验证过。


本轮最重要的结论

这轮实验最核心的,不是多创建了几个对象,而是把以下规律变成了可观察事实:

  1. API 对象更新,不等于应用配置已经生效。
  2. 环境变量注入是启动时快照,改对象不会自动刷新运行中进程。
  3. 普通 volume / projected volume 会热更新文件,但依赖 kubelet 同步,不是秒变。
  4. subPath 是配置热更新高频坑,因为它通常不会跟随新版本自动切换。
  5. Secret 的 base64 不是加密,容器内读取到的仍是明文。
  6. Secret 在这套集群里以 tmpfs 挂载,体现了更高敏感度。
  7. 生产工程里,env / subPath 配置更新常常靠 checksum 触发滚动更新,而不是幻想运行中自动变。
  8. 真实集群里已经存在一个反例:把敏感字段塞进 ConfigMap。

本轮新增交付物

下一轮最自然的延伸是:

  • 存储与持久化
  • emptyDir、PVC、PV、StorageClass
  • NFS 与本地盘的行为差异
  • 为什么配置、Secret、业务数据必须分层设计