GitOps 账本仓复盘:什么设计扛住了,什么坑反复踩

一套跑了半年的两仓发布链路

Posted by walikrence on September 8, 2026

背景脱敏:一套服务端的发布链路。源码仓的 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 基本发现不了。

反复踩:门禁把正确流程判成了违规

这是我见过最有教育意义的一个。

漂移门禁的作用是比对「源码仓的配置」和「账本仓的配置」是否一致,不一致就拦住发布。它有两个属性:

  1. 比对是对称的:取两边键的并集逐个比,不区分谁领先;
  2. 它在流水线里排在同步之前,是构建任务的前置。

而把源码仓配置推进账本的那个同步任务,排在构建之后

于是:任何一次「在源码仓正常改配置并合入主干」的行为,下一个批次一定会被判为漂移——因为账本还没来得及被同步——然后构建、同步、部署全被跳过。

更糟的是它的副作用。想解除拦截,最快的办法是人工先把账本推到源码仓前面。门禁把正确流程判成违规,反过来把「先手工推账本」这个危险操作训练成了标准操作。

修法是给比对加方向:

  • 账本有而源码仓没有的键 → 阻断(账本被推到源仓前面,这才是真危险)
  • 源码仓领先 → 放行,交给本批次的同步收敛

但「放行」引入了一个未经证明的假设:本批次的同步真的会收敛。此前没有任何东西验证这句话——changes: 漏路径、白名单失配、拷贝路径写错,都会让它变成空话,而症状是源码仓的意图在生产环境从未生效。所以又补了一个同步之后的无方向全量比对,只告警不阻断(跑到那一步已经部署完了,拦也没用)。

反复踩:紧急止血没有合法落点,就会逼出违规操作

一次线上服务 OOM,止血时有人直接改了账本仓的配置文件。后果是双重的:

  • 下一次同步会用源码仓的版本整份覆盖,改动静默消失
  • 在被覆盖之前,漂移门禁判「账本有源仓没有」,整个发版批次被阻断

翻账本历史,同一模式至少三次,都是「临时调个参数,随后改回」,然后没有人记得改回。

后果极不对称:直写账本只要 30 秒,代价是下一个发版批次一件事都做不了。

还有个细节值得单独说。这次热修写的是一个 list 类型的键(环境变量列表),而 chart 里本来就有一个标量键能达到同样目的。Helm 对 list 是整份替换而不是合并,所以 list 键在跨文件叠加时必然互相顶掉,而标量键可以干净合并。如果当时写的是标量,这次漂移根本不会是阻断级的。

最终的解法不是「禁止直写」,而是给紧急操作一个合法落点:新增一个 override 文件,排在所有 values 的最后,账本独有、不参与同步与比对。它有三条硬规则:

  1. 只允许标量键,禁止 list;
  2. 每一项必须带注释写明工单、负责人、过期日期;
  3. 常态是空文件——空即目标状态。

配套两道检查:出现 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 天。

教训

  • 一个文件一个主人。分档啰嗦,但把静默覆盖变成了显式的权属问题。
  • 只跑出通过不算验证过,每道门禁都要造一次红——门禁坏掉的样子可能和通过一模一样。
  • 给门禁本身加门禁。白名单类的失配没有报错,只有靠数数式的测试才能发现。
  • 门禁的顺序和方向都是语义的一部分。对称比对表达的是「两份必须相等」,而权属约定表达的是「一方跟随另一方」,这两件事在窗口期内必然冲突。
  • 紧急操作要有合法落点,否则一定被逼出违规直写,而且没有回收机制。
  • 纪律要有一个会数数的检查,不然半年后你才知道它早就反过来了。
  • 不对称即遗漏。两个同构系统的能力差,通常不是深思熟虑的取舍。