GitOps — 把定义状态的权力交给仓库
一句话总结
GitOps 不是某个部署工具的名称,而是一项所有权声明:只有 Git 仓库有权定义集群状态。
为什么需要了解这些
使用 kubectl apply 部署的团队,必然会遇到这个问题:“当前生产环境运行的究竟是哪一个提交?”却没有人能自信回答。因为上周故障期间,有人通过 kubectl scale 增加了 Pod,有人使用 kubectl edit 修改了环境变量,而这些变更没有记录在任何地方。时间一长,集群就会漂移到仓库中不存在的状态,这就是漂移。
漂移真正的成本不是“配置略有不同”,而是无法复现。需要重建集群时,即使原样应用仓库内容,也无法得到之前相同的系统。作者把家庭实验室从 3 个节点扩展到 7 个节点时,新节点上残留着旧集群的内容(旧版 kubelet、旧证书);从人们开始依靠记忆手动对齐差异的那一刻起,错误就出现了。
GitOps 通过一条规则扭转这个问题:仓库中没有的内容不应存在于集群,仓库中存在的内容必须存在于集群。 这样,“当前运行着什么”就成为可以通过 git log 回答的问题。
工作原理
GitOps 有四项原则,必须全部满足。
| 原则 | 核心问题 | 违反后的症状 |
|---|---|---|
| 声明式 | 期望状态是否写成代码 | 无法复现、配置漂移 |
| 版本管理 | 所有变更是否都保留为提交 | 不知道谁在何时为何修改 |
| 自动应用 | 是否无需人工操作即可应用 | 发布延迟、人为错误 |
| 自我修复 | 出现偏差时是否自动恢复 | 漂移累积、环境不一致 |
第三项采用 pull 模式尤其重要。CI 直接向集群推送的 push 模式,必须把集群凭据交给 CI。pull 模式由集群内的代理读取仓库,凭据不会离开集群。这项选择会彻底改变安全边界。
实际工作还要增加两条规则。第一,将应用代码仓库与清单仓库分开。 如果放在同一仓库,每次应用 CI 运行都会触发同步,代码评审与发布评审也会混在一个 PR 中。第二,不使用 :latest 标签。 如果同一个提交在昨天和今天启动的镜像不同,该仓库就不再能够定义状态。镜像标签必须使用提交哈希、语义化版本等一经确定就不再改变的值。
生产现场中的常见情况
第一,提交消息是判断回滚目标的唯一依据。 故障期间,人们查看的不是代码,而是 git log --oneline 的一屏输出。如果十条消息都是 fix、update,就无法判断应回滚哪个提交。GitOps 中的回滚就是 git revert,而选择 revert 对象,本质上就是阅读提交消息。
第二,紧急手工修改必须回到代码中。 如果通过 kubectl scale 救活服务,就必须二选一:把该值提交到仓库,或恢复到仓库基准。若保持不管,下一次同步会悄悄撤销修改,再次触发同一故障。这与 Ansible 使用 -e 或 Terraform 手工修改导致的事故结构完全相同。
第三,仓库结构就是权限边界。 作者的家庭实验室把清单存放在 Gitea(10.0.0.200),由 ArgoCD(10.0.0.201)读取。按应用、环境划分目录后,未来可以沿用这些边界划分评审者和发布权限。反过来,如果所有内容都堆在同一目录,就没有划分权限的位置。
GitOps 真正困难的三个地方
原则很简单,但投入运维后,总会在同样三个地方遇到阻碍。
第一,秘密不能提交到 Git。 因此,“一切都在 Git 中”的原则最先被打破。 解决方案有三种:加密后提交(SOPS、sealed-secrets)、引用外部存储(External Secrets),或完全从集群外注入。前两种更符合 GitOps,但必须从一开始就承认:无论选择哪一种,仍然需要管理密钥的位置。
第二,有些值会由集群自行修改。 HPA 会调整 replicas,Webhook 会添加注解,Operator 会写入状态。如果 Git 持续撤销这些变化,就会产生无休止的回退循环。应明确配置忽略这些字段。
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers: ["/spec/replicas"]
第三,紧急手工修复会被悄悄撤销。 故障时通过 kubectl edit 临时阻止问题后,如果忘记处理,自动同步会在几分钟后恢复原状态。因此,手工修改后必须把事实记录到 Git,这种习惯应成为流程的一部分。暂时关闭自动同步也是方法,但如果忘记重新启用,从那一刻起 Git 与现实就会分离。
Synced 绿灯表示“与 Git 相同”,并不表示“运行正常”。 即使清单已按原样应用,Pod 仍可能处于 CrashLoopBackOff。健康状态必须另行检查,部署是否真正完成则要通过镜像摘要或修订版本确认。
回滚是 GitOps 最大的收益。 回滚变成撤销一个提交,因此变更内容与方式会自动留下记录。不过,数据库模式等不可回滚的变更不在这一收益范围内,这类变更必须拆分发布并保持前后兼容。
下一次实验要做什么
在 /root/gitops/repo 创建并提交清单仓库,把声明应用到 gitops-lab 命名空间。随后故意使用 kubectl scale 制造漂移,确认 kubectl diff 如何发现差异(存在差异时退出码为 1),再恢复到仓库基准。最后,亲自编写执行 diff → apply → 记录已应用提交的同步脚本,手动体验 ArgoCD 控制器究竟替你完成了哪些工作。