上一篇让 Deployment 能滚动升级和回滚。本篇把环境差异从镜像中移出:普通配置进入 ConfigMap,敏感值通过 Secret 注入,并明确更新传播、触发重启和生产密钥管理的边界。
一、痛点:同一镜像如何进入不同环境
把 API 地址、功能开关或数据库密码写进镜像,会迫使每个环境重新构建,也会让秘密进入镜像层和构建日志。Kubernetes 提供 ConfigMap 保存非敏感键值,Secret 保存敏感数据。二者可注入环境变量或挂载成文件,应用无需调用 Kubernetes API。
Secret 并非天然保险箱。清单中的 data 只是 Base64 编码,不是加密;拥有读取 Secret 权限的人仍可还原。生产要启用 etcd 静态加密、严格 RBAC、审计和外部密钥系统,避免把明文 Secret 提交 Git。External Secrets Operator、Secrets Store CSI Driver 或云厂商方案可让 Git 只保存引用,但选型应结合威胁模型与运维能力。
环境变量简单,却只在容器启动时读取,更新对象不会改变已运行进程。卷挂载文件通常会被 kubelet最终同步更新,但应用还要监听或定期重读;通过 subPath 挂载单文件时不会获得自动更新。是否热加载必须由应用契约明确,不能假定 Kubernetes 会替应用刷新内存。
二、原理:数据投影与发布语义
ConfigMap 和 Secret 都受对象大小限制,不适合大文件或二进制制品。键可以通过 envFrom 批量导入,也可逐项映射以控制变量名;文件投影会把每个键变成文件。逐项声明较啰嗦,却能在评审中看见应用真正依赖哪些值,并避免未来新增键意外进入进程环境。
下面保存为 config.yaml。示例 Secret 通过命令生成,不把明文写进清单;先创建 namespace 中的对象,再应用 Deployment。学习值不是生产凭据,运行后仍应删除。应用使用 Nginx,通过 ConfigMap 挂载完整配置,并把示例令牌作为环境变量展示注入方法,但不会打印令牌。
apiVersion: v1
kind: ConfigMap
metadata:
name: web-config
data:
default.conf: |
server {
listen 8080;
location /healthz {
access_log off;
return 200 "ok\n";
}
location / {
default_type text/plain;
return 200 "configured web\n";
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: web
template:
metadata:
labels:
app.kubernetes.io/name: web
spec:
containers:
- name: web
image: nginx:1.27.4-alpine
ports:
- name: http
containerPort: 8080
env:
- name: APP_TOKEN
valueFrom:
secretKeyRef:
name: web-secret
key: token
volumeMounts:
- name: config
mountPath: /etc/nginx/conf.d
readOnly: true
readinessProbe:
httpGet:
path: /healthz
port: http
volumes:
- name: config
configMap:
name: web-config
三、实现:创建、验证和轮换
脚本从环境变量读取学习令牌,使用客户端 dry-run 生成对象再 apply,避免命令行采用裸字面值。注意进程环境和 shell 历史也可能泄密,这种方式仅用于本地实验;CI 应从受保护的密钥存储注入,并禁止调试回显。
#!/usr/bin/env bash
set -euo pipefail
: "${LAB_APP_TOKEN:?export LAB_APP_TOKEN with a disposable value}"
test "$(kubectl config current-context)" = "kind-k8s-lab"
kubectl create secret generic web-secret \
--from-literal="token=${LAB_APP_TOKEN}" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f config.yaml
kubectl rollout status deployment/web --timeout=120s
pod_name="$(kubectl get pod -l app.kubernetes.io/name=web -o jsonpath='{.items[0].metadata.name}')"
kubectl exec "${pod_name}" -- nginx -t
kubectl exec "${pod_name}" -- wget -qO- http://127.0.0.1:8080/healthz
kubectl patch configmap web-config --type=merge -p \
'{"data":{"default.conf":"server { listen 8080; location /healthz { return 200 \"ok\\n\"; } location / { return 200 \"configured-v2\\n\"; } }"}}'
kubectl rollout restart deployment/web
kubectl rollout status deployment/web --timeout=120s
kubectl create secret generic web-secret \
--from-literal="token=rotated-disposable-value" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl rollout restart deployment/web
kubectl rollout status deployment/web --timeout=120s
脚本会让 nginx -t 成功并让健康端点返回 ok。Secret 轮换后必须重启,因为环境变量不会动态刷新。若使用卷并且应用支持热加载,可选择不重启,但仍应监控加载是否成功,并保留旧凭据的短暂重叠窗口,避免连接长时间存活时瞬间失效。
GitOps 中常给 Pod template annotation 写入配置内容的哈希;配置变化会改变 template,从而自然触发 Deployment rollout。原生 Kubernetes 不会自动为 ConfigMap 更新重启所有引用它的 Pod。Helm 篇会实现 checksum annotation;纯清单也可用 Kustomize 的 configMapGenerator 生成带哈希名称,实现不可变配置引用。
四、踩坑:泄露通常发生在边界处
不要在文章、工单、日志和截图里打印 Secret,也不要用 kubectl get secret -o yaml 作为普通排障动作。Base64 字符串可轻易解码。RBAC 应让工作负载 ServiceAccount 只读取必要对象,最好通过卷注入而不是授予应用列出整个 namespace Secret 的 API 权限。
把 ConfigMap 挂在目录会遮蔽镜像中该目录原有文件,因此应确认应用所需文件全部存在。文件更新采用符号链接切换,长期打开文件描述符的程序可能仍读旧内容。配置语法错误也可能让新 Pod 启动失败;应在流水线先运行 nginx -t 等专用校验,再依赖 rollout 的 readiness 保护旧副本。
Secret immutable: true 可防止误改并降低 API 服务器监听负担,但轮换时需创建新名字并更新引用。对于证书、双密钥或数据库密码,应设计新旧并存、消费者切换、验证、撤销旧值的阶段流程,而不是覆盖后立即删除旧值。
五、验证:证明配置和密钥都按预期生效
验收应覆盖配置内容可读、探针通过、秘密未出现在 PodSpec 明文字段和常规日志、更新后运行实例取得新值、旧凭据最终被撤销。可以用 kubectl auth can-i get secret/web-secret --as=system:serviceaccount:lab:default 检查权限,但这只回答授权,不代表应用必须拥有该权限。
可靠配置管理的核心是把“值是什么”和“何时进入运行实例”分开记录。对象版本、Pod template 修订、应用加载结果都应可追踪。测试结束删除 web-secret,并从 shell 环境清除实验变量;若误用真实凭据,应立即在源系统撤销而不是只删 Kubernetes 对象。
下一篇将处理另一种跨重建状态:用 emptyDir 理解 Pod 生命周期,再用 PersistentVolumeClaim 让数据独立于 Pod,验证删除与重建后的持久化。
为配置建立数据字典也很重要:记录键名、类型、默认值、合法范围、是否敏感、拥有者、变更是否需要重启以及兼容窗口。应用启动时应校验必填值并输出不含秘密的配置摘要;发现非法配置立即失败,比带着半有效状态接流量更安全。删除配置键要经过“消费者不再读取、所有实例已升级、观察期结束”三个阶段,不能仅凭仓库搜索不到旧代码就立刻清理。
灾难演练可故意提交语法错误的配置,确认新 Pod 不就绪、旧 Pod 继续服务、流水线超时并留下诊断;随后恢复正确配置并验证滚动过程。另一次演练轮换一次性令牌,确认新旧凭据重叠时间符合设计,旧值最终在源系统失效。这里验收的是完整传播路径,而不仅是接口中的资源版本已经变化。
配置是否应触发发布,可以用内容摘要确定,而不依赖人工记版本号。下面程序对规范化后的非敏感配置计算摘要;键顺序变化不会误触发,值变化一定产生新摘要。真实项目把摘要写入 Pod template annotation,即可让 Deployment 自动滚动。
import hashlib
import json
def config_digest(config: dict[str, str]) -> str:
payload = json.dumps(
config,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
return hashlib.sha256(payload).hexdigest()[:12]
first = {"LOG_LEVEL": "info", "REGION": "cn-north"}
same = {"REGION": "cn-north", "LOG_LEVEL": "info"}
changed = {"LOG_LEVEL": "warning", "REGION": "cn-north"}
first_hash = config_digest(first)
same_hash = config_digest(same)
changed_hash = config_digest(changed)
print(f"stable_order={first_hash == same_hash}")
print(f"change_detected={first_hash != changed_hash}")
print(f"rollout={'required' if first_hash != changed_hash else 'skipped'}")
运行输出:
stable_order=True
change_detected=True
rollout=required
Secret 的 base64 只是编码。第二个程序不打印秘密,只检查必填键、值是否为空以及轮换版本是否增加;这种“只输出结论、不输出内容”的模式也适用于 CI 日志。
from dataclasses import dataclass
@dataclass(frozen=True)
class SecretMetadata:
name: str
keys: tuple[str, ...]
nonempty: tuple[str, ...]
version: int
def validate(secret: SecretMetadata, previous_version: int) -> list[str]:
errors: list[str] = []
required = {"username", "password"}
if not required.issubset(secret.keys):
errors.append("missing required key")
if not required.issubset(secret.nonempty):
errors.append("empty secret value")
if secret.version <= previous_version:
errors.append("rotation version did not increase")
return errors
candidate = SecretMetadata(
name="web-credentials",
keys=("username", "password"),
nonempty=("username", "password"),
version=4,
)
errors = validate(candidate, previous_version=3)
print(f"secret={candidate.name}")
print(f"checked_keys={len(candidate.keys)}")
print("validation=" + ("PASS" if not errors else "FAIL"))
运行输出:
secret=web-credentials
checked_keys=2
validation=PASS
参考来源
👍 觉得有用就点个 赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。
🚀 本文属于 《Kubernetes 上手实战》 系列,持续更新,关注不迷路。
📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到 [email protected],我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_67153745/article/details/163966039



