把 312 个微服务从 Jenkins 迁到 Argo CD 的 14 个月:省下的钱,和赔进去的三件事

🔑 关键词:Argo CD,Jenkins 迁移,GitOps,Kubernetes,平台工程

📖 摘要:一次真实的 CI/CD 迁移复盘。312 个微服务、87 个 Jenkins 插件、47 个 AppProject,从 stuck 的流水线到 GitOps,附具体报错、具体数字,以及三个和主流说法不太一样的结论。

那套 Jenkins,其实早就没人敢改了

图片

2022 年 11 月我接手这套 CI 的时候,它已经跑了 6 年多。主节点 8C16G,EBS gp2 500G,从上线到我接手那天的 1476 天里没重启过,uptime 是我见过最漂亮的数字,也是最危险的信号。

插件装了 87 个,其中 31 个最后更新时间在 2020 年之前。Pipeline 用的是 Scripted 语法混着 Declarative,最大的那个 Jenkinsfile 有 1240 行,里面 14 个 if 分支按环境、按分支、按 tag 走不同逻辑。团队里没人完整读懂过它,改一行要祈祷。

每周平均 3.2 次构建队列阻塞,最长一次排了 47 分钟,因为 9 台 c5.2xlarge 静态 agent 全被一个跑全量 e2e 的服务占着。单个服务从 push 到部署平均 11 分 20 秒,看着还行,但这是 P50,P95 是 34 分钟。

图片

所以迁移动机不是 'Jenkins 不行'——Jenkins 依然能干活。是流水线本身变成了不可维护的资产

迁移过程中的三个真实报错

我们最终选了 Argo CD 2.9.3 做 CD,CI 部分拆给了 Tekton 和一部分 GitHub Actions;集群侧 47 个 namespace、312 个 Deployment 全部收敛成 312 个 Application,挂在 47 个 AppProject 下面,用 ApplicationSet 的 cluster generator 批量生成。

图片

第一个坑是 etcd 的硬限制。有个 Prometheus Operator 的 CRD 实例,加了 kubectl.kubernetes.io/last-applied-configuration 注解之后直接撑到 1.9MB,apply 报 metadata.annotations: Too long: must have at most 262144 bytes。这个 262144 不是 etcd 的 1.5MB limit,是 kube-apiserver 对单个 annotation 的限制。最后改成 server-side apply(--server-side --field-manager=argocd-controller)才算绕过去。

第二个坑更疼:repo-server OOM。有个 chart 的 template 渲染出来接近 8000 行 YAML,helm template 峰值内存 2.3GB,而 Argo CD 默认给 repo-server 的 limit 是 512Mi。那个凌晨两点我盯着 CrashLoopBackOff 数了下,重启了 31 次。后来把 repo-server 的 limit 提到 4Gi,并且开了 --repo-cache-expiration,才稳定下来。

第三个坑是 Helm hook 和 sync-wave 的语义冲突。数据库 migration 的 Job 用的是 helm.sh/hook: pre-install,在 Helm CLI 里跑得好好的,在 Argo CD 里默认根本不会当 hook 处理。Argo CD 2.6 之后虽然支持了 hook,但它的执行时机、失败重试策略跟 Helm 原生是两套逻辑。我们最后放弃 hook,改成显式的 PreSync Job + SyncWave -1,反而更可控。

图片

顺便说下 Secret 方案。我们试过 Sealed Secrets,用了三个月放弃了,原因是 key rotation 要重新加密所有 sealed secret,312 个服务的工作量不可接受。最后换 External Secrets Operator + Vault,把一个 Secret 的轮转从 '批量重加密' 变成 '改 Vault 里的值,等 60s 刷新'。

四种方案,我按实际踩过的坑打分

维度 Jenkins Argo CD + Tekton Flux CD GitHub Actions
上手成本 低(但维护成本高) 中高
多集群支持 靠插件,配置分散 ApplicationSet 原生 Kustomization 原生 要自己写
权限模型 粗,通常一把 cluster-admin AppProject + RBAC,细 基于 SA,较粗 依赖 GitHub 权限
回滚 重跑旧构建 git revert 或 rollback git revert 重跑 workflow
Diff 可读性 有,但噪音多
我们实际月成本 约 $2100(9 台 agent) 约 $480(3 节点 HA) 未落地 已含在 GitHub 账单

图片

这里我个人的判断是:如果你的服务数少于 30 个,上 Argo CD 属于过度工程,Jenkins 或者 GitHub Actions 直接 kubectl apply 完全够用。GitOps 的收益是从 '多集群 + 多人协作 + 审计要求' 这三件事同时出现的时候才开始体现的。

三个和主流说法不太一样的结论

第一,GitOps 真正的门槛不在 Argo CD,在 AppProject 的 RBAC。 迁移之前,Jenkins 拿着一把 cluster-admin 的 kubeconfig 到处部署,谁都拦不住。迁移之后我们花了两周设计 AppProject:每个团队只能 sync 自己 namespace 下的 Application,能不能创建 Application 本身是另一个权限。这件事带来的安全收益,比 '声明式管理' 这个卖点大得多。省下的钱也不是 agent 机器那 1600 刀,是 '新人不需要理解流水线就能发布' 这件事。

图片

第二,Diff 噪音比部署失败更容易搞垮一个团队。 我们的 chart 里有一堆 Helm 生成的随机 annotation 和时间戳,argocd app diff 出来动辄 200 行 '+' '-',其中真正有意义的可能就 3 行。三个月后我观察到一个很危险的现象:大家不再看 diff 了,直接点 Sync。这比手工 kubectl apply 还危险,因为手工操作至少心里有数。后来我们强制给所有 chart 加了 checksum/config 的规范化,还用 ignoreDifferences 屏蔽掉一批噪音字段,diff 行数才降到平均 12 行以内。

第三,迁移之后 MTTR 降了,但 on-call 的心理负担没降。 数字上:部署频率从每周 42 次涨到每周 210 次,变更失败率从 18% 降到 6.4%,MTTR 从 47 分钟降到 12 分钟。但排障路径变长了——以前出问题看 Jenkins 一个地方,现在要看 Argo CD 的 sync 状态、K8s event、Vault 里 secret 有没有刷新、还有上游 CI 的镜像 tag 对不对。四个入口。

如果你正在做类似的迁移,我的建议是:先把 AppProject 和 ignoreDifferences 设计好,再迁第一个服务。 这两件事往后拖,后面要还的债比你想象得多。

🏷️ 标签: