背景脱敏:一套服务端的发布链路。源码仓的 CI 构建镜像,把「本次部署哪个版本」写进一个独立的部署账本仓;集群里的 ArgoCD 只认账本仓,照着账本把线上改成账本描述的样子,人和 CI 都没有直接改集群的权限。本文复盘半年下来,哪些设计扛住了,哪些坑反复踩。
扛住了:一个配置文件只能有一个主人
账本仓里同一个 Helm chart 的 values 被拆成五档,ArgoCD 按顺序叠加:
| 文件 | 内容 | 唯一写入方 |
|---|---|---|
values.yaml |
服务清单、默认资源档位 | 源码仓(CI 整份覆盖同步) |
values-prod.yaml |
副本数、资源、环境变量 | 源码仓(CI 整份覆盖同步) |
values-prod-secrets.yaml |
密码与密钥 | 运维(账本独有) |
values-prod-runtime.yaml |
只有一行:本批次镜像 tag | 只有 CI |
values-prod-override.yaml |
事故临时覆盖,排最后 | 运维应急 |
分档看着啰嗦,但它解决的是 GitOps 最经典的失败模式:两个写入方争同一个文件,谁后写谁赢,且没有人知道发生过覆盖。分档之后每次冲突都变成一个明确的权属问题——「这个键归谁」——而不是一次静默丢失。
早期没分档时踩过一次典型的:CI 每批次要写镜像 tag,同一份文件里还有人手写的配置,CI 的整份覆盖会把人的改动抹掉,而 CI 的日志显示「同步成功」。
扛住了:每一道门禁都必须被证明能红
我们给每道门禁都留了一张验证表,逐项写「怎么造出红灯」。这不是形式主义。开发期真的踩到过一次:
一个用 awk 解析配置的门禁脚本,变量名取了 awk 的内建函数名,导致整段语法错误 → awk 什么都不输出 → 脚本判断「没有发现违规」→ 打印「✅ 合规」。
门禁坏掉的样子,和门禁通过的样子,一模一样。
修法有两条:变量改名只是治标,真正的修复是对 awk 的退出码做硬校验——解析器不可用一律当失败。从此定了一条规矩:只跑出「通过」不算验证过,必须造一次红。
扛住了:给门禁本身也加门禁
同步脚本有一份「哪些 chart 模板要同步到账本」的白名单,CI 又有一份 changes: 规则决定「改了哪些文件才触发同步」。这两份清单必须一一对应,否则:
- 白名单有、
changes:没有 → 改了不触发同步,改动静默不生效 - 反过来 → 触发了但不同步,同样静默
这类失配没有任何报错,症状只是「我明明改了,线上没变」。所以写了一个单元测试,双向比对这两份清单,任何一边加了项另一边没加,测试直接失败。同一个测试还强制「每个 chart 模板都必须在某份清单里有归属」——曾经一次性查出 7 个没有归属的模板。
能被这类测试发现的问题,靠 code review 基本发现不了。
反复踩:门禁把正确流程判成了违规
这是我见过最有教育意义的一个。
漂移门禁的作用是比对「源码仓的配置」和「账本仓的配置」是否一致,不一致就拦住发布。它有两个属性:
- 比对是对称的:取两边键的并集逐个比,不区分谁领先;
- 它在流水线里排在同步之前,是构建任务的前置。
而把源码仓配置推进账本的那个同步任务,排在构建之后。
于是:任何一次「在源码仓正常改配置并合入主干」的行为,下一个批次一定会被判为漂移——因为账本还没来得及被同步——然后构建、同步、部署全被跳过。
更糟的是它的副作用。想解除拦截,最快的办法是人工先把账本推到源码仓前面。门禁把正确流程判成违规,反过来把「先手工推账本」这个危险操作训练成了标准操作。
修法是给比对加方向:
- 账本有而源码仓没有的键 → 阻断(账本被推到源仓前面,这才是真危险)
- 源码仓领先 → 放行,交给本批次的同步收敛
但「放行」引入了一个未经证明的假设:本批次的同步真的会收敛。此前没有任何东西验证这句话——changes: 漏路径、白名单失配、拷贝路径写错,都会让它变成空话,而症状是源码仓的意图在生产环境从未生效。所以又补了一个同步之后的无方向全量比对,只告警不阻断(跑到那一步已经部署完了,拦也没用)。
反复踩:紧急止血没有合法落点,就会逼出违规操作
一次线上服务 OOM,止血时有人直接改了账本仓的配置文件。后果是双重的:
- 下一次同步会用源码仓的版本整份覆盖,改动静默消失;
- 在被覆盖之前,漂移门禁判「账本有源仓没有」,整个发版批次被阻断。
翻账本历史,同一模式至少三次,都是「临时调个参数,随后改回」,然后没有人记得改回。
后果极不对称:直写账本只要 30 秒,代价是下一个发版批次一件事都做不了。
还有个细节值得单独说。这次热修写的是一个 list 类型的键(环境变量列表),而 chart 里本来就有一个标量键能达到同样目的。Helm 对 list 是整份替换而不是合并,所以 list 键在跨文件叠加时必然互相顶掉,而标量键可以干净合并。如果当时写的是标量,这次漂移根本不会是阻断级的。
最终的解法不是「禁止直写」,而是给紧急操作一个合法落点:新增一个 override 文件,排在所有 values 的最后,账本独有、不参与同步与比对。它有三条硬规则:
- 只允许标量键,禁止 list;
- 每一项必须带注释写明工单、负责人、过期日期;
- 常态是空文件——空即目标状态。
配套两道检查:出现 list 键直接阻断(它真能弄坏生产);条目过期只告警不阻断(台账卫生问题不该让整个批次停摆),但每个发布批次都念一次,这是过期条目唯一的回收压力来源。
约束的合规成本高于绕行成本时,一定会逼出绕行设计。 与其禁止,不如把正道修得比歪路好走。
反复踩:纪律写进了文档,但没有人在数
海外集群用一条长期发布分支做部署快照,定案写得很清楚:一切配置改动先进主干,发布分支纯做快照、零专有提交,发布就是把主干合并进来。理由充分——发布分支上的专有提交会让两条分支永久分叉,下次合并必冲突。
半年后实测:
git rev-list --count main..release-branch → 74
git rev-list --count release-branch..main → 1
发布分支不只有专有提交,还领先主干 74 个提交。纪律不是偏离,是完全反过来了。
问题不在于人不守纪律,在于没有任何东西在数这个数字。上面那条命令是一秒钟的事,接进 CI 或日报,超阈值就点名,半年前就该报警。
这条教训在别的地方也成立:凡是「迁移」「收口」「下线」类任务,判据必须是旧路径的调用方计数归零,而不是「新东西已经建好了」——这两件事之间隔着全部调用方。所以这类任务必须先有一个会数数的检查,完成声明里附上这个数从 N 降到 0。
反复踩:陈旧注释比没有注释更危险
排查时被自己仓库里的注释坑过不止一次:
- 比对工具的头部注释写着「同步脚本不同步某某文件」——那是三个月前拆分权属之前的事实,信了它会把排查方向带偏一整轮;
- 一份部署文档写着「跑某某命令部署监控规则」——链路早已改成 GitOps,照着做的结果是几分钟后被自动回退,症状是「生效了又消失」。
判断部署真源要查集群里 ArgoCD 的 Application 定义,不信注释。 没有注释顶多是不知道,陈旧注释是看起来像答案。
一条元教训:不对称就是遗漏信号
这套链路上有两个系统:主服务端和管理后台,共用同一套 ArgoCD、同一个团队。复盘时把两边逐项对齐,结果很清楚:
| 主服务端 | 管理后台 | |
|---|---|---|
| 密钥独立分档 | 有 | 无(明文混在配置里) |
| 账本仓 CI 门禁 | 6 项 | 一个 CI 文件都没有 |
| 前端构建检查 | — | 主干上不经任何检查直接构建 |
| 部署后验证 | 机器人真实登录跑一轮业务 | 无 |
管理后台的每一个问题,主服务端都已经解决过,方案现成。这不是取舍,是遗漏。
同类信号在更小的尺度上也管用:同一张配置表里,A 类型有「道具 ID」字段而并列的 B 类型没有,但两者都要发放奖励——这个不对称本身就该触发追问。我们踩过一次,缺失的字段在表里躺了 20 天。
教训
- 一个文件一个主人。分档啰嗦,但把静默覆盖变成了显式的权属问题。
- 只跑出通过不算验证过,每道门禁都要造一次红——门禁坏掉的样子可能和通过一模一样。
- 给门禁本身加门禁。白名单类的失配没有报错,只有靠数数式的测试才能发现。
- 门禁的顺序和方向都是语义的一部分。对称比对表达的是「两份必须相等」,而权属约定表达的是「一方跟随另一方」,这两件事在窗口期内必然冲突。
- 紧急操作要有合法落点,否则一定被逼出违规直写,而且没有回收机制。
- 纪律要有一个会数数的检查,不然半年后你才知道它早就反过来了。
- 不对称即遗漏。两个同构系统的能力差,通常不是深思熟虑的取舍。