2026/11/3 16:50:36

GitOps 与 Argo CD 生产落地:核心模型、配置与踩坑排查

GitOps 与 Argo CD 生产落地:核心模型、配置与踩坑排查 集群里的 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 推送式流水线跑 kubectlFluxArgo 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、directorydestination段写目标集群 API 地址和 namespacesyncPolicy段写同步策略。这三层是逐级约束的关系——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 时级联清理二是打开PruneLasttrue这个 syncOption让被删的资源等到其他资源都健康之后再删避免先把 Service 删了导致流量中断这类顺序问题。selfHeal控制的是集群里的资源被人手工改了之后要不要自动改回去。打开它有人kubectl edit把副本数从 3 改成 10几秒到几分钟内会被拉回 3。这个能力在生产环境是刚需但也意味着你失去了临时改一下应个急的余地。我建议生产打开、预发也打开、本地开发环境关掉否则调试时改什么都被弹回去很烦。allowEmpty默认是 false它防的是渲染结果为空这种危险情况——比如 Git 里的路径写错了、Helm values 渲染出来一个资源都没有如果允许空同步prune 就会把这个应用下的所有资源全删掉。保持默认的 false 是对的除非你有特殊的场景确实需要清空。syncPolicy: automated: prune: true selfHeal: true allowEmpty: false syncOptions: - CreateNamespacetrue - PruneLasttrue - ApplyOutOfSyncOnlytrue - ServerSideApplytrue retry: limit: 5 backoff: duration: 5s factor: 2 maxDuration: 3m上面这段里ApplyOutOfSyncOnlytrue在大应用上能明显降低同步耗时因为它只对 OutOfSync 的资源发 apply而不是全量重刷。ServerSideApplytrue则是把 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。它的好处是没有模板语法输出一定是合法 YAMLdiff 结果干净配合 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: - CreateNamespacetrue - PruneLasttrue 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 入口暴露、证书与 RBACIngress 配置本身不难坑主要在 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 的 policyapiVersion: 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 pushcd 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 和 EventsUI 里点进应用能看到。如果状态是ComparisonError说明连对比这一步都没成功问题在仓库或渲染不在集群。第二步看repo-server 日志kubectl -n argocd logs deploy/argocd-repo-server --tail200重点找 clone 失败、chart 拉取失败、kustomize build 报错。第三步如果对比成功但同步失败就是 apply 阶段的问题看 controller 日志里的具体资源名和 API Server 返回的错误码。下面这张表是我整理的速查清单覆盖了我遇到过的八成情况现象大概率原因处理方式ComparisonError: authentication required仓库凭据缺失或过期检查 argocd namespace 下 repository Secret重新录入令牌ComparisonError: app path does not existsource.path 写错或分支不对核对仓库里的实际目录结构注意大小写failed to apply: no matches for kindCRD 未创建就 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-server4.2 六个让人抓狂的疑难场景场景一HPA 把副本数改回去又被 Argo CD 改回来来回抖动。这是最经典的冲突。Deployment 清单里写了replicas: 3HPA 根据负载调到 8Argo CD 检测到差异又设回 3HPA 再调上去。解决方式是用了 HPA 就别在清单里写死 replicas或者用ignoreDifferences忽略/spec/replicas。我倾向后者因为清单里留个基线值可读性更好spec: ignoreDifferences: - group: apps kind: Deployment jsonPointers: - /spec/replicas配置完之后要记得在 Application 上把RespectIgnoreDifferencestrue加到 syncOptions 里否则忽略差异只在检测阶段生效、同步阶段还是会写回去。这个细节文档里提得不多我调了半天才发现。场景二Service 的 clusterIP 导致永久 OutOfSync。Kubernetes 创建 Service 时会自动分配spec.clusterIPGit 里没这个字段于是每次比对都显示有差异。同理还有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 用 -5namespace 和基础配置用 -3业务资源用 0需要等业务就绪之后才跑的东西比如数据迁移 Job用 5。配合SkipDryRunOnMissingResourcetrue可以避免 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 SecretsGit 里只放引用实际值从外部密钥管理服务拉、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、合并就生效习惯自然就养成了。这套东西我推到第三个团队的时候才真正意识到工具只是载体收敛的是协作方式。