主题
这份文档是什么
一份完整的 GitOps 改造手册:延续《若依微服务上 K8s》的环境(同一套服务器规划),把「Jenkins 直接 kubectl apply」这种传统方式,改造成 Jenkins 只做 CI、Argo CD 负责 CD 的现代形态。
读完你会得到:
- 理解 CI 与 CD 为什么要分开(不是一个"架构潮流",而是解决 4 个具体问题)
- 一套可运行的 Argo CD 部署流程(含国内镜像全链路,所有地址实测可用)
- GitOps 仓库的组织方式与 Kustomize 用法
- 改造后的 Jenkinsfile(CI 只负责产出镜像 + 更新版本声明)
- 灰度切换的 8 步操作表,每一步都带「出问题怎么退回去」
- 回滚、故障处理、风险清单
前置条件
本文档假设你已经完成《若依微服务上 K8s》里的环境搭建:K8s 集群就绪、Harbor 可用、Nacos 与数据库可用、Jenkins 已能手工构建并部署一个服务。
如果你还没做,先看那份文档——本文档的第 4 章会给出需要确认的前置检查项。
01 环境规划与目标架构
1.1 服务器规划总表
沿用《若依微服务上 K8s》的 9 台服务器,本方案不新增机器。
| # | IP | 主机名 | 配置 | 角色 | 部署的内容 | 本方案的变化 |
|---|---|---|---|---|---|---|
| 1 | 192.168.0.10 | k8s-master | 4C8G | K8s 控制平面 | kube-apiserver / etcd / controller-manager / scheduler | 不变 |
| 2 | 192.168.0.11 | k8s-node1 | 8C16G | K8s 工作节点 | 若依业务 Pod | 新增:Argo CD 全部组件(约 500m CPU / 1Gi 内存) |
| 3 | 192.168.0.12 | k8s-node2 | 8C16G | K8s 工作节点 | 若依业务 Pod | 不变 |
| 4 | 192.168.0.20 | ci-jenkins | 8C16G | CI 构建机 | Jenkins / JDK 17 / Maven / Docker | 变化:不再持有集群 kubeconfig,不再执行 kubectl |
| 5 | 192.168.0.21 | harbor | 4C8G | 镜像仓库 | Harbor 2.15.2 | 新增:项目 infra,用于存放 Argo CD 自身镜像 |
| 6 | 192.168.0.22 | mysql | 4C8G | 数据库 | MySQL 8.0 | 不变 |
| 7 | 192.168.0.23 | redis | 2C4G | 缓存 | Redis 7 | 不变 |
| 8 | 192.168.0.24 | nacos | 4C8G | 注册/配置中心 | Nacos 2.5.4 | 不变 |
| 9 | 192.168.0.25 | git | 2C4G | 代码仓库 | Gitea 1.27.3 | 新增:一个独立的 GitOps 清单仓库 ruoyi-gitops |
为什么 Argo CD 装在 node1 而不是 master
Argo CD 虽然"管集群",但它自身也是集群里的普通工作负载(几个 Deployment)。装在哪儿取决于资源余量:
| 位置 | 评估 |
|---|---|
控制平面节点(.10,4C8G) | 控制平面组件(etcd 尤其娇贵)已经吃掉不少资源,不建议再挂业务负载 |
工作节点(.11,8C16G) | ✅ 推荐。资源充裕,且可以用 nodeSelector 钉住,避免和其他业务 Pod 抢资源时被一起驱逐 |
如果将来要做高可用:Argo CD 的 HA 部署(manifests/ha/install.yaml)需要至少 3 个副本,那时建议单独准备 1~2 台节点承载。
1.2 架构对比:改造前 vs 改造后
改造前(传统方式):
text
开发者 push ──▶ Gitea ──▶ Jenkins
│ 1) mvn package
│ 2) docker build
│ 3) docker push ──▶ Harbor
│ 4) sed 改清单
└ 5) kubectl apply ──▶ K8s 集群
(集群里发生了什么,没有任何记录)改造后(GitOps):
text
开发者 push ──▶ Gitea(代码仓库)
│
▼
Jenkins(CI,只做 3 件事)
│ 1) mvn package
│ 2) docker build + push ──▶ Harbor
│ 3) 改 GitOps 仓库里的一行 tag
└──────────────┐
▼
Gitea(GitOps 仓库 ruoyi-gitops)
这里是"集群应该长什么样"的唯一事实来源
│
│ webhook / 每 3 分钟轮询
▼
Argo CD(CD,装在集群里)
│ 持续对比「Git 里的期望状态」与「集群的实际状态」
│ 不一致 → 自动同步(或提示你手动同步)
▼
K8s 集群(ruoyi 命名空间)1.3 一次发布的完整流程图
text
① 开发者提交代码
│
▼
② Jenkins 拉代码、编译、打镜像(tag = git 短 SHA)
│
▼
③ 镜像推送到 Harbor
│
▼
④ Jenkins 修改 GitOps 仓库中该服务的 kustomization.yaml
(只改 newTag 一行,然后 git commit + push)
│
▼
⑤ Gitea 通过 webhook 通知 Argo CD(或 Argo CD 自己轮询发现)
│
▼
⑥ Argo CD 拉取 GitOps 仓库,渲染出最终清单
│
▼
⑦ 与集群实际状态对比(diff)
│
├── 一致 → 什么都不做
└── 不一致 → 按配置的同步策略处理
├─ 手动模式:UI 上显示 OutOfSync,等你点 Sync
└─ 自动模式:自动 apply,并持续纠正偏移
│
▼
⑧ 滚动更新完成,Argo CD 显示 Synced + Healthy这张图里最关键的变化在第 ④ 和第 ⑧ 步
- 第 ④ 步:Jenkins 从"直接操作集群"变成"只修改一个 Git 文件"。它不再需要集群凭据。
- 第 ⑧ 步:集群的最终状态,在 Git 里有完整记录。任何时候你都可以
git log出「谁在什么时候把哪个服务改成了哪个镜像」。
02 为什么要拆开 CI 和 CD
2.1 传统方式(Jenkins 直接 kubectl apply)的四个具体问题
不是"架构不够优雅",而是会在真实运维中造成具体麻烦:
问题一:Jenkins 持有集群最高权限
kubectl apply 需要 kubeconfig。而大多数人会把 admin.conf 直接给 Jenkins —— 等于把集群的完全控制权交给了一个每天被触发几十次的构建系统。
一旦 Jenkins 的凭据泄露(或有人恶意提交一个改了 Jenkinsfile 的 PR),攻击者就拿到了整个集群。更糟的是:这种执行没有审计,因为 kubectl apply 用的就是管理员身份,日志里看不出"是流水线干的还是人干的"。
问题二:实际状态没有记录,回滚靠猜
kubectl apply 之后,集群里跑的是什么版本?取决于最后一次构建推了什么 tag。
想回滚时:
kubectl rollout undo只能回退一个版本,再往前就没辙了- 你不知道三天前那个"正常的版本"对应的镜像 tag 是什么
- 中间可能还被人手动
kubectl edit改过,仓库里根本看不出来
这就是"配置漂移"(Configuration Drift):集群的实际状态和任何一份文档都对不上。
问题三:构建与发布耦合,状态含义模糊
一个 Jenkins Job 里,编译失败、镜像推失败、部署失败都会让 Job 变红。排查时得先看日志才知道失败在哪一步。
更麻烦的是**"部署成功"到底意味着什么**:kubectl apply 返回 0 只代表"API Server 接受了这个请求",不代表 Pod 起来了、更不代表应用能正常工作。
问题四:无法处理"人为改动"
有人为了紧急排障,直接 kubectl scale deployment xxx --replicas=0,然后忘了改回来。
传统方式下,这个改动会一直保留下去——因为没人会重新跑一次流水线。集群慢慢变成一个"谁也不敢动的黑盒"。
2.2 拆分后的职责边界
| 职责 | Jenkins(CI) | Argo CD(CD) |
|---|---|---|
| 拉代码、编译、跑测试 | ✅ | ❌ |
| 构建镜像并推送 | ✅ | ❌ |
| 决定"用哪个镜像版本" | ✅(写入 Git) | ❌(只读 Git) |
| 把清单应用到集群 | ❌ | ✅ |
| 需要集群凭据 | 不需要 | 需要(但它就在集群里) |
| 检测并纠正配置漂移 | ❌ | ✅(selfHeal) |
| 提供"此刻集群应该是什么样"的记录 | ❌ | ✅(Git 提交历史) |
| 回滚 | ❌ | ✅(回退 Git 提交,或指定历史版本同步) |
一句话概括职责划分:
Jenkins 负责"生产一个新版本",Argo CD 负责"让集群变成 Git 描述的样子"。
2.3 GitOps 的四个核心概念
理解这四个词,整套架构就通了。
| 概念 | 英文 | 含义 | 在本方案里的体现 |
|---|---|---|---|
| 期望状态 | Desired State | 用声明式文件描述的"系统应该长什么样" | GitOps 仓库里每个服务的 deployment.yaml + kustomization.yaml |
| 实际状态 | Live State / Observed State | 集群当前真实的资源状态 | kubectl get deployment -o yaml 看到的内容 |
| 协调循环 | Reconciliation Loop | 持续对比两者、并把实际状态拉向期望状态的过程 | Argo CD 的 application-controller 每 3 分钟做一次 |
| 漂移 | Drift | 实际状态偏离了期望状态 | 有人手动 kubectl edit 改了副本数 |
「声明式」是这一切的前提
命令式(Imperative):「把 ruoyi-gateway 的副本数扩到 3」—— 你需要知道当前是几,然后执行变更。
声明式(Declarative):「ruoyi-gateway 的副本数应该是 3」—— 你只描述目标,由控制器负责达成。
为什么这很重要:声明式的描述可以反复执行且结果一致(幂等)。这使得"持续对比、自动纠正"成为可能。
K8s 本身就是声明式的(你写 YAML,它去达成),而 GitOps 把"声明"这份文件放进了 Git —— 于是 Git 成了唯一事实来源。
2.4 四个立即能感知到的好处
| 好处 | 具体表现 |
|---|---|
| 审计 | git log ruoyi-gateway/kustomization.yaml 能看到每一次版本变更、是谁、什么时候、从哪个 SHA 到哪个 SHA |
| 回滚变简单 | 回到上一个 Git 提交,或指定任意历史版本同步。不再"只能回退一版" |
| 漂移自动纠正 | 开启 selfHeal 后,手动改的东西会在几分钟内被改回来 |
| 权限收紧 | Jenkins 不再需要 kubeconfig;集群凭据只存在于集群内部 |
03 版本选型:为集群挑 Argo CD 版本
3.1 先看清 Argo CD 由哪些组件组成
install.yaml 会创建大约 6 个 Deployment(或 StatefulSet)+ 一堆 RBAC 与配置:
| 组件 | 类型 | 职责 |
|---|---|---|
argocd-server | Deployment | API Server + Web UI。你操作 Argo CD 的入口 |
argocd-repo-server | Deployment | 拉取 Git 仓库、渲染 Helm/Kustomize 模板。产生最终清单的地方 |
argocd-application-controller | StatefulSet | 核心控制器。持续对比期望状态与实际状态,执行同步 |
argocd-applicationset-controller | Deployment | 用模板批量生成 Application(多集群/多环境时用) |
argocd-dex-server | Deployment | 对接 SSO(LDAP/OIDC/GitHub 等)。只用本地账号时可以不装 |
argocd-redis | Deployment | 缓存。Argo CD 依赖 Redis,不能省 |
argocd-notifications-controller | Deployment | 同步结果的通知(钉钉/企微/邮件) |
Argo CD 3.x 的两个新变化(与 2.x 不同)
如果你看过基于 Argo CD 2.x 的教程,注意这两点差异:
- 默认清单里包含 NetworkPolicy(7 个)。这要求 CNI 支持 NetworkPolicy(Calico 支持;flannel 默认不支持,会创建但不生效)。
- 3.5 起引入组件间的 mTLS。
repo-server会要求其他组件出示客户端证书;如果没有自定义证书,它会在内存里生成自签证书,不需要你额外配置。但如果你的环境里有 Service Mesh 或强制 TLS 拦截的中间设备,需要留意为它放行。
3.2 选版本的三步法
第一步:确认你的 K8s 版本。
bash
# 在 master 上执行
kubectl version
# 关注 Server Version,例如 v1.36.5第二步:查 Argo CD 的支持窗口。
Argo CD 的支持策略与 K8s 官方对齐:只覆盖最近 3 个 minor 版本。同时它的补丁发布节奏是「最新的 3 个 minor 版本线」——例如写作时的 3.5.x / 3.4.x / 3.3.x。
text
写作时的时间线:
K8s 官方最新: v1.37.x → 维护窗口 1.35 / 1.36 / 1.37
Argo CD 最新: 3.5.3 → 覆盖 K8s 1.35 ~ 1.37
所有工具的交集: 1.35 / 1.36
→ 本文档的集群选 v1.36.x,Argo CD 选 3.5.3第三步:验证镜像真的能拉到。
这一步不能省。 网上抄来的"国内镜像地址"十有八九是失效的。本文档给的所有地址都在 §5.2 里列了实测结果。
如果 K8s 版本太老会发生什么
假设你的集群是 v1.28,而当前 Argo CD 最新是 3.5.x:
| 选择 | 结果 |
|---|---|
| 装 Argo CD 3.5.x | ❌ 官方测试矩阵不覆盖 1.28,大概率遇到 CRD 字段不支持、API 已废弃等问题 |
| 装覆盖 1.28 的老版本(如 2.14.x) | ✅ 能用,但该分支已停止维护,安全补丁不再发布 |
| 升级集群再装新版(推荐) | ✅ 一次性解决,后续所有工具都不再撞墙 |
结论:集群版本是工具链的总闸门。 如果你打算长期建设这套流程,把"集群升级"当成一个正式项目来做,比每次装工具时绕路划算得多。
3.3 本方案选定的版本
| 组件 | 版本 | 选择理由 |
|---|---|---|
| Kubernetes | v1.36.x | 落在所有主流工具的支持窗口内 |
| Argo CD | v3.5.3 | 写作时的最新稳定版 |
| Kustomize | 内置(Argo CD 自带) | 不需要单独安装 |
| Harbor | v2.15.2 | 与文档 ① 一致 |
| Jenkins | 2.528.3 | 与文档 ① 一致 |
Argo CD 的资源预算(非 HA 部署):
| 组件 | CPU request | 内存 request | 内存 limit |
|---|---|---|---|
| argocd-server | 100m | 128Mi | 512Mi |
| argocd-repo-server | 100m | 256Mi | 1Gi |
| argocd-application-controller | 100m | 256Mi | 1Gi |
| argocd-redis | 50m | 64Mi | 256Mi |
| dex / applicationset / notifications | 各 10~50m | 各 64Mi | 各 256Mi |
| 合计 | 约 400m | 约 800Mi | 约 3Gi |
一台 8C16G 的节点完全够用
上表是"所有组件加起来"的量。放在 192.168.0.11(8C16G)上,只占用约 5% 的 CPU 和 5% 的内存。
但要注意 limit 之和:如果节点上还有很多业务 Pod,且它们的 limit 加起来已经接近节点容量,新 Pod 的调度会受影响。所以 §5.4 里会给 Argo CD 显式设置资源限额。
04 阶段零:前置检查
在动手改造之前,先把这些确认一遍。任何一项不通过,后面的步骤都会失败。
4.1 检查清单
bash
# ===== 在 master(192.168.0.10)上执行 =====
# ① 集群健康:三个节点都 Ready
kubectl get nodes
# 期望:k8s-master / k8s-node1 / k8s-node2 全部 Ready
# ② 集群版本
kubectl version --short 2>/dev/null || kubectl version
# 期望:Server Version: v1.36.x
# ③ CNI 是否支持 NetworkPolicy(Argo CD 3.x 会创建 NetworkPolicy)
kubectl get pods -n kube-system | grep calico
# 期望:calico-node 在每个节点上都有 Running 实例
# ④ 若依业务是否正常运行
kubectl get pods -n ruoyi
# 期望:7 个服务全部 Running
# ⑤ 服务端点是否正常(Endpoints 不为空)
kubectl get endpoints -n ruoyi
# 期望:每个 Service 后面都有 Pod IP
# ⑥ Harbor 是否可访问
curl -s -o /dev/null -w "harbor: HTTP %{http_code}\n" http://192.168.0.21:10086
# 期望:HTTP 200
# ⑦ 节点资源余量(决定 Argo CD 装在哪)
kubectl top nodes
# 期望:至少有一个节点剩下 1 CPU 和 2Gi 内存这些检查在确认什么
| 检查 | 不通过会怎样 |
|---|---|
| ① ② | 集群本身有问题,先修集群 |
| ③ | NetworkPolicy 不生效(不会报错,但隔离机制形同虚设) |
| ④ ⑤ | 业务本身有问题,先修业务——GitOps 改造不会修好已有的故障,只会把它搬到 Git 里 |
| ⑥ | Argo CD 装不上(镜像推不进去) |
| ⑦ | Argo CD 的 Pod 会一直 Pending |
4.2 准备 Harbor 的 infra 项目
Argo CD 自身的三个镜像需要存放位置。不要和业务镜像混在 ruoyi-cloud 项目里,分开更清晰,也便于权限管理。
bash
# 方式一:在 Harbor Web UI 里创建
# 打开 http://192.168.0.21:10086 → 项目 → 新建项目
# 项目名称:infra
# 访问级别:私有
# 存储配额:-1(不限制)
# 方式二:用 Harbor API 创建(适合脚本化)
curl -k -u 'admin:Harbor12345' -X POST \
"https://192.168.0.21:10086/api/v2.0/projects" \
-H "Content-Type: application/json" \
-d '{"project_name":"infra","metadata":{"public":"false"},"storage_limit":-1}'这条命令在做什么
- 方式二用的是 Harbor 的 v2.0 API。
-u admin:密码做基本认证,-d传 JSON 体。 "public":"false"表示私有项目。- 前提:需要在
harbor.yml里没有配置 HTTPS 时用http;配了 HTTPS 则用https并可能需要-k跳过证书校验。
bash
# 验证 infra 项目已创建
curl -k -u 'admin:Harbor12345' "https://192.168.0.21:10086/api/v2.0/projects?page_size=20" \
| grep -o '"name":"[^"]*"'
# 期望输出里包含 "name":"infra"4.3 创建 GitOps 仓库
bash
# 在 Gitea 上创建仓库(也可以先本地初始化,稍后 push)
# Web UI: http://192.168.0.25:3000 → 新建仓库
# 仓库名:ruoyi-gitops
# 可见性:私有
# 初始化:勾选「初始化仓库」(这样会生成 main 分支和 README)
# 或者用 Gitea API 创建
curl -s -X POST "http://192.168.0.25:3000/api/v1/user/repos" \
-H "Content-Type: application/json" \
-H "Authorization: token <你的 Gitea Token>" \
-d '{"name":"ruoyi-gitops","private":true,"auto_init":true,"default_branch":"main"}'这条命令在做什么
Authorization: token <Token>:Gitea 的 API 认证方式。Token 在「用户设置 → 应用」里生成,权限需要write:repository。"auto_init":true:自动创建初始提交(含 README 和 .gitignore),否则这是个空仓库,git clone会警告。
为什么 GitOps 清单要单独一个仓库
| 方案 | 优点 | 缺点 |
|---|---|---|
| 单独的 GitOps 仓库(本文档采用) | 清单与代码解耦;一个仓库管所有服务的部署状态;便于只给 Argo CD 读权限 | 需要跨仓库操作(CI 要能写这个仓库) |
| 放在业务代码仓库里 | 不用跨仓库 | 每个服务一个仓库就有 7 份清单;Argo CD 需要读 7 个仓库;回滚时容易漏掉某个服务 |
判断标准:服务数量少(1~2 个)可以合在代码仓库里;服务多(≥3 个)强烈建议独立仓库。
05 阶段一:安装 Argo CD
5.1 第一步:准备安装清单与 CLI
bash
# ===== 在 master(192.168.0.10)上执行 =====
mkdir -p /root/argocd/manifests && cd /root/argocd/manifests
# 1) 下载官方安装清单(走国内加速,实测可下载)
curl -fL -o install.yaml \
https://gh-proxy.com/https://raw.githubusercontent.com/argoproj/argo-cd/v3.5.3/manifests/install.yaml
# 2) 校验:写作时该文件为 1,917,766 字节
ls -l install.yaml
# 3) 下载 argocd CLI(用于命令行操作与验收)
curl -fL -o /usr/local/bin/argocd \
https://files.m.daocloud.io/github.com/argoproj/argo-cd/releases/download/v3.5.3/argocd-linux-amd64
chmod +x /usr/local/bin/argocd
# 4) 验证 CLI
argocd version --client
# 期望输出:argocd: v3.5.3+...这几条命令在做什么
- 为什么用固定版本 URL 而不是
stable:.../manifests/stable/install.yaml会指向"当前稳定版",意味着你某天重跑命令时可能装上了一个未经测试的新版本。生产环境永远锁定具体版本号。 files.m.daocloud.io/github.com/...:daocloud 提供的 GitHub Release 二进制加速。它的路径规则是「把https://github.com/换成https://files.m.daocloud.io/github.com/」。chmod +x后放到/usr/local/bin,就能直接以argocd调用。
为什么建议装 CLI
Web UI 能做几乎所有事,但 CLI 有两个不可替代的场景:
- 脚本化验收:
argocd app get ruoyi-gateway --output json可以直接喂给监控系统 - 无法访问 UI 时排障:例如 Ingress 配错了,只能用 CLI 从集群内部操作
5.2 第二步:把三个镜像搬进自己的 Harbor
这是国内环境最关键的一步。Argo CD 的安装清单里引用了 3 个外部镜像,全部在境外仓库:
| 清单里的原始地址 | 用途 | 实测可用的国内源 |
|---|---|---|
quay.io/argoproj/argocd:v3.5.3 | Argo CD 主程序(server / controller / repo-server 共用) | quay.m.daocloud.io/argoproj/argocd:v3.5.3 ✅ |
ghcr.io/dexidp/dex:v2.45.1 | SSO 组件 | ghcr.m.daocloud.io/dexidp/dex:v2.45.1 ✅ |
public.ecr.aws/docker/library/redis:8.2.3-alpine | 缓存 | m.daocloud.io/public.ecr.aws/docker/library/redis:8.2.3-alpine ✅ |
注意 redis 这个镜像的地址有点特别
Argo CD 3.x 用的 redis 镜像不在 Docker Hub 上,而是在 AWS 的公共 ECR(public.ecr.aws)。所以:
- ❌ 不能用
m.daocloud.io/docker.io/library/redis:xxx(那是 Docker Hub 的 redis,虽然也是 redis,但不是同一个镜像源,版本号可能对不上) - ✅ 正确写法是把
public.ecr.aws整体拼在 daocloud 前缀后面:m.daocloud.io/public.ecr.aws/docker/library/redis:8.2.3-alpine
这条实测有效(见上表)。
bash
# ===== 在 master 上执行(docker 命令;master 上没装 docker 的话就在 ci-jenkins 上做) =====
# 0) 登录自己的 Harbor
docker login 192.168.0.21:10086
# 输入 admin / 你在 harbor.yml 里设置的密码
# 1) 定义「源地址 -> 目标地址」的对应关系
SRC_ACD="quay.m.daocloud.io/argoproj/argocd:v3.5.3"
DST_ACD="192.168.0.21:10086/infra/argocd:v3.5.3"
SRC_DEX="ghcr.m.daocloud.io/dexidp/dex:v2.45.1"
DST_DEX="192.168.0.21:10086/infra/dex:v2.45.1"
SRC_REDIS="m.daocloud.io/public.ecr.aws/docker/library/redis:8.2.3-alpine"
DST_REDIS="192.168.0.21:10086/infra/redis:8.2.3-alpine"
# 2) 拉取 -> 重打标签 -> 推送
for pair in "$SRC_ACD|$DST_ACD" "$SRC_DEX|$DST_DEX" "$SRC_REDIS|$DST_REDIS"; do
src="${pair%%|*}"; dst="${pair##*|}"
echo ">>> $src -> $dst"
docker pull "$src"
docker tag "$src" "$dst"
docker push "$dst"
done
# 3) 清理本地临时镜像(避免占满磁盘)
docker rmi "$SRC_ACD" "$DST_ACD" "$SRC_DEX" "$DST_DEX" "$SRC_REDIS" "$DST_REDIS" || true
# 4) 验证:三个镜像都在 Harbor 的 infra 项目里
curl -k -u 'admin:Harbor12345' \
"https://192.168.0.21:10086/api/v2.0/projects/infra/repositories?page_size=20" \
| grep -o '"name":"[^"]*"'
# 期望看到 infra/argocd、infra/dex、infra/redis这段脚本在做什么
- 为什么必须先搬到自己的 Harbor:Argo CD 的 Pod 每次启动都要拉镜像。如果直接引用境外的
quay.io,一旦网络抖动或镜像站限流,Pod 会卡在ImagePullBackOff。搬到内网后,拉取速度是本地网络级别,且完全可控。 for pair in "$A|$B" ...:用|作为分隔符把两个地址打包在一个变量里。这是个实用技巧 —— 避免写两套变量(SRC1/DST1/SRC2/DST2...),脚本更短更清晰。${pair%%|*}取|之前的,${pair##*|}取之后的。这是 bash 的参数展开语法。- 最后的
docker rmi ... || true:|| true保证即使某个镜像删不掉(比如被容器占用),脚本也不会中断。
一定要用「同一版本」的镜像,不要图省事换别的
常见错误:把 public.ecr.aws/docker/library/redis:8.2.3-alpine 换成 docker.io/library/redis:7-alpine,理由是"都是 redis"。
这可能导致 Argo CD 行为异常:
- Argo CD 3.x 的 redis 使用方式与 2.x 有差异(命令、数据结构)
- 大版本跨越(8.x → 7.x)可能缺少用到的命令
规则:搬运镜像时,保持 tag 完全不变,只换 registry 前缀。
5.3 第三步:改造清单并安装
bash
# 仍在 /root/argocd/manifests 目录
# 1) 备份原文件(改之前永远先备份)
cp install.yaml install.yaml.orig
# 2) 把三个外部镜像地址替换成自己的 Harbor
sed -i 's#quay.io/argoproj/argocd:v3.5.3#192.168.0.21:10086/infra/argocd:v3.5.3#g' install.yaml
sed -i 's#ghcr.io/dexidp/dex:v2.45.1#192.168.0.21:10086/infra/dex:v2.45.1#g' install.yaml
sed -i 's#public.ecr.aws/docker/library/redis:8.2.3-alpine#192.168.0.21:10086/infra/redis:8.2.3-alpine#g' install.yaml
# 3) 关键校验:确认已经没有任何境外地址残留
echo "--- 残留的境外地址(应该为空)---"
grep -nE 'image:\s*(quay\.io|ghcr\.io|public\.ecr\.aws|docker\.io)' install.yaml || echo "✅ 已全部替换"
echo "--- 现在使用的镜像 ---"
grep -oE 'image:\s*[^ ]+' install.yaml | sort -u
# 期望只看到 192.168.0.21:10086/infra/ 开头的三个地址这几条 sed 在做什么
s#旧#新#g:用#作为分隔符(因为地址里有/)。g表示全局替换(文件里同一个镜像出现了 10 次,全部要换)。- 第 3 步的校验不能省。手工替换很容易漏(比如某处多了个空格、或者用了不同的 tag)。校验命令比替换命令更重要。
bash
# 4) 创建命名空间
kubectl create namespace argocd
# 5) 安装(注意 --server-side 必须加)
kubectl apply -n argocd --server-side --force-conflicts -f install.yaml
# 6) 观察 Pod 启动(约 1~2 分钟)
kubectl get pods -n argocd -w这条命令在做什么
--server-side:这个参数不能省。Argo CD 的 CRD(applications.argoproj.io等)定义非常大,超过了kubectl apply默认客户端策略能处理的 262KB 限制,会报metadata.annotations: Too long。--force-conflicts:配合--server-side使用,允许覆盖字段冲突。kubectl get pods -w:实时观察。等所有 Pod 变成Running且READY列为1/1后按 Ctrl+C。
bash
# 7) 等待关键 Deployment 就绪(更严谨的等待方式)
kubectl -n argocd wait --for=condition=available deployment --all --timeout=300s
# 8) 最终确认
kubectl get pods -n argocd
kubectl get svc -n argocd这条命令在做什么
kubectl wait --for=condition=available:这是脚本化验收的标准写法。它阻塞直到条件满足或超时,返回码可以直接用于判断成败(比grep输出可靠得多)。
5.4 第四步:打补丁(节点固定、资源限额、拉取凭据)
原始 install.yaml 是"通用"的:没有节点选择、没有资源限额、没有私有仓库凭据。这三个补丁是让它适配你的环境的关键。
bash
cd /root/argocd/manifests
# 补丁 1:让所有 Argo CD 组件都用 harbor-secret 拉镜像
cat > patch-imagepullsecrets.yaml <<'EOF'
---
# 给每个 Deployment / StatefulSet 都加上 imagePullSecrets
apiVersion: apps/v1
kind: Deployment
metadata:
name: argocd-server
namespace: argocd
spec:
template:
spec:
imagePullSecrets:
- name: harbor-secret
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: argocd-repo-server
namespace: argocd
spec:
template:
spec:
imagePullSecrets:
- name: harbor-secret
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: argocd-redis
namespace: argocd
spec:
template:
spec:
imagePullSecrets:
- name: harbor-secret
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: argocd-dex-server
namespace: argocd
spec:
template:
spec:
imagePullSecrets:
- name: harbor-secret
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: argocd-application-controller
namespace: argocd
spec:
template:
spec:
imagePullSecrets:
- name: harbor-secret
EOF
# 补丁 2:固定到指定节点 + 资源限额
cat > patch-nodeselector.yaml <<'EOF'
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: argocd-server
namespace: argocd
spec:
template:
spec:
nodeSelector:
kubernetes.io/hostname: k8s-node1
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: argocd-repo-server
namespace: argocd
spec:
template:
spec:
nodeSelector:
kubernetes.io/hostname: k8s-node1
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: argocd-application-controller
namespace: argocd
spec:
template:
spec:
nodeSelector:
kubernetes.io/hostname: k8s-node1
EOF这两个补丁在做什么
imagePullSecrets:infra项目是私有的,没有这个字段,Pod 会报pull access denied。nodeSelector:把 Argo CD 钉在k8s-node1上。为什么值得钉住:- Argo CD 是"管理集群"的组件,如果它和某个业务 Pod 挤在内存紧张的节点上被一起驱逐,会造成"集群管理能力中断"
- 钉住之后,
kubectl top node k8s-node1的读数就有了明确含义
bash
# 3) 先在 argocd 命名空间创建拉取凭据
kubectl create secret docker-registry harbor-secret \
--namespace argocd \
--docker-server=192.168.0.21:10086 \
--docker-username='robot$ruoyi-cloud+jenkins-robot' \
--docker-password='<机器人账号 Token>' \
--docker-email=dev@example.com
# 4) 应用三个补丁
kubectl patch -n argocd --patch-file patch-imagepullsecrets.yaml --type=merge
kubectl patch -n argocd --patch-file patch-nodeselector.yaml --type=merge
# 5) 等 Pod 重新就绪
kubectl get pods -n argocd -w这条命令在做什么
kubectl patch --type=merge:合并式更新。它只改动补丁里写到的字段,其他字段保持不动。 对比:--type=strategic是默认值(对列表有特殊合并规则),--type=json需要指定完整的 JSON Patch 路径。- 为什么用
patch而不是重新apply整个 install.yaml:改一处只动一处,降低误改风险。
校验补丁是否生效
bash
# 确认 nodeSelector 生效
kubectl -n argocd get deployment argocd-server -o jsonpath='{.spec.template.spec.nodeSelector}'
echo
# 确认 Pod 真的跑在 k8s-node1 上
kubectl -n argocd get pods -o wide | grep argocd-server为什么反向验证:kubectl patch 成功只代表"API Server 接受了",Pod 是否真的按新约束重建、是否真的调度到了目标节点,要看实际状态。
bash
# 6) 给 argocd-server 设置资源限额(可选但建议)
kubectl -n argocd patch deployment argocd-server --type=merge -p '{
"spec": {"template": {"spec": {"containers": [{
"name": "argocd-server",
"resources": {
"requests": {"cpu": "100m", "memory": "128Mi"},
"limits": {"cpu": "1", "memory": "512Mi"}
}
}]}}}
}'这条命令在做什么
-p '{...}':直接内联传 JSON 补丁,适合小改动(不用专门写文件)。- 注意:合并列表类型(
containers数组)时,--type=merge是全量替换数组,所以这里必须写完整的 container 对象(包含name),不能只写resources。
5.5 第五步:暴露 Web UI
有两种方式,按你的环境选:
方式 A:NodePort(最简单,适合内网学习环境)
bash
# 把 argocd-server 改成 NodePort 类型
kubectl -n argocd patch svc argocd-server -p '{
"spec": {"type": "NodePort"}
}'
# 查看分配到的端口
kubectl -n argocd get svc argocd-server
# 例如输出 80:31234/TCP,则访问 http://192.168.0.11:31234方式 B:Ingress(如果集群已有 ingress-nginx)
yaml
# argocd-server-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argocd-server-ingress
namespace: argocd
annotations:
# 关键:Argo CD 用 gRPC,后端必须声明 HTTP/2
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
nginx.ingress.kubernetes.io/force-ssl-redirect: "false"
# gRPC 需要的额外配置
nginx.ingress.kubernetes.io/ssl-passthrough: "false"
spec:
ingressClassName: nginx
rules:
- host: argocd.example.com # 换成你的域名
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: argocd-server
port:
number: 80bash
# 应用 Ingress
kubectl apply -f argocd-server-ingress.yaml
# 让 argocd-server 以 HTTP 提供服务(Ingress 后端用 HTTP 更简单)
kubectl -n argocd patch deployment argocd-server --type=json -p '[{
"op": "add",
"path": "/spec/template/spec/containers/0/command/-",
"value": "--insecure"
}]'
# 确认生效
kubectl -n argocd get deployment argocd-server -o jsonpath='{.spec.template.spec.containers[0].command}'
echo--insecure 是什么
- Argo CD 默认在 UI/API 上启用 TLS(内置自签证书),而 Ingress 转发到后端时用的是 HTTP。加上
--insecure让 Argo CD 只提供 HTTP,由 Ingress 负责 TLS 终止 —— 这是最清晰的分工。 - 注意
--insecure只是"不自己终止 TLS",不影响对外安全性(外层的证书由 Ingress 提供)。
5.6 第六步:登录与验收
bash
# 1) 获取初始管理员密码
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d; echo
# 2) 用 CLI 登录(NodePort 场景)
argocd login 192.168.0.11:31234 --username admin --insecure --grpc-web
# 3) 改密码(强烈建议)
argocd account update-password
# 4) 查看版本与连接状态
argocd version这几条命令在做什么
argocd-initial-admin-secret:首次安装时生成的一次性密码。它会一直存在,改完密码后建议删除这个 Secret:bashkubectl -n argocd delete secret argocd-initial-admin-secret--insecure:跳过 TLS 证书校验(因为我们用 HTTP/NodePort 访问)。--grpc-web:在没有配置真实 TLS 时必需。Argo CD 的 CLI 默认走 gRPC over HTTP/2,浏览器和很多代理只支持 HTTP/1.1,--grpc-web让它改用 gRPC-Web 协议。
bash
# 5) 验收清单:逐条执行
echo "=== ① 所有 Pod 就绪 ==="
kubectl -n argocd get pods
echo "=== ② 关键 Deployment 可用 ==="
kubectl -n argocd get deployment,statefulset
echo "=== ③ CRD 已安装 ==="
kubectl get crd | grep argoproj
# 期望:applications.argoproj.io / applicationsets.argoproj.io / appprojects.argoproj.io
echo "=== ④ CLI 能连上 ==="
argocd version --short
echo "=== ⑤ 当前没有任何 Application(干净状态)==="
argocd app list
# 期望输出空列表
echo "=== ⑥ 服务端配置已加载 ==="
kubectl -n argocd get configmap argocd-cm -o jsonpath='{.data}' | head -c 500; echo到这里为止,Argo CD 只是一个"空壳"
它已经能跑了,但还没有任何 Application,所以它不会管理任何东西。接下来要做的两件事:
- 把清单变成 Argo CD 能读的仓库结构(第 6 章)
- 创建 Application 让 Argo CD 知道"要管什么"(第 7 章)
06 阶段二:构建 GitOps 清单仓库
6.1 核心原则:第一版清单必须让 diff 为空
这是整套改造里最容易出事的一步
如果 Argo CD 第一次同步时产生了大量 diff,它会按清单里的内容去改集群——包括删掉它认为"多余"的东西。
真实风险举例:某服务的线上 Deployment 实际挂载了一个 ConfigMap,但仓库里的清单这段是注释掉的。Argo CD 同步后会把 volumeMount 删掉,服务下次重启就起不来了。
所以第一版清单必须满足:apply 之后 diff 为空。
实现这一点的做法是:从活着的集群导出清单,而不是从代码仓库复制。
| 来源 | 风险 |
|---|---|
❌ 从业务代码仓库的 k8s/deployment.yaml 复制 | 可能与线上不一致(线上被手动改过、或清单本身就有遗漏) |
✅ 从线上集群导出(kubectl get -o yaml) | 100% 反映当前真实状态,首次同步 diff 为空 |
bash
# ===== 在 master 上执行 =====
# 1) 导出 ruoyi 命名空间下所有的应用类资源
kubectl -n ruoyi get deployment,service,configmap,ingress,secret \
-o yaml > /root/argocd/ruoyi-live-raw.yaml
# 2) 看一下导出了多少东西
grep -c "^kind:" /root/argocd/ruoyi-live-raw.yaml
wc -l /root/argocd/ruoyi-live-raw.yaml这条命令在做什么
kubectl get <类型列表> -o yaml:一次导出多种资源。逗号分隔的列表是kubectl支持的语法。- 为什么导出 Secret:里面可能有业务需要的配置(如镜像拉取凭据)。但 Secret 的内容是 base64 编码的明文,直接提交到 Git 有泄露风险 —— 下一节会处理。
bash
# 3) 关键校验:确认 gateway 的 volumeMounts 在导出的内容里
grep -A 10 "volumeMounts" /root/argocd/ruoyi-live-raw.yaml | head -30
# 4) 确认配置了哪些 ConfigMap
grep -B 2 -A 5 "^kind: ConfigMap" /root/argocd/ruoyi-live-raw.yaml | grep -E "name:|^ [a-z]"为什么专门校验 volumeMounts
这是"仓库清单与线上漂移"最典型的体现:业务仓库里的清单把这段注释掉了,但线上实际是挂载的。导出时必须确认这段在,否则你会把一个正确配置改坏。
6.2 清洗导出的清单
kubectl get -o yaml 导出的内容包含大量运行时字段,直接提交到 Git 会有问题:
| 需要删除的字段 | 为什么 |
|---|---|
metadata.creationTimestamp | 每次导出都不一样,会造成无意义的 diff |
metadata.resourceVersion | 集群内部版本号,提交上去没意义且每次都变 |
metadata.uid | 同上 |
metadata.generation | 同上 |
metadata.selfLink | 已废弃字段 |
status | 整个状态段都要删 —— 那是运行时信息,不是期望状态 |
metadata.annotations.kubectl.kubernetes.io/last-applied-configuration | kubectl 的历史记录,可能很大 |
metadata.annotations.deployment.kubernetes.io/revision | 同上 |
spec.clusterIP、spec.clusterIPs(Service) | 由集群分配,写死会导致冲突 |
处理方式有两种:手工清洗(用 kubectl neat 插件或脚本)或让 Argo CD 自己忽略(在 Application 里配置 ignoreDifferences)。
推荐用下面的方式一次性清洗干净:
bash
# 安装 kubectl-neat 插件(专门做这件事)
curl -fL -o /tmp/kubectl-neat.tar.gz \
https://gh-proxy.com/https://github.com/itaysk/kubectl-neat/releases/download/v2.0.4/kubectl-neat_linux_amd64.tar.gz
tar -xzf /tmp/kubectl-neat.tar.gz -C /tmp
mv /tmp/kubectl-neat /usr/local/bin/
chmod +x /usr/local/bin/kubectl-neat
# 验证
kubectl neat --help | head -3kubectl-neat 是什么
- 一个专门"清理 K8s 清单"的插件:删掉所有运行时字段(
status、uid、resourceVersion、creationTimestamp等),产出可以安全提交到 Git 的干净 YAML。 - 它是
kubectl的插件机制(可执行文件名以kubectl-开头,放在 PATH 里就能被识别)。
bash
# 用它清洗导出的文件
kubectl neat -f /root/argocd/ruoyi-live-raw.yaml > /root/argocd/ruoyi-live-clean.yaml
# 对比一下清洗前后的差异
echo "清洗前: $(wc -l < /root/argocd/ruoyi-live-raw.yaml) 行"
echo "清洗后: $(wc -l < /root/argocd/ruoyi-live-clean.yaml) 行"
# 确认 status 段已经没了
grep -c "^status:" /root/argocd/ruoyi-live-clean.yaml
# 期望输出 0
# 确认关键内容还在
grep -c "volumeMounts" /root/argocd/ruoyi-live-clean.yaml
# 期望 ≥ 1关于 Secret 的处理
导出的 Secret 里包含 base64 编码的凭据(如 harbor-secret)。有三种处理方式:
| 方式 | 做法 | 适用 |
|---|---|---|
| 不纳入 GitOps(本文档采用) | 从清单里删掉 Secret,改为手工在集群里创建 | 简单、安全 |
| 加密后提交 | 用 Sealed Secrets / SOPS / External Secrets Operator | 生产推荐 |
| 明文提交 | ❌ 绝对不要 | 无 |
本文档采用第一种:Secret 由管理员手工创建(§5.4 已经做过),Git 里只保存 Deployment/Service/ConfigMap/Ingress。
bash
# 从清洗后的清单里剔除 Secret(避免明文入库)
# 用 python 做精确切分(比 sed 更可靠)
python3 - <<'PY'
import re
src = "/root/argocd/ruoyi-live-clean.yaml"
dst = "/root/argocd/ruoyi-live-clean-nosecret.yaml"
docs = open(src, encoding="utf-8").read().split("\n---\n")
keep = [d for d in docs if d.strip() and not re.search(r"^kind:\s*Secret\s*$", d, re.M)]
open(dst, "w", encoding="utf-8").write("\n---\n".join(keep))
print(f"原始文档数: {len(docs)}")
print(f"保留文档数: {len(keep)}(剔除了 {len(docs)-len(keep)} 个 Secret)")
PY6.3 GitOps 仓库的目录结构
这里用 Kustomize 的 base + overlay 结构来组织。
Kustomize 是什么,为什么用它
Kustomize 是 K8s 原生的清单定制工具(kubectl -k 直接支持,不需要额外安装)。
它解决的问题:同一套应用,在不同环境(dev/prod)的差异只是镜像 tag 和副本数。如果给每个环境复制一份完整 YAML,改一处就要改 N 处。
text
base/ ← 「这个应用长什么样」的结构骨架(不含环境和版本)
overlays/prod/ ← 「生产环境要什么」的差异(几个副本、哪个镜像版本)为什么 GitOps 场景特别适合它:CI 只需要改 overlays/prod/kustomization.yaml 里的一行 newTag,就完成了一次发布。这一行改动小到可以精确审计。
对比:如果直接改 deployment.yaml 里的 image: 字段,虽然也能工作,但"哪个字段是版本、哪个是配置"就混在一起了,将来想做多环境就得重构。
text
ruoyi-gitops/
├── README.md
├── base/ # 所有服务共享的结构(可选,也可以每个服务独立)
│
├── apps/ # 每个服务一个目录
│ ├── gateway/
│ │ ├── base/
│ │ │ ├── deployment.yaml # 从集群导出的 Deployment
│ │ │ ├── service.yaml # 从集群导出的 Service
│ │ │ └── kustomization.yaml # 声明"这个 base 由哪些文件组成"
│ │ └── overlays/
│ │ └── prod/
│ │ └── kustomization.yaml # 声明"生产环境用哪个 tag、几个副本"
│ │
│ ├── auth/
│ │ └── ...(同上结构)
│ ├── modules-system/
│ ├── modules-gen/
│ ├── modules-job/
│ ├── modules-file/
│ └── visual-monitor/
│
└── argocd/ # Argo CD 自身的配置(AppProject / Application)
├── project-ruoyi.yaml
├── app-gateway.yaml
└── ...目录结构没有唯一正确答案
上面是一种常见组织方式。关键约束只有两条:
- 每个 Application 指向的路径里必须有
kustomization.yaml(除非直接用普通 YAML 目录) - CI 要修改的那个文件位置固定且唯一(这里就是
apps/<服务名>/overlays/prod/kustomization.yaml)
如果你觉得 base/overlays 两层太啰嗦,也可以简化为每个服务只有一个目录 + 一个 kustomization.yaml,把 Deployment/Service 直接放进去。先跑通,再优化结构。
一个服务的完整示例(以 gateway 为例):
yaml
# apps/gateway/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# 声明这个 base 包含哪些资源文件
resources:
- deployment.yaml
- service.yaml
# 统一的公共标签(会加到所有资源上)
commonLabels:
app.kubernetes.io/part-of: ruoyi
app.kubernetes.io/managed-by: argocdyaml
# apps/gateway/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# 引用 base
resources:
- ../../base
# 命名空间(会覆盖 base 里所有资源的 namespace)
namespace: ruoyi
# ★★★ 这是 CI 唯一会修改的地方 ★★★
images:
- name: 192.168.0.21:10086/ruoyi-cloud/ruoyi-gateway
newTag: "20260924143012" # <- CI 把这一行改成 git 短 SHA
# 副本数(可以在这里覆盖 base 的值)
replicas:
- name: ruoyi-gateway-deploy
count: 2
# 资源名前缀/后缀(可选)
# namePrefix: prod-这段配置在做什么(重点看 images)
| 字段 | 作用 |
|---|---|
resources: [../../base] | 继承 base 的全部内容 |
namespace: ruoyi | 统一指定命名空间,不用在每个资源文件里写 |
images[].name | 要替换的镜像地址(不含 tag) |
images[].newTag | 新 tag。Kustomize 会找到所有 image 等于 name 的容器,把 tag 换掉 |
replicas[].count | 覆盖副本数 |
images 的匹配规则有个坑
images[].name 必须是镜像地址去掉 tag 和 digest 之后的部分。
yaml
# ✅ 正确:name 里不带 tag
images:
- name: 192.168.0.21:10086/ruoyi-cloud/ruoyi-gateway
newTag: "abc1234"
# ❌ 错误:name 里带 tag,Kustomize 匹配不上,静默不生效
images:
- name: 192.168.0.21:10086/ruoyi-cloud/ruoyi-gateway:latest
newTag: "abc1234""静默不生效"是最坑的地方 —— Kustomize 不会报错,只是替换没发生,部署的还是旧镜像。所以 CI 里一定要加校验(见 §8.3)。
bash
# 本地验证 Kustomize 渲染结果(在改 GitOps 仓库时必做)
kubectl kustomize apps/gateway/overlays/prod/
# 只看渲染出来的镜像是什么
kubectl kustomize apps/gateway/overlays/prod/ | grep "image:"
# 期望:看到 192.168.0.21:10086/ruoyi-cloud/ruoyi-gateway:<你设置的 tag>这条命令在做什么
kubectl kustomize <目录>:在本地渲染(不接触集群)。这是验证 Kustomize 配置最快的方式。- 它等价于 Argo CD 在 repo-server 里做的事。本地渲染成功,Argo CD 就能渲染成功。
6.4 把清单拆分到各服务目录
如果按 §6.2 导出的是一整个大文件(包含 7 个服务的所有资源),需要按服务拆开。
bash
# 在 GitOps 仓库目录里执行
cd /root/ruoyi-gitops
# 用 python 按资源名拆分(比手工复制可靠)
python3 - <<'PY'
import re, os, yaml
src = "/root/argocd/ruoyi-live-clean-nosecret.yaml"
docs = [d for d in open(src, encoding="utf-8").read().split("\n---\n") if d.strip()]
# 服务名 -> 目录名 的映射
mapping = {
"ruoyi-gateway": "gateway",
"ruoyi-auth": "auth",
"ruoyi-modules-system": "modules-system",
"ruoyi-modules-gen": "modules-gen",
"ruoyi-modules-job": "modules-job",
"ruoyi-modules-file": "modules-file",
"ruoyi-visual-monitor": "visual-monitor",
}
count = {}
for d in docs:
try:
obj = yaml.safe_load(d)
except Exception:
continue
if not obj or "kind" not in obj:
continue
name = obj.get("metadata", {}).get("name", "")
kind = obj["kind"].lower()
target = None
for prefix, dirname in mapping.items():
if name.startswith(prefix):
target = dirname
break
if not target:
print(f" [跳过] {kind}/{name}(未匹配到服务)")
continue
d_dir = os.path.join("apps", target, "base")
os.makedirs(d_dir, exist_ok=True)
fname = f"{kind}.yaml"
with open(os.path.join(d_dir, fname), "w", encoding="utf-8") as f:
f.write(d if d.endswith("\n") else d + "\n")
count.setdefault(target, []).append(f"{kind}/{name}")
print("\n=== 拆分结果 ===")
for k in sorted(count):
print(f" {k:<18} {len(count[k])} 个资源")
PY这段脚本在做什么
- 用
yaml.safe_load把每个 YAML 文档解析成 Python 对象,读取kind和metadata.name。 - 按名字前缀匹配到服务目录,写成
<目录>/base/<kind>.yaml。 - 为什么用 Python 而不是 sed/awk:YAML 的结构化解析能正确处理缩进、注释、多行字符串。用文本工具很容易切错。
bash
# 为每个服务的 base 生成 kustomization.yaml
for svc in gateway auth modules-system modules-gen modules-job modules-file visual-monitor; do
d="apps/$svc/base"
[ -d "$d" ] || continue
# 根据目录里实际存在的文件生成 resources 列表
res=$(ls "$d" | grep -E '\.yaml$' | grep -v kustomization | sed 's/^/ - /')
cat > "$d/kustomization.yaml" <<EOF
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
$res
commonLabels:
app.kubernetes.io/part-of: ruoyi
app.kubernetes.io/managed-by: argocd
EOF
echo "已生成 $d/kustomization.yaml"
donebash
# 为每个服务生成 overlays/prod/kustomization.yaml
for svc in gateway auth modules-system modules-gen modules-job modules-file visual-monitor; do
d="apps/$svc/overlays/prod"
mkdir -p "$d"
cat > "$d/kustomization.yaml" <<EOF
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: ruoyi
# ★ CI 发布时只修改下面这一段 ★
images:
- name: 192.168.0.21:10086/ruoyi-cloud/ruoyi-$svc
newTag: "latest"
EOF
echo "已生成 $d/kustomization.yaml"
done注意 ruoyi-$svc 的命名
若依的镜像名与服务目录名的对应关系:
| 目录名 | 镜像名 |
|---|---|
gateway | ruoyi-gateway |
auth | ruoyi-auth |
modules-system | ruoyi-modules-system |
visual-monitor | ruoyi-visual-monitor |
所以拼出来是 ruoyi- + 目录名。但 modules-system 这类要注意:拼出来是 ruoyi-modules-system,正好对上。
更稳妥的做法是从导出的清单里读取真实的镜像名,而不是靠命名约定。生成后务必用 kubectl kustomize 验证一遍。
bash
# 批量验证所有服务的 Kustomize 渲染
echo "=== Kustomize 渲染验证 ==="
for svc in gateway auth modules-system modules-gen modules-job modules-file visual-monitor; do
printf " %-18s " "$svc"
if out=$(kubectl kustomize "apps/$svc/overlays/prod" 2>&1); then
img=$(echo "$out" | grep -m1 "image:" | awk '{print $2}')
echo "OK 渲染出镜像: $img"
else
echo "FAIL $out"
fi
done6.5 推送到 Gitea
bash
cd /root/ruoyi-gitops
# 1) 初始化并首次提交
git init -b main
cat > .gitignore <<'EOF'
# 不要提交 Secret 的明文
*-secret.yaml
.secret/
EOF
cat > README.md <<'EOF'
# ruoyi-gitops
若依微服务在 K8s 上的部署清单(GitOps 单一事实来源)。
## 目录结构
- `apps/<服务名>/base/` —— 从集群导出的原始清单
- `apps/<服务名>/overlays/prod/` —— 生产环境的差异(镜像 tag、副本数)
- `argocd/` —— Argo CD 的 AppProject 与 Application
## 发布流程
CI(Jenkins)修改 `apps/<服务名>/overlays/prod/kustomization.yaml` 里的 `newTag`,
提交推送后由 Argo CD 自动同步到集群。
## 注意
清单是从线上集群导出的,修改前请先确认 diff。
EOF
git add -A
git commit -m "chore: 初始化 GitOps 清单仓库(从线上集群导出)"
# 2) 关联远端并推送
git remote add origin http://192.168.0.25:3000/ruoyi/ruoyi-gitops.git
git push -u origin main
# 3) 确认推送成功
git log --oneline | head -3这段在做什么
git init -b main:初始化仓库并把默认分支设为main(避免新版 git 的master/main提示)。.gitignore里排除*-secret.yaml:这是一道保险,防止后续有人不小心把 Secret 提交上去。git remote add origin http://192.168.0.25:3000/ruoyi/ruoyi-gitops.git:Gitea 的 HTTP 克隆地址格式是http://<gitea地址>/<组织或用户>/<仓库>.git。
用 SSH 还是 HTTP
| 方式 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| HTTP | 配置简单,用 Token 认证 | 需要每次输入或在 URL 里带凭据 | CI 环境(本文档用这个) |
| SSH | 无需每次认证 | 需要生成和分发密钥 | 人工频繁推送的场景 |
CI 环境推荐 HTTP + Token,因为 Token 可以精确控制权限(只给 write:repository),并且能随时吊销。
07 阶段三:创建 AppProject 与 Application
7.1 先理解这两个 CRD
| 资源 | 回答的问题 | 类比 |
|---|---|---|
| AppProject | 「哪些仓库、哪些集群、哪些资源类型是允许被管理的?」 | 权限边界 / 租户隔离 |
| Application | 「把某个 Git 路径下的清单,部署到某个集群的某个命名空间」 | 一条部署规则 |
它们的层级关系:一个 AppProject 下面可以有多个 Application。
为什么不用 default 项目
Argo CD 装好后自带一个 default 项目,允许访问任何仓库、任何集群、任何资源类型。
如果你把 Application 都放在 default 里,意味着任何能创建 Application 的人都能拿到集群的完全控制权(通过让 Argo CD 同步一个恶意清单)。
建独立 AppProject 的收益:明确限定"只能读这个仓库、只能写这个命名空间"。
7.2 创建 AppProject
yaml
# argocd/project-ruoyi.yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: ruoyi
namespace: argocd
# 最后同步的标记(可选,用于"删除项目前需先删应用"的保护)
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: "若依微服务项目(生产环境)"
# ===== 允许从哪些 Git 仓库拉清单 =====
sourceRepos:
- "http://192.168.0.25:3000/ruoyi/ruoyi-gitops.git"
# ===== 允许部署到哪些集群与命名空间 =====
destinations:
- namespace: ruoyi
server: https://kubernetes.default.svc # 表示"Argo CD 所在的那个集群"
- namespace: ruoyi-dev
server: https://kubernetes.default.svc
# ===== 允许管理的资源类型 =====
# 不写则默认允许全部;写了则成为一种白名单保护
clusterResourceWhitelist: [] # 集群级资源一律不允许(安全)
namespaceResourceWhitelist:
- group: apps
kind: Deployment
- group: apps
kind: StatefulSet
- group: ""
kind: Service
- group: ""
kind: ConfigMap
- group: ""
kind: Secret
- group: networking.k8s.io
kind: Ingress
- group: batch
kind: Job
- group: autoscaling
kind: HorizontalPodAutoscaler
# ===== 同步窗口(可选,限制只能在特定时间同步) =====
# syncWindows:
# - kind: deny
# schedule: "0 22 * * *" # 每天 22:00 起禁止同步
# duration: 8h
# timeZone: Asia/Shanghai
# applications: ["*"]
# ===== 角色与权限(多人协作时才需要) =====
# roles:
# - name: developer
# policies:
# - p, proj:ruoyi:developer, applications, sync, ruoyi/*, allow
# groups:
# - ruoyi-devs关键字段说明
| 字段 | 作用 | 注意点 |
|---|---|---|
sourceRepos | 白名单:只允许从这里拉清单 | 写错仓库地址会让 Application 报 repo not permitted |
destinations[].server | https://kubernetes.default.svc 是特殊值,指"Argo CD 自己所在集群" | 多集群时才需要填实际地址 |
namespaceResourceWhitelist | 白名单:只允许创建这些类型的资源 | 不配置则默认允许所有;配置后超出范围的资源会被拒绝同步 |
clusterResourceWhitelist: [] | 空数组表示"一个都不允许" | 这是一种安全加固:防止应用清单里夹带 CRD/ClusterRole 这类高权限资源 |
白名单漏了资源类型会导致同步失败
假设你在清单里用了一个 HorizontalPodAutoscaler,但 namespaceResourceWhitelist 里没列它,同步时会报:
text
resource :HorizontalPodAutoscaler is not permitted in project ruoyi排查方法:看 Argo CD 的同步错误信息,把缺的类型补进白名单。建议一开始就把可能用到的类型列全(Deployment / StatefulSet / DaemonSet / Service / ConfigMap / Secret / Ingress / Job / CronJob / HPA / PDB / ServiceAccount / Role / RoleBinding)。
bash
# 应用 AppProject
kubectl apply -f argocd/project-ruoyi.yaml
# 验证
kubectl -n argocd get appproject ruoyi
argocd proj get ruoyi7.3 为每个服务创建 Application
每个服务一个 Application,好处是:可以单独同步、单独回滚、单独看健康状态。
yaml
# argocd/app-gateway.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ruoyi-gateway
namespace: argocd
# 级联删除:删 Application 时一并删掉它创建的资源
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: ruoyi
# ===== 清单位置 =====
source:
repoURL: http://192.168.0.25:3000/ruoyi/ruoyi-gitops.git
targetRevision: main # 分支/标签/commit SHA
path: apps/gateway/overlays/prod # 清单目录(含 kustomization.yaml)
# ===== 部署目标 =====
destination:
server: https://kubernetes.default.svc
namespace: ruoyi
# ===== 同步策略 =====
syncPolicy:
# syncOptions 写在 syncPolicy 下
syncOptions:
- CreateNamespace=false # 命名空间已存在,不需要创建
- PrunePropagationPolicy=foreground
- ApplyOutOfSyncOnly=true # 只 apply 变化的资源(减少 API 压力)
# 自动化(第一阶段先注释掉,见第 9 章的切换步骤)
# automated:
# prune: true # 删除 Git 里不存在的资源
# selfHeal: true # 集群被手动改动后自动纠偏
# allowEmpty: false # 清单为空时拒绝同步(防止误删一切)
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
# ===== 让 Argo CD 忽略某些字段的差异(避免无意义的 OutOfSync) =====
ignoreDifferences:
# HPA 会改 replicas,忽略掉避免打架
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas关键字段说明
| 字段 | 作用 | 常见坑 |
|---|---|---|
source.path | 清单在仓库里的目录路径(相对仓库根) | 路径写错会报 path does not exist |
targetRevision: main | 跟踪哪个分支/tag/commit | 用 HEAD 也可以,但不推荐(含义不明确) |
destination.namespace | 部署到哪个命名空间 | 与 AppProject 的 destinations 不匹配会被拒绝 |
syncOptions | 同步行为微调 | ApplyOutOfSyncOnly=true 在资源多时能显著减少 API 压力 |
automated.prune | Git 里删掉的资源,集群里也删 | ⚠️ 最危险的开关,见下面的警告 |
automated.selfHeal | 手动改动会被自动改回 | ⚠️ 排障时手动改配置会被"改回去",需要知道怎么临时绕过 |
allowEmpty: false | 渲染结果为空时拒绝同步 | 强烈建议开启,它可以防止"路径配错导致 Argo CD 认为该删除一切" |
ignoreDifferences | 忽略指定字段的差异 | 常用于 HPA 改 replicas、或 webhook 注入 sidecar 的场景 |
prune: true 是这套体系里最危险的一个开关
开启后,只要 Git 里没有的资源,Argo CD 就会从集群里删掉。
典型事故:有人在 GitOps 仓库里把某个服务的目录重命名了(modules-system → system),但忘了更新 Application 的 path。结果:
- Argo CD 发现新路径下没有这个服务的 Deployment
- 认为"应该删掉"
- 直接删除了正在运行的生产服务
三条防护措施(建议全部开启):
allowEmpty: false—— 渲染结果为空时拒绝同步- 开启
prune之前先跑一次 dry-run:argocd app diff <name>看清楚会删什么 - 用
PrunePropagationPolicy=foreground+ 先观察几次同步结果再开 prune
bash
# 批量创建 7 个 Application(用模板生成)
cd /root/ruoyi-gitops
for svc in gateway auth modules-system modules-gen modules-job modules-file visual-monitor; do
cat > "argocd/app-$svc.yaml" <<EOF
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ruoyi-$svc
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: ruoyi
source:
repoURL: http://192.168.0.25:3000/ruoyi/ruoyi-gitops.git
targetRevision: main
path: apps/$svc/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: ruoyi
syncPolicy:
syncOptions:
- CreateNamespace=false
- ApplyOutOfSyncOnly=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas
EOF
done
ls -1 argocd/app-*.yamlbash
# 一次性应用所有 Application
kubectl apply -f argocd/app-*.yaml
# 查看状态
argocd app list
# 说明:
# NAME CLUSTER NAMESPACE PROJECT STATUS HEALTH
# ruoyi-gateway in-cluster ruoyi ruoyi OutOfSync Missing
# ...
# 此刻是 OutOfSync 是正常的——还没执行同步7.4 关于同步顺序(sync-wave)
问题:如果某个服务依赖另一个(例如 gateway 依赖 auth),同时同步可能造成短暂的启动失败。
Argo CD 的解法:用 sync-wave 注解把资源分组,按 wave 从小到大依次同步。
yaml
# 在 base/deployment.yaml 的 metadata.annotations 里加
metadata:
annotations:
argocd.argoproj.io/sync-wave: "10" # 数字越小越先同步推荐的分组:
| Wave | 资源 | 理由 |
|---|---|---|
-10 | Namespace、ConfigMap、Secret | 其他资源的前提 |
0 | 基础服务(auth、visual-monitor) | 不依赖其他业务服务 |
5 | 业务模块(modules-system / gen / job / file) | 依赖 auth |
10 | gateway | 最后启动,它要转发到上面所有服务 |
bash
# 给各服务的 base deployment 加上 sync-wave
add_wave() {
local f="$1" wave="$2"
# 如果已有 annotations 段则插入,否则在 metadata 下新建
if grep -q "^ annotations:" "$f"; then
sed -i "0,/^ annotations:/s// annotations:\n argocd.argoproj.io\/sync-wave: \"$wave\"/" "$f"
else
sed -i "0,/^metadata:/s//metadata:\n annotations:\n argocd.argoproj.io\/sync-wave: \"$wave\"/" "$f"
fi
}
add_wave apps/auth/base/deployment.yaml "0"
add_wave apps/visual-monitor/base/deployment.yaml "0"
add_wave apps/modules-system/base/deployment.yaml "5"
add_wave apps/modules-gen/base/deployment.yaml "5"
add_wave apps/modules-job/base/deployment.yaml "5"
add_wave apps/modules-file/base/deployment.yaml "5"
add_wave apps/gateway/base/deployment.yaml "10"
# 验证
grep -r "sync-wave" apps/*/base/deployment.yamlsync-wave 只在"同一次同步"内生效
如果你在 Argo CD 里分别点每个 Application 的 Sync,wave 是不会跨 Application 生效的。
要让 wave 真正起作用,需要:
- 把所有服务放在同一个 Application 里(用多路径 source),或者
- 用 Argo CD 的 App-of-Apps 模式(一个父 Application 管所有子 Application),或者
- 接受"分别同步"的现状,手工按顺序点(学习阶段完全够用)
本文档采用「每个服务一个 Application」的结构,所以 sync-wave 主要在单次同步内的多资源之间起作用(比如 Deployment 和 Service 的先后)。跨服务的顺序控制,靠切换时的手工顺序(见第 9 章)。
7.5 让 Argo CD 感知到 Git 变更
Argo CD 默认每 3 分钟轮询一次仓库。要更快,就配 webhook。
bash
# 方式一:在 Gitea 仓库里配 webhook
# 仓库 ruoyi-gitops → 设置 → Web 钩子 → 添加 Web 钩子 → Gitea
# 目标 URL: http://<argocd-server的地址>/api/webhook
# 密钥: 下面配置的那个值
# 触发事件: 推送事件
# 方式二:用 CLI 配置(需要在集群里改 argocd-secret)
# 更简单的是用 argocd-cm 配置 gitea 的 webhook 密钥
kubectl -n argocd patch configmap argocd-cm --type=merge -p '{
"data": {
"webhook.gitea.secret": "your-webhook-secret-here"
}
}'
# 重启 argocd-server 让配置生效
kubectl -n argocd rollout restart deployment argocd-server这条命令在做什么
argocd-cm是 Argo CD 的主配置 ConfigMap。webhook.gitea.secret字段声明了"验证 Gitea webhook 请求时用的密钥"。- 为什么需要密钥:webhook 的 URL 是公开可访问的,任何人只要知道地址就能触发同步。密钥用于验证请求确实来自 Gitea(通过 HMAC 签名)。
- 改完 ConfigMap 后必须重启 argocd-server(它不会热加载)。
bash
# 调整轮询间隔(可选,默认 3 分钟)
kubectl -n argocd patch configmap argocd-cm --type=merge -p '{
"data": {
"timeout.reconciliation": "180s"
}
}'
kubectl -n argocd rollout restart statefulset argocd-application-controller这条命令在做什么
timeout.reconciliation:application-controller 扫描一遍所有 Application 的间隔。- 不建议设太小(如 10s):每次扫描都要访问 Git 仓库,仓库多或仓库大时会打爆 Gitea 和 repo-server。
08 阶段四:改造 Jenkinsfile
8.1 版本号规则:从时间戳换成 Git 短 SHA
| 方案 | 例子 | 优点 | 缺点 |
|---|---|---|---|
| 时间戳 | 20260924143012 | 天然唯一、可看出时间 | 无法对应到代码提交,排查时不知道这个镜像对应哪次 commit |
| Git 短 SHA | a1b2c3d | 可直接定位到代码、天然唯一(同一提交永远同一版本) | 看不出时间(但可以配合 git show 查) |
| 分支+序号 | prod-42 | 可看出第几次构建 | 需要维护计数器 |
| Git SHA + 时间戳 | a1b2c3d-20260924 | 兼顾两者 | 稍长 |
本文档推荐 Git 短 SHA:它与 GitOps 的理念最契合——"部署的就是某个确定的代码版本"。
bash
# 在 CI 里获取短 SHA 的两种方式
git rev-parse --short=7 HEAD # 输出如 a1b2c3d
# 也可以在构建时同时带上时间信息(更容易读)
echo "$(git rev-parse --short=7 HEAD)-$(date '+%m%d%H%M')"
# 输出如 a1b2c3d-092414308.2 改造后的 Jenkinsfile(CI 部分)
核心变化:第 4 个 stage 从「kubectl apply」变成「修改 GitOps 仓库的一个文件并 push」。
groovy
pipeline {
agent any
tools {
maven 'Maven-3.8.9'
jdk 'JDK-17'
}
environment {
APP_NAME = 'ruoyi-gateway'
// 镜像地址:注意这里不带 tag,tag 由 VERSION 决定
IMAGE_REPO = '192.168.0.21:10086/ruoyi-cloud/ruoyi-gateway'
// 版本号 = git 短 SHA(可追溯到代码提交)
VERSION = sh(script: "git rev-parse --short=7 HEAD", returnStdout: true).trim()
HARBOR_CREDENTIALS_ID = 'harbor-credentials'
// GitOps 仓库的推送凭据
GITOPS_CREDENTIALS_ID = 'gitea-token'
GITOPS_REPO = '192.168.0.25:3000/ruoyi/ruoyi-gitops.git'
GITOPS_BRANCH = 'main'
// 要修改的 kustomization.yaml 路径
GITOPS_FILE = "apps/gateway/overlays/prod/kustomization.yaml"
MAVEN_OPTS = '-Dmaven.compiler.source=17 -Dmaven.compiler.target=17 -Dproject.build.sourceEncoding=UTF-8'
}
options {
disableConcurrentBuilds()
timestamps()
timeout(time: 20, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '20'))
}
stages {
stage('代码编译打包') {
steps {
script {
sh 'mvn clean package -Dmaven.test.skip=true'
}
}
}
stage('构建并推送镜像') {
steps {
script {
withCredentials([usernamePassword(
credentialsId: HARBOR_CREDENTIALS_ID,
usernameVariable: 'HARBOR_USER',
passwordVariable: 'HARBOR_PASS')]) {
sh """
echo "\${HARBOR_PASS}" | docker login 192.168.0.21:10086 \\
-u "\${HARBOR_USER}" --password-stdin
docker build --build-arg JAR_FILE=target/${APP_NAME}.jar \\
-t ${IMAGE_REPO}:${VERSION} .
docker push ${IMAGE_REPO}:${VERSION}
"""
}
}
}
}
stage('更新 GitOps 仓库') {
steps {
script {
withCredentials([usernamePassword(
credentialsId: GITOPS_CREDENTIALS_ID,
usernameVariable: 'GIT_USER',
passwordVariable: 'GIT_PASS')]) {
sh """
set -e
# 1) 克隆 GitOps 仓库(只取最新一层,速度快)
rm -rf .gitops
git clone --depth 1 --branch ${GITOPS_BRANCH} \\
http://\${GIT_USER}:\${GIT_PASS}@${GITOPS_REPO} .gitops
cd .gitops
# 2) 只改 newTag 这一行(精确匹配,避免误改)
sed -i 's#^\\( *newTag: \\).*#\\1"${VERSION}"#' ${GITOPS_FILE}
# 3) ★ 关键校验:确认真的改成功了 ★
echo "--- 修改后的 images 段 ---"
grep -A 3 '^images:' ${GITOPS_FILE}
if ! grep -q "newTag: \\"${VERSION}\\"" ${GITOPS_FILE}; then
echo "❌ newTag 修改失败,实际内容:"
cat ${GITOPS_FILE}
exit 1
fi
# 4) 本地渲染校验:确保 Kustomize 能解析
if ! kubectl kustomize "apps/gateway/overlays/prod" > /tmp/rendered.yaml; then
echo "❌ Kustomize 渲染失败"
exit 1
fi
echo "--- 渲染出的镜像 ---"
grep 'image:' /tmp/rendered.yaml
# 5) 提交并推送
git config user.email "jenkins@example.com"
git config user.name "Jenkins CI"
git add ${GITOPS_FILE}
if git diff --cached --quiet; then
echo "ℹ️ 版本未变化(${VERSION}),跳过提交"
exit 0
fi
git commit -m "chore(${APP_NAME}): bump image to ${VERSION} [skip ci]"
git push origin ${GITOPS_BRANCH}
echo "✅ 已推送 GitOps 变更,等待 Argo CD 同步"
"""
}
}
}
}
stage('等待 Argo CD 同步(可选)') {
steps {
script {
// 如果 Argo CD 开了 automated,可以用 CLI 等待同步完成
// 这里用超时容忍的方式,避免因网络问题让整个构建失败
sh """
kubectl -n argocd wait --for=jsonpath='{.status.sync.status}'=Synced \\
application/${APP_NAME} --timeout=180s || \\
echo "⚠️ 等待同步超时,请到 Argo CD 界面确认"
"""
}
}
}
}
post {
success {
echo "✅ CI 完成,镜像:${IMAGE_REPO}:${VERSION}"
echo " Argo CD 将自动同步(或手动点击 Sync)"
}
failure {
echo "❌ CI 失败,集群不会发生任何变化(这正是 CI/CD 分离的好处)"
}
always {
// 清理临时目录
sh 'rm -rf .gitops /tmp/rendered.yaml || true'
// 清理本地镜像
sh "docker rmi ${IMAGE_REPO}:${VERSION} || true"
}
}
}8.3 逐段解读(与文档 ① 的差异)
① 第 2 个 stage 合并了原来的 2、3 两阶段
原来「构建镜像」和「推送镜像」是分开的两个 stage。这里合并是因为:
- 镜像构建完不推送就没意义(中间失败的话镜像要清理)
- 合并后失败状态更明确:"镜像环节失败了"
② 第 3 个 stage 的三个校验,一个都不能省
这三处校验是这个 Jenkinsfile 里最有价值的部分
bash
# 校验一:sed 之后确认内容真的变了
if ! grep -q "newTag: \"${VERSION}\"" ${GITOPS_FILE}; then
exit 1
fi
# 校验二:Kustomize 能正确渲染
if ! kubectl kustomize "apps/gateway/overlays/prod" > /tmp/rendered.yaml; then
exit 1
fi
# 校验三:渲染出来的镜像确实是新版本
grep 'image:' /tmp/rendered.yaml为什么必须有:
| 校验 | 不做的后果 |
|---|---|
| 一 | sed 正则没匹配上(缩进变了、写成了 newtag),静默失败,推送后 Argo CD 什么也不会变,你会以为是 Argo CD 的 bug |
| 二 | Kustomize 配置有语法错误,CI 推送成功但 Argo CD 同步时报 kustomize build failed,问题延迟到部署阶段才暴露 |
| 三 | images[].name 与清单里的镜像地址不匹配,Kustomize 静默不替换,部署的还是旧镜像 |
这三条校验的共同点:它们防的都是"静默失败"——命令返回成功,但结果不对。这类问题是最难排查的,因为日志里没有任何错误。
③ [skip ci] 的用途
bash
git commit -m "chore(${APP_NAME}): bump image to ${VERSION} [skip ci]"[skip ci]是一个约定俗成的标记,Jenkins / GitHub Actions 等 CI 系统看到后会跳过这次提交的构建。- 为什么需要:GitOps 仓库如果也配了流水线,这次提交会触发不必要的构建(而且是循环触发:改 tag → 构建 → 再改 tag)。
- 注意:Jenkins 需要安装相应的插件或配置才能识别这个标记(例如 Git 插件的 "Polling ignores commits with certain messages" 配置)。
④ 第 4 个 stage 的等待是可选的
groovy
kubectl -n argocd wait --for=jsonpath='{.status.sync.status}'=Synced application/${APP_NAME} --timeout=180s- 优点:CI 的绿色 = 真的部署成功了。
- 缺点:如果 Argo CD 配的是手动同步模式,这个等待会一直超时(所以要有
|| echo兜底)。 - 建议:初期(手动同步阶段)先注释掉这个 stage,等你确认流程稳定、开启了
automated之后再打开。
⑤ post.always 里的清理
groovy
sh 'rm -rf .gitops /tmp/rendered.yaml || true'.gitops是临时克隆目录。不清理的话:下次构建时里面还有旧内容,虽然脚本开头会rm -rf,但如果 clone 失败,cd .gitops会进到旧目录,导致改错文件。|| true保证清理失败不影响构建结果。
8.4 需要新增的凭据
| ID | 类型 | 内容 | 权限要求 |
|---|---|---|---|
harbor-credentials | Username/Password | Harbor 机器人账号 | 对 ruoyi-cloud 有推送权限 |
gitea-token(新增) | Username/Password | Gitea 用户名 + 访问令牌 | 对 ruoyi-gitops 有 write 权限 |
为 GitOps 写操作单独建 Token
建议不要复用拉代码的那个 Token。理由:
- 拉代码只需要
read:repository - 写 GitOps 仓库需要
write:repository - 权限分离后,即使 CI 机器的拉代码凭据泄露,也无法篡改 GitOps 仓库
在 Gitea 里:用户设置 → 应用 → 生成令牌 → 权限勾选 write:repository。
8.5 各服务的差异点
7 个服务的 Jenkinsfile 只有环境变量不同:
| 服务 | APP_NAME | IMAGE_REPO 后缀 | GITOPS_FILE |
|---|---|---|---|
| gateway | ruoyi-gateway | /ruoyi-gateway | apps/gateway/overlays/prod/kustomization.yaml |
| auth | ruoyi-auth | /ruoyi-auth | apps/auth/overlays/prod/kustomization.yaml |
| modules-system | ruoyi-modules-system | /ruoyi-modules-system | apps/modules-system/overlays/prod/kustomization.yaml |
| modules-gen | ruoyi-modules-gen | /ruoyi-modules-gen | apps/modules-gen/overlays/prod/kustomization.yaml |
| modules-job | ruoyi-modules-job | /ruoyi-modules-job | apps/modules-job/overlays/prod/kustomization.yaml |
| modules-file | ruoyi-modules-file | /ruoyi-modules-file | apps/modules-file/overlays/prod/kustomization.yaml |
| visual-monitor | ruoyi-visual-monitor | /ruoyi-visual-monitor | apps/visual-monitor/overlays/prod/kustomization.yaml |
更好的做法:把差异抽到 Jenkins 参数里
如果不想维护 7 份几乎一样的 Jenkinsfile,可以:
- 把公共部分做成共享库(Jenkins Shared Library),每个仓库的 Jenkinsfile 只调用
ruoyiPipeline(appName: 'ruoyi-gateway') - 或者用环境变量 + 参数化构建,把差异作为参数传入
本文档选择每个仓库一份完整 Jenkinsfile 的原因:可读性最好,每个服务的流水线可以独立修改。当服务多到 20 个以上时再考虑抽象。
09 阶段五:灰度切换(8 步,每步可回退)
最重要的一条原则:不要一次性全切
你现在有一个正常工作的生产环境。改造过程中任何一步出问题,都要能立即退回去。
因此本章把切换拆成 8 步,每步只做一件事,且都有明确的回退动作。每步之间建议间隔 1~2 天,确认稳定再走下一步。
9.1 切换步骤表
| 步骤 | 做什么 | 验证方法 | 出问题怎么退 |
|---|---|---|---|
| 1 | 安装 Argo CD,不创建任何 Application | kubectl -n argocd get pods 全 Running | kubectl delete ns argocd(干净卸载,不影响业务) |
| 2 | 导出线上清单、构建 GitOps 仓库、推送 | kubectl kustomize apps/gateway/overlays/prod 能渲染 | 仓库还在 Git 里,业务无感知 |
| 3 | 创建 AppProject 与 Application,但用 targetRevision 指向一个空分支或保持手动同步 | argocd app list 显示 OutOfSync(预期) | kubectl delete -f argocd/app-*.yaml |
| 4 | 手工执行一次 diff,确认差异为空 | argocd app diff ruoyi-gateway 无输出或只有无害差异 | 修正清单后重新 diff |
| 5 | 手工点 Sync(单个服务) | 该服务的 Pod 被重建,健康状态正常 | Argo CD 的 Sync 是幂等的;如需回退,kubectl rollout undo |
| 6 | 改 Jenkinsfile,让 CI 只更新 GitOps 仓库;先只改造 1 个非核心服务(如 modules-gen) | 触发一次构建,看到 GitOps 仓库有新 commit,Argo CD 显示 OutOfSync | 把 Jenkinsfile 恢复成旧版本 |
| 7 | 手工 Sync 验证 CI → GitOps → 集群 的完整链路 | 服务确实更新到了新版本 | 同上 |
| 8 | 打开 automated: {selfHeal: true},但先不开 prune | 手动改副本数,观察 3 分钟后被改回 | 编辑 Application 注释掉 selfHeal |
| 9 | 确认一切稳定后,最后打开 prune | argocd app diff 确认无误后开启 | 注释掉 prune |
9.2 关键步骤的详细操作
第 3 步:创建 Application 时先不要开自动同步
yaml
spec:
syncPolicy:
syncOptions:
- ApplyOutOfSyncOnly=true
# automated 段先注释掉!
# automated:
# prune: true
# selfHeal: true为什么:手动同步模式让你有机会先看清 diff。这是最重要的安全阀。
第 4 步:diff 必须为空
bash
# 查看 Application 的同步状态与差异
argocd app get ruoyi-gateway
# 只看差异(这是关键命令)
argocd app diff ruoyi-gateway
# 如果差异很大,先搞清楚每一处差异是什么
# 三种处理方式:
# a) 差异是你的清单缺了东西 → 修正清单,让它和线上一致
# b) 差异是无关紧要的运行时字段 → 用 ignoreDifferences 忽略
# c) 差异说明线上有不该有的东西 → 先手工处理线上,再让清单与之一致argocd app diff 的输出怎么读
text
===== apps/Deployment ruoyi/ruoyi-gateway-deploy ======
spec:
template:
spec:
containers:
- image: ...:latest
+ image: ...:a1b2c3d ← 加号表示「同步后会变成这样」+开头:同步后会新增/修改的内容-开头:同步后会删除的内容- 重点关注
-的部分:那意味着集群里的东西会被删掉
如果 diff 里有 - 删除项,必须逐条确认
bash
argocd app diff ruoyi-gateway | grep -E '^\s*-' | head -30每一个 - 都要能回答"为什么它应该被删掉"。如果回答不出来,就不要同步。
第 5 步:手工同步单个服务
bash
# 同步一个服务
argocd app sync ruoyi-gateway
# 观察同步过程(会显示每个资源的操作)
argocd app get ruoyi-gateway --refresh
# 等待并确认健康
argocd app wait ruoyi-gateway --health --timeout 300这条命令在做什么
argocd app sync:触发一次同步。Argo CD 会渲染清单、计算 diff、执行 apply。--refresh:强制从 Git 重新拉取(而不是用缓存)。argocd app wait --health:阻塞直到 Application 的 Health 变成 Healthy。
bash
# 同步后确认集群侧状态
kubectl get pods -n ruoyi
kubectl get deploy -n ruoyi
# 确认 Argo CD 视角也是健康的
argocd app list
# 期望:STATUS=Synced,HEALTH=Healthy第 6 步:改造一个非核心服务的 Jenkinsfile
bash
# 选择 modules-gen(代码生成服务,业务影响最小)
# 在它的仓库里替换 Jenkinsfile,然后触发一次构建
# 构建完成后,检查 GitOps 仓库是否有了新提交
cd /root/ruoyi-gitops && git pull
git log --oneline | head -3
# 期望看到:chore(ruoyi-modules-gen): bump image to xxxxxxx [skip ci]这一步验证的核心是:「CI 能正确写 GitOps 仓库」。
bash
# 确认 kustomization.yaml 的 newTag 变了
git show HEAD -- apps/modules-gen/overlays/prod/kustomization.yaml | tail -20第 8 步:开启 selfHeal,验证漂移纠正
bash
# 1) 先改一下 Application 的 syncPolicy,打开 selfHeal(但不开 prune)
kubectl -n argocd patch application ruoyi-modules-gen --type=merge -p '{
"spec": {"syncPolicy": {"automated": {"selfHeal": true, "prune": false}}}
}'
# 2) 人为制造漂移:把副本数改掉
kubectl -n ruoyi scale deployment ruoyi-modules-gen-deploy --replicas=3
# 3) 立即观察(应该是 3 个)
kubectl -n ruoyi get deploy ruoyi-modules-gen-deploy
# 4) 等 3 分钟左右(Argo CD 的协调周期)
sleep 180
# 5) 再观察:应该被改回 1 个(或 kustomization 里配置的数量)
kubectl -n ruoyi get deploy ruoyi-modules-gen-deploy这几条命令在做什么
- 第 2 步是在**模拟"有人手动改了集群"**这个最常见的漂移场景。
- 第 5 步如果看到副本数被改回去了,说明
selfHeal生效了。 - ⚠️ 注意:前面在 Application 里配置了
ignoreDifferences: [/spec/replicas],如果保留这个配置,selfHeal 不会纠正副本数。测试时可以先临时去掉这个 ignoreDifferences。
selfHeal 的双刃剑
好处:集群状态永远与 Git 一致,不会被人的临时改动污染。
代价:排障时你的临时改动会被改回去。
例如某服务 OOM 了,你临时 kubectl set resources 调大内存限制——3 分钟后被 Argo CD 改回原样,Pod 继续 OOM。
正确的做法:
- 改 Git,而不是改集群(这正是 GitOps 的意义)
- 紧急情况下,先暂停自动同步:
bash
# 暂停自动同步(保留手动同步能力)
kubectl -n argocd patch application ruoyi-gateway --type=merge -p '{
"spec": {"syncPolicy": {"automated": null}}
}'
# 处理完问题后恢复
kubectl -n argocd patch application ruoyi-gateway --type=merge -p '{
"spec": {"syncPolicy": {"automated": {"selfHeal": true, "prune": false}}}
}'9.3 验收通过标准
全部满足才算改造成功:
bash
# ① 所有 Application 都是 Synced + Healthy
argocd app list
# ② 集群资源与 Git 完全一致(diff 为空)
for app in ruoyi-gateway ruoyi-auth ruoyi-modules-system ruoyi-modules-gen \
ruoyi-modules-job ruoyi-modules-file ruoyi-visual-monitor; do
printf "%-26s " "$app"
if [ -z "$(argocd app diff $app 2>/dev/null)" ]; then
echo "✅ diff 为空"
else
echo "⚠️ 有差异"
fi
done
# ③ 端到端业务可用
curl -s -o /dev/null -w "gateway: HTTP %{http_code}\n" http://192.168.0.11:31080/
curl -s -X POST "http://192.168.0.11:31080/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}' | head -c 200
echo
# ④ 完整闭环演练:改代码 → CI → GitOps → 自动部署
# 在开发机上改一行代码并 push,然后:
watch -n 5 'kubectl -n ruoyi get deploy ruoyi-gateway-deploy -o jsonpath="{.spec.template.spec.containers[0].image}"; echo'
# 期望:约 3~5 分钟后镜像 tag 变成新的短 SHA
# ⑤ 漂移纠正有效(做了第 8 步的话)
# 手动改副本数 → 3 分钟后被改回
# ⑥ 回滚可用(下面会验证)10 阶段六:回滚与故障处理
10.1 回滚的三个层次
GitOps 让回滚变得非常简单
这是引入 Argo CD 最直接的好处之一:回滚 = 让 Git 回到之前的提交。
层次一:回退 Git(推荐,最符合 GitOps 理念)
bash
cd /root/ruoyi-gitops
# 查看历史,找到要回退到的提交
git log --oneline -10 -- apps/gateway/overlays/prod/kustomization.yaml
# 方式 A:用 revert 创建一个"反向提交"(推荐,保留完整历史)
git revert <坏的提交SHA> --no-edit
git push origin main
# Argo CD 会自动(或手动)同步回退后的状态
# 方式 B:直接把文件恢复到某个版本并提交
git checkout <好的提交SHA> -- apps/gateway/overlays/prod/kustomization.yaml
git commit -m "revert(gateway): 回退到 <好的提交SHA>"
git push origin main为什么推荐 git revert 而不是 git reset
revert是新增一个提交来撤销之前的改动,历史是线性的、可追溯的reset会抹掉历史,而且需要 force push(危险,可能覆盖别人的提交)- 如果 GitOps 仓库有权限控制,
reset+ force push 通常是被禁止的
层次二:在 Argo CD 里指定历史版本同步
bash
# 查看某个 Application 的历史版本
argocd app history ruoyi-gateway
# 输出形如:
# ID DATE REVISION
# 0 2026-09-24 10:00:00 +0800 CST main (a1b2c3d)
# 1 2026-09-23 18:30:00 +0800 CST main (9f8e7d6)
# 同步到指定历史版本(不修改 Git,只是让集群临时回到那个状态)
argocd app rollback ruoyi-gateway 1argocd app rollback 是临时的
它只是让集群当前变成历史版本的样子,Git 里的内容没变。
如果开了 selfHeal,下一次协调循环会把它改回 Git 里的最新版本。
所以 rollback 适合"紧急止血",止血之后要立刻去 Git 里做正式的回退(层次一)。
层次三:K8s 原生的回滚(最后手段)
bash
# 回退 Deployment 到上一个版本
kubectl -n ruoyi rollout undo deployment/ruoyi-gateway-deploy
# 查看可回退的历史版本
kubectl -n ruoyi rollout history deployment/ruoyi-gateway-deploy
# 回退到指定的 revision
kubectl -n ruoyi rollout undo deployment/ruoyi-gateway-deploy --to-revision=3这种方式在 GitOps 下是"违规操作"
kubectl rollout undo 直接改集群。如果 selfHeal 开着,几分钟后会被 Argo CD 改回去。
只在 Argo CD 本身不可用时使用(此时也确实没别的办法了)。
10.2 三级回滚对照表
| 场景 | 用哪一级 | 耗时 | 需要什么权限 |
|---|---|---|---|
| 常规回滚(CI 推错了版本) | 层次一:回退 Git | 1~5 分钟(等 Argo CD 同步) | GitOps 仓库的写权限 |
| 紧急止血(服务正在出故障) | 层次二:Argo CD rollback | 30 秒~1 分钟 | Argo CD 的操作权限 |
| Argo CD 挂了 | 层次三:kubectl rollout undo | 30 秒 | 集群权限 |
10.3 故障排查速查表
Argo CD 自身的问题
| 现象 | 原因 | 解决 |
|---|---|---|
Pod ImagePullBackOff | imagePullSecrets 未配 / Harbor 不通 | kubectl -n argocd describe pod 看 Events |
Pod Pending | nodeSelector 指向的节点资源不足 | kubectl -n argocd describe pod 看 Events |
登录报 x509 / connection refused | 端口/协议不对 | 确认用 --insecure --grpc-web |
| UI 打不开 | --insecure 没加 / NodePort 没配 | 检查 Service 类型与 Deployment 的 command |
CLI 报 rpc error: code = Unavailable | 没加 --grpc-web | 加上该参数 |
argocd-server 反复重启 | 资源 limit 太小(OOMKilled) | kubectl -n argocd describe pod 看 Last State |
Application 的问题
| 现象 | 原因 | 解决 |
|---|---|---|
Unknown 状态 | Argo CD 读不到 Git 仓库 | 检查仓库地址、凭据、网络 |
ComparisonError | 渲染失败(YAML 错误 / Kustomize 配置错) | argocd app get <name> 看详细报错 |
OutOfSync 但 diff 看不出来 | 某些字段被运行时改写 | 用 ignoreDifferences 忽略 |
SyncFailed: not permitted in project | AppProject 的白名单缺资源类型 | 补进 namespaceResourceWhitelist |
path does not exist | source.path 写错 | 确认目录里有 kustomization.yaml |
repo not permitted | sourceRepos 与 repoURL 不匹配 | 注意 .git 后缀和协议(http/https)要完全一致 |
| 同步成功但 Pod 没变 | images[].name 不匹配,Kustomize 静默不替换 | 检查 name 是否带了 tag |
业务的问题
| 现象 | 原因 | 解决 |
|---|---|---|
| 同步后服务不可用 | 清单里的配置与线上不一致(漂移) | 恢复线上配置,修正清单,重新导出 |
| 同步后 Pod 一直重启 | 新镜像有问题 | 回滚(见 10.1) |
selfHeal 把排障改动改回去了 | 预期行为 | 临时关闭 automated(见 §9.2 第 8 步的提示) |
10.4 日常运维要点
bash
# ===== 每天可以扫一眼 =====
argocd app list # 所有应用状态
argocd app list -o wide # 带同步策略与仓库信息
# ===== 出问题时的三板斧 =====
argocd app get <name> # 看状态、条件、事件
argocd app diff <name> # 看与 Git 的差异
argocd app history <name> # 看历史版本
# ===== Argo CD 自身的维护 =====
# 查看组件日志
kubectl -n argocd logs -l app.kubernetes.io/name=argocd-application-controller --tail=100
kubectl -n argocd logs -l app.kubernetes.io/name=argocd-repo-server --tail=100
# 重启某个组件(例如配置改了不生效)
kubectl -n argocd rollout restart deployment argocd-server
# 备份 Argo CD 的配置(Application / AppProject 都在 K8s 里)
kubectl -n argocd get applications,appprojects -o yaml > argocd-backup-$(date +%F).yaml把 Argo CD 自己也纳入 GitOps(进阶)
成熟的做法是让 Argo CD 管理自己的配置:把 argocd/ 目录下的 Application / AppProject 清单也放进 GitOps 仓库,然后创建一个"根 Application"来管理它们。
这就是 App-of-Apps 模式,好处是"Argo CD 的配置也有版本历史和回滚能力"。建议熟悉基础流程后再做这一步。
11 风险清单
按严重程度排序,每一条都建议在实施前读一遍。
① prune: true 会删除 Git 里不存在的资源
风险:误删生产资源(尤其当 path 配错导致渲染结果为空时)。
缓解:
allowEmpty: false- 开启 prune 前先
argocd app diff确认 - 加
PrunePropagationPolicy=foreground,删除时可以看到进度
② 清单漂移:导出的清单与线上不一致
风险:Argo CD 首次同步时"纠正"了它认为不对的地方,反而把正常的东西改坏。
缓解:
- 第一版清单必须从线上集群导出(§6.1)
- 手工同步,先看 diff(§9 第 4 步)
- 关键字段(如
volumeMounts)导出后专门校验一遍
③ selfHeal 与人工排障冲突
风险:紧急排障时的临时改动被自动改回。
缓解:知道怎么临时关闭 automated(§9.2);把"改 Git"作为常态做法。
④ sourceRepos 的字符串必须完全匹配
风险:http://...git 与 http://...git/(多个斜杠)、或 https 与 http 不一致,都会报 repo not permitted,而且报错信息不提示"是协议/格式不一致"。
缓解:从 argocd app get <name> 的输出里复制实际使用的 URL,粘进 AppProject。
⑤ webhook 密钥泄露可被伪造触发
风险:任何人都能触发同步(虽然不是提权,但可能触发不需要的部署)。
缓解:配置 webhook.gitea.secret,并定期轮换。
⑥ Redis 数据丢失
风险:Argo CD 用 Redis 缓存 Git 仓库的渲染结果。Redis 重启后缓存丢失,不会丢失任何配置(配置都在 Git 里),但会造成短时间的全量重新渲染,仓库多时可能压满 CPU。
缓解:给 argocd-redis 设置合理的资源 limit;仓库很大时考虑用持久化卷(本文档环境无 StorageClass,Redis 数据在内存,属于可接受的权衡)。
⑦ 大量 Application 同时同步会打爆 API Server
风险:7 个服务同时同步,每个包含若干资源,短时间内对 API Server 的请求量激增。
缓解:
- 用
ApplyOutOfSyncOnly=true - 用
sync-wave分批 - 避免在业务高峰做批量同步
⑧ 无持久化存储的取舍
背景:本文档的集群没有 StorageClass / PV / PVC。
影响:
- Argo CD 的
application-controller用 StatefulSet,但不需要持久卷(状态都存在 K8s 对象和 Redis 里) - 如果将来要启用 Argo CD 的高可用部署,或需要保留 Redis 数据,就需要补存储
缓解:当前阶段可以接受;需要时再引入 NFS / Longhorn / OpenEBS。
⑨ imagePullPolicy 与可变 tag 的配合
风险:清单里如果用了 :latest 且 imagePullPolicy: IfNotPresent,节点上已有旧镜像时不会重新拉取,导致"改了 tag 但部署没变"。
缓解:
- 用不可变 tag(短 SHA),这是 GitOps 的标准做法
- 或者配
imagePullPolicy: Always(但每次启动都要检查仓库,慢且增加仓库压力) - 不要用
latest作为生产 tag
⑩ Jenkins 与 Argo CD 的权限边界
风险:改造不彻底 —— Jenkins 仍然保留着 kubeconfig,只是不用了。
缓解:改造完成后主动撤销 Jenkins 的集群凭据(删掉那个 Credential),从物理上保证"CI 无法直接改集群"。
⑪ 时间同步问题会伪装成各种故障
风险:Argo CD 的 webhook 签名验证、与 Git 的 TLS 握手、与 K8s API Server 的认证都依赖时间准确。
缓解:确保所有节点(尤其是跑 Argo CD 的节点)的 chronyd 正常工作。
12 附录
12.1 安装命令速查
bash
# ============ 1. 准备清单与 CLI(master 上执行) ============
mkdir -p /root/argocd/manifests && cd /root/argocd/manifests
curl -fL -o install.yaml https://gh-proxy.com/https://raw.githubusercontent.com/argoproj/argo-cd/v3.5.3/manifests/install.yaml
curl -fL -o /usr/local/bin/argocd https://files.m.daocloud.io/github.com/argoproj/argo-cd/releases/download/v3.5.3/argocd-linux-amd64
chmod +x /usr/local/bin/argocd
# ============ 2. 把镜像搬进自己的 Harbor ============
docker login 192.168.0.21:10086
docker pull quay.m.daocloud.io/argoproj/argocd:v3.5.3
docker tag quay.m.daocloud.io/argoproj/argocd:v3.5.3 192.168.0.21:10086/infra/argocd:v3.5.3
docker push 192.168.0.21:10086/infra/argocd:v3.5.3
docker pull ghcr.m.daocloud.io/dexidp/dex:v2.45.1
docker tag ghcr.m.daocloud.io/dexidp/dex:v2.45.1 192.168.0.21:10086/infra/dex:v2.45.1
docker push 192.168.0.21:10086/infra/dex:v2.45.1
docker pull m.daocloud.io/public.ecr.aws/docker/library/redis:8.2.3-alpine
docker tag m.daocloud.io/public.ecr.aws/docker/library/redis:8.2.3-alpine 192.168.0.21:10086/infra/redis:8.2.3-alpine
docker push 192.168.0.21:10086/infra/redis:8.2.3-alpine
# ============ 3. 改镜像地址并安装 ============
sed -i 's#quay.io/argoproj/argocd:v3.5.3#192.168.0.21:10086/infra/argocd:v3.5.3#g' install.yaml
sed -i 's#ghcr.io/dexidp/dex:v2.45.1#192.168.0.21:10086/infra/dex:v2.45.1#g' install.yaml
sed -i 's#public.ecr.aws/docker/library/redis:8.2.3-alpine#192.168.0.21:10086/infra/redis:8.2.3-alpine#g' install.yaml
grep -nE 'image:\s*(quay\.io|ghcr\.io|public\.ecr\.aws|docker\.io)' install.yaml || echo "✅ 无境外地址残留"
kubectl create namespace argocd
kubectl apply -n argocd --server-side --force-conflicts -f install.yaml
kubectl -n argocd wait --for=condition=available deployment --all --timeout=300s
# ============ 4. 打补丁 ============
kubectl create secret docker-registry harbor-secret -n argocd \
--docker-server=192.168.0.21:10086 \
--docker-username='robot$ruoyi-cloud+jenkins-robot' \
--docker-password='<Token>' --docker-email=dev@example.com
kubectl patch -n argocd --patch-file patch-imagepullsecrets.yaml --type=merge
kubectl patch -n argocd --patch-file patch-nodeselector.yaml --type=merge
# ============ 5. 暴露 UI 并登录 ============
kubectl -n argocd patch svc argocd-server -p '{"spec":{"type":"NodePort"}}'
kubectl -n argocd get svc argocd-server
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echo
argocd login 192.168.0.11:<NodePort> --username admin --insecure --grpc-web
argocd account update-password
# ============ 6. 日常命令 ============
argocd app list # 所有应用状态
argocd app get <name> # 详情
argocd app diff <name> # 与 Git 的差异 ★
argocd app sync <name> # 手动同步
argocd app history <name> # 历史版本
argocd app rollback <name> <id> # 回滚到历史版本
argocd app wait <name> --health --timeout 300 # 等待健康12.2 镜像与下载地址汇总(写作时实测)
| 用途 | 地址 | 实测 |
|---|---|---|
| Argo CD 主程序 | quay.m.daocloud.io/argoproj/argocd:v3.5.3 | ✅ 含 amd64 |
| Dex | ghcr.m.daocloud.io/dexidp/dex:v2.45.1 | ✅ 含 amd64 |
| Redis(注意是 AWS ECR 源) | m.daocloud.io/public.ecr.aws/docker/library/redis:8.2.3-alpine | ✅ 含 amd64 |
| install.yaml | https://gh-proxy.com/https://raw.githubusercontent.com/argoproj/argo-cd/v3.5.3/manifests/install.yaml | ✅ 1,917,766 字节 |
| argocd CLI | https://files.m.daocloud.io/github.com/argoproj/argo-cd/releases/download/v3.5.3/argocd-linux-amd64 | ✅ |
| kubectl-neat | https://gh-proxy.com/https://github.com/itaysk/kubectl-neat/releases/download/v2.0.4/kubectl-neat_linux_amd64.tar.gz | ✅ |
已实测失效的地址(不要用)
| 地址 | 结果 |
|---|---|
swr.cn-north-4.myhuaweicloud.com/ddn-k8s/quay.io/argoproj/argocd | 404 |
docker.m.daocloud.io/argoproj/argocd:v3.5.3 | 403(该域名只代理 Docker Hub) |
m.daocloud.io/docker.io/library/redis:8.2.3-alpine | ❌ 这是错误的地址——Argo CD 用的 redis 来自 public.ecr.aws,Docker Hub 上没有这个 tag |
12.3 与其它文档的关系
| 文档 | 关系 |
|---|---|
| ① 《若依微服务上 K8s》 | 本文档的前置。提供集群、中间件、Jenkins、业务部署的完整搭建过程 |
| ③ 《客户端 / 服务端类软件发布》 | 本文档的 CD 部分(Argo CD + 灰度)在客户端类项目里同样适用,但客户端多了一条发布通道,需要那份文档补充 |
| 本站「我的环境实施记录」 | 这套方案在一个真实环境(4 节点集群)上的勘察与实施记录 |
三句话总结
- CI 与 CD 的分界线是"Git 仓库":CI 负责产出一个可部署的版本并写进 Git;CD 负责让集群变成 Git 描述的样子。跨越这条线的任何操作(比如 CI 直接 kubectl)都是在把两件事重新耦合起来。
- 切换要慢,回退要快:第 9 章的 8 步切换表,每一步都可以单独退回去。"先手动同步、再 selfHeal、最后才 prune" 这个顺序不能颠倒。
- 本文档最大的价值不是"会用 Argo CD",而是建立了"集群状态可审计、可回滚"的机制。工具可以换(Flux、Argo CD、甚至自研),但这套「声明式 + Git 作为事实来源」的思想是一致的。