Repository Reading Site
本轮操作记录:ConfigMap 与 Secret 注入、更新与安全边界实验
这一轮的目标不是简单演示: 而是要把更关键的东西讲透: 1. ConfigMap / Secret 有哪些运行时注入方式 2. 为什么同样是“改配置”,有的容器会变,有的不会变 3. kubelet 到底是怎么把配置同步进容器的 4. Secret 为什么是敏感对象,但又为什么不能被神化成“天然加密保险箱” 5. 真实业务里常见的好模式和坏模式分别是什么 -
本轮操作记录:ConfigMap 与 Secret 注入、更新与安全边界实验
本轮目标
这一轮的目标不是简单演示:
- 怎么
kubectl create configmap - 怎么
kubectl create secret
而是要把更关键的东西讲透:
- ConfigMap / Secret 有哪些运行时注入方式
- 为什么同样是“改配置”,有的容器会变,有的不会变
- kubelet 到底是怎么把配置同步进容器的
- Secret 为什么是敏感对象,但又为什么不能被神化成“天然加密保险箱”
- 真实业务里常见的好模式和坏模式分别是什么
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'
为什么先做这一步
因为我要先确认两件事:
- 仓库里现有材料讲到了什么程度
- 真实集群里已经有哪些 ConfigMap / Secret 在被使用
我看到的结果
集群里已经有大量真实对象,例如:
argocd-cmharbor-coremonitoring-grafanagitea-*- 各 namespace 的
kube-root-ca.crt
Secret 也同样很多,例如:
argocd-secretmonitoring-grafanagitea-postgresqlharbor-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-configSecret/db-creds
它们是本轮实验的一个历史基线。
我看到的结果
db-creds 的 password 字段在 YAML 里是:
- base64 字符串
而 base64 -d 后直接得到:
SuperSecret123
原理解释
这一步是为了在课程一开始就打掉一个错误认知:
Secret 的 base64 不是加密,只是编码。
Step 3:设计本轮实验对象
我创建了哪些文件
路径:
对象包括:
Namespace/config-labConfigMap/app-configSecret/app-secretConfigMap/immutable-configPod/env-demoPod/volume-demoPod/subpath-demoPod/projected-demo
为什么这样设计
这不是多建几个 Pod 而已,而是为了做四种注入模型并行对照:
- 环境变量
- 普通 volume
subPath- projected volume
再加一个不可变 ConfigMap 做对照。
这一步体现的工程原则
做实验不能只看“功能能不能实现”,要把最容易混淆的模型并排摆出来。
Step 4:apply 实验清单
实际命令
KUBECONFIG=~/.kube/config-k8s-lab kubectl apply -f manifests/07-config-secret
为什么直接按目录 apply
因为这一轮对象之间有明确关联:
- Pod 依赖 ConfigMap / Secret
- 命名空间隔离实验环境
目录级清单更适合:
- 复现
- 审核
- 后续回放
实际结果
成功创建:
config-labapp-configapp-secretimmutable-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=blueLOG_LEVEL=infoPASSWORD=InitialPass123API_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-demo 的 env 数组里明确使用了:
configMapKeyRefsecretKeyRef
原理解释
这证明环境变量不是“容器自己知道去哪里拿”,而是 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.xxxxxconfig -> ..data/configsecret -> ..data/secret
原理解释
这说明 kubelet 并不是原地修改现有目录,而是:
- 写新版本目录
- 切换
..data - 让上层路径自然指向新版本
这就是原子更新实现的关键。
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=blueLOG_LEVEL=info
subpath-demo 仍然是旧文件内容:
blueinfofalse
原理解释
这一步把两类最容易混淆的行为一次分开了:
- 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/PASSWORDprojected-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=InitialPass123API_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 才能拿到新值
第一次尝试时踩到的真实坑
我一开始先:
- 删
env-demo - 立即 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 拿到了最新值:
greendebugRotatedPass456token-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.passwordstorage.access_keystorage.secret_keyauth.jwt_secret
这说明真实集群里已经出现了:
- 把敏感值塞进 ConfigMap 的反模式
案例二:monitoring-grafana
它同时体现了三种典型工程实践:
- 敏感信息通过
secretKeyRef注入 - 某些配置文件通过
subPath挂载 - Pod 模板上有:
checksum/configchecksum/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-demo、projected-demo已体现新文件subpath-demo仍保留旧文件视图env-demo经重建后已拿到新环境变量
原理解释
这一步相当于给整轮实验做收口,确保:
- API 对象状态
- 挂载文件状态
- Pod 进程看到的状态
三者都被验证过。
本轮最重要的结论
这轮实验最核心的,不是多创建了几个对象,而是把以下规律变成了可观察事实:
- API 对象更新,不等于应用配置已经生效。
- 环境变量注入是启动时快照,改对象不会自动刷新运行中进程。
- 普通 volume / projected volume 会热更新文件,但依赖 kubelet 同步,不是秒变。
subPath是配置热更新高频坑,因为它通常不会跟随新版本自动切换。- Secret 的 base64 不是加密,容器内读取到的仍是明文。
- Secret 在这套集群里以
tmpfs挂载,体现了更高敏感度。 - 生产工程里,env /
subPath配置更新常常靠 checksum 触发滚动更新,而不是幻想运行中自动变。 - 真实集群里已经存在一个反例:把敏感字段塞进 ConfigMap。
本轮新增交付物
下一轮最自然的延伸是:
- 存储与持久化
emptyDir、PVC、PV、StorageClass- NFS 与本地盘的行为差异
- 为什么配置、Secret、业务数据必须分层设计