我在评审一套平台架构时,经常听到一句话:
我们的 YAML 已经全部放到 Git 里了,所以现在已经是 GitOps。
这句话不能说完全错,但它把“使用 Git”与“形成 GitOps 闭环”混在了一起。
Git 解决的是版本管理、协作和审计问题。GitOps 还要继续回答:谁把 Git 中的期望状态带到目标环境?谁持续观察实际状态?有人绕过 Git 改了线上资源之后,系统会发生什么?
所以,我不会先问团队“你们是不是 GitOps”,而会先问四个更具体的问题。
我的结论:存入 Git 是起点,不是终点
严格意义上的 GitOps,通常应同时具备四项能力:
- Declarative:系统的期望状态以声明式配置表达。
- Versioned and Immutable:期望状态被版本化保存,历史完整且不可随意篡改。
- Pulled Automatically:软件代理从期望状态来源自动拉取声明。
- Continuously Reconciled:软件代理持续观察实际状态,并尝试让它回到期望状态。
这四项原则中,把文件放入 Git,主要覆盖了第二项;如果 Git 中保存的是声明式配置,还可以覆盖第一项。但第三项“自动拉取”和第四项“持续调谐”,不会因为仓库里多了一个 deployment.yaml 就自动出现。
我更愿意把常见实践分成三个层级:
| 层级 | 典型形态 | Git 的角色 | 变更如何进入环境 | 漂移如何处理 |
|---|---|---|---|---|
| Git-backed configuration | YAML、脚本或参数保存在 Git | 版本仓库和审计记录 | 人工执行或从控制台操作 | 通常依赖人工发现 |
| Git-driven delivery | 合并 PR 后由 CI/CD 主动部署 | 期望状态来源和流程入口 | GitHub Actions、Jenkins 等从外部 Push | 定时扫描、下次流水线或人工修复 |
| Pull-based GitOps | 集群内 Controller 持续关注 Git | 期望状态的权威来源 | Controller 自动 Pull 并 Reconcile | 持续告警或自动恢复 |
这三层都可能是合理的工程选择。问题不在于第二层是不是“足够高级”,而在于团队不能用第三层的名字,掩盖第二层尚未具备的能力。
同一份 YAML,可能对应三种完全不同的系统
假设 Git 中保存了下面这份声明:
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout-api
spec:
replicas: 3
仅看文件内容,我们无法判断系统是不是 GitOps。真正决定系统性质的是围绕这份文件建立的控制回路。
场景一:Git 只是存储位置
工程师合并配置后,在本地执行:
git pull
kubectl apply -f deployment.yaml
这里已经比“只在控制台里点”更好:配置有历史,变更可以 review,也可以找到责任边界。但 Git 仍然只是文件来源,最终执行依赖某个人的电脑、权限和操作时机。
如果有人在线上把副本数改成 5,Git 不会知道,环境也不会自动恢复。
场景二:Git 驱动 Push-based delivery
合并 PR 后,GitHub Actions 或 Jenkins 执行:
Git -> CI/CD runner -> kubectl apply -> Kubernetes
这时,交付过程已经自动化。我们还可以增加审批、策略检查、manifest render、live diff、审计证据和回滚门禁。它完全可以是一套严谨、可靠的持续交付系统。
但从控制方向看,仍然是集群外的流水线拿着写权限向目标环境 Push。线上副本数被改成 5 以后,如果没有独立的 drift detection,系统可能一直保持 5;即使有定时扫描,恢复也取决于下一次 workflow 何时运行。
场景三:Pull-based continuous reconciliation
目标环境中运行 Argo CD、Flux 或其他符合相同控制模型的 Controller:
Git <- Controller 主动拉取
|
v
Compare desired/live
|
v
Reconcile drift
Controller 持续看到 Git 期望副本数为 3、线上实际副本数为 5。根据配置的 reconciliation policy,它会发出告警,或自动把副本数恢复为 3。
这一步才是 GitOps 与普通“Git + 自动部署”最关键的分水岭:Git 不只是触发一次部署,而是持续参与系统控制。
我用四个问题判断,而不是看工具名称
一、Git 中保存的是“期望状态”,还是“操作步骤”?
声明式配置描述系统最终应该是什么样子:
checkout-api 应运行 3 个副本
命令式脚本描述人或机器应该依次做什么:
先登录服务器
再停止旧进程
复制文件
执行启动命令
把一组 Bash 脚本存进 Git,当然能获得版本历史,但脚本本身不一定构成可持续比较的 desired state。Controller 很难仅凭“执行过哪些步骤”判断当前系统是否已经偏离目标。
所以我首先看的是配置模型,不是仓库地址。
二、历史真的版本化且不可变吗?
“使用 Git”也有不同质量。
如果团队允许 force push、直接修改默认分支、覆盖 release tag,或者部署时依赖仓库外的一组可变参数,那么 Git 中的记录未必能完整还原当时部署的状态。
我会继续检查:
- 生产变更是否必须通过 PR 和 review。
- 默认分支是否受到保护。
- release tag 和制品是否禁止覆盖。
- 部署是否绑定明确 commit,而不是浮动分支名。
- Git 外部参数是否同样有版本和审计记录。
不可变不是一句口号,而是仓库规则、制品规则和审批规则共同形成的约束。
三、到底是谁发起变更?
这是最容易被流程图隐藏的问题。
Push-based:CI/CD -> Cluster API
Pull-based:Controller -> Git / Artifact source
在 Push-based 模型里,CI/CD runner 通常需要目标环境写权限。流水线是部署执行者,集群是被操作对象。
在 Pull-based 模型里,集群内 Controller 主动获取已批准的期望状态。CI 可以负责测试、扫描、render 和生成 evidence,但通常不需要直接持有生产集群写权限。
两者的权限模型、网络边界、故障模式和灾备方式都不同。架构文档如果只写“通过 GitOps 部署”,却不画出箭头方向,我会认为这个设计还没有说清楚。
四、发生漂移以后,谁负责闭环?
这是我最看重的问题。
设想一次很常见的应急操作:值班人员为了止损,临时把 Deployment 副本数从 3 调到 5。事件结束后,Git 仍然写着 3。
接下来可能有四种结果:
- 没有人发现,线上长期保持 5。
- 下次发布时被新的
kubectl apply顺带覆盖。 - 定时 drift workflow 发现差异,再由人决定是否修复。
- Controller 持续检测差异,并按策略告警或自动恢复。
前两种没有形成漂移治理闭环;第三种是可以落地的补偿机制;第四种才是 continuous reconciliation。
我不要求所有资源都必须开启自动恢复。生产环境里,有些高风险资源更适合先告警、再人工确认。但无论采用哪种 policy,系统都应该明确回答:检测频率是什么、谁收到告警、谁有权接受或修复漂移、紧急变更如何回写 Git。
为什么“广义 GitOps”仍然有价值
在真实组织里,“GitOps”经常被用作一个更宽泛的 umbrella term,泛指:
- Git 作为变更入口。
- Pull Request 作为审批载体。
- 配置即代码。
- 自动化测试和部署。
- 用 Git 历史完成审计与回滚。
我理解这种用法。它帮助团队从控制台手工操作走向可 review、可追踪的工程流程,本身就是重要进步。
但我会给“广义 GitOps”加一个前提:在架构和风险讨论中,必须把具体实现写出来。
例如,与其写:
本平台采用 GitOps 部署。
不如写:
本阶段采用 Git-as-source、PR-governed、Push-based continuous delivery。
GitHub Actions 在审批后向目标集群应用非敏感资源;
漂移由定时 workflow 检测,尚未引入集群内持续调谐 Controller。
第二种表述没有第一种听起来简洁,却让权限、安全、漂移和恢复边界一目了然。对架构师来说,这种准确性比术语显得先进更重要。
Push-based 不是错误答案
我不赞成把 GitOps 讨论变成工具或流派之争。
迁移初期选择 Push-based delivery,可能有非常现实的理由:
- 现有 CI/CD 和审批体系已经成熟。
- 团队需要先做 shadow diff,再逐步接管在线资源。
- 管理 Controller 自身的 bootstrap 和灾备方案尚未确定。
- 某些目标环境暂时不允许安装额外 Controller。
- 团队希望先建立资源 allowlist、人工门禁和变更证据。
这些都可以是负责任的阶段性决策。我的要求只有两个:
- 准确命名当前架构,不借 GitOps 标签隐藏能力缺口。
- 对漂移检测、集群写权限、失败恢复和下一阶段演进作出显式设计。
一个边界清楚的 Push-based 平台,往往比一个只装了 Argo CD、却允许所有人绕过 Git 直接修改生产环境的平台更可靠。
装了 Argo CD,也不自动等于做好了 GitOps
GitOps 是操作模型,不是产品安装清单。
即使集群里已经运行 Argo CD,如果 Application 长期关闭自动同步、团队习惯在 UI 里直接改参数、紧急变更从不回写 Git、关键资源被设置为永久忽略差异,那么整个系统仍可能没有建立可信的 desired-state 闭环。
反过来,工具也不只 Argo CD。Flux 或其他 Controller 同样可以实现 Pull 和 Reconcile。判断标准应该回到控制模型:声明在哪里,谁拉取,谁比较,谁调谐。
我建议的渐进式落地路径
不是所有团队都要一步跳到最终形态。我更倾向按能力逐层补齐。
第一阶段:先让 Git 成为可信变更入口
- 将声明式配置纳入 Git。
- 生产变更通过 PR、review 和受保护分支。
- 禁止 release ref 和不可变制品被覆盖。
- 消除只存在于个人电脑和控制台里的关键参数。
- 建立 Git commit、构建制品与部署记录之间的追踪关系。
第二阶段:建立受治理的 Push-based delivery
- 由 CI/CD 统一执行 render、validate、diff 和 deploy。
- 对生产环境设置审批、资源 allowlist 和最小权限身份。
- 将部署失败、部分成功和回滚流程设计成明确状态机。
- 增加定时 drift detection,并定义告警与修复责任。
- Secret 使用专门的 secret manager,不以明文进入 Git 或日志。
第三阶段:引入 Pull-based reconciliation
- 在目标环境中部署并保护 GitOps Controller。
- 让 Controller 拉取经过批准的期望状态。
- 按资源风险定义 auto-sync、self-heal、prune 与人工审批策略。
- 将 CI 的职责收敛到测试、策略、安全扫描和变更证据。
- 补齐 Controller bootstrap、自身升级、凭证轮换、监控和灾备。
这条路径的重点不是“终于有资格叫 GitOps”,而是每一步都减少一个明确的风险:不可审计变更、人工部署错误、流水线过度授权、配置漂移或恢复依赖个人经验。
一张表完成架构自检
| 检查项 | 评估结果 | 否时意味着什么 |
|---|---|---|
| Git 中保存可比较的声明式期望状态 | 待评估 | 可能只是脚本版本管理 |
| 生产变更受 PR、review 和分支保护约束 | 待评估 | 历史可信度与审批边界不足 |
| 部署绑定明确 commit 和不可变制品 | 待评估 | 同一次部署难以精确复现 |
| 目标环境中的 Agent 自动拉取声明 | 待评估 | 当前仍是人工或 Push-based delivery |
| Agent 持续比较 desired state 与 live state | 待评估 | 漂移可能长期不可见 |
| 漂移有明确的告警或自动恢复策略 | 待评估 | 发现问题后无法形成闭环 |
| 紧急线上变更会回写 Git | 待评估 | Git 可能失去权威事实源地位 |
| CI 不需要持有生产集群广泛写权限 | 待评估 | 需要额外控制 runner 与凭证风险 |
| Controller 自身有升级、监控和灾备方案 | 待评估 | GitOps 控制面可能成为新的单点风险 |
如果前两三项已经完成,我会称它为 Git-backed 或 Git-governed delivery;如果由 CI 主动部署,我会明确写 Push-based;当自动拉取与持续调谐也形成闭环时,再称为严格意义上的 Pull-based GitOps。
最后:不要用一个名词代替架构设计
“存到 Git 就是 GitOps 吗?”
我的答案是:不是。它是必要基础之一,但不是充分条件。
GitOps 真正改变的,不是 YAML 存放在哪里,而是谁持续负责让现实世界接近期望状态。
作为 DevOps 架构师,我更关心的从来不是系统能不能贴上一个流行标签。我关心的是:Git 是否真的是可信事实源,变更是否可审计,执行身份是否最小权限,线上漂移是否可见,以及系统在没有某个工程师手工介入时,能不能稳定回到我们声明的状态。
当这些问题都有明确答案,GitOps 才不只是仓库里的一批文件,而是一套真正运行起来的控制系统。