基础设施篇:Reloader 让 Secret 变更自动滚动 Pod
本文是全栈电商微服务从 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 的位置一目了然:
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 Secret | ESO |
| ③ 引用方滚动 | 让引用了该 Secret 的 Pod 重启、用上新值 | Reloader(或手工 rollout restart) |
| ④ 非 Secret 消费方同步 | 把新值写给不读 K8s Secret 的应用 | 项目自己的脚本 |
第 ④ 段是本项目特有的,后面单独讲。先把第 ③ 段说清楚。
Reloader 是什么
Reloader 是 stakater 维护的 Kubernetes 控制器。它监听 Secret 与 ConfigMap;某个 Deployment、StatefulSet、DaemonSet 或 Argo Rollout 带了它的注解并引用了发生变化的对象,它就修改这个工作负载的 Pod 模板,触发一次标准的滚动更新。
它做的事只有一件:把「Secret 变了」翻译成「滚动一次」。它不读 Secret 的内容,不连凭据后端,不改应用的配置文件。
接入方式是在工作负载的 metadata.annotations 上加一行:
apiVersion: apps/v1kind: Deploymentmetadata: 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 自己排除在外
它有几个开关,第一次装就该关掉:
| 开关 | 本项目取值 | 为什么 |
|---|---|---|
autoReloadAll | false | 全局模式会把 cert-manager 续期的证书、CNPG 每 90 天续的 CA 都算进去,证书一续业务 Pod 就滚 |
ignoreJobs / ignoreCronJobs | true | Secret 变了不该重跑一次性任务 |
reloadOnCreate / reloadOnDelete | false | ESO 首次物化时工作负载还没起;删除更不该滚 |
syncAfterRestart | false | 否则 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 没有增量价值。
所以部署资产已经写好、放在集群安装器的组件目录里,开关默认关着。什么时候打开,看三个信号,任一出现就够:
- 凭据后端配了定期自动轮换,值会在脚本之外变化。
- 多人操作凭据后端,没法保证每次改值都跟着跑脚本。
- 第 ④ 段的脚本已经能被 Secret 变更事件触发,③ 和 ④ 可以同时自动化。
在那之前,链路是「改凭据后端 → 脚本重写 Config Center → 脚本重启提供方」,三步都由同一个脚本按顺序做,不一致窗口只有几分钟。
验证要看什么
「Reloader 的 Pod 是 Running」什么都证明不了。要证明的是「Secret 改值之后,带注解的工作负载真的滚动了,而且新 Pod 读到了新值」。
自检脚本做的事:
- 在临时命名空间建一个 Secret,
PASSWORD=v1。 - 建一个带
secret.reloader.stakater.com/reload注解的 Deployment,容器启动时打印PASSWORD。 - 记下 Deployment 的
status.observedGeneration。 - 把 Secret 改成
PASSWORD=v2。 - 60 秒内
observedGeneration必须递增;等新 Pod 就绪后,日志里必须出现PASSWORD=v2。
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 |
| Reloader | Secret 变了滚动引用方 | 不读值、不连后端、不管非 Secret 消费方 |
| Config Center 同步脚本 | 把地址与凭据写进配置中心并滚动业务服务 | 不管理凭据后端 |
| Argo CD | 让集群状态收敛到 Git | Secret 的值不在 Git 里,它看不见变化 |
对本系列的结论
Reloader 是「提供方自动跟上凭据后端」的正确工具,接入成本是一行注解,运行成本不到 30 MiB。但它只自动化了凭据链路的一段。在这个项目里,真正决定轮换能不能闭环的是那段它看不见的路——配置中心里的消费方。先把那段的脚本做扎实,再打开 Reloader,顺序不能反。