Skip to content

这份文档是什么

一份完整的 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主机名配置角色部署的内容本方案的变化
1192.168.0.10k8s-master4C8GK8s 控制平面kube-apiserver / etcd / controller-manager / scheduler不变
2192.168.0.11k8s-node18C16GK8s 工作节点若依业务 Pod新增:Argo CD 全部组件(约 500m CPU / 1Gi 内存)
3192.168.0.12k8s-node28C16GK8s 工作节点若依业务 Pod不变
4192.168.0.20ci-jenkins8C16GCI 构建机Jenkins / JDK 17 / Maven / Docker变化:不再持有集群 kubeconfig,不再执行 kubectl
5192.168.0.21harbor4C8G镜像仓库Harbor 2.15.2新增:项目 infra,用于存放 Argo CD 自身镜像
6192.168.0.22mysql4C8G数据库MySQL 8.0不变
7192.168.0.23redis2C4G缓存Redis 7不变
8192.168.0.24nacos4C8G注册/配置中心Nacos 2.5.4不变
9192.168.0.25git2C4G代码仓库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-serverDeploymentAPI Server + Web UI。你操作 Argo CD 的入口
argocd-repo-serverDeployment拉取 Git 仓库、渲染 Helm/Kustomize 模板。产生最终清单的地方
argocd-application-controllerStatefulSet核心控制器。持续对比期望状态与实际状态,执行同步
argocd-applicationset-controllerDeployment用模板批量生成 Application(多集群/多环境时用)
argocd-dex-serverDeployment对接 SSO(LDAP/OIDC/GitHub 等)。只用本地账号时可以不装
argocd-redisDeployment缓存。Argo CD 依赖 Redis,不能省
argocd-notifications-controllerDeployment同步结果的通知(钉钉/企微/邮件)

Argo CD 3.x 的两个新变化(与 2.x 不同)

如果你看过基于 Argo CD 2.x 的教程,注意这两点差异:

  1. 默认清单里包含 NetworkPolicy(7 个)。这要求 CNI 支持 NetworkPolicy(Calico 支持;flannel 默认不支持,会创建但不生效)。
  2. 3.5 起引入组件间的 mTLSrepo-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 本方案选定的版本

组件版本选择理由
Kubernetesv1.36.x落在所有主流工具的支持窗口内
Argo CDv3.5.3写作时的最新稳定版
Kustomize内置(Argo CD 自带)不需要单独安装
Harborv2.15.2与文档 ① 一致
Jenkins2.528.3与文档 ① 一致

Argo CD 的资源预算(非 HA 部署):

组件CPU request内存 request内存 limit
argocd-server100m128Mi512Mi
argocd-repo-server100m256Mi1Gi
argocd-application-controller100m256Mi1Gi
argocd-redis50m64Mi256Mi
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 有两个不可替代的场景:

  1. 脚本化验收argocd app get ruoyi-gateway --output json 可以直接喂给监控系统
  2. 无法访问 UI 时排障:例如 Ingress 配错了,只能用 CLI 从集群内部操作

5.2 第二步:把三个镜像搬进自己的 Harbor

这是国内环境最关键的一步。Argo CD 的安装清单里引用了 3 个外部镜像,全部在境外仓库:

清单里的原始地址用途实测可用的国内源
quay.io/argoproj/argocd:v3.5.3Argo CD 主程序(server / controller / repo-server 共用)quay.m.daocloud.io/argoproj/argocd:v3.5.3
ghcr.io/dexidp/dex:v2.45.1SSO 组件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 变成 RunningREADY 列为 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

这两个补丁在做什么

  • imagePullSecretsinfra 项目是私有的,没有这个字段,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: 80
bash
# 应用 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:
    bash
    kubectl -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,所以它不会管理任何东西。接下来要做的两件事:

  1. 把清单变成 Argo CD 能读的仓库结构(第 6 章)
  2. 创建 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 yaml100% 反映当前真实状态,首次同步 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-configurationkubectl 的历史记录,可能很大
metadata.annotations.deployment.kubernetes.io/revision同上
spec.clusterIPspec.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 -3

kubectl-neat 是什么

  • 一个专门"清理 K8s 清单"的插件:删掉所有运行时字段(statusuidresourceVersioncreationTimestamp 等),产出可以安全提交到 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)")
