为什么我放弃了传统方案,改用 ArgoCD
一切始于一次凌晨三点的发布事故
先说说我是怎么“入坑” ArgoCD 的。
去年我们团队维护着一套十几个微服务的 Kubernetes 集群,部署方式是很多公司都熟悉的组合:CI 流水线(Jenkins)在构建完成后,执行一段 kubectl apply -f k8s/ 的脚本。听起来很简单对吧?
问题出现在一个周五晚上。一个同事改了 Deployment 的副本数,想临时扩容应对流量高峰。改完之后不到十分钟,流水线恰好因为另一个不相关的提交被触发,kubectl apply 一跑,副本数直接被打回去了。线上服务响应变慢,告警响到凌晨。
事后复盘时我们发现了一个更深层的问题:集群里的实际状态,和 Git 仓库里的声明,早就对不上了。有人在集群里手动改过 resource limits,有人用 kubectl edit 紧急改过环境变量,这些变更都没有回到代码仓库。Git 说是 A,集群跑的是 B,谁也不知道哪个才是“真相”。
这就是我决定认真研究 GitOps 和 ArgoCD 的直接原因。
传统 CI 部署方式的几个痛点
在动手之前,我先总结一下当时方案的四个问题,你可以对照看看自己是不是也中招了:
- 推模式(Push)没有兜底:CI 推完一次就完事了。之后集群里发生任何漂移,没人知道,也没人纠正。
- 凭证散落各处:Jenkins 需要 kubeconfig 或 ServiceAccount token 才能操作集群,CI 服务器一旦被攻破,等于把整个集群交出去了。
- 发布状态不透明:“现在线上跑的是哪个版本的配置?”这个问题要靠人肉去集群里查。
- 回滚靠嘴喊:回滚要么 re-run 旧流水线,要么手动
kubectl rollout undo,和 Git 历史完全脱节。
第一次尝试:踩的坑比想的多
安装与初始配置
安装本身很顺利:
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
然后我做的第一件事是改默认 admin 密码(这一步别忘,不然 admin 密码就是 argocd-server Pod 的名字,任何能看到 Pod 列表的人都能登录):
argocd account update-password
接着我在 UI 里创建了第一个 Application,指向我们的 Git 仓库。同步成功,Pod 起来了——当时我觉得这也太简单了。
第一个坑:Application 卡在 OutOfSync 死循环
第二天问题来了。ArgoCD 一直显示某个 Deployment 是 OutOfSync,自动同步反复执行,改了又改。
原因是我们的 Helm chart 会往 Deployment 里注入一个带时间戳的 annotation,每次渲染出来的结果都不一样。ArgoCD 一对比:“诶,不一样”,就同步;同步完渲染又是新时间戳,又不一样……无限循环。
解决办法是在 Application 里配置 ignoreDifferences:
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/template/metadata/annotations
或者更根本一点,把 chart 里那个时间戳直接删掉。我们选了后者,因为给 ArgoCD 喂“确定性”的渲染输出,比不停地打补丁健康得多。
第二个坑:CRD 不会被自动同步
我们用了一堆 CRD(Prometheus Operator 的、Istio 的)。第一次同步后我天真地以为万事大吉,结果后来升级 Operator 时才发现 CRD 版本停在了旧版本上。
ArgoCD 默认不管 CRD 的更新(只管首次安装)。解法是用 argocd app sync --resources 显式同步,或者把 CRD 放到一个单独的、用 Replace=true 同步策略的 Application 里专门管理。这也是我后来形成的一个架构习惯:CRD 单独一个 Application,和业务应用解耦。
第三个坑:Secret 不能明文进 Git
GitOps 的一切都在 Git 里,那数据库密码怎么办?总不能明文提交吧。
我们试了两个方案:
- Sealed Secrets:用集群的公钥加密,只有集群内的 controller 能解密。简单,但密钥轮换麻烦,密文和集群绑死了。
- External Secrets Operator:Git 里只存
ExternalSecret引用,真实值放在 Vault 或云厂商的 Secrets Manager 里。
我们最终选了后者。Git 里长这样:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: payment-db
spec:
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: payment-db-credentials
data:
- secretKey: password
remoteRef:
key: payment/db
property: password
稳定下来后的仓库结构
踩完坑之后,我们的仓库长这样,推荐直接抄:
gitops-repo/
├── apps/ # 每个 Application 一个文件
│ ├── payment.yaml
│ └── order.yaml
├── clusters/
│ ├── prod/
│ └── staging/
└── services/
├── payment/
│ ├── Chart.yaml
│ ├── values.yaml # 公共默认值
│ ├── values-prod.yaml
│ └── values-staging.yaml
└── order/
└── ...
核心技巧是 App of Apps 模式:一个根 Application 指向 apps/ 目录,里面每个文件定义一个子 Application。新服务上线只需要加一个 YAML 文件,ArgoCD 会自动发现并部署。
每个 Application 的关键配置:
syncPolicy:
automated:
prune: true # Git 里删了资源,集群里也删
selfHeal: true # 有人手动改集群,自动改回来
syncOptions:
- CreateNamespace=true
selfHeal: true 就是对我那次凌晨事故的直接回应——现在再有人在集群里手动改东西,ArgoCD 会在几分钟内把它掰回 Git 声明的状态。注意 prune: true 要慎用,建议先在 staging 集群上开,prod 环境观察一段时间再说。
回滚体验:这是我最满意的部分
现在回滚是这样的:git revert 那个提交,push,ArgoCD 自动同步,完事。整个回滚有完整的审计记录,和代码历史完全一致。发版日再也不用有人盯着流水线了——合并到 main 分支就是发布,ArgoCD 大约三分钟内就能收敛到目标状态。
实用建议
- 渐进式迁移:先让 ArgoCD 以“只读”方式接入现有集群(
automated关掉),观察它报的 drift,清理干净后再开自动同步。 - 用 sync window 限制发布时段:我们给 prod 配了工作时间段同步窗口,周五晚上想发布?ArgoCD 会给你排到下周一。
- 前置 hook 处理数据库迁移:Job 类型加上
argocd.argoproj.io/hook: PreSync,同步前自动跑 migration。 - 保留 CI:ArgoCD 只接管 CD,构建、测试、打镜像仍然在 CI 里做。ArgoCD Image Updater 可以监控新镜像并自动更新 Git 里的 tag,形成闭环。
诚实的局限性
- 多集群管理有学习成本:官方推荐用 ApplicationSet,但概念不少,我们花了两三周才理顺。
- UI 有误导性:UI 上的资源树很好看,但复杂应用的同步失败原因藏得很深,最终还是得看
argocd app diff和 controller 日志。 - 不是所有资源都适合 GitOps:比如频繁变更的配置中心数据,硬塞进 Git 会让仓库变成垃圾场,这类东西还是留给专业的配置服务。
- 团队纪律是前提:GitOps 的价值建立在“Git 是唯一事实来源”上。如果有人还是习惯
kubectl apply,那一切白搭。我们是通过把开发者的 kubeconfig 收紧成只读权限来强制执行的。
一句话总结:ArgoCD 解决的不是“怎么部署”,而是“怎么让集群永远和你说的一致”。如果你的团队已经体会过配置漂移的痛,那它值得这份迁移成本。