3. Secrets生命周期管理
Secret 不是“创建完就结束”的静态对象。在真实集群里,它从被创建那一刻起就进入一条生命周期:创建 → 挂载/注入 → 更新 → 生效 → 轮换 → 删除 → 回收。任何一个环节处理不当,都会导致两类典型事故:一是旧凭据未及时失效,二是新凭据未及时生效,业务在轮换窗口内出现 401/403 或连接失败。本节围绕这条生命周期链,讲清楚每个阶段的机制、可观察现象与判断依据。
创建:命令式与声明式两条路径
创建 Secret 有两种方式。命令式适合临时调试,声明式适合纳入 GitOps 与审计。命令式最常用的是从字面量或文件生成:
# 从字面量创建(值会被 base64 编码存储)
kubectl create secret generic db-cred \
--from-literal=username=app \
--from-literal=password='S3cr3t-2024!'
# 从文件创建,键名默认取文件名
kubectl create secret generic tls-cert \
--from-file=./tls.crt --from-file=./tls.key
# 查看(注意 data 是 base64,不是加密)
kubectl get secret db-cred -o jsonpath='{.data.password}' | base64 -d
声明式则用 YAML,配合 stringData 字段可直接写明文,API Server 在写入时自动转成 data 的 base64。这里必须强调一个常见误解:base64 是编码而非加密,任何能读取 etcd 或拥有 get secret 权限的人都能还原明文。因此创建阶段就要决定是否启用静态加密(EncryptionConfiguration)与最小权限 RBAC。
创建后,Secret 通过三种方式被消费:作为环境变量(envFrom/valueFrom)、作为卷挂载(volume mount)、或由 kubelet 用于拉取镜像(imagePullSecrets)。这三种方式的“更新可见性”完全不同,是理解后续更新的关键。
更新:三种消费方式的生效差异
更新 Secret 后,Pod 是否立即看到新值,取决于消费方式:
- 环境变量:进程启动时读取一次,之后永不更新。要生效必须重启 Pod(滚动重启或删除重建)。
- 卷挂载:kubelet 会周期性同步(默认约 60 秒,受
--sync-frequency与configMapAndSecretChangeDetectionStrategy影响),挂载文件内容会被替换。但应用是否重新读取文件,取决于应用自身逻辑。 - subPath 挂载:不会随 Secret 更新而更新,这是最容易被忽略的坑。
因此“更新 Secret”本身不等于“凭据已轮换”。一个可判断的验证方法是:进入容器执行 cat /etc/secret/password 对比新旧值,并观察应用日志中是否出现重新加载或鉴权成功记录。若使用环境变量,则必须触发一次重启,例如:
kubectl rollout restart deployment/app-api
kubectl rollout status deployment/app-api
删除与垃圾回收:谁负责清理
删除 Secret 用 kubectl delete secret db-cred。但真正需要设计的是“谁在什么时候删”。Kubernetes 的垃圾回收基于 ownerReferences:当父对象被删除,且子对象的 ownerReference 指向它、blockOwnerDeletion 未阻止时,子对象会被级联回收。典型场景是 Helm 管理的 Secret 带有 helm.sh/hook 或 owner 注解,卸载 Release 时被一并清理。
反过来,如果 Secret 被 Pod 以卷方式引用,删除 Secret 不会立即删除 Pod,但挂载点会变成空或报错,新调度到该节点的 Pod 可能因找不到 Secret 而卡在 ContainerCreating。因此删除前应先确认引用关系:
# 查找哪些 Pod 引用了该 Secret(需结合 describe 或审计)
kubectl get pods -A -o json | \
jq '.items[] | select(.spec.volumes[]?.secret.secretName=="db-cred") | .metadata.name'
对于动态生成的凭据(如 Vault Agent 注入、External Secrets Operator 同步),删除策略应由控制器决定:是保留、重建还是标记为过期。建议为每个 Secret 打上 owner、rotation-policy、expires-at 等标签,让回收有据可依。
不可变 Secret:把“变更”变成“替换”
从 Kubernetes v1.21 起,Secret 支持 immutable: true。一旦设为不可变,data 字段不能再修改,只能删除后重建。这带来两个直接收益:一是防止误改导致线上凭据漂移;二是 kubelet 不再需要 watch 该 Secret,显著降低大规模集群中 API Server 与 kubelet 的负载。
apiVersion: v1
kind: Secret
metadata:
name: db-cred-v2
type: Opaque
immutable: true
stringData:
password: "S3cr3t-2024-07!"
不可变 Secret 的代价是:更新必须走“新建 + 切换引用 + 删除旧对象”的路径。这恰好与版本控制策略天然契合——把每次轮换视为一个新版本对象,而不是原地修改。
版本控制策略:命名、切换与回滚
推荐做法是给 Secret 名称带版本后缀,例如 db-cred-v1、db-cred-v2,并在 Deployment 中通过引用切换。切换时先创建新 Secret,再更新工作负载引用,最后观察就绪,确认无误后删除旧版本。这样任何一步失败都能快速回滚到旧名称。
下面这张图把整条生命周期与三种消费方式的生效差异放在一起,便于对照判断“更新后到底有没有生效”。
把这张图落到操作上,一个可复用的轮换闭环是:新建带版本后缀的 Secret → 更新工作负载引用 → 滚动发布并等待就绪 → 验证鉴权与日志 → 删除旧版本 Secret。其中“验证”这一步不能省,因为卷挂载的同步有延迟、环境变量需要重启、subPath 根本不更新,三者表现不同,只有实际观察才能确认轮换成功。
immutable: true + 版本后缀命名,把“更新”转化为“替换”;对必须原地更新的场景,明确记录消费方式,并在变更单中写清“是否需要重启 Pod”。最后给出一个可直接套用的轮换检查清单,字段按本节语境设计:
| 检查项 | 判断依据 | 不通过时的动作 |
|---|---|---|
| 新 Secret 已创建且键名一致 | kubectl get secret db-cred-v2 -o jsonpath='{.data}' | 核对键名与旧版本一致 |
| 工作负载已切换引用 | kubectl get deploy app-api -o yaml 中 secretName 为新版本 | 修正 YAML 并重新 apply |
| Pod 已滚动完成 | kubectl rollout status 返回 successfully rolled out | 查看事件与容器日志定位失败原因 |
| 容器内实际值已更新 | 卷挂载:cat /etc/secret/password;env:需重启后核对 | 卷挂载等待同步;env 触发重启 |
| 业务鉴权正常 | 应用日志无 401/403,健康检查通过 | 回滚到旧版本 Secret 引用 |
| 旧版本已清理 | kubectl get secret db-cred-v1 返回 NotFound | 确认无引用后删除 |
至此,Secret 的生命周期不再是零散命令的集合,而是一条可设计、可验证、可回滚的工程链路:创建时决定消费方式与是否不可变,更新时区分三种生效路径,删除时依赖 ownerReferences 与引用检查,版本控制则把“改”变成“换”,让每一次轮换都有明确的起点、验证点和回退点。