GitOps:Git 成为系统运行时的真相之源
一、引言
前三篇已经解决了三个核心问题:容器让交付可重复,Kubernetes 让编排声明化,IaC 让基础设施可版本化。但有一个问题还悬在空中——这些声明怎么到达集群?变更怎么被执行?谁来触发,谁来保证?
“代码即声明”已经成立。但”执行声明”这件事本身,也需要被纳入工程纪律。否则,精心设计的声明式系统,最终还是靠一条手动执行的命令来触发——工程纪律在最后一公里断掉了。
GitOps 是这个问题的回答。它不是一个新工具,而是一套已经被社区验证过的交付方法论:让 Git 成为系统运行时的唯一真相之源,让集群自己来拿它需要的东西。
二、为什么:Push 模型的隐患,以及方向的翻转
传统 CI/CD 的 Push 模型
典型的传统 CD 流水线长这样:
flowchart LR Dev[开发者\n合并代码] --> CI[CI/CD 系统\nJenkins / GitHub Actions] CI -- "kubectl apply\n⚠️ 需要集群高权限凭证" --> K8s[Kubernetes 集群] CI -- "凭证存储在 CI Secrets" --> Cred[(高权限\nkubeconfig)] Cred --> CI
方向是向外的:外部系统主动把变更推进集群。这套模式在小规模下工作正常,但有两个随规模增长而放大的根本性隐患。
两个根本隐患
第一个:凭证暴露。
流水线要能操作集群,就必须持有集群的高权限凭证——kubeconfig、Service Account Token、云厂商的 Access Key。这些凭证存在 CI 系统的 Secrets 里。CI 系统一旦被攻破,攻击者直接获得集群的操作权限。
更隐蔽的问题是权限边界的模糊:流水线”能部署”几乎等价于”能做任何事”——因为 apply 一份恶意的 YAML 足以在集群里创建一个具有任意权限的 Pod。凭证的最小权限原则在 Push 模型下很难真正落实。
第二个:交付状态不可知。
流水线里的 kubectl apply 返回成功,只代表 API Server 接受了你的声明。Pod 有没有真正启动,有没有 CrashLoop,有没有因为调度失败而卡在 Pending——流水线不知道,也不再关心,它已经标记这次部署为”成功”并继续下一个任务了。
部署成功和服务可用之间有一条沟,Push 模型对这条沟视而不见。
Pull 模型的翻转
GitOps 把方向反过来:不是外部系统把变更推进集群,而是集群里运行的 operator 持续 watch Git 仓库,发现差异就主动拉取并执行 sync。
flowchart LR Dev[开发者] --> Git[Git 仓库\nConfig Repo] Git -- "watch 变化" --> Op[GitOps Operator\n运行在集群内] Op -- "pull + sync\n凭证不离开集群" --> K8s[Kubernetes 集群] CI[CI 流水线] -- "只需写权限" --> Git
权限模型的根本改变:流水线只需要能写 Git 仓库,不再需要能操作集群。凭证的暴露面从”集群高权限”收缩到”仓库写权限”。集群的凭证不再需要离开集群。
Git 获得了运维语义
Pull 模型最深刻的副产品是:运维操作终于和代码变更拥有了同等的工程纪律。
当 Git 是真相之源:
git commit= 一次部署意图的提交,有作者,有时间戳,有 diffgit revert= 回滚(不是”撤销操作”,是”用旧的声明替换现在的声明”,是一个新的正向提交)git log= 完整的变更审计,谁在什么时候做了什么- Pull Request = 基础设施变更的审批流程,可以 review,可以讨论,可以拒绝
“紧急扩容”不再是一条没有记录的 kubectl 命令,而是一个有 reviewer 的 PR。运维的工程纪律和开发的工程纪律,第一次真正对齐了。
三、GitOps 的核心机制
Git 作为 desired state store
这里需要澄清一个认知:Git 在 GitOps 里不是备份,不是文档存档,是系统运行时的输入。
Operator 实时消费 Git 的内容来决定集群应该处于什么状态。这和 etcd 对 Kubernetes 的意义类似——etcd 是 K8s 集群的真相之源,Git 是整个交付体系的真相之源。
这个定位带来一个强推论:如果 Git 里的声明和集群现状不一致,Git 赢。不是”我去判断哪个更正确”,是 Git 说什么就是什么,集群需要收敛过去。任何绕过 Git 的直接修改,在哲学上都是无效的——它会被 operator 纠正。
Operator 循环:watch → diff → sync
GitOps operator 跑着一个和 K8s reconciliation loop 完全同构的循环:
loop:
git_state = 拉取 Git 仓库里的 manifest(期望状态)
live_state = 观察集群当前状态(现实状态)
if git_state != live_state:
sync() # 把集群往 git_state 收敛这不是巧合,是有意的设计继承。ArgoCD 本身就是一个 Kubernetes Operator,用 Application CRD 描述”哪个 Git 仓库对应哪个集群/namespace”,跑在 K8s 上,通过 K8s API 执行 apply。GitOps 是在 K8s reconciliation loop 之上再加了一层——K8s 负责让 pod 收敛到 Deployment 描述的状态,GitOps operator 负责让 Deployment 收敛到 Git 描述的状态。
Self-heal
Self-heal 是 GitOps 最有力的保证,也是和传统 CD 最本质的区别。
任何绕过 Git 直接修改集群的操作——手动 kubectl edit、紧急补丁、直接修改 ConfigMap——operator 在下一次 sync 时会把它改回 Git 里的状态。开启 auto-sync 的情况下,这个纠正通常在几分钟内发生。
这不是 bug,是 feature。Git 是真相,和 Git 不一致的东西不应该存在。唯一合法的修改路径是先改 Git,然后让 operator 来执行。Self-heal 把”先改 Git”从一个软性约定变成了一个系统强制的规则。
四、社区最佳实践全景
App Repo 与 Config Repo 分离
这是 GitOps 社区最早收敛、也最重要的实践。
两个仓库,两种职责
App Repo(源代码仓库)
├── 存放:业务代码、单元测试、Dockerfile
├── 谁在用:开发者
├── 变更频率:高(一天几十次 commit)
└── CI 在这里触发,产出物是容器镜像
Config Repo(GitOps 仓库,也叫 Infra Repo)
├── 存放:Kubernetes YAML / Helm values / Kustomize overlays
├── 谁在用:平台工程师、DevOps、以及 CI 自动更新
├── 变更频率:低(一天几次正式部署)
└── GitOps operator watch 这个仓库为什么必须分开
最直接的原因是频率不匹配:App Repo 一天有几十次 commit(开发者在提功能、修 bug、跑 CI),如果 Config Repo 和 App Repo 是同一个,每次代码 commit 都可能触发 GitOps sync,而其中 99% 根本不需要部署任何东西。
更重要的是职责边界:CI 的终点是”产出经过测试的镜像”,CD 的起点是”把这个镜像版本号写进 Config Repo”。两件事分开,各自清晰,互不干扰。
CI 与 CD 的重新定义
flowchart LR subgraph CI["CI(持续集成)"] Code[代码提交] --> Test[运行测试] Test --> Build[构建镜像] Build --> Push[推送到 Registry] Push --> UpdateTag[更新 Config Repo\n镜像 tag] end subgraph CD["CD(持续交付)"] Watch[Operator watch\nConfig Repo] --> Detect[检测到 OutOfSync] Detect --> Sync[sync 到集群] Sync --> Converge[K8s 完成收敛] end UpdateTag --> Watch
流水线的权限收缩
分离之后,CI 流水线只需要两个权限:推镜像到 Registry + 写 Config Repo。集群的 kubeconfig 不再需要出现在 CI 系统里。这是安全模型的本质改善。
多环境管理:社区收敛的两种模式
目录结构区分环境(主流)
config-repo/
├── base/ # 所有环境共用的基础声明
│ ├── deployment.yaml
│ ├── service.yaml
│ └── kustomization.yaml
└── overlays/
├── dev/
│ ├── kustomization.yaml # replicas: 1, image: :latest
│ └── patch-resources.yaml # 降低资源配置
├── staging/
│ ├── kustomization.yaml # replicas: 2, image: :rc-xxx
│ └── patch-resources.yaml
└── prod/
├── kustomization.yaml # replicas: 5, image: :v1.2.3
└── patch-resources.yaml # 生产级资源配置每个环境的 ArgoCD Application 指向对应的 overlay 目录,base 只写一份,差异用 patch 表达。
分支区分环境(较少用)
main 对应 prod,staging 对应 staging,dev 对应 dev。合并到对应分支触发对应环境的 sync。缺点明显:分支间 cherry-pick 容易产生漂移,“dev 有的改动 prod 没有”的情况很难察觉。
社区趋势是目录模式:所有环境的声明在同一个分支同一个视角里可见,变更历史在同一条线上,不存在跨分支遗漏的问题。
Kustomize 与 Helm 的定位
两者解决的问题不同,不是替代关系:
Kustomize:纯 overlay 思路,没有模板语法,只是 YAML 的结构化”叠加”。base 写通用声明,overlay 只写和 base 的差异(patch)。适合环境差异小、以 K8s 原生 YAML 为主的场景。ArgoCD 和 Flux 都原生支持 Kustomize,无需额外安装。
# overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
- path: patch-replicas.yaml
images:
- name: myapp
newTag: v1.2.3 # 只改这一行来发布新版本Helm:chart 打包,用 values.yaml 参数化所有可变部分。适合需要封装复用的复杂应用,或直接使用社区 chart 的场景(Prometheus、Nginx Ingress、cert-manager 等都以 Helm chart 形式分发)。
# ArgoCD Application 使用 Helm chart
spec:
source:
repoURL: https://prometheus-community.github.io/helm-charts
chart: kube-prometheus-stack
targetRevision: 56.0.0
helm:
valuesFiles:
- values-prod.yaml两者不互斥。常见组合:用 Helm chart 渲染出 manifest,再用 Kustomize 做最后一层环境级 patch——社区称之为”Helm + Kustomize”模式。
ArgoCD vs Flux:选型依据
不是”哪个更好”,是”哪个更适合你的团队和场景”。
ArgoCD
核心概念是 Application CRD,一个 Application 描述”这个 Git 路径下的 manifest 应该部署到哪个集群的哪个 namespace”:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp-prod
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/config-repo
targetRevision: main
path: overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: myapp
syncPolicy:
automated:
selfHeal: true # 开启 self-heal
prune: true # 删除 Git 里没有的资源适合场景:有 Web UI 可视化需求(多团队需要看到各自服务的同步状态);需要完善的多租户 RBAC;平台工程师希望给开发团队暴露一个友好的 GitOps 界面。
Flux
组件化的 GitOps toolkit:source-controller 负责拉取 Git / Helm / OCI 内容,kustomize-controller 负责 Kustomize reconciliation,helm-controller 负责 Helm release 管理,notification-controller 负责通知……每个组件独立、可替换。
适合场景:平台团队需要深度定制 GitOps 行为;不需要 UI;希望 GitOps 的配置本身也完全通过 Git 管理(Flux 的配置是 CRD,也可以放在 Git 里,用 Flux 管理 Flux 本身)。
核心判断:选哪个工具不是最重要的,重要的是理解 operator 模式和 pull 模型——这套思维在任何工具上都成立。从学习角度看,ArgoCD 的 Web UI 让同步状态更直观,适合入门。
渐进式交付:GitOps + Argo Rollouts
传统滚动更新(K8s 默认的 RollingUpdate)是全量替换——新版本逐步替换旧版本,但最终所有流量都打到新版本,出了问题影响全部用户。
渐进式交付在 GitOps 框架内做流量切割:先把一小部分流量发给新版本,观察错误率、延迟等指标,无异常再逐步扩大比例,最终全量。
Argo Rollouts 把发布策略变成声明,放进 Git:
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: myapp
spec:
replicas: 10
strategy:
canary:
steps:
- setWeight: 10 # 10% 流量到新版本,90% 到旧版本
- pause: {duration: 5m} # 观察 5 分钟
- setWeight: 30
- pause: {duration: 5m}
- setWeight: 60
- pause: {} # 暂停,等人工确认后继续
- setWeight: 100
selector:
matchLabels:
app: myapp
template:
# ... pod template发布策略也是代码,也在 Git 里,也可以被审查和回滚——这是渐进式交付和 GitOps 结合的核心价值。遇到问题,git revert 发布策略,ArgoCD 把 Rollout 对象改回旧状态,流量立刻切回旧版本。
五、GitOps 在整个体系里的位置
GitOps 不是孤立的工具,是把前三篇串起来的粘合层。
容器(01)提供了制品:镜像是 GitOps 交付的内容。CI 构建镜像、推到 Registry、把镜像 tag 写进 Config Repo——这是 CI 的终点,也是 GitOps 的起点。
Kubernetes(02)是执行环境:GitOps operator 本身是一个 K8s Operator,跑在 K8s 上,通过 K8s API 执行 apply。K8s 的 reconciliation loop 是 GitOps 的宿主——GitOps 负责让 Deployment spec 收敛到 Git 的声明,K8s 负责让 pod 收敛到 Deployment spec。
IaC(03)管理下一层:应用层用 GitOps(K8s manifest 在 Git 里,operator 负责 sync);基础设施层用 IaC(Terraform .tf 文件在 Git 里,CI/CD 负责 apply)。两者分工明确——GitOps 管 K8s 上的工作负载,IaC 管 K8s 集群本身和云资源。
完整的变更路径——端到端全景:
flowchart TD A[开发者 git push\nApp Repo] --> B[CI 流水线\n测试 → 构建镜像 → 推 Registry] B --> C[Registry\n存储新镜像] B --> D[更新 Config Repo\noverlay/prod 镜像 tag] D --> E[ArgoCD\n检测到 OutOfSync] E --> F[ArgoCD sync\nkubectl apply 到集群] F --> G[K8s 滚动更新\n新 Pod 起来,旧 Pod 销毁] G --> H[kubelet 拉取新镜像\nreadinessProbe 通过] H --> I[Service 切流量\n部署完成]
每一步都有记录,每一步都可以回溯,每一步的权限边界都是明确的。这条路径的起点是一个 git push,终点是服务运行新版本——中间没有任何人工操作。
六、故障剧场:亲手把它弄坏
实验一:直接 kubectl edit,看 self-heal 纠正
# 前提:ArgoCD 的 Application 开启了 auto-sync + selfHeal
# 直接修改 ArgoCD 管理的 Deployment(改 replicas 或 image)
kubectl edit deployment myapp -n myapp-namespace
# 观察 ArgoCD UI 或用 CLI 查看
argocd app get myapp
# Status: OutOfSync(立刻检测到偏差)
# 等待 auto-sync 触发(默认 3 分钟内)
# ArgoCD 把 Deployment 改回 Git 里的状态
kubectl get deployment myapp -n myapp-namespace
# replicas 和 image 回到了 Git 里的值这个实验让 self-heal 变得具体:你的手动修改没有”存活”超过三分钟。Git 是真相,和 Git 不一致的状态不被允许长期存在。
实验二:git revert 体验回滚语义
# 假设刚刚部署了一个有问题的新版本(v1.3.0)
# Config Repo 里的镜像 tag 是 v1.3.0
# 用 git revert 撤销这次变更
git log --oneline # 找到那次更新 image tag 的 commit hash
git revert <commit-hash> # 产生一个新的 commit,内容是"回到 v1.2.3"
git push origin main
# ArgoCD 检测到 Config Repo 变化,执行 sync
# 集群回滚到 v1.2.3对比手工”撤销”(直接 kubectl set image):git revert 是一个新的正向 commit,有作者,有时间戳,有 diff,有清晰的”我在什么时候因为什么原因回滚了”的历史。手工 kubectl 改动则什么都没有留下。
实验三:推一个格式错误的 manifest
# 故意在 Config Repo 里提交一个有语法错误的 YAML
# 比如缩进错误,或者引用了不存在的 ConfigMap
git add broken-deployment.yaml
git commit -m "broken: test sync failure"
git push
# ArgoCD 检测到变化,尝试 sync
# sync 失败:parsing error / validation error
argocd app get myapp
# Health: Degraded
# Sync Status: OutOfSync
# Message: yaml: line 12: did not find expected key
# 集群保持当前状态——坏的 manifest 没有被应用
kubectl get pods -n myapp-namespace
# 仍然是旧版本在跑,一切正常GitOps 的一个重要保证:坏的声明不会被应用,失败停在 operator 层,正在运行的工作负载不受影响。 这比 Push 模型里”流水线直接 apply 并因为格式错误破坏集群”要安全得多。
实验四:ArgoCD 宕机,GitOps 循环停了
# 模拟 ArgoCD 宕机
kubectl scale deployment argocd-server -n argocd --replicas=0
kubectl scale deployment argocd-application-controller -n argocd --replicas=0
# 向 Config Repo 推一个变更
git commit -m "update: bump image to v1.4.0"
git push
# 等待,观察集群
kubectl get pods -n myapp-namespace
# 仍然是旧版本——没有 operator 在 watch,变更不会自动生效
# 恢复 ArgoCD
kubectl scale deployment argocd-server -n argocd --replicas=1
kubectl scale deployment argocd-application-controller -n argocd --replicas=1
# ArgoCD 恢复后,检测到积累的差异,执行 sync
# 集群更新到 v1.4.0这个实验揭示了 GitOps 的一个隐含依赖:GitOps 的保证依赖 operator 在线。operator 不在,Git 里的变更不会自动生效,集群停留在 operator 宕机前的状态。ArgoCD 本身需要被监控和保障高可用——它是整个交付体系的关键路径。
七、日常排障:GitOps 流程坏了怎么查
App 一直 OutOfSync
# 查看具体 diff 在哪里
argocd app diff myapp
# 或者在 UI 里查看 diff 视图(ArgoCD UI 会高亮显示差异字段)
# 用 CLI 查看详细状态
argocd app get myapp --show-operation常见原因:
- 有人直接 kubectl edit 了资源:开启 selfHeal 后会自动纠正,或手动 sync 一次
- Helm chart 产生了不确定性渲染:某些 chart 会在每次渲染时生成随机字符串或时间戳,导致每次 diff 都不同——需要在 values 里固定这些值,或给字段加
argocd.argoproj.io/compare-options: IgnoreExtraneous注解 - Config Repo 里引用了不存在的资源:比如 Deployment 引用的 Secret 还没创建,argocd app get 会显示
MissingResource
Sync 失败
# 查看 sync 操作的详细错误
argocd app get myapp
# 错误信息在 "Operation" 部分
# 也可以看 K8s 层面的事件
kubectl get events -n myapp-namespace --sort-by='.lastTimestamp'常见原因及处理:
| 错误 | 原因 | 处理 |
|---|---|---|
yaml: unmarshal error | YAML 格式错误 | 修复 Config Repo 里的 manifest |
already exists | 试图创建已存在且不被 ArgoCD 管理的资源 | argocd app sync --force 或先删除冲突资源 |
is forbidden: User cannot create | ArgoCD Service Account 权限不足 | 给 ArgoCD 的 SA 补充必要的 RBAC 权限 |
context deadline exceeded | sync 超时(资源太多或集群响应慢) | 检查集群状态,调大 timeout 配置 |
CI 产出了新镜像,但没有触发 sync
排查衔接断在哪一步:
# 第一步:镜像真的推到 Registry 了吗?
docker pull registry.example.com/myapp:v1.4.0
# 第二步:Config Repo 里的 image tag 更新了吗?
git log --oneline config-repo/overlays/prod/kustomization.yaml
# 第三步:ArgoCD 检测到变化了吗?
argocd app get myapp | grep "Last Sync"
# 第四步:有没有 sync 失败的记录?
argocd app history myapp最常见的断点:CI 更新 Config Repo 的步骤静默失败了——比如 PR 没有自动合并(需要人工审批但没人审),或者 CI 脚本里更新 Config Repo 的命令失败了但没有被标记为关键错误,导致 CI 整体显示绿色但 Config Repo 实际上没有更新。
ArgoCD 不可用时的紧急操作
偶尔会遇到需要立即部署的紧急情况,但 ArgoCD 恰好不可用:
# 紧急时可以直接 kubectl apply
kubectl apply -f overlays/prod/ -n myapp-namespace
# 救火完成后,必须同步更新 Config Repo
# 确保 Config Repo 里的声明和你刚刚 apply 的内容一致
# ArgoCD 恢复后,sync 一次确认状态
argocd app sync myapp原则:先救火,后补 Git,不能只救火不补 Git。 如果只操作了集群而没有更新 Config Repo,ArgoCD 恢复后会把你的紧急修改覆盖掉——因为 Git 是真相,Git 里没有的东西不应该存在。
八、往哪走
Policy as Code:sync 前的策略校验
GitOps 保证”Git 里有什么,集群就运行什么”。但 Git 里的东西合法吗?符合安全规范吗?
OPA(Open Policy Agent)和 Kyverno 作为 K8s Admission Controller,在 sync 触发的 apply 到达 API Server 之前做策略校验:
- 没有设
resources.limits的 Deployment 拒绝入集群 - 容器镜像必须来自受信任的 Registry(防止供应链攻击)
- 禁止
--privileged容器 - 必须有
readinessProbe
策略本身也是代码,也在 Git 里,也通过 GitOps 部署。策略即代码(Policy as Code)是 GitOps 安全链条的最后一环。
Secrets 管理的难题
Git 里不能存明文密钥——这是 GitOps 一个棘手的例外。两种社区主流方案:
Sealed Secrets:用公钥在 CI 侧加密密钥,加密后的 SealedSecret 对象可以安全提交到 Git(加密内容在 Git 里,解密只能在集群内发生),集群里的 controller 负责解密为真正的 Secret。
External Secrets Operator:Secret 的真实内容存在 Vault / AWS Secrets Manager / GCP Secret Manager,Git 里只存”从哪里取”的引用:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: myapp-db-password
spec:
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: db-password
data:
- secretKey: password
remoteRef:
key: prod/myapp/db-password # 只有路径,没有值Git 里只有”去哪里取密钥”的声明,没有密钥本身——GitOps 的声明式原则在密钥管理上得以保留,同时避免了密钥泄露。
这条线的终点:GitOps 完成了一个闭环——代码、配置、基础设施的变更,全部通过 Git 这一个入口流入系统,所有操作可审计、可回溯、可回滚。但随之而来的问题是:工具链本身(K8s + IaC + GitOps + 服务网格 + 可观测性……)已经变成了新的复杂度来源,普通应用开发者面对的认知负担越来越重。一个刚入职的工程师,要搞清楚 Dockerfile → CI → Config Repo → ArgoCD → K8s → 服务网格,才能部署一个”Hello World”。这是下一篇平台工程要回答的问题。
关联
- → 05-平台工程-把开发者体验当产品来建:GitOps 是 IDP 内部的交付机制,IDP 对开发者屏蔽了 GitOps 的复杂性
- → 03-IaC-把基础设施变成可版本化的意图:应用层 GitOps + 基础设施层 IaC,是当前社区最成熟的交付组合
- → 02-Kubernetes-声明意图,控制器维持世界:GitOps operator 本质上是一个 K8s 控制器,reconciliation loop 是两者的共同范式