希曼日记

基础设施篇:Reloader 让 Secret 变更自动滚动 Pod

约 6 分钟2,204 字

本文是全栈电商微服务从 0 到 1 再到 10系列的基础设施篇。上一篇:基础设施篇:Spegel 镜像 P2P 分发;下一篇:GitOps 篇 01:用 Gitleaks 阻止凭据进入 Git 历史。

凭据不进 Git,是这个系列反复强调的规则。做到这一点的常见办法是把密码放进 Vault 或 OpenBao 这类凭据后端,再由 External Secrets Operator(ESO)把它同步成 Kubernetes Secret,应用照常用 secretKeyRef 读。链路很干净,但它有一个默认行为,第一次踩到时会很意外:Secret 的值变了,Pod 不会重启。

ESO 只负责把值写进 Secret。Deployment 引用的 Secret 名字没变,Kubernetes 认为 Pod 模板没变,进程就一直用启动时读到的旧密码,直到某个不相干的原因让它重启。凭据后端里明明已经轮换过,集群里还跑着旧值,这个状态可以静默持续几天。

Reloader 解决的就是这一段。

先看它位于哪一段

把电商项目的凭据链路画出来,Reloader 的位置一目了然:

Mermaid
graph LR
Backend[OpenBao / Vault 改值] --> ESO[ESO 按 refreshInterval 同步]
ESO --> Secret[Kubernetes Secret]
Secret --> Reloader[Reloader 滚动引用方]
Reloader --> Provider[Dragonfly 等提供方 Pod]
Secret -. 不经过 .-> CC[Config Center]
Harvest[harvest 脚本] --> CC
CC --> Services[10 个业务微服务]

四段各有各的负责人:

段负责什么谁负责
① 凭据后端改值密码、master key 的唯一真相源OpenBao / Vault
② 物化为 Secret按刷新周期把值写进 K8s SecretESO
③ 引用方滚动让引用了该 Secret 的 Pod 重启、用上新值Reloader(或手工 rollout restart)
④ 非 Secret 消费方同步把新值写给不读 K8s Secret 的应用项目自己的脚本

第 ④ 段是本项目特有的,后面单独讲。先把第 ③ 段说清楚。

Reloader 是什么

Reloader 是 stakater 维护的 Kubernetes 控制器。它监听 Secret 与 ConfigMap;某个 Deployment、StatefulSet、DaemonSet 或 Argo Rollout 带了它的注解并引用了发生变化的对象,它就修改这个工作负载的 Pod 模板,触发一次标准的滚动更新。

它做的事只有一件:把「Secret 变了」翻译成「滚动一次」。它不读 Secret 的内容,不连凭据后端,不改应用的配置文件。

接入方式是在工作负载的 metadata.annotations 上加一行:

YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: dragonfly
annotations:
# 点名:只盯这一个 Secret
secret.reloader.stakater.com/reload: "dragonfly-password-secret"
# 或者:该工作负载引用到的所有 Secret / ConfigMap
# reloader.stakater.com/auto: "true"

注解必须写在 Deployment 自己的 metadata 上,不是 Pod 模板的 metadata。这是最常见的「加了注解却不滚动」的原因。

滚动的方式有两种,由 reloadStrategy 决定:

  • env-vars(默认):往容器塞一个 STAKATER_<NAME>_LAST_RELOADED 环境变量,值变了 Pod 模板就变了。
  • annotations:改 Pod 模板的注解,和 kubectl rollout restart 留下的痕迹同构。

本项目选 annotations。理由是排障时只需要认一种痕迹,而且不往业务容器里注入额外的环境变量。

它能解决什么问题

Vault 改值后进程还是旧密码

这是提问的原始场景,也是 Reloader 的本职。ESO 同步了新值、Secret 变了,Reloader 让引用它的 Dragonfly 滚动一次,新 Pod 从 Secret 读到新密码。整个过程不需要人介入,也不需要记得跑什么命令。

让 ConfigMap 变更生效

ConfigMap 挂成文件时 kubelet 会异步更新文件内容,但大多数应用只在启动时读一次配置。给引用它的工作负载加注解,配置改动就会随一次滚动生效。

把 Reloader 自己排除在外

它有几个开关,第一次装就该关掉:

开关本项目取值为什么
autoReloadAllfalse全局模式会把 cert-manager 续期的证书、CNPG 每 90 天续的 CA 都算进去,证书一续业务 Pod 就滚
ignoreJobs / ignoreCronJobstrueSecret 变了不该重跑一次性任务
reloadOnCreate / reloadOnDeletefalseESO 首次物化时工作负载还没起;删除更不该滚
syncAfterRestartfalse否则 Reloader 自己一升级,全集群带注解的工作负载滚一遍
ignoreNamespaces控制面命名空间kube-system、cert-manager、external-secrets、openbao 一律不看

