集群里的 YAML 堆到几百个文件之后,每次发版我都要先翻文档确认上次是谁、用什么参数、apply 到了哪个 namespace。更难受的是出事回滚:kubectl rollout undo只能救一个 Deployment,救不了 ConfigMap、Secret、Ingress 和 CRD 之间的连带关系,最后往往变成几个人同时开着终端,一边看日志一边手敲命令。这套流程在集群规模小的时候还能凑合,一旦多环境、多团队并行,问题就从"麻烦"变成了"不可控"。GitOps 和 Argo CD 解决的正是这件事——把集群的期望状态全部放进 Git,让一个常驻集群内的控制器持续把实际状态拉回期望状态。这篇文章面向已经会写基本 k8s 清单、但还没把交付链路收敛成声明式的人,我会把 GitOps 的核心模型、Argo CD 的组件职责、从零落地的完整步骤,以及我踩过的那些坑,一次讲透。
1. 从手工 apply 到 GitOps:为什么这条路由不得绕
1.1 手工交付链路的三个致命卡点
先说清楚痛点,不然容易把 GitOps 当成又一个"时髦概念"。我经历过的最典型的手工交付链路是这样的:开发提交代码,CI 构建镜像,然后运维或者开发自己在本地跑kubectl set image或者kubectl apply -f,集群状态靠人的记忆和终端历史来维护。这条链路有三个卡点,且都是结构性的,不是靠"更细心"能解决的。
第一是状态不可追溯。Git 里的清单是"某次提交时的样子",集群里的实际状态是"某个人某天下午改过的样子",两者之间没有强绑定关系。想知道某个 Deployment 为什么是 3 副本而不是 5 副本,只能去问人,或者去翻审计日志。第二是漂移无法收敛。有人在半夜排查问题时临时把副本数调大,第二天忘了改回来;有人手工改了一个 Service 的端口验证连通性,之后就留在那儿了。这些"临时改动"会一点点累积,最终集群和 Git 完全对不上,谁也不敢再拿 Git 当真相来源。第三是回滚粒度太粗。一次发版往往涉及十几个资源对象,手工回滚意味着你要么逐个 undo,要么凭印象重新 apply 一份旧清单,中间任何一步漏掉都会留下半新半旧的状态。
GitOps 的价值就在于把这三个卡点一次性堵死:Git 是唯一真相来源,集群状态由控制器持续对齐,回滚就是git revert一个 commit。听起来简单,但要落到位,需要理解它的模型边界和工具实现方式。
1.2 GitOps 的四根支柱与 Argo CD 站在什么位置
业界把 GitOps 归纳成四条核心原则,我在实际使用中觉得这个归纳挺准确:声明式描述(集群期望状态必须是声明式的,不能是命令式的脚本)、版本化且不可变(所有期望状态存在 Git 里,每次变更是可追溯的提交)、自动拉取(集群内的 agent 主动拉取并比对,而不是外部系统往里推)、持续协调(控制器不断检测漂移并修正)。
这里最关键的是第三点"拉取"而不是"推送"。传统的 CI 推送模式(流水线里跑kubectl apply)有个硬伤:CI 系统需要持有集群的写权限凭据。一旦 CI 平台被入侵或者凭据泄露,攻击面直接对着生产集群。而拉取模式下,集群内部的控制器主动读取 Git 仓库,凭据只存在于集群内,CI 系统完全不需要接触集群,这就是安全模型上的本质差异。
Argo CD 在这个模型里的角色是"协调器"。它常驻集群内,以 Application 这个 CRD 为单位,声明"哪个 Git 仓库的哪个路径,要同步到哪个集群的哪个 namespace",然后周期性做三件事:拉取 Git 内容、渲染成 k8s 清单、与集群实时状态做 diff。如果比对结果不一致,就标记为OutOfSync;如果开启了自动同步,就执行 apply;如果开启了自愈,还会把被外部改动的资源拉回 Git 描述的状态。它不负责构建镜像,不负责跑测试,那些仍然是 CI 的活。这条边界一定要划清楚,否则很容易把 Argo CD 当成万能流水线用,最后搞出一个又慢又难维护的东西。
1.3 三种落地方案的对照取舍
真要落地,绕不开方案选型。我实际用过的有三类:CI 内嵌推送式部署、Flux、Argo CD。这里把差异摊开讲,不是为了评优劣,而是帮你判断自己场景下该选哪个。
| 维度 | CI 推送式(流水线跑 kubectl) | Flux | Argo CD |
|---|---|---|---|
| 凭据位置 | CI 平台持有集群凭据 | 集群内 | 集群内 |
| 可视化 | 无,靠日志和终端 | 有 CLI 和基础的 UI 能力 | 完整 Web UI,资源树和 diff 视图 |
| 多集群管理 | 每个集群配一套凭据 | 支持,但配置偏声明式编程 | 原生支持,注册集群后统一管理 |
| 漂移检测 | 无 | 有 | 有,且带健康状态聚合 |
| 学习曲线 | 最低 | 中等,偏 GitOps 原生思路 | 中等,UI 降低了理解成本 |
| 适合场景 | 单集群、小团队、快速验证 | 团队熟悉声明式、偏好轻量组件 | 多团队多环境、需要审计和可视化 |
我最后选 Argo CD 的核心原因是可视化和资源树。多团队协作时,"这个应用现在是什么状态、哪些资源 OutOfSync、上次同步失败在哪一步"这类问题每天要被问很多遍,有 UI 能直接截图沟通,比让大家去读控制器日志高效得多。Flux 在纯声明式思路上更纯粹,组件也更轻,如果你的团队已经习惯了用 Kustomize 组织一切、不需要可视化,Flux 是很好的选择。至于 CI 内嵌推送,我建议只用在临时验证环境,别往生产上带,凭据管理的风险不值得省那点学习成本。
2. Argo CD 的架构与核心概念拆解
2.1 控制面里到底跑着什么
要排查问题,得先知道请求是在哪个组件断掉的。Argo CD 装完之后,argocd 这个 namespace 下会跑起一组职责明确的组件,每个组件挂了之后的症状都不一样。
argocd-server是对外的 API 和 UI 服务,你浏览器里看到的界面、CLI 发的请求,都打到它这里。它本身不做同步,只负责接收指令、读写 Application 对象。argocd-repo-server负责跟 Git 打交道——克隆仓库、缓存内容、根据目录里的 kustomization.yaml 或 Chart.yaml 渲染出最终的 k8s 清单。这个组件是性能瓶颈的高发区,仓库大、Helm chart 依赖多的时候,渲染会明显变慢。argocd-application-controller是真正干活的,它 watch 所有 Application 对象,定期比对 Git 渲染结果和集群实际状态,决定要不要同步、要不要自愈。argocd-redis是缓存层,controller 和 server 都靠它读写应用状态,它挂了之后 UI 会大面积显示未知状态。argocd-dex-server是可选的 SSO 组件,不接企业统一登录的话可以不管。较新版本还会有argocd-applicationset-controller(批量生成 Application)和argocd-notifications-controller(同步事件通知)。
理解这个分工的直接好处是排查有方向:UI 打不开但同步正常,看 server;所有应用都卡在Unknown或者ComparisonError,先看 repo-server 的日志;某个应用一直不重新比对,看 controller 的队列和 resync 间隔。我见过有人一遇到问题就重启整个 argocd namespace,这相当于把线索全扔了,非常不明智。
2.2 Application、AppProject 与仓库凭据的三角关系
Argo CD 的权限模型是三层结构,理解了就能避开大部分"配置了但同步不了"的坑。
最外层是仓库凭据,以 Secret 的形式存在 argocd namespace 里,带argocd.argoproj.io/secret-type: repository标签,里面写仓库地址、认证方式(SSH 私钥、用户名密码、访问令牌)。repo-server 用这个凭据去拉代码。凭据不对的典型报错是rpc error: code = Unknown desc = authentication required,或者 SSH 场景下的Permission denied (publickey)。
中间层是AppProject,它限定了"这个项目下的 Application 能访问哪些仓库、能部署到哪些集群和 namespace、能创建哪些集群级资源"。默认有个default项目,允许一切,很多教程图省事就让大家用 default,这是隐患。生产环境我建议每个业务域或团队建一个 AppProject,用sourceRepos白名单限定仓库,用destinations限定集群和 namespace 组合,用clusterResourceWhitelist精确控制能不能建 CRD、ClusterRole 这类集群级资源。
最内层是Application,它是最核心的 CRD,描述了源和目标:source段写仓库地址、revision(分支或 commit)、path,以及渲染方式(kustomize、helm、directory);destination段写目标集群 API 地址和 namespace;syncPolicy段写同步策略。这三层是逐级约束的关系——Application 想访问的仓库,必须在它所属 AppProject 的 sourceRepos 列表里,否则直接被拒绝。我踩过一次坑:新加了一个仓库,Application 也配好了,但一直报权限错误,排查半天发现是 AppProject 的 sourceRepos 没加白名单,而报错信息在 UI 上被折叠得不太显眼。
2.3 自动同步、自愈、剪枝:三个开关怎么组合
syncPolicy.automated下面有三个关键开关,它们的组合决定了同步行为的"攻击性",这部分值得单独拎出来讲,因为配错了会出事。
prune控制的是"Git 里删掉的资源,集群里要不要跟着删"。关掉它,你从 Git 里移除一个 Deployment 的清单,集群里那个 Deployment 会继续活着,变成孤儿资源;打开它,Argo CD 会真的去删。生产环境我倾向于打开,但要配合两个保护措施:一是 Application 上加resources-finalizer.argocd.argoproj.io这个 finalizer,保证删除 Application 时级联清理;二是打开PruneLast=true这个 syncOption,让被删的资源等到其他资源都健康之后再删,避免"先把 Service 删了导致流量中断"这类顺序问题。
selfHeal控制的是"集群里的资源被人手工改了之后,要不要自动改回去"。打开它,有人kubectl edit把副本数从 3 改成 10,几秒到几分钟内会被拉回 3。这个能力在生产环境是刚需,但也意味着你失去了"临时改一下应个急"的余地。我建议生产打开、预发也打开、本地开发环境关掉,否则调试时改什么都被弹回去,很烦。
allowEmpty默认是 false,它防的是"渲染结果为空"这种危险情况——比如 Git 里的路径写错了、Helm values 渲染出来一个资源都没有,如果允许空同步,prune 就会把这个应用下的所有资源全删掉。保持默认的 false 是对的,除非你有特殊的场景确实需要清空。
syncPolicy: automated: prune: true selfHeal: true allowEmpty: false syncOptions: - CreateNamespace=true - PruneLast=true - ApplyOutOfSyncOnly=true - ServerSideApply=true retry: limit: 5 backoff: duration: 5s factor: 2 maxDuration: 3m上面这段里,ApplyOutOfSyncOnly=true在大应用上能明显降低同步耗时,因为它只对 OutOfSync 的资源发 apply,而不是全量重刷。ServerSideApply=true则是把 apply 交给 API Server 的字段管理机制做,能规避一大批"最后应用者字段冲突"的问题,尤其是多个控制器同时修改同一资源(比如 HPA 改 replicas、Service 被云厂商控制器分配 clusterIP)的场景。retry 那段是我后来必配的,因为 CRD 创建和 CR 创建之间经常有几十毫秒的窗口期,第一次同步失败了等几秒自动重试就能过,没必要人工介入。
2.4 目录分层:Kustomize 与 Helm 的取舍
Argo CD 支持多种渲染方式,实际项目里最常见的是 Kustomize 和 Helm,也在同一个仓库里混用。选择逻辑我总结成一句话:你要改的是"资源的结构组成",用 Kustomize;你要改的是"参数的取值",用 Helm。
Kustomize 的思路是 base 加 overlay。base 里放完整的、可直接部署的清单,overlay 里只放差异——改镜像 tag、加副本数、换 namespace 前缀、打 label。它的好处是没有模板语法,输出一定是合法 YAML,diff 结果干净,配合 Argo CD 的 diff 视图非常舒服。缺点是处理"可选资源"比较别扭,比如某个环境不需要某个组件,Kustomize 得用 patch 删掉,写起来不太优雅。
Helm 的思路是模板加 values,灵活性更高,但代价是渲染结果依赖 values 的完整性,而且模板里的条件判断一旦写复杂,就会变得难以维护。我见过一个 chart 里有七层嵌套的if,最后没人敢改。所以我的做法是:第三方组件用官方 Helm chart,自己的业务用 Kustomize。业务代码的资源组成是稳定的,只是参数不同,Kustomize 完全够用,而且不需要额外学模板语法。
目录结构我一般这么组织,这个结构用了两年多,扩展性还不错:
repo/ ├── apps/ # Application 清单,每个应用一个文件 │ ├── team-a-backend.yaml │ └── team-b-frontend.yaml ├── manifests/ │ ├── base/ │ │ └── backend/ │ │ ├── deployment.yaml │ │ ├── service.yaml │ │ └── kustomization.yaml │ └── overlays/ │ ├── staging/ │ │ └── backend/ │ │ ├── kustomization.yaml │ │ └── patch-replicas.yaml │ └── prod/ │ └── backend/ └── bootstrap/ └── root-app.yaml # app-of-apps 的入口bootstrap/root-app.yaml指向apps/目录,Argo CD 同步它之后会自动创建目录下的所有 Application。这个模式叫 app-of-apps,好处是新加一个应用只需要往apps/里丢一个文件,不用登录 UI 手工点。
3. 从零搭一条能上生产的 GitOps 链路
3.1 集群侧准备与 Argo CD 安装
前置条件不复杂:一个可用的 k8s 集群(1.24 以上比较稳),kubectl能正常访问,一个能拉取的 Git 仓库。另外要确认集群里有 Ingress Controller 或者至少一个 NodePort 方案,否则 Argo CD 的 UI 出不来。
安装分单机版和高可用版。测试环境用单机版就够,生产建议上 HA 版本,它的 controller 支持分片、repo-server 有多副本。安装方式我偏好 kubectl 直接 apply 官方发布的 manifest,好处是所见即所得,不像 Helm chart 那样遮了一层:
kubectl create namespace argocd # 把官方发布页提供的 install.yaml 下载到本地后执行 kubectl apply -n argocd -f install.yaml # 等待组件就绪,通常 1-2 分钟 kubectl -n argocd get pods -w组件全部 Running 之后,取初始管理员密码:
kubectl -n argocd get secret argocd-initial-admin-secret \ -o jsonpath="{.data.password}" | base64 -d用户名是admin。第一次登录后务必改密码,因为初始密码就明文存在集群的 Secret 里,任何有 Secret 读权限的人都能拿到。生产环境更推荐直接接 SSO,把 dex 配起来对接内部身份系统,避免本地账号满天飞。
访问 UI 最简单的方式是端口转发,适合临时排查:
kubectl -n argocd port-forward svc/argocd-server 8080:443但长期用端口转发不现实,还是得暴露入口。这里有个常见配置项要注意:Argo CD 默认自己处理 TLS,如果你想让 Ingress 层终止 TLS,需要给argocd-server传--insecure参数,把server.insecure在 argocd-cmd-params-cm 里设成 true,否则会出现 Ingress 到后端之间的 TLS 握手失败,症状是 502 或者空响应。
3.2 仓库结构与 Application 清单
仓库这块有个原则:Application 清单和它引用的资源清单,最好放在同一个仓库。分开放在两个仓库里理论上是解耦,实际上是给回滚埋雷——资源清单回滚了但 Application 没回滚,或者反过来,两边对不上。
一个最小可用的 Application 长这样:
apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: team-a-backend-prod namespace: argocd finalizers: - resources-finalizer.argocd.argoproj.io spec: project: team-a source: repoURL: <你的 Git 仓库地址> targetRevision: main path: manifests/overlays/prod/backend destination: server: https://kubernetes.default.svc namespace: team-a-prod syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true - PruneLast=true retry: limit: 5 backoff: duration: 5s factor: 2 maxDuration: 3m revisionHistoryLimit: 10几处细节值得说明。targetRevision写分支名意味着跟分支走,任何提交都会触发同步;写 commit SHA 则意味着钉死版本,需要人工更新。生产环境我建议用分支但配合同步窗口,或者干脆用 tag。revisionHistoryLimit: 10保留最近 10 次同步历史,方便在 UI 上一键回退到某个历史版本,这个功能在紧急情况下非常好用,但很多人不知道它能配。
destination.server里的https://kubernetes.default.svc表示部署到 Argo CD 自己所在的集群。要部署到外部集群,需要先argocd cluster add <context-name>把目标集群注册进来,注册后 UI 里会出现一个新的集群条目。
3.3 入口暴露、证书与 RBAC
Ingress 配置本身不难,坑主要在 websocket 和后端协议上。Argo CD 的 UI 用了 websocket 做实时刷新,Ingress 层如果没透传 Upgrade 头,界面会变成"必须手动刷新才能看到状态变化"。Nginx Ingress 的注解里加上nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"和nginx.ingress.kubernetes.io/ssl-passthrough相关的配置(或者干脆让后端明文、Ingress 层做 TLS),两种方案选一种,别混着来。
RBAC 这块用 argocd-rbac-cm 这个 ConfigMap 配,格式是 Casbin 的 policy:
apiVersion: v1 kind: ConfigMap metadata: name: argocd-rbac-cm namespace: argocd data: policy.default: role:readonly policy.csv: | p, role:team-a-dev, applications, get, team-a/*, allow p, role:team-a-dev, applications, sync, team-a/*, allow p, role:team-a-dev, logs, get, team-a/*, allow g, <你的身份系统组名>, role:team-a-dev关键在policy.default,默认值往往是role:readonly,意味着没被显式授权的用户只能看。我给团队配的通常是"开发能 sync 不能 delete、能看日志不能改配置",删除 Application 这种操作留给少数人。这里有个容易忽略的点:sync 权限本身就等于对目标 namespace 的写权限,因为它会 apply 任意资源,所以 AppProject 的 destinations 白名单必须收紧,不然一个 sync 权限就能往别的团队 namespace 里塞东西。
3.4 CI 与 CD 的边界:镜像标签怎么写回 Git
这是整个链路里最需要想清楚的一环。GitOps 下 CI 的终点应该是"把新镜像的 tag 写进 Git",而不是"把镜像部署到集群"。
两种写法我都用过。第一种是CI 里直接改文件,用kustomize edit set image或者yq修改 values.yaml,然后 commit push:
cd manifests/overlays/prod/backend kustomize edit set image backend=<你的镜像仓库>/team/backend:${GIT_SHA} git add kustomization.yaml git commit -m "chore: bump backend to ${GIT_SHA}" git push这种方式的优点是简单直接、不引入额外组件,缺点是 commit 记录会有大量机器提交,而且需要给 CI 一个能写 Git 的凭据。第二种是argocd-image-updater,它跑在集群里,定期扫描镜像仓库的 tag,发现新版本就自己改 Git(或者改 Application 参数)。优点是 CI 完全不需要写 Git 权限,缺点是它有自己的 tag 排序规则,容易误判(比如latest、stable这类浮动 tag 它处理得不好),需要配update-strategy: semver或者newest-build明确语义。
我现在的做法是混合的:业务应用用 CI 改文件的方式,因为 tag 就是 commit SHA,语义明确,不存在误判;基础设施组件(各种 operator、监控 agent)用 image-updater,它们跟着上游发版走,用 semver 策略比较合适。另外无论哪种方式,都建议在 Application 上加ignoreDifferences忽略掉镜像 tag 之外的字段,避免因为 imagePullPolicy 之类的默认值差异导致永久 OutOfSync。
4. 踩坑与排查实录
4.1 同步失败的通用排查路径
Argo CD 的报错信息大多藏在 UI 的"Sync failed"折叠块里,很多人只看第一行就下结论,其实真正的错误在后面的one or more objects failed to apply明细里。我给自己定了一条固定排查路径,按顺序走基本能定位。
第一步看Application 的 Conditions 和 Events,UI 里点进应用能看到。如果状态是ComparisonError,说明连"对比"这一步都没成功,问题在仓库或渲染,不在集群。第二步看repo-server 日志,kubectl -n argocd logs deploy/argocd-repo-server --tail=200,重点找 clone 失败、chart 拉取失败、kustomize build 报错。第三步如果对比成功但同步失败,就是 apply 阶段的问题,看 controller 日志里的具体资源名和 API Server 返回的错误码。
下面这张表是我整理的速查清单,覆盖了我遇到过的八成情况:
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
ComparisonError: authentication required | 仓库凭据缺失或过期 | 检查 argocd namespace 下 repository Secret,重新录入令牌 |
ComparisonError: app path does not exist | source.path 写错或分支不对 | 核对仓库里的实际目录结构,注意大小写 |
failed to apply: no matches for kind | CRD 未创建就 apply CR | 用 sync-wave 注解让 CRD 先于 CR 同步 |
metadata.resourceVersion: Invalid value | 资源被其他控制器并发修改 | 开 ServerSideApply,或加 retry |
状态一直Progressing不结束 | 自定义资源没有健康检查定义 | 在 argocd-cm 里写 Lua 健康检查 |
一直OutOfSync但 diff 为空 | 默认字段差异(如 clusterIP、caBundle) | 配 ignoreDifferences |
| 同步成功但 Pod 起不来 | 镜像拉取失败或资源不足 | 这是 k8s 层问题,看 Pod events |
| 大应用同步极慢 | 全量 apply 或仓库渲染慢 | 开 ApplyOutOfSyncOnly,扩 repo-server |
4.2 六个让人抓狂的疑难场景
场景一:HPA 把副本数改回去又被 Argo CD 改回来,来回抖动。这是最经典的冲突。Deployment 清单里写了replicas: 3,HPA 根据负载调到 8,Argo CD 检测到差异又设回 3,HPA 再调上去。解决方式是用了 HPA 就别在清单里写死 replicas,或者用ignoreDifferences忽略/spec/replicas。我倾向后者,因为清单里留个基线值可读性更好:
spec: ignoreDifferences: - group: apps kind: Deployment jsonPointers: - /spec/replicas配置完之后要记得在 Application 上把RespectIgnoreDifferences=true加到 syncOptions 里,否则忽略差异只在检测阶段生效、同步阶段还是会写回去。这个细节文档里提得不多,我调了半天才发现。
场景二:Service 的 clusterIP 导致永久 OutOfSync。Kubernetes 创建 Service 时会自动分配spec.clusterIP,Git 里没这个字段,于是每次比对都显示有差异。同理还有spec.ports[].nodePort、Secret 里的caBundle(被 webhook 注入)、Deployment 的status字段。这些都是"服务端自动填充"的典型案例,统一用 ignoreDifferences 忽略 JSON pointer 就行。
场景三:CRD 和 CR 同一次同步,CR 报 no matches for kind。Argo CD 默认按资源类型排序同步,但 CRD 和 CR 之间的依赖它不一定能推断出来。这时候用同步波次注解,数字越小越先同步:
metadata: annotations: argocd.argoproj.io/sync-wave: "-5"我的习惯是 CRD 用 -5,namespace 和基础配置用 -3,业务资源用 0,需要等业务就绪之后才跑的东西(比如数据迁移 Job)用 5。配合SkipDryRunOnMissingResource=true可以避免 dry-run 阶段的误报。
场景四:sync window 配了不生效。同步窗口用来限制"只能在某个时间段同步",比如只允许工作日上午发布,避免周五晚上出事。配置在 AppProject 里:
spec: syncWindows: - kind: deny schedule: "0 18 * * 5" duration: 60h timeZone: "Asia/Shanghai" applications: - "*-prod"坑在于schedule 的语法和时区。这里的 cron 是五段式,duration支持 h/m,但如果你不写timeZone,它用的是 controller 所在容器的时区,容器默认是 UTC,于是你以为的下午六点变成了凌晨两点。我第一次配的时候整整观察了一周才反应过来。
场景五:Secret 明文进了 Git。这个不用多说,是安全红线。处理方式有三类:Sealed Secrets(加密后可以安全提交)、External Secrets(Git 里只放引用,实际值从外部密钥管理服务拉)、SOPS(文件级加密)。我一般推荐 External Secrets,因为密钥轮换不需要改 Git,代价是部署时多一个组件的可用性依赖。
场景六:应用数量上来之后 controller CPU 打满。每个 Application 默认 3 分钟 resync 一次(--app-resync默认 180 秒),算一下:300 个应用意味着每分钟大约 100 次全量比对,每次比对都要 list 一遍目标 namespace 的资源,API Server 压力不小。三个优化方向:一是把 resync 间隔调大,比如 300 秒到 600 秒,反正有 Git webhook 兜底;二是开 controller 分片,--shards参数把它拆成多个副本各管一部分;三是把 repo-server 的--repo-cache-expiration调大,减少重复克隆。
4.3 上生产前必须想清楚的几件事
第一件是自愈的副作用。selfHeal 打开后,任何手工改动都会被回滚。这在生产是好事,但排查紧急故障时会很碍事——你改了副本数想扛一波流量,三十秒后被打回去。我的处理方式是准备一个"紧急放行"流程:临时把 Application 的 automated 关掉(用argocd app set <name> --sync-policy none),改完再开回来。这比关掉整个自愈安全,因为只影响一个应用。
第二件是删除保护。prune 打开意味着从 Git 删清单就是删生产资源。我给所有生产 Application 都加了resources-finalizer,并且把删除 Application 的权限收到两三个人手里。另外强烈建议开启PruneLast,避免删资源的时候连带影响还在跑的流量。
第三件是回滚的可信度。Argo CD 的 history 和 rollback 功能很好用,但要注意它回滚的是"当时那个 commit 渲染出来的清单",如果那次同步里包含了一个不可逆的操作(比如数据库 schema 变更的 Job),回滚清单并不会撤销已执行的操作。所以我的原则是:不可逆的变更永远走独立的、手动触发的 Hook,不放进自动同步里。用argocd.argoproj.io/hook: PreSync配合hook-delete-policy,让它只在明确点击同步时执行。
第四件是监控和通知。同步失败如果没人知道,GitOps 就成了"看起来很美"的摆设。最少要配两条告警:Application 状态非 Healthy 超过 N 分钟、以及连续同步失败超过 3 次。argocd-notifications-controller 支持把事件推到内部消息渠道,配起来大概二十分钟,但能在半夜省下大量救火时间。
我在实际使用中发现,GitOps 真正的门槛不在装 Argo CD,而在能不能让所有人接受"改集群的正确方式是提交代码"。刚开始总会有人绕过去直接 kubectl,这时候别急着批评,先看看是不是改一个东西要走三个仓库、提两次 PR,流程太重自然会有人绕路。把路径缩短到"改一个文件、提一个 PR、合并就生效",习惯自然就养成了。这套东西我推到第三个团队的时候,才真正意识到工具只是载体,收敛的是协作方式。