PY

6.3 GitOps 仓库的目录结构

这里用 Kustomizebase + 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
    └── ...

目录结构没有唯一正确答案

上面是一种常见组织方式。关键约束只有两条

  1. 每个 Application 指向的路径里必须有 kustomization.yaml(除非直接用普通 YAML 目录)
  2. 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: argocd
yaml
# 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 对象,读取 kindmetadata.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"
done
bash
# 为每个服务生成 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 的命名

若依的镜像名与服务目录名的对应关系:

目录名镜像名
gatewayruoyi-gateway
authruoyi-auth
modules-systemruoyi-modules-system
visual-monitorruoyi-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
done

6.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[].serverhttps://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 ruoyi

7.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/commitHEAD 也可以,但不推荐(含义不明确)
destination.namespace部署到哪个命名空间与 AppProject 的 destinations 不匹配会被拒绝
syncOptions同步行为微调ApplyOutOfSyncOnly=true 在资源多时能显著减少 API 压力
automated.pruneGit 里删掉的资源,集群里也删⚠️ 最危险的开关,见下面的警告
automated.selfHeal手动改动会被自动改回⚠️ 排障时手动改配置会被"改回去",需要知道怎么临时绕过
allowEmpty: false渲染结果为空时拒绝同步强烈建议开启,它可以防止"路径配错导致 Argo CD 认为该删除一切"
ignoreDifferences忽略指定字段的差异常用于 HPA 改 replicas、或 webhook 注入 sidecar 的场景

prune: true 是这套体系里最危险的一个开关

开启后,只要 Git 里没有的资源,Argo CD 就会从集群里删掉

典型事故:有人在 GitOps 仓库里把某个服务的目录重命名了(modules-systemsystem),但忘了更新 Application 的 path。结果:

  • Argo CD 发现新路径下没有这个服务的 Deployment
  • 认为"应该删掉"
  • 直接删除了正在运行的生产服务

三条防护措施(建议全部开启):

  1. allowEmpty: false —— 渲染结果为空时拒绝同步
  2. 开启 prune 之前先跑一次 dry-run:argocd app diff <name> 看清楚会删什么
  3. 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-*.yaml
bash
# 一次性应用所有 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资源理由
-10Namespace、ConfigMap、Secret其他资源的前提
0基础服务(auth、visual-monitor)不依赖其他业务服务
5业务模块(modules-system / gen / job / file)依赖 auth
10gateway最后启动,它要转发到上面所有服务
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.yaml