它不能做什么

这一节比上一节重要。

它管不到不读 Secret 的消费方

回到那张图的第 ④ 段。本项目的 10 个业务微服务不从 K8s Secret 读数据库密码——它们的 bootstrap.yaml(PostgreSQL、Dragonfly、Consul 的地址与凭据)由网关仓的 Config Center 统一下发,服务只持有一枚 machine token。这个设计在开篇里提过:配置管理收在网关这一层,微服务只关心自己的业务边界。

于是出现一个 Reloader 看不见的断层:Dragonfly 密码变了,Reloader 把 Dragonfly 滚了一遍,新 Pod 用上了新密码;但 cart、order、user 在 Config Center 里记着的还是旧密码,下一次连接就会在 AUTH 阶段失败。

这段只能由项目自己的脚本补:读集群里各组件的地址与凭据,按每个服务的 JSON Schema 把值写进 Config Center 对应的键,签发新的 machine token,再滚动业务服务。它和 Reloader 是并列的两段,谁也替代不了谁。

它会让断连窗口提前,而不是消失

如果第 ④ 段还是手工触发,装 Reloader 反而有副作用。没有它的时候,操作者可以先跑脚本更新 Config Center,再重启 Dragonfly,把两边不一致的窗口压到最短。有了它,凭据后端一改值 Dragonfly 就自动重启了,脚本没跟上,消费方就一直连不上,直到有人想起来。

所以对共享凭据来说,「提供方自动跟上」并不天然是好事。它需要「消费方也自动跟上」配套,否则只是把不一致从提供方挪到了消费方。

它管不了只在首次初始化读密码的应用

Grafana 的 admin 密码、Harbor 的数据库口令、Bugsink 和 healthchecks 的超级用户,都是第一次启动时读一次、写进自己的数据库,之后 Secret 再怎么变都不看。给这类工作负载加注解,Secret 变了它们会重启,密码却不会换,只多了一次无意义的中断。

一句话:Reloader 只负责重启,不负责应用语义。

放到电商项目里怎么看

当前的现实是:凭据后端里的值只会因为「人跑脚本」而变化。脚本自己知道什么时候该重启提供方,显式 kubectl rollout restart 的时机是可控的。这个阶段 Reloader 没有增量价值。

所以部署资产已经写好、放在集群安装器的组件目录里,开关默认关着。什么时候打开,看三个信号,任一出现就够:

  1. 凭据后端配了定期自动轮换,值会在脚本之外变化。
  2. 多人操作凭据后端,没法保证每次改值都跟着跑脚本。
  3. 第 ④ 段的脚本已经能被 Secret 变更事件触发,③ 和 ④ 可以同时自动化。

在那之前,链路是「改凭据后端 → 脚本重写 Config Center → 脚本重启提供方」,三步都由同一个脚本按顺序做,不一致窗口只有几分钟。

验证要看什么

「Reloader 的 Pod 是 Running」什么都证明不了。要证明的是「Secret 改值之后,带注解的工作负载真的滚动了,而且新 Pod 读到了新值」。

自检脚本做的事:

  1. 在临时命名空间建一个 Secret,PASSWORD=v1。
  2. 建一个带 secret.reloader.stakater.com/reload 注解的 Deployment,容器启动时打印 PASSWORD。
  3. 记下 Deployment 的 status.observedGeneration。
  4. 把 Secret 改成 PASSWORD=v2。
  5. 60 秒内 observedGeneration 必须递增;等新 Pod 就绪后,日志里必须出现 PASSWORD=v2。
Shell
bash components/reloader/examples/selftest.sh
# ✔ Reloader 自检通过: generation 1 → 2, 新 Pod PASSWORD=v2

真实链路的验证要走完全部四段:在 OpenBao 改 Dragonfly 的密码 → ESO 同步 → kubectl -n dragonfly rollout history deploy/dragonfly 多出一版 → 跑脚本更新 Config Center → 业务服务的 readyz 恢复 200。缺任何一段都不能说轮换闭环了。

与相邻工具的边界

工具负责什么不负责什么
OpenBao / Vault凭据真相源不知道谁在用
ESO把真相源的值物化成 K8s Secret不重启任何 Pod
ReloaderSecret 变了滚动引用方不读值、不连后端、不管非 Secret 消费方
Config Center 同步脚本把地址与凭据写进配置中心并滚动业务服务不管理凭据后端
Argo CD让集群状态收敛到 GitSecret 的值不在 Git 里,它看不见变化

对本系列的结论

Reloader 是「提供方自动跟上凭据后端」的正确工具,接入成本是一行注解,运行成本不到 30 MiB。但它只自动化了凭据链路的一段。在这个项目里,真正决定轮换能不能闭环的是那段它看不见的路——配置中心里的消费方。先把那段的脚本做扎实,再打开 Reloader,顺序不能反。

相关文章