sync-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 短 SHAa1b2c3d可直接定位到代码、天然唯一(同一提交永远同一版本)看不出时间(但可以配合 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-09241430

8.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-credentialsUsername/PasswordHarbor 机器人账号ruoyi-cloud 有推送权限
gitea-token(新增)Username/PasswordGitea 用户名 + 访问令牌ruoyi-gitopswrite 权限

为 GitOps 写操作单独建 Token

建议不要复用拉代码的那个 Token。理由:

  • 拉代码只需要 read:repository
  • 写 GitOps 仓库需要 write:repository
  • 权限分离后,即使 CI 机器的拉代码凭据泄露,也无法篡改 GitOps 仓库

在 Gitea 里:用户设置 → 应用 → 生成令牌 → 权限勾选 write:repository

8.5 各服务的差异点

7 个服务的 Jenkinsfile 只有环境变量不同:

服务APP_NAMEIMAGE_REPO 后缀GITOPS_FILE
gatewayruoyi-gateway/ruoyi-gatewayapps/gateway/overlays/prod/kustomization.yaml
authruoyi-auth/ruoyi-authapps/auth/overlays/prod/kustomization.yaml
modules-systemruoyi-modules-system/ruoyi-modules-systemapps/modules-system/overlays/prod/kustomization.yaml
modules-genruoyi-modules-gen/ruoyi-modules-genapps/modules-gen/overlays/prod/kustomization.yaml
modules-jobruoyi-modules-job/ruoyi-modules-jobapps/modules-job/overlays/prod/kustomization.yaml
modules-fileruoyi-modules-file/ruoyi-modules-fileapps/modules-file/overlays/prod/kustomization.yaml
visual-monitorruoyi-visual-monitor/ruoyi-visual-monitorapps/visual-monitor/overlays/prod/kustomization.yaml

更好的做法:把差异抽到 Jenkins 参数里

如果不想维护 7 份几乎一样的 Jenkinsfile,可以:

  1. 把公共部分做成共享库(Jenkins Shared Library),每个仓库的 Jenkinsfile 只调用 ruoyiPipeline(appName: 'ruoyi-gateway')
  2. 或者用环境变量 + 参数化构建,把差异作为参数传入

本文档选择每个仓库一份完整 Jenkinsfile 的原因:可读性最好,每个服务的流水线可以独立修改。当服务多到 20 个以上时再考虑抽象。

09 阶段五:灰度切换(8 步,每步可回退)

最重要的一条原则:不要一次性全切

你现在有一个正常工作的生产环境。改造过程中任何一步出问题,都要能立即退回去

因此本章把切换拆成 8 步,每步只做一件事,且都有明确的回退动作每步之间建议间隔 1~2 天,确认稳定再走下一步。

9.1 切换步骤表

步骤做什么验证方法出问题怎么退
1安装 Argo CD,不创建任何 Applicationkubectl -n argocd get pods 全 Runningkubectl 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确认一切稳定后,最后打开 pruneargocd 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。

正确的做法

  1. 改 Git,而不是改集群(这正是 GitOps 的意义)
  2. 紧急情况下,先暂停自动同步
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 1

argocd 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 推错了版本)层次一:回退 Git1~5 分钟(等 Argo CD 同步)GitOps 仓库的写权限
紧急止血(服务正在出故障)层次二:Argo CD rollback30 秒~1 分钟Argo CD 的操作权限
Argo CD 挂了层次三:kubectl rollout undo30 秒集群权限

10.3 故障排查速查表

Argo CD 自身的问题

现象原因解决
Pod ImagePullBackOffimagePullSecrets 未配 / Harbor 不通kubectl -n argocd describe pod 看 Events
Pod PendingnodeSelector 指向的节点资源不足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 podLast State

Application 的问题

现象原因解决
Unknown 状态Argo CD 读不到 Git 仓库检查仓库地址、凭据、网络
ComparisonError渲染失败(YAML 错误 / Kustomize 配置错)argocd app get <name> 看详细报错
OutOfSync 但 diff 看不出来某些字段被运行时改写ignoreDifferences 忽略
SyncFailed: not permitted in projectAppProject 的白名单缺资源类型补进 namespaceResourceWhitelist
path does not existsource.path 写错确认目录里有 kustomization.yaml
repo not permittedsourceReposrepoURL 不匹配注意 .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://...githttp://...git/(多个斜杠)、或 httpshttp 不一致,都会报 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 的配合

风险:清单里如果用了 :latestimagePullPolicy: 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
Dexghcr.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.yamlhttps://gh-proxy.com/https://raw.githubusercontent.com/argoproj/argo-cd/v3.5.3/manifests/install.yaml✅ 1,917,766 字节
argocd CLIhttps://files.m.daocloud.io/github.com/argoproj/argo-cd/releases/download/v3.5.3/argocd-linux-amd64
kubectl-neathttps://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/argocd404
docker.m.daocloud.io/argoproj/argocd:v3.5.3403(该域名只代理 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 节点集群)上的勘察与实施记录

三句话总结

  1. CI 与 CD 的分界线是"Git 仓库":CI 负责产出一个可部署的版本并写进 Git;CD 负责让集群变成 Git 描述的样子。跨越这条线的任何操作(比如 CI 直接 kubectl)都是在把两件事重新耦合起来。
  2. 切换要慢,回退要快:第 9 章的 8 步切换表,每一步都可以单独退回去。"先手动同步、再 selfHeal、最后才 prune" 这个顺序不能颠倒。
  3. 本文档最大的价值不是"会用 Argo CD",而是建立了"集群状态可审计、可回滚"的机制。工具可以换(Flux、Argo CD、甚至自研),但这套「声明式 + Git 作为事实来源」的思想是一致的。

基于 VitePress 构建 · 内容为个人学习与工程实践笔记