argocd+jenkins+gitlab+harbor+多环境k8s集群
这份文档给谁看 :想搞懂这套 CI/CD 是怎么跑的、想照着部署一套、或想接手维护的人。 文档定位 :先讲"为什么"和"怎么跑通",再讲"每个文件干什么" ,最后才是部署与排障。 覆盖仓库 :
ops/devops/zcyw-jenkins-shared-library(共享库)|slp/gitops(GitOps 配置)|slp/vue(业务示例) 文档基准版本:2026-09-21
怎么用这份文档
| 你的情况 | 建议阅读顺序 |
|---|---|
| 完全不懂这套东西,想学 | 1.5(五方如何配合,全文总纲) → 第一篇其余(概念)→ 第三篇 9 章(谁在哪配)→ 第二篇(代码) |
| 要部署一套新环境 | 第三篇 14 章(检查清单)→ 第四篇 15 章(SOP) |
| 要接入一个新项目 | 第四篇 16 章(接入指南)→ 第二篇第 8 章(map 参数) |
| 构建/部署报错了 | 第四篇 17 章(排障手册) |
| 想改某个行为 | 附录 E(变更操作手册:改 X 要去哪改) |
目录
第一篇 · 概念与全景(建议先读)
-
[1. 这套系统在解决什么问题](#1. 这套系统在解决什么问题)
-
[2. 五个核心概念](#2. 五个核心概念)
-
[3. 一次构建的完整旅程](#3. 一次构建的完整旅程)
-
[4. 命名规则总表](#4. 命名规则总表)
第二篇 · 代码逐层拆解
-
[5. 三个仓库的职责](#5. 三个仓库的职责)
-
[6. 共享库详解](#6. 共享库详解)
-
[7. GitOps 仓库详解](#7. GitOps 仓库详解)
-
[8. 业务仓库详解](#8. 业务仓库详解)
第三篇 · 配置全景(谁在哪配)
-
[9. 配置归属总表:手工 vs 代码](#9. 配置归属总表:手工 vs 代码)
-
[10. Jenkins](#10. Jenkins)
-
[11. ArgoCD](#11. ArgoCD)
-
[12. Harbor 镜像仓库](#12. Harbor 镜像仓库)
-
[13. 目标集群 / DNS / 基础镜像](#13. 目标集群 / DNS / 基础镜像)
-
[14. 部署前置准备检查清单](#14. 部署前置准备检查清单)
第四篇 · 运维与排障
-
[15. 首次部署操作步骤(SOP)](#15. 首次部署操作步骤(SOP))
-
[16. 接入指南:新项目 / 新环境 / 新集群](#16. 接入指南:新项目 / 新环境 / 新集群)
-
[17. 排障手册](#17. 排障手册)
-
[18. 已知隐患与改进建议](#18. 已知隐患与改进建议)
附录
-
[附录 A · 术语表](#附录 A · 术语表)
-
[附录 B · 学习路径与动手实验](#附录 B · 学习路径与动手实验)
-
[附录 C · Jenkins 凭证与账号速查](#附录 C · Jenkins 凭证与账号速查)
-
[附录 D · 部署前一页纸检查清单](#附录 D · 部署前一页纸检查清单)
-
[附录 E · 变更操作手册](#附录 E · 变更操作手册)
-
[附录 F · 后续需要完善的功能与存在的不足](#附录 F · 后续需要完善的功能与存在的不足)
本项目实例(全文示例的真实值)
全文的讲解、命令、日志片段都以
slp-vue-demo这个项目为例。 下表是示例中出现的全部真实值 ------ 阅读时对照它,就不会把占位符和实际值搞混。
项目与仓库
| 项 | 真实值 |
|---|---|
| Jenkins Job 名 / 项目名 / K8s 命名空间 | slp-vue-demo |
| 业务代码仓库 | http://gitlab.yunwdh.cn/slp/vue |
| GitOps 配置仓库 | http://gitlab.yunwdh.cn/slp/gitops |
| 共享库仓库 | http://gitlab.yunwdh.cn/ops/devops/zcyw-jenkins-shared-library |
环境 / 集群 / 域名 / 镜像
| 环境 | 集群名 | API Server | 访问域名 | 镜像仓库地址 |
|---|---|---|---|---|
| dev | bj-dev-01 |
https://192.168.10.107:6443 |
slp-vue-demo-dev.yunwdh.cn |
bj-harbor.yunwdh.cn/dev/slp-vue-demo/slp-vue-demo |
| test | cq-test-01 |
https://192.168.10.110:6443 |
slp-vue-demo-test.yunwdh.cn |
cq-harbor.yunwdh.cn/test/slp-vue-demo/slp-vue-demo |
| prod | sz-prod-01 |
https://192.168.10.112:6443 |
slp-vue-demo-prod.yunwdh.cn |
sz-harbor.yunwdh.cn/prod/slp-vue-demo/slp-vue-demo |
cq-test-01集群的 Ingress 对外入口实测为192.168.10.111(80 端口)。
平台地址
| 项 | 真实值 |
|---|---|
| GitLab | http://gitlab.yunwdh.cn |
| Jenkins | http://jenkins.yunwdh.cn(本项目 webhook:http://jenkins.yunwdh.cn/project/slp-vue-demo) |
| ArgoCD | https://argocd.yunwdh.cn(命名空间固定为 argo-cd) |
| Harbor | http://{bj,cq,sz}-harbor.yunwdh.cn(项目:dev / test / prod) |
| Jenkins Agent 命名空间 | jenkins |
一次真实构建(prod / sz-prod-01)的产物
| 项 | 真实值 |
|---|---|
| 构建号 | #47 |
| 业务代码 commit | 46faaaa4(slp/vue 的一次提交) |
| 镜像 tag | 202609200731-46faaaa4-v47(= 日期 + commit8 + 构建号) |
| GitOps 仓库提交信息 | jenkins ci auto commit |
| 父 Application | slp-vue-demo |
| ApplicationSet(prod) | slp-vue-demo-slp-vue-demo-prod-sz-prod-01 |
| 子 Application(prod) | slp-vue-demo-prod-sz-prod-01-slp-vue-demo |
| 子 Application(公共资源) | slp-vue-demo-prod-sz-prod-01-common-k8s-resource |
| 集群里的资源 | Namespace slp-vue-demo → Deployment / Service / Ingress(同名) |
第一篇 · 概念与全景
1. 这套系统在解决什么问题
1.1 没有 CI/CD 之前是怎么干的
一个前端项目上线,传统做法是:
本地 npm run build → 手工打包 dist/ → scp 到服务器 → 解压覆盖 → 重启 nginx
↑
出问题只能靠回忆改过什么
痛点:
| 痛点 | 后果 |
|---|---|
| 构建靠人 | 不同人本地环境不一致,"我这儿是好的" |
| 上线靠手 | 漏文件、漏重启、改错目录 |
| 无记录 | 出事不知道改了啥、怎么回滚 |
| 多环境 | dev/test/prod 各维护一套命令,经常搞混 |
| 需要集群权限 | 每个开发都要拿 K8s 凭证,权限失控 |
1.2 CI/CD 分别是什么
| 缩写 | 全称 | 干什么 |
|---|---|---|
| CI | Continuous Integration 持续集成 | 代码提交后自动构建、测试、产出制品(这里是 Docker 镜像) |
| CD | Continuous Delivery/Deployment 持续交付/部署 | 把制品自动部署到目标环境 |
这套系统的实现:
CI = Jenkins 的 PreRunCheck / GitClone / ImageBuild 三个阶段
CD = Jenkins 的 SedYaml / DeployK8s + ArgoCD 的同步
1.3 关键设计:为什么用 GitOps,而不是 Jenkins 直接 kubectl
传统做法(反例):
Jenkins ── kubectl apply ──► 业务 K8s 集群
↑
└─ 需要把每个集群的 kubeconfig 都配在 Jenkins 上(安全风险 + 难以审计)
本系统的做法(GitOps):
Jenkins ──只做两件事──► ① 推镜像到 Harbor
② 改 GitOps 仓库的 YAML 并 git push
│
▼
ArgoCD 监听仓库 ──► apply 到目标集群
(集群凭证只配在 ArgoCD 侧)
换来五个好处:
| 好处 | 说明 |
|---|---|
| Jenkins 不需要集群凭证 | 攻击面变小;新增集群不用改 Jenkins |
| 所有变更都是 Git 提交 | 谁、什么时候、改了什么,一目了然;回滚 = git revert |
| 配置与代码分离 | 业务仓库只管业务代码;部署配置在独立的 GitOps 仓库 |
| 状态可观测 | ArgoCD 持续对比"期望状态 vs 实际状态",手动改动会被自动纠偏(selfHeal) |
| 多环境天然隔离 | dev/test/prod 就是三份 overlay 目录 |
1.4 一张图看懂全部角色
┌──────────────┐
│ 开发者 │ git push
└──────┬───────┘
▼
┌───────────────────────────────────────────────────────────────────────────┐
│ GitLab(代码的源头,共 3 个仓库) │
│ slp/vue 业务代码 │
│ ops/devops/zcyw-jenkins-shared-library 流水线逻辑(共享库) │
│ slp/gitops 部署配置(GitOps 仓库) │
└──────┬────────────────────────────────────────────────────────────────────┘
│ ① 读取 Jenkinsfile + 加载共享库
▼
┌───────────────────────────────────────────────────────────────────────────┐
│ Jenkins(流水线调度者:构建 + 改配置,不碰业务集群) │
│ 在 K8s 上动态起 Pod 跑构建(ci-build / cd-deploy / jnlp 三个容器) │
│ 6 个阶段:PreRunCheck→GitClone→ImageBuild→SedYaml→DeployK8s │
└──────┬──────────────────────────────────┬─────────────────────────────────┘
│ ② docker push │ ③ git push(改 YAML)
▼ ▼
┌──────────────────────┐ ┌──────────────────────────────────────────┐
│ Harbor(镜像仓库) │ │ GitLab: slp/gitops │
│ 项目 = 环境名 │ │ template/ 脚手架模板 │
│ dev / test / prod │ │ argocd/<项目>/ ApplicationSet │
└──────────┬───────────┘ │ <项目>/<应用>/base Deployment+Service │
│ │ <项目>/<应用>/overlays/<环境>/ │
│ └────────────────┬─────────────────────────┘
│ │ ④ 监听(每 3 分钟)
│ ▼
│ ┌──────────────────────────────────────────┐
│ │ ArgoCD(部署执行者) │
│ │ 父 Application → ApplicationSet → │
│ │ 子 Application → 渲染 Kustomize 清单 │
│ └────────────────┬─────────────────────────┘
│ ⑤ kubelet 拉取镜像 │ ⑥ apply
▼ ▼
┌───────────────────────────────────────────────────────────────────────────┐
│ 目标 K8s 集群(bj-dev-01 / cq-test-01 / sz-prod-01) │
│ 命名空间 = 项目名 → Deployment + Service + Ingress │
│ 对外域名 = <应用名>-<环境>.yunwdh.cn │
└───────────────────────────────────────────────────────────────────────────┘
一句话先记住 :Jenkins 负责"打镜像 + 改 GitOps 仓库",ArgoCD 负责"读 GitOps 仓库 + apply 到集群",GitLab 只负责"存东西"。 五个角色的完整职责边界与交接方式,见下一节 1.5。
1.5 五方如何配合:一次变更的完整协作链路
这一节是全文的总纲 。 CI/CD 是目标,Jenkins / ArgoCD / GitLab / GitOps 是达成目标的五个角色 ------ 理解它们怎么配合,整套系统就没有黑盒了。
1.5.1 核心洞察:五方之间只流动"两样东西"
整套系统里,跨组件的交流只有两种介质:
| 介质 | 流向 | 载体 |
|---|---|---|
| Git 提交 | 开发者 → GitLab → Jenkins / ArgoCD | 业务代码、流水线逻辑、期望状态 YAML |
| Docker 镜像 | Jenkins → Harbor → 目标集群 | 构建产物 |
没有第三种。 任何环节出问题,都能在这两个介质上定位 ------ 这是这套架构最值得记住的一点。
1.5.2 一次变更的七棒交接
以 slp-vue-demo 部署到 test / cq-test-01 为例:
| 棒次 | 从 → 到 | 交接物 | 由谁触发 / 靠什么 |
|---|---|---|---|
| 1 | 开发者 → GitLab 业务仓库 | 代码提交 | git push |
| 2 | 业务仓库 → Jenkins | 代码 + Jenkinsfile |
GitLab webhook(或手动触发) |
| 3 | Jenkins → Harbor | Docker 镜像 | docker push |
| 4 | Jenkins → GitLab GitOps 仓库 | 期望状态 YAML(一次 commit) | git push(SedYaml 阶段) |
| 5 | GitOps 仓库 → ArgoCD | 期望状态 | ArgoCD 每 3 分钟拉取(或手工 app sync) |
| 6 | ArgoCD → 目标集群 | K8s 资源 | ArgoCD 执行 apply |
| 7 | 目标集群 → Harbor | 拉取镜像 | kubelet 按 imagePullSecrets 拉取 |
第 3 棒和第 4 棒是"并行"的 :Jenkins 同时把镜像推到 Harbor、把期望状态写进 GitOps 仓库。 第 6、7 棒完全没有 Jenkins 参与 ------ 这是 GitOps 的关键:部署阶段与构建阶段彻底解耦。
1.5.3 五个角色的职责边界
| 角色 | 在 CI/CD 中扮演 | 只做 | 绝不做 | 为什么这样切 |
|---|---|---|---|---|
| GitLab(三个仓库) | 唯一事实来源 | 存业务代码、流水线逻辑、期望状态;提供变更记录 | 不执行任何动作 | 一切变更可审计、可回滚 |
| Jenkins(CI) | 构建执行者 | 拉代码、跑编译、打镜像、推镜像、改 GitOps 仓库并 push | 不连任何业务 K8s 集群 | 业务集群凭证不需要放到 Jenkins |
| Harbor | 制品仓库 | 存镜像(按环境分项目)、供各集群拉取 | 不参与调度与部署 | 镜像不可变,tag 带 commit 可溯源 |
| ArgoCD(CD) | 部署执行者 | 读 GitOps 仓库、渲染 Kustomize、apply 到集群、持续纠偏 | 不构建镜像 | 集群凭证只配在这里 |
| GitOps 仓库 | CI 与 CD 的契约 / 解耦层 | 用 YAML 描述"集群应该长什么样" | 不放业务代码 | Jenkins 与 ArgoCD 互不认识,只通过它交接 |
1.5.4 为什么中间必须有 GitOps 这一层
| 维度 | 没有 GitOps(Jenkins 直接 kubectl) | 有 GitOps(本系统) |
|---|---|---|
| 凭证 | 每个集群的 kubeconfig 都要配在 Jenkins | 只需配在 ArgoCD |
| 审计 | 只知道"Jenkins 改过",不知道改成了什么 | 每次部署 = 一次 Git 提交,diff 可见 |
| 回滚 | 只能重新跑一次旧构建 | git revert 一次提交即可 |
| 状态漂移 | 手工改动无人纠正,越漂越远 | selfHeal 自动纠偏回 Git 状态 |
| 多环境 | 每个环境一套脚本/参数 | 每个环境一个 overlay 目录 |
| 新增集群 | 改 Jenkins 配置 + 加凭证 | 给集群打个 cluster-name 标签即可 |
1.5.5 七个交接点"断了会怎样"
排障时先判断断在哪一棒,再去对应章节:
| 交接点 | 断了的症状 | 去哪查 |
|---|---|---|
| ① 代码 → Jenkins | 构建根本不触发 | 第 10.1 节 Triggers / GitLab webhook |
| ② Jenkins → Harbor | docker login failed / denied |
第 12.2、12.4 节 |
| ③ Jenkins → GitOps 仓库 | git push ... 403 / rejected |
第 14.3 节(权限)、并发构建开关 |
| ④ GitOps 仓库 → ArgoCD | 一直 OutOfSync、子 App 不生成 |
第 11.2 节(集群 label)、第 17.5 节 |
| ⑤ ArgoCD → 目标集群 | cluster ... is not accessible、application is deleting |
第 11.2、17.3 节 |
| ⑥ 目标集群 → Harbor | Pod ImagePullBackOff |
第 12.6 节(harbor-secret) |
| ⑦ 集群 → 用户 | 域名 404 / 503 / 连不上 | 第 17.4 节(Ingress / endpoints / DNS) |
1.5.6 一句话概括协作模式
Jenkins 负责"把代码变成镜像 + 把期望状态写进 Git"; ArgoCD 负责"把 Git 里的期望状态变成集群里的现实"。 两者不直接对话 ------ GitOps 仓库就是它们之间的契约。
2. 五个核心概念
这一章用最小例子解释文档后面反复出现的五个词。懂了这五个,整套系统就没有黑盒了。
2.1 Jenkins Pipeline 与共享库
什么是 Pipeline
Jenkins 的流水线就是一段 Groovy 脚本,描述"按顺序做哪些事":
pipeline {
agent any
stages {
stage('构建') { steps { sh 'npm run build' } }
stage('部署') { steps { sh 'kubectl apply -f k8s.yaml' } }
}
}
为什么需要"共享库"
假设有 20 个项目,每个项目的流水线都写 200 行 ------ 只要流程要改一处(比如换个镜像仓库),就得改 20 个地方。
Jenkins 共享库(Shared Library) 就是把这些公共逻辑抽出来单独放一个 Git 仓库:
共享库仓库(只维护一份)
├── vars/ ← 目录下的文件名 = 全局可调用的方法名
└── src/ ← 普通 Groovy 类,按包名组织
业务方只需要:
library(identifier: "zcyw-jenkins-shared-library@master", retriever: modernSCM([...]))
def map = [:] // 声明差异
map.put('gitUrl', 'http://.../slp/vue')
map.put('buildCmd', 'npm run build')
Pipeline_GitOps_Run(map) // 调用共享库入口
关键机制:
-
vars/Pipeline_GitOps_Run.groovy里的def call(Map map)是隐式入口,所以能直接写Pipeline_GitOps_Run(map) -
library(...)里写了@master,每次构建都会去拉共享库最新版本 → 改共享库,所有项目立即生效
本系统的共享库就是
zcyw-jenkins-shared-library,它承载了全部流水线逻辑(约 9 个 Groovy 文件)。
2.2 Kustomize(base / overlay)
问题:dev / test / prod 的 Deployment 99% 相同,只有镜像地址、副本数等不同。复制三份 → 改一处要改三处。
Kustomize 的解法:
base/ ← 公共部分(只写一份)
deployment.yaml
service.yaml
kustomization.yaml ← 列出 base 包含哪些资源
overlays/
dev/kustomization.yaml ← 只写"和 base 的差异"
test/kustomization.yaml
prod/kustomization.yaml
一个 overlay 的典型内容:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: slp-vue-demo # 给所有资源统一加命名空间
resources:
- ../../base # 引用 base
- ingress-patch.yaml # 再加上本环境特有的资源
images: # 镜像替换器:把 base 里的锚点名替换成真实地址
- name: app-image
newName: sz-harbor.yunwdh.cn/prod/slp-vue-demo/slp-vue-demo
newTag: 202609200731-46faaaa4-v47
base 里对应写一个锚点名:
containers:
- name: slp-vue-demo
image: app-image:latest # ← 会被上面的 images 替换器改写
渲染命令:
kubectl kustomize overlays/prod # 输出最终要 apply 的 YAML
⚠️
kustomization.yaml会严格校验字段 :写了不存在的字段会直接报错 (例如imagePullSecrets不是 它的合法字段,必须写在 Deployment 的spec.template.spec里)。
2.3 ArgoCD Application / ApplicationSet
| 概念 | 是什么 | 类比 |
|---|---|---|
| Application | 一条"部署指令":从哪个仓库的哪个路径,部署到哪个集群的哪个命名空间 | 一份"施工任务单" |
| ApplicationSet | 一个"任务单生成器":按条件批量生成多个 Application | 一个"模板 + 循环" |
| AppProject | 权限边界:限定哪些仓库/集群/命名空间可以被部署 | 一个"项目组" |
为什么需要 ApplicationSet? 假设有 3 个集群,要部署同一个应用:
-
不用 ApplicationSet:手写 3 个 Application,加一个集群就要加一个
-
用 ApplicationSet:写 1 个,靠集群标签自动匹配
spec:
generators:
- clusters: # 集群生成器
selector:
matchLabels:
cluster-name: sz-prod-01 # 只挑打了这个标签的集群
template:
metadata:
name: slp-vue-demo-prod-sz-prod-01-slp-vue-demo # 生成的 Application 名
spec:
destination:
server: '{{server}}' # 自动注入被选中集群的地址
所以给 ArgoCD 添加集群时必须打
cluster-name标签,否则 ApplicationSet 匹配不到,子 Application 永远不生成。
2.4 Harbor 与环境化镜像
Harbor 是私有 Docker 镜像仓库。本系统给镜像定的规则:
{registryServer}/{环境}/{项目名}/{应用名}:{日期}-{commit8}-v{构建号}
│ │ │ │
bj-harbor... prod slp-vue-demo slp-vue-demo
为什么 Harbor 的"项目"要按环境划分 (dev / test / prod 三个项目)?
| 好处 | 说明 |
|---|---|
| 权限隔离 | dev 镜像可以人人可推,prod 镜像只有流水线能推 |
| 保留策略不同 | prod 镜像保留久、dev 镜像保留短 |
| 一眼看出用途 | 在 Harbor 里看到项目名就知道是哪个环境的 |
镜像 tag 为什么要带 commit 哈希 :202609200731-46faaaa4-v47 里的 46faaaa4 是业务仓库的 commit 前 8 位 → 任何一个镜像都能反查出对应的代码版本,回滚和排查时极其有用。
2.5 GitOps 仓库
一句话:把「集群里应该长什么样」全部用 YAML 写在 Git 里,让工具去对比和落地。
本系统的 GitOps 仓库 = slp/gitops,它的内容变化只有两个来源:
| 来源 | 谁 | 提交信息 |
|---|---|---|
| 流水线自动 | Jenkins SedYaml 阶段 |
jenkins ci auto commit |
| 人工调整 | 运维 | fix: ... / reset: ... |
所以这个仓库每次构建都会前进一个提交 ------ 这是完全正常的。
3. 一次构建的完整旅程
这一章用一次真实构建 (prod + sz-prod-01)把整条链路走一遍。建议对照 Jenkins 构建日志一起读。
3.1 怎么触发
| 方式 | 说明 |
|---|---|
| 手动 | Jenkins → Job → Build with Parameters → 选分支 / 环境 / 集群 |
| 自动 | 业务仓库 push → GitLab webhook 打到 http://jenkins.yunwdh.cn/project/<Job名> → 触发构建 |
⚠️ 自动触发时用的是 Job 里保存的默认参数。所以"自动部署"前一定要确认默认值指向哪个环境。
3.2 十个步骤的时间线
| # | 阶段 | 动作 | 日志关键字 |
|---|---|---|---|
| 1 | 准备 | 加载共享库 @master,解析 Jenkinsfile 的 map |
Loading library zcyw-jenkins-shared-library@master |
| 2 | 准备 | properties() 生成三个构建参数 |
[Pipeline] properties |
| 3 | 准备 | 在 K8s 上创建 Agent Pod(3 容器) | Created Pod: kubernetes jenkins/<job>-<N>-xxxx |
| 4 | PreRunCheck | 打印参数 → 校验集群 → prod 权限门 | >>>用户: xxx, 管理员权限: true<<< |
| 5 | GitClone | 拉业务代码,取 commit 前 8 位 | commitHash: 46faaaa4,代码获取成功 |
| 6 | ImageBuild | 识别技术栈 → 生成 Dockerfile → 构建 → 推送 | Login Succeeded → Successfully tagged ... |
| 7 | SedYaml | 克隆 GitOps 仓库 → 改 YAML → 提交推送 | >>>部署新服务<<< / >>>部署新环境<<< / >>>已部署过服务或环境,执行服务更新镜像<<< |
| 8 | DeployK8s | 登录 ArgoCD → 建 Project/App → 同步 | 'admin:login' logged in successfully → Phase: Succeeded |
| 9 | 收尾 | post 打印结果 |
>>>构建成功!<<< |
| 10 | ArgoCD 自动同步 | 拉 GitOps 仓库 → 渲染 Kustomize → apply | (不在 Jenkins 日志里,看 ArgoCD UI) |
3.3 逐阶段详解
①-③ 准备阶段
Agent Pod 由 resources/Pod-CICD-Base-Build-Env.yaml 定义,含三个容器:
| 容器 | 用在哪 | 用途 |
|---|---|---|
jnlp |
GitClone |
Jenkins 入站代理(名称是插件写死的,不能改) |
ci-build |
PreRunCheck / ImageBuild |
默认容器,跑 docker 命令打镜像 |
cd-deploy |
SedYaml / DeployK8s |
改 GitOps 仓库、调 argocd CLI |
workspace 是三个容器共享的 emptyDir ------ 所以
jnlp里拉下来的代码,ci-build也能看到。
④ PreRunCheck ------ 三道检查
preRunCheck.printBuildParameters() // 打印:分支 / 环境 / 集群
preRunCheck.paramSelectCheck() // 校验:必须且只能选一个集群
preRunCheck.prodPermissionCheck() // 管控:prod 只允许 Jenkins 管理员
prod 权限门判定链:
-
取
BUILD_USER_ID(Build User Vars 插件提供) -
为空 → 非 prod 放行;prod 报错中止
-
User.getById(id)查不到用户 → 同上 -
判
Jenkins.ADMINISTER→ 管理员放行;非管理员只有非 prod 放行
这就是为什么必须启用 Jenkins 安全认证 :否则
BUILD_USER_ID = anonymous,prod 永远发不了。
⑤ GitClone
checkout scmGit(branches: [[name: "${params.Git_Branch_Name_Parameter}"]], ...)
def fullCommitHash = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
env.commitHash = fullCommitHash.take(8) // 取前 8 位写进 env
用
env.commitHash而不是裸赋值 ------ 裸赋值会被 Jenkins 警告"可能内存泄漏",且env变量跨阶段可见。
⑥ ImageBuild ------ 技术栈自动识别
def registryServer = 'bj-harbor.yunwdh.cn'
if (customDockerfile 有值) buildByWriteDockerfile()
else if (dockerFile 有值) buildByDockerfile()
else if (fileExists('./pom.xml')) javaBuild()
else if (fileExists('./package.json') || './app.vue') vueBuild()
else if (fileExists('./go.mod')) goBuild() // ⚠️ 未实现
else if (fileExists('./gcc')) gccBuild() // ⚠️ 未实现
else if (fileExists('./php')) phpBuild() // ⚠️ 未实现
else netBuild() // ⚠️ 未实现
以 vueBuild 为例,会动态生成两阶段 Dockerfile:
# 阶段一:构建
FROM registry.cn-chengdu.aliyuncs.com/qiuyl01/node:22.23.1-alpine AS builder
WORKDIR /build-app
COPY . .
RUN mkdir -p dist && cp index.html dist/index.html # ← 这里就是 map.buildCmd
# 阶段二:运行
FROM registry.cn-chengdu.aliyuncs.com/qiuyl01/nginx:alpine
RUN echo "Asia/Shanghai" > /etc/timezone
COPY --from=builder /build-app/./dist /data/app # ← 这里是 map.compOutputPath
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
产物 :bj-harbor.yunwdh.cn/prod/slp-vue-demo/slp-vue-demo:202609200731-46faaaa4-v47
⑦ SedYaml ------ 三种场景(本系统的灵魂)
先清空工作目录,再浅克隆 GitOps 仓库:
sh '''rm -rf ./* .git'''
checkout scmGit(branches: [[name: "*/master"]], extensions: [[$class:'CloneOption', depth:1, shallow:true]], ...)
def ingressHost = "${JOB_BASE_NAME}-${params.Release_Env_Type_Parameter}.yunwdh.cn"
def imageTag = "${date}-${commitHash}-v${BUILD_ID}"
然后按目录是否存在,走三种不同分支:
| 场景 | 判定条件 | 做什么 | 首次部署会打印 |
|---|---|---|---|
| 部署新服务 | ./<项目>/<应用> 不存在 |
从 template/ 拷全套 (base + overlays + applicationset + config.json),sed 替换所有占位符 |
>>>部署新服务<<< |
| 部署新环境 | 服务在,overlays/<环境> 不存在 |
只补 overlays、config.json 和当前「环境+集群」的 ApplicationSet | >>>部署新环境<<< |
| 服务更新 | 都存在 | 只改 newTag |
>>>已部署过服务或环境,执行服务更新镜像<<< |
} else {
// 服务更新分支:只改镜像 tag,其他什么都不动
sh """ sed -i 's/newTag:.*/newTag: ${imageTag}/' ${projectName}/${JOB_BASE_NAME}/overlays/${env}/kustomization.yaml """
}
⚠️ 关键推论 :改了
template/之后,已存在的服务和环境不会自动更新 ------ 因为模板只在「部署新服务」分支被复制。 要让已有应用生效,必须直接改 gitops 里已生成的文件(或清空项目目录让它重建)。
最后提交推送:
git add -A
git config user.name "${BUILD_USER_ID}"
git commit -m "jenkins ci auto commit" || echo ''
git pull --depth=1 http://${GIT_USERNAME}:${GIT_PASSWORD}@gitlab.yunwdh.cn/slp/gitops.git master
git push http://${GIT_USERNAME}:${GIT_PASSWORD}@gitlab.yunwdh.cn/slp/gitops.git master
⑧ DeployK8s ------ ArgoCD 三层模型
// ① Project 不存在则创建
if (projCount == '0') sh "argocd proj create slp-vue-demo -d '*,*' -s '*' --allow-cluster-resource '*/*'"
// ② 父 Application 不存在则创建(path 指向存放 ApplicationSet 的目录)
if (fatherApplicationCount == '0') sh """
argocd app create --project default --name slp-vue-demo \
--repo http://gitlab.yunwdh.cn/slp/gitops.git \
--path argocd/slp-vue-demo \
--dest-server https://kubernetes.default.svc --dest-namespace argo-cd \
--revision master --sync-policy automated
"""
// ③ 同步父 App(让 ApplicationSet 生效)→ 再同步子 App(下面以 prod / sz-prod-01 为例)
"argocd app sync slp-vue-demo > /dev/null"
'sleep 5s'
"argocd app sync slp-vue-demo-prod-sz-prod-01-slp-vue-demo --prune --timeout 180"
"argocd app sync slp-vue-demo-prod-sz-prod-01-common-k8s-resource --prune --timeout 180"
三层对象的关系 (各级对象的字段取值详见 7.4):
父 Application(slp-vue-demo)
├─► ApplicationSet(slp-vue-demo-slp-vue-demo-prod-sz-prod-01) ← 按集群标签筛选
│ └─► 子 Application(slp-vue-demo-prod-sz-prod-01-slp-vue-demo) → 业务资源
└─► ApplicationSet(slp-vue-demo-common-k8s-resource-prod-sz-prod-01)
└─► 子 Application(slp-vue-demo-prod-sz-prod-01-common-k8s-resource) → 公共资源
⑨-⑩ 收尾与 ArgoCD 自动同步
子 Application 的 syncPolicy 是 automated: {prune: true, selfHeal: true},targetRevision: HEAD:
-
自动同步:ArgoCD 每 3 分钟拉一次仓库,发现差异就 apply
-
自动纠偏:有人手工改集群里的资源,会被改回去
-
prune:只删「之前被 ArgoCD 管理、现在清单里没有了」的资源
4. 命名规则总表
设 JOB_NAME = JOB_BASE_NAME = projectName = appNamespace = slp-vue-demo,环境 prod,集群 sz-prod-01:
| 对象 | 规则 | 示例 |
|---|---|---|
| K8s 命名空间 | appNamespace |
slp-vue-demo |
| 项目名(GitOps 目录名) | 同 appNamespace |
slp-vue-demo |
| Harbor 镜像地址 | {registryServer}/{环境}/{项目名}/{JOB_BASE_NAME} |
sz-harbor.yunwdh.cn/prod/slp-vue-demo/slp-vue-demo |
| 镜像 tag | {日期}-{commit8}-v{BUILD_ID} |
202609200731-46faaaa4-v47 |
| AppProject | {项目名} |
slp-vue-demo |
| 父 Application | {项目名} |
slp-vue-demo |
| 父 App 的 source path | argocd/{项目名} |
argocd/slp-vue-demo |
| ApplicationSet(业务) | {项目名}-{JOB_BASE_NAME}-{环境}-{集群} |
slp-vue-demo-slp-vue-demo-prod-sz-prod-01 |
| ApplicationSet(公共资源) | {项目名}-common-k8s-resource-{环境}-{集群} |
slp-vue-demo-common-k8s-resource-prod-sz-prod-01 |
| 子 Application(业务) | {项目名}-{环境}-{集群}-{JOB_BASE_NAME} |
slp-vue-demo-prod-sz-prod-01-slp-vue-demo |
| 子 Application(公共资源) | {项目名}-{环境}-{集群}-common-k8s-resource |
slp-vue-demo-prod-sz-prod-01-common-k8s-resource |
| Service 名 | {JOB_BASE_NAME} |
slp-vue-demo |
| Ingress 域名 | {JOB_BASE_NAME}-{环境}.yunwdh.cn |
slp-vue-demo-prod.yunwdh.cn |
| GitOps 提交信息 | 固定 | jenkins ci auto commit |
⚠️
appNamespace的坑 :echo "${JOB_NAME}" | awk -F'/' '{print $NR}'取的是第一段 而非最后一段。 Job 名带/分组(如yunwdh/ywdh-vue)时结果是yunwdh,所以 Job 必须建在 Jenkins 根目录,不能分组。
第二篇 · 代码逐层拆解
5. 三个仓库的职责
| 仓库 | GitLab 地址 | 角色 | 谁维护 | 变更频率 |
|---|---|---|---|---|
| 共享库 | http://gitlab.yunwdh.cn/ops/devops/zcyw-jenkins-shared-library |
生产者:承载全部流水线逻辑 | 运维 / DevOps | 低(改一次影响所有项目) |
| GitOps 仓库 | http://gitlab.yunwdh.cn/slp/gitops |
部署配置:模板 + 各项目实际 YAML | 流水线自动提交为主,人工兜底 | 每次构建都会变 |
| 业务仓库 | http://gitlab.yunwdh.cn/slp/vue |
消费者 :业务代码 + 自己的 Jenkinsfile |
业务开发 | 跟随业务迭代 |
三者的关系:
业务仓库(声明差异:我要构建什么、怎么构建)
│ Jenkinsfile
▼
共享库(定义流程:怎么构建、怎么改配置、怎么部署)
│ SedYaml 阶段写入
▼
GitOps 仓库(描述结果:集群里应该长什么样)
│ ArgoCD 读取
▼
目标集群
6. 共享库详解
仓库:ops/devops/zcyw-jenkins-shared-library
6.1 目录结构与文件作用
zcyw-jenkins-shared-library/
├── README.md # 简要说明(架构、目录、技术栈支持)
├── vars/
│ └── Pipeline_GitOps_Run.groovy # ★ 流水线主入口(文件名 = 全局方法名)
├── src/org/devops/
│ ├── Common.groovy # 彩色日志输出工具
│ ├── GetParameters.groovy # 动态生成构建参数(分支→环境→集群 三级级联)
│ ├── PreRunCheck.groovy # 构建前校验(参数校验 + prod 权限门)
│ ├── DockerBuild.groovy # 多技术栈 Dockerfile 生成与镜像构建
│ ├── SedYaml.groovy # ★ GitOps 仓库 YAML 修改与提交
│ └── Deploy.groovy # ★ ArgoCD Project / Application / ApplicationSet 编排
├── resources/
│ ├── Jenkinsfile # 消费者 Jenkinsfile 模板(含 customDockerfile 示例)
│ ├── Jenkinsfile-test # 同上,测试用
│ ├── Pod-CICD-Base-Build-Env.yaml # ★ K8s Pod Agent 定义(三容器)
│ └── Pvc-CICD-Base-Build-Env.yaml # Node.js 运行时 PVC(需手动预创建)
├── demo/
│ ├── 声明式-demo.groovy # 声明式 pipeline{} 教学示例
│ └── 脚本式-demo.groovy # 脚本式 node{}+try/catch 教学示例
├── docs/
│ └── 代码功能分析.md # 详细代码分析
└── 报错.txt # 历史报错排查记录
为什么
vars/和src/分两个目录?
vars/下的文件名 就是 Jenkins 的全局方法名(vars/MyStep.groovy→ 可以写MyStep())
src/下是普通 Groovy 类,需要new org.devops.XXX()才能用 所以只有"入口"放在vars/,其他业务逻辑放src/。
6.2 vars/Pipeline_GitOps_Run.groovy --- 流水线主入口
Agent 定义
agent {
kubernetes {
yaml libraryResource('Pod-CICD-Base-Build-Env.yaml') // 从 resources/ 加载 Pod 定义
cloud 'kubernetes' // ★ 对应 Jenkins「系统管理 → 云」中的名字,必须一致
defaultContainer 'ci-build' // 未显式指定的 sh 都在此容器执行
instanceCap 30 // 并发 Pod 上限
podRetention never() // 用完立即删除
}
}
⚠️ 不要加
retries------ Declarative 会用它把整个stages包进一次 retry:某个 stage 失败会导致整条流水线重跑一遍(第二遍因currentBuild.result已是 FAILURE 而全部秒跳过,只在阶段视图里多出一整套空列)。
Options
options {
timestamps() // 日志带时间戳
timeout(time: 1, unit: 'HOURS') // 整条流水线超时 1 小时
buildDiscarder(logRotator(numToKeepStr: '15')) // 保留最近 15 次构建
}
Environment(全局变量)
environment {
appNamespace = sh(returnStdout: true, script: "echo \"${JOB_NAME}\"| awk -F'/' '{print \$NR}'").trim()
projectName = "${appNamespace}"
date = sh(returnStdout: true, script: 'date +%Y%m%d%H%M').trim()
code = sh(returnStdout: true, script: 'head /dev/urandom | tr -dc 0-9 | head -c 6').trim()
// 三个凭证都有 UUID 兜底 —— 业务方不传也能跑(前提是 Jenkins 里存在对应凭证)
gitCredId = "${map?.gitCredId ?: 'a3dea81a-e05d-47db-827e-e4effb10cd6f'}"
imgRegCredId = "${map?.imgRegCredId ?: '54b99cbd-eb0e-4602-bd72-eb70b9c28454'}"
argocdCerdId = "${map?.argocdCerdId ?: '0ee5a2bf-ba0d-41c7-84af-d07475cb7eb2'}"
gitopsBranch = 'master'
allowProdWithoutUserIdentity = "${map?.allowProdWithoutUserIdentity ?: 'false'}"
nodeVersion = "${map.nodeVersion}"; jdkVersion = "${map.jdkVersion}"
goVersion = "${map.goVersion}"; dotnetVersion = "${map.dotnetVersion}"
buildCmd = "${map?.buildCmd ?: 'null'}".replace('env_type', params.Release_Env_Type_Parameter)
jarPath = "${map?.jarPath ?: './'}"
nexusProfile = "${map.nexusProfile}"; startupPath = "${map.startupPath}"
portNumber = "${map?.portNumber ?: '8080'}"
dockerFile = "${map?.dockerFile ?: 'null'}"
customDockerfile = "${map?.customDockerfile ?: 'null'}"
compOutputPath = "${map?.compOutputPath ?: 'dist'}"
}
两个最巧妙的设计:
-
buildCmd的env_type替换buildCmd = "${map?.buildCmd ?: 'null'}".replace('env_type', params.Release_Env_Type_Parameter)业务方写
npm run build:env_type,选 prod 时自动变成npm run build:prod------ 构建命令随环境切换,无需为每个环境写一份 Jenkinsfile。 -
所有
map参数都有默认值 ------ 降低接入门槛。
六个执行阶段
所有阶段都有统一守卫,前置阶段失败则后续全部 skip:
when { expression { currentBuild.result == null || currentBuild.result == 'SUCCESS' } }
| 阶段 | 容器 | 重试 | 做什么 |
|---|---|---|---|
PreRunCheck |
ci-build |
--- | 打印参数 → 校验集群选择 → prod 权限门 |
GitClone |
jnlp |
retry(6) |
按所选分支拉码,取 commit 前 8 位 |
ImageBuild |
ci-build |
--- | 按文件特征分发到不同构建方法 |
SedYaml |
cd-deploy |
retry(8) |
浅克隆 GitOps 仓库,改 YAML + commit + push |
DeployK8s |
cd-deploy |
内部 retry(2) |
ArgoCD 登录、建 Project/App、sync |
镜像仓库二级路由(SedYaml 阶段)
def clusterPrefix = sh(returnStdout: true, script: "echo \"${params.Dest_Cluster_Name_Parameter}\"| awk -F'-' '{print \$NR}'").trim()
switch (clusterPrefix) {
case 'sz': imageServer = 'sz-harbor.yunwdh.cn'; break
case 'cq': imageServer = 'cq-harbor.yunwdh.cn'; break
case 'bj': imageServer = 'bj-harbor.yunwdh.cn'; break
default: imageServer = 'bj-harbor.yunwdh.cn'; break
}
按目标集群名前缀选 Harbor :sz-prod-01 → sz-harbor,cq-test-01 → cq-harbor,bj-dev-01 → bj-harbor。
而 ImageBuild 阶段的推送地址是写死的:
def registryServer = 'bj-harbor.yunwdh.cn' // ⚠️ 与上面的拉取路由不一致,见第 18 章隐患 #3
Post
post {
always { println('always') }
success { printOutputColor('构建成功!', 'green'); currentBuild.description = '构建成功!' }
failure { printOutputColor('构建失败!', 'red'); currentBuild.description = '构建失败!' }
aborted { printOutputColor('构建取消!', 'blue'); currentBuild.description = '构建取消!' }
}
6.3 GetParameters.groovy --- 动态生成参数表单
def getParameters(gitUrl) {
properties([ parameters([
gitParameter(name: 'Git_Branch_Name_Parameter', type: 'PT_BRANCH_TAG', useRepository: gitUrl, defaultValue: 'master', ...),
[$class: 'CascadeChoiceParameter', name: 'Release_Env_Type_Parameter', referencedParameters: 'Git_Branch_Name_Parameter', ...],
[$class: 'CascadeChoiceParameter', name: 'Dest_Cluster_Name_Parameter', referencedParameters: 'Release_Env_Type_Parameter', ...]
]) ])
}
properties()会把参数定义持久化到 Job 配置里 ------ 这就是为什么你在 Jenkins 页面上能看到这三个参数。
环境列表脚本
def dev = ["dev","test","prod"]
def release = ["dev","test","prod"]
def masterList = ["dev","test","prod"]
def other = ["dev","test","prod"]
// 按分支名 switch,返回对应列表
⚠️ 四个分支返回完全相同的列表 ,所以级联「限制」实际未生效 ------ 任何分支都能选 prod。 真正的防线是
PreRunCheck.prodPermissionCheck()。
集群列表脚本(这一步级联是有效的)
def devCluster = ["bj-dev-01"]
def testCluster = ["cq-test-01"]
def prodCluster = ["sz-prod-01"]
def mergedCluster = devCluster + testCluster + prodCluster
// 按 Release_Env_Type_Parameter 返回
⚠️
Dest_Cluster_Name_Parameter声明为多选PT_CHECKBOX,但paramSelectCheck()又报错禁止含逗号 → 伪单选 。 ⚠️fallbackScript里的集群名(cd-gx-az01-biz-priv-dev-01)与正式脚本(bj-dev-01)不一致,建议统一。
6.4 PreRunCheck.groovy --- 三道检查
def paramSelectCheck() {
if (params.Dest_Cluster_Name_Parameter == 'null' || ... == '') error('错误: 未勾选发布的目标集群')
if (params.Dest_Cluster_Name_Parameter?.contains(',')) error('错误: 只允许选择一个目标集群')
}
prodPermissionCheck() 判定链:
def allowWithoutIdentity = "${allowProdWithoutUserIdentity ?: 'false'}" == 'true'
def buildUserId = "${BUILD_USER_ID}"
if (!buildUserId || buildUserId == 'null' || buildUserId == '') {
handleUnidentifiedUser(envType, '未识别到构建用户 (BUILD_USER_ID 为空)', allowWithoutIdentity); return
}
def user = hudson.model.User.getById(buildUserId, false)
if (user == null) {
handleUnidentifiedUser(envType, "Jenkins 中未找到用户: ${buildUserId}", allowWithoutIdentity); return
}
def isAdmin = Jenkins.instance.getACL().hasPermission(user.impersonate(), jenkins.model.Jenkins.ADMINISTER)
// 管理员 → 放行;非管理员 + 非 prod → 放行;非管理员 + prod → 报错中止
报错信息(已带解决办法):
权限错误: prod 环境要求可识别的 Jenkins 管理员,但 Jenkins 中未找到用户: anonymous。
解决办法: 在 Jenkins「系统管理 → 全局安全配置」中启用安全认证,并用管理员账号登录后触发构建;
若确认该 Jenkins 无需认证,可在 Jenkinsfile 的 map 中设置 allowProdWithoutUserIdentity=true 临时放行。
6.5 DockerBuild.groovy --- 动态生成 Dockerfile
| 方法 | 触发条件 | 行为 |
|---|---|---|
buildByWriteDockerfile |
map.customDockerfile 有值 |
writeFile('./Dockerfile', customDockerfile) 后构建(优先级最高) |
buildByDockerfile |
map.dockerFile 有值 |
用指定路径的 Dockerfile 构建 |
vueBuild |
package.json 或 app.vue |
按 nodeVersion 选基础镜像(22/23/24),区分 nuxt.config.js / nuxt.config.ts / 普通 Vue,生成两阶段 Dockerfile |
javaBuild |
pom.xml |
Maven 构建 → JRE 运行,内置 JAVA_OPTS(容器内存感知、OOM dump、/dev/./urandom) |
goBuild / gccBuild / phpBuild / netBuild |
--- | ⚠️ 未实现 (会抛 MissingMethodException) |
四个已实现方法的收尾一致:
docker.withRegistry("https://${registryServer}", "${imgRegCredId}") {
docker.build("${registryServer}/${params.Release_Env_Type_Parameter}/${projectName}/${JOB_BASE_NAME}:${date}-${commitHash}-v${BUILD_ID}", '-f ./Dockerfile .').push()
sh "docker rmi ${registryServer}/${params.Release_Env_Type_Parameter}/${projectName}/${JOB_BASE_NAME}:${date}-${commitHash}-v${BUILD_ID}"
}
6.6 SedYaml.groovy --- GitOps 的灵魂
这是全库最重要的文件,详见第 3.3 节 ⑦ 与第 7 章。要点:
| 要点 | 说明 |
|---|---|
| 每次清空重建 | sh '''rm -rf ./* .git''' 保证工作目录干净 |
| 浅克隆 | CloneOption(depth:1, shallow:true) 加速 |
| 三种场景 | 部署新服务 / 部署新环境 / 服务更新(只改 tag) |
| 兜底逻辑 | 独立校验当前「环境+集群」的 ApplicationSet 是否存在,缺失就补(与目录分支解耦,是"一个应用多环境并存"的关键) |
common-k8s-resource |
项目级公共资源(ConfigMap / Secret 等),独立一套 ApplicationSet |
| 推送 | 用 UsernamePasswordMultiBinding 注入凭证,git add/commit/pull/push |
6.7 Deploy.groovy --- ArgoCD 编排
def gitOps(argocdCerdId) {
withCredentials([usernamePassword(credentialsId: argocdCerdId, passwordVariable: 'password', usernameVariable: 'username')]) {
sh "argocd login argocd.yunwdh.cn --username ${username} --password ${password} --insecure --grpc-web"
def projCount = ... // argocd proj list | grep -w <项目名> | wc -l
def fatherApplicationCount = ... // argocd app get <项目名> | wc -l
def sonApplicationCount = ... // argocd app list | grep <项目>-<环境>-<集群>-<应用> | wc -l
def sonApplicationServer = ... // argocd app get <项目>-<环境>-<集群>-<应用> | grep Server | awk '{print $2}'
if (projCount == '0') sh "argocd proj create ..."
if (fatherApplicationCount == '0') sh "argocd app create ... --path argocd/<项目名> ..."
if (sonApplicationCount == '0' || sonApplicationServer != destCluster) { /* 完整流程 */ }
else { /* 仅同步子 App */ }
retry(2) { try { sh shellCommand.join('; ') } catch (e) { sleep(10); error(...) } }
}
}
注 :
sonApplicationServer != destCluster是「集群 server URL」与「集群名」比较,恒为真 → 总是走完整流程。功能上无害,但属冗余判断。
6.8 resources/Pod-CICD-Base-Build-Env.yaml --- 构建环境定义
三容器 Pod(jenkins 命名空间):
| 容器 | 镜像 | 用途 | 关键点 |
|---|---|---|---|
initContainers/fix-workspace-permissions |
busybox | chmod -R 777 /home/jenkins/agent |
解决 workspace 权限 |
jnlp |
inbound-agent | Jenkins 入站代理 | 名称不可改(插件源码写死) |
cd-deploy |
CentOS 7.9 + git | GitOps 操作与 ArgoCD 部署 | 挂 hostPath 的 argocd CLI |
ci-build |
busybox | 默认容器,镜像构建 | privileged: true、挂 docker.sock、limits 4C/8Gi |
Volumes:
| 卷 | 类型 | 挂载点 | 说明 |
|---|---|---|---|
workspace-volume |
emptyDir |
/home/jenkins/agent |
并发构建天然隔离(曾用 hostPath 会撞 .git/config.lock) |
docker-sock |
hostPath | /var/run/docker.sock |
复用宿主机 Docker 守护进程 |
docker-cli |
hostPath (File) | /usr/bin/docker |
复用宿主机 docker 客户端 |
argocd-cli |
hostPath (File) | /usr/local/bin/argocd |
复用宿主机 argocd CLI |
m2-cache |
hostPath | /root/.m2 |
Maven 缓存加速 |
go-cache |
hostPath | /root/go/pkg |
Go 模块缓存加速 |
nodejs-package |
PVC | /root/.nvm/versions/node/(只读) |
Node 运行时预装在宿主机,Pod 不下载 |
⚠️ hostPath 要求 Pod 被调度到"预置了这些路径的节点" ,否则 Pod 启动失败。建议给这类节点打标签并用
nodeSelector限定。
6.9 resources/Pvc-CICD-Base-Build-Env.yaml
# 需事先在K8s构建集群手动创建,待基础构建Pod运行时就会自动与之绑定
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nodejs-package
namespace: jenkins
spec:
accessModes: [ReadWriteMany] # ★ 必须 RWX,否则多节点并发构建失败
resources:
requests:
storage: 20Gi
⚠️ PVC 不会被流水线自动创建,必须手工预建并把 Node 运行时放进去。
7. GitOps 仓库详解
仓库:http://gitlab.yunwdh.cn/slp/gitops,分支固定 master。
7.1 目录结构
slp/gitops/
├── template/ # ★ 脚手架模板(人工维护,绝对不能删)
│ ├── kustomize/ # 业务应用模板
│ │ ├── base/deployment.yaml # Deployment(含 imagePullSecrets)
│ │ ├── base/service.yaml # Service(ClusterIP)
│ │ ├── base/kustomization.yaml # resources: [deployment.yaml, service.yaml]
│ │ ├── overlays/env/kustomization.yaml # overlay(namespace + ingress 引用 + images 替换)
│ │ └── overlays/env/ingress-patch.yaml # Ingress
│ ├── kustomize-common-resource/ # 项目级公共资源模板
│ │ ├── base/configmap.yaml # 占位 ConfigMap
│ │ ├── base/kustomization.yaml
│ │ └── overlays/env/kustomization.yaml
│ └── argocd/ # ArgoCD 模板
│ ├── applicationset.yaml # 业务应用的 ApplicationSet
│ ├── common-k8s-resource-applicationset.yaml
│ └── applicationset-config/clusterNameEnv/config.json
│
├── argocd/ # ★ 流水线生成的 ApplicationSet(父 App 的 source path)
│ └── <项目名>/
│ ├── <项目>-<应用>-<环境>-<集群>-applicationset.yaml
│ └── <项目>-common-k8s-resource-<环境>-<集群>-applicationset.yaml
│
└── <项目名>/ # ★ 项目目录
├── <应用名>/ # 业务应用
│ ├── base/deployment.yaml # 已实例化
│ ├── base/service.yaml
│ ├── base/kustomization.yaml
│ ├── overlays/<环境>/kustomization.yaml # 每个环境一份(镜像地址 + tag)
│ ├── overlays/<环境>/ingress-patch.yaml
│ └── applicationset-config/<集群>/config.json
└── common-k8s-resource/ # 项目级公共资源(同结构)
当前实际内容 (项目 slp-vue-demo,应用 slp-vue-demo,3 个环境):
argocd/slp-vue-demo/
slp-vue-demo-slp-vue-demo-dev-bj-dev-01-applicationset.yaml
slp-vue-demo-slp-vue-demo-test-cq-test-01-applicationset.yaml
slp-vue-demo-slp-vue-demo-prod-sz-prod-01-applicationset.yaml
slp-vue-demo-common-k8s-resource-dev-bj-dev-01-applicationset.yaml
slp-vue-demo-common-k8s-resource-test-cq-test-01-applicationset.yaml
slp-vue-demo-common-k8s-resource-prod-sz-prod-01-applicationset.yaml
slp-vue-demo/slp-vue-demo/
base/{deployment.yaml, service.yaml, kustomization.yaml}
overlays/{dev,test,prod}/{kustomization.yaml, ingress-patch.yaml}
applicationset-config/{bj-dev-01,cq-test-01,sz-prod-01}/config.json
7.2 模板占位符对照表(★ 核心)
SedYaml.groovy 通过 sed 把这些占位符替换为实际值:
| 占位符 | 出现位置 | 替换为 | 示例 |
|---|---|---|---|
serviceNameEnv |
deployment / service / ingress / kustomization | JOB_BASE_NAME(应用名) |
slp-vue-demo |
portNumberEnv |
deployment / service / ingress | portNumber(默认 8080) |
80 |
ingressHostEnv |
ingress-patch | {应用}-{环境}.yunwdh.cn |
slp-vue-demo-prod.yunwdh.cn |
projectNameEnv |
全部 | projectName(= 命名空间) |
slp-vue-demo |
imagesNameEnv |
kustomization | {Harbor}/{环境}/{项目}/{应用} |
sz-harbor.yunwdh.cn/prod/slp-vue-demo/slp-vue-demo |
imageTagNameEnv |
kustomization | {日期}-{commit8}-v{BUILD_ID} |
202609200731-46faaaa4-v47 |
cdAppNameEnv |
applicationset | ApplicationSet 名 | slp-vue-demo-slp-vue-demo-prod-sz-prod-01 |
appNameEnv |
applicationset | JOB_BASE_NAME |
slp-vue-demo |
appNamespaceEnv |
applicationset | 命名空间 | slp-vue-demo |
targetClusterEnv |
applicationset / config.json | 目标集群名 | sz-prod-01 |
applicationNameEnv |
applicationset / config.json | 子 Application 名 | slp-vue-demo-prod-sz-prod-01-slp-vue-demo |
sourceFilePathEnv |
applicationset / config.json | overlay 路径 | slp-vue-demo/slp-vue-demo/overlays/prod |
⚠️ 占位符是模板的「契约」 :模板里必须保留占位符,
grep才能匹配到文件,sed才有输入。 一旦把占位符写成写死值(例如把namespace: serviceNameEnv改成namespace: common-k8s-resource), 就会导致sed: no input files报错、流水线失败。 改模板时务必用grep -rl <占位符> template/检查一遍。
7.3 各模板文件详解
template/kustomize/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: serviceNameEnv
labels: { app: serviceNameEnv }
spec:
replicas: 1
selector: { matchLabels: { app: serviceNameEnv } }
template:
metadata: { labels: { app: serviceNameEnv } }
spec:
imagePullSecrets:
- name: harbor-secret # ⚠️ 每个目标命名空间必须存在同名 Secret
containers:
- name: serviceNameEnv
image: app-image:latest # ← 由 overlay 的 images 替换器改写
ports: [{ containerPort: portNumberEnv }]
env: [{ name: PROJECT_NAME, value: projectNameEnv }]
image: app-image:latest只是一个锚点名 ,overlay 里用 Kustomize 的images替换器改成真实地址。
template/kustomize/base/service.yaml
apiVersion: v1
kind: Service
metadata:
name: serviceNameEnv
labels:
app: serviceNameEnv
spec:
type: ClusterIP
selector:
app: serviceNameEnv # ★ 必须与 Deployment 的 pod 标签一致,否则没有 endpoints
ports:
- name: http
protocol: TCP
port: portNumberEnv # ★ Ingress 的 backend.port.number 引用的是这个
targetPort: portNumberEnv
template/kustomize/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
template/kustomize/overlays/env/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: serviceNameEnv
resources:
- ../../base
- ingress-patch.yaml # ★ 不写这行,ingress-patch.yaml 就不会被渲染
images:
- name: app-image
newName: imagesNameEnv
newTag: imageTagNameEnv
template/kustomize/overlays/env/ingress-patch.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: serviceNameEnv
spec:
rules:
- host: ingressHostEnv
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: serviceNameEnv
port:
number: portNumberEnv
⚠️ 没有
ingressClassName也没有 annotations ------ 依赖目标集群存在默认 IngressClass 。 实测 cq-test-01 集群的默认 IngressClass 是nginx✅
template/argocd/applicationset.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: cdAppNameEnv
namespace: argo-cd # ★ 命名空间硬编码为 argo-cd
spec:
generators:
- clusters:
selector:
matchLabels:
cluster-name: targetClusterEnv # ★ 靠集群 Secret 的 label 筛选
template:
metadata:
name: applicationNameEnv
labels: { app: appNameEnv, project: projectNameEnv }
spec:
project: projectNameEnv
source:
repoURL: http://gitlab.yunwdh.cn/slp/gitops.git
targetRevision: HEAD
path: sourceFilePathEnv
destination:
server: '{{server}}' # ★ ApplicationSet 自动注入目标集群地址
namespace: appNameEnv
syncPolicy:
automated: { prune: true, selfHeal: true }
syncOptions: [CreateNamespace=true] # ★ 命名空间自动创建
template/kustomize-common-resource/base/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: common-k8s-resource
labels: { app: common-k8s-resource }
data:
README: "此 ConfigMap 由 Jenkins 流水线自动生成,请按项目实际需要替换为真实的公共资源"
用途:放项目级的公共资源(多个服务共用的 ConfigMap / Secret / 中间件),避免每个服务重复部署。
template/argocd/applicationset-config/clusterNameEnv/config.json
{
"cluster": { "name": "targetClusterEnv" },
"application": { "name": "applicationNameEnv" },
"source": { "repoURL": "http://192.168.10.104/slp/gitops.git", "path": "sourceFilePathEnv" }
}
⚠️ 该文件由流水线生成,但当前 ApplicationSet 模板并未引用它 ------ 属预留/冗余,或供外部平台读取。
7.4 ArgoCD 三层应用模型
父 Application:slp-vue-demo
│ project = slp-vue-demo
│ repoURL = http://gitlab.yunwdh.cn/slp/gitops.git
│ path = argocd/slp-vue-demo ← 该目录下放的都是 ApplicationSet
│ destination= https://kubernetes.default.svc(in-cluster,即 ArgoCD 所在集群)
│ dest ns = argo-cd
│ syncPolicy = automated
│
├─► ApplicationSet:slp-vue-demo-slp-vue-demo-prod-sz-prod-01
│ clusters 生成器筛选 cluster-name=sz-prod-01
│ └─► 子 Application:slp-vue-demo-prod-sz-prod-01-slp-vue-demo
│ source.path = slp-vue-demo/slp-vue-demo/overlays/prod
│ dest.server = https://192.168.10.112:6443
│ dest.ns = slp-vue-demo
│ → apply Deployment + Service + Ingress
│
└─► ApplicationSet:slp-vue-demo-common-k8s-resource-prod-sz-prod-01
└─► 子 Application:slp-vue-demo-prod-sz-prod-01-common-k8s-resource
source.path = slp-vue-demo/common-k8s-resource/overlays/prod
dest.ns = common-k8s-resource
→ apply ConfigMap
为什么不直接建 Application,而要多一层 ApplicationSet?
| 好处 | 说明 |
|---|---|
| 集群维度可扩展 | 加一个集群只需给它打上 cluster-name 标签,ApplicationSet 自动生成新的子 App,不用改 YAML、不用改流水线 |
| 环境维度隔离 | ApplicationSet 按「应用+环境+集群」命名,同一应用的不同环境互不干扰 |
| 配置集中 | 所有 ArgoCD 对象都在 GitOps 仓库里,父 App 一同步就全部生效 |
7.5 GitOps 仓库的变更来源
| 变更类型 | 谁做的 | 提交信息 |
|---|---|---|
| 服务更新(改镜像 tag) | 流水线 SedYaml |
jenkins ci auto commit |
| 新服务 / 新环境接入 | 流水线 SedYaml |
jenkins ci auto commit |
| 模板调整、补 Service、引用 ingress | 人工 | fix: ... |
| 清理历史目录 | 人工 | reset: 清空业务目录 |
8. 业务仓库详解
以 slp/vue 为例:
slp/vue/
├── Jenkinsfile # ★ 流水线入口:加载共享库 + 传 map
├── index.html # 前端入口页
├── package.json # 依赖与构建脚本(决定走 vueBuild)
├── nginx.conf # 运行时 Nginx 配置(被打进镜像)
├── src/main.js # createApp(App).mount('#app')
├── src/App.vue # 根组件
└── .gitignore
8.1 Jenkinsfile(业务方唯一需要写的文件)
#!groovy
library(
identifier: "zcyw-jenkins-shared-library@master", retriever: modernSCM([
$class: "GitSCMSource",
remote: "http://gitlab.yunwdh.cn/ops/devops/zcyw-jenkins-shared-library.git",
credentialsId: 'gitlab-credentials'
])
)
def map = [:]
map.put('gitUrl', 'http://gitlab.yunwdh.cn/slp/vue')
map.put('gitCredId', 'gitlab-credentials')
map.put('imgRegCredId', 'harbor-credentials')
map.put('buildCmd', 'mkdir -p dist && cp index.html dist/index.html')
map.put('portNumber', '80')
map.put('compOutputPath','./dist')
map.put('nodeVersion', '22')
Pipeline_GitOps_Run(map)
业务仓库使用这套流水线的全部成本,就是上面这几行。
8.2 map 支持的完整参数清单
| 参数 | 必填 | 默认值 | 说明 |
|---|---|---|---|
gitUrl |
✅ | --- | 业务仓库地址(同时用于 Git Parameter 的分支列表) |
gitCredId |
建议 | a3dea81a-... |
拉代码凭证 ID |
imgRegCredId |
建议 | 54b99cbd-... |
Harbor 凭证 ID |
argocdCerdId |
建议 | 0ee5a2bf-... |
ArgoCD 凭证 ID |
buildCmd |
前端/Java 必填 | null |
编译命令,支持 env_type 占位 |
nodeVersion |
前端必填 | --- | 22 / 23 / 24 |
jdkVersion |
Java 可选 | --- | 1.8 / 17 |
goVersion / dotnetVersion |
可选 | --- | ⚠️ 对应构建方法未实现 |
portNumber |
可选 | 8080 |
容器端口(本项目传 80) |
compOutputPath |
前端可选 | dist |
静态产物目录,不要写 ./ |
jarPath |
Java 可选 | ./ |
jar 相对路径 |
nexusProfile |
Java 可选 | --- | Maven profile |
startupPath |
.NET 可选 | --- | ⚠️ 对应构建方法未实现 |
dockerFile |
可选 | null |
自带 Dockerfile 路径 |
customDockerfile |
可选 | null |
内联 Dockerfile。设置后 buildCmd/portNumber/compOutputPath/nodeVersion 全部失效 |
allowProdWithoutUserIdentity |
可选 | false |
无法识别构建用户时是否放行 prod。临时开关,不推荐开启 |
8.3 各文件作用
| 文件 | 作用 | 是否必需 |
|---|---|---|
Jenkinsfile |
流水线入口,声明 map |
✅ 必需 |
package.json |
技术栈识别依据(vueBuild);提供 scripts.build |
前端项目必需 |
nginx.conf |
被打进 Nginx 运行阶段镜像:root /data/app、try_files $uri $uri/ /index.html(解决 SPA 刷新 404)、开启 gzip |
前端项目必需 |
pom.xml |
Java 项目识别依据 | Java 项目必需 |
go.mod |
Go 项目识别依据 | ⚠️ Go 构建方法未实现 |
Dockerfile |
通过 map.dockerFile 指定 |
可选 |
src/ |
业务源码 | --- |
.gitignore |
排除构建产物 | 建议 |
⚠️ 当前
index.html没有<script>标签 去加载main.js,所以App.vue/main.js永远不会执行,页面是空白的。如果要看视觉效果,需要补:
<script type="module" src="/src/main.js"></script>
第三篇 · 配置全景(谁在哪配)
9. 配置归属总表:手工 vs 代码
这一章是新手最容易糊涂的地方:打开 Jenkins 页面看到一堆配置,到底哪些是我要手工维护的?哪些是代码自动生成的?
9.1 一张图看清三层
Jenkins 页面(✍️ 手工创建,每个项目只做一次)
└── Job「slp-vue-demo」
├── Definition: Pipeline script from SCM
├── SCM: http://gitlab.yunwdh.cn/slp/vue ← 去这里取 Jenkinsfile
├── 脚本路径: Jenkinsfile
├── Branches to build: */master
├── 触发器: GitLab webhook
├── 凭证: gitlab-credentials
└── 三个构建参数 ← ⚠️ 你在页面看到它们,但**不是你建的**
▲
│ properties() 自动回写
│
业务仓库 slp/vue ──► Jenkinsfile ──► library(...) 加载共享库
(📄 代码) │
▼
共享库 zcyw-jenkins-shared-library(📄 代码)
├── GetParameters.groovy → 生成 3 个构建参数
├── PreRunCheck.groovy → 参数校验 + prod 权限门
├── DockerBuild.groovy → 生成 Dockerfile、打镜像推镜像
├── SedYaml.groovy → 改 gitops 仓库并 push
├── Deploy.groovy → 创建/同步 ArgoCD 对象
└── Pipeline_GitOps_Run → 串起 6 个阶段
9.2 逐项归属表
| 配置项 | 在哪 | 谁维护 | 手工 / 自动 |
|---|---|---|---|
Job 对象本身 (名称 <项目名>) |
Jenkins 页面 | 你 | ✍️ 手工(每个项目建一次) |
| Definition = Pipeline script from SCM | Jenkins 页面 | 你 | ✍️ 手工 |
SCM 仓库 http://gitlab.yunwdh.cn/slp/vue |
Jenkins 页面 | 你 | ✍️ 手工 |
凭证 gitlab-credentials |
Jenkins 页面 | 你 | ✍️ 手工 |
脚本路径 Jenkinsfile |
Jenkins 页面 | 你 | ✍️ 手工 |
分支 */master |
Jenkins 页面 | 你 | ✍️ 手工 |
| 触发器(GitLab webhook) | Jenkins 页面 | 你 | ✍️ 手工 |
| 不允许并发构建 / 保留 15 次 | Jenkins 页面 | 你 | ✍️ 手工 (代码里也有 buildDiscarder) |
| 三个构建参数(分支 / 环境 / 集群) | 页面显示,实际由流水线写入 | 你 流水线 | 🤖 自动生成 |
| 六个执行阶段 | 共享库 Pipeline_GitOps_Run.groovy |
运维 | 📄 代码 |
| 参数的可选值与级联规则 | 共享库 GetParameters.groovy |
运维 | 📄 代码 |
| prod 权限管控 | 共享库 PreRunCheck.groovy |
运维 | 📄 代码 |
| 打镜像的方式 / Dockerfile 内容 | 共享库 DockerBuild.groovy |
运维 | 📄 代码 |
| GitOps 仓库里生成什么文件 | gitops 的 template/ + 共享库 SedYaml.groovy |
运维 | 📄 代码 |
| ArgoCD 怎么建 Project/App、怎么同步 | 共享库 Deploy.groovy |
运维 | 📄 代码 |
map(gitUrl / buildCmd / 端口 / Node 版本...) |
业务仓库 Jenkinsfile |
业务开发 | 📄 代码 |
| 部署后集群里的样子(副本数、探针、资源限制) | gitops 仓库 base/deployment.yaml |
运维 | 📄 代码(ArgoCD 自动同步) |
| AppProject / Application / ApplicationSet | ArgoCD(由流水线创建) | 流水线 | 🤖 自动 |
集群注册与 cluster-name 标签 |
ArgoCD 页面 / kubectl | 运维 | ✍️ 手工 |
| Harbor 项目(dev/test/prod)、账号 | Harbor 页面 | 运维 | ✍️ 手工 |
各命名空间的 harbor-secret |
目标集群 | 运维 | ✍️ 手工 |
DNS 记录 *-<环境>.yunwdh.cn |
DNS 服务 | 运维/网管 | ✍️ 手工 |
9.3 关于那三个参数(最容易误解)
你在 Job 配置页看到:
☑ 参数化构建过程
Git_Branch_Name_Parameter ← Git 参数(分支或标签)
Release_Env_Type_Parameter ← Active Choices Reactive Parameter
Dest_Cluster_Name_Parameter ← Active Choices Reactive Parameter
它们不是你手工加的,是流水线第一次运行时自动写进去的:
// Pipeline_GitOps_Run.groovy:15 ← 注意:在 pipeline{} 之前调用
parameters.getParameters("${map.gitUrl}")
// GetParameters.groovy:56
def getParameters(gitUrl) {
properties([ parameters([ ...三个参数定义... ]) ]) // ← properties() 会持久化到 Job 配置
}
properties() 是 Pipeline 的内置步骤,作用是设置当前 Job 的属性并保存到 Job 配置。所以你刷新页面就能看到。
⚠️ 重要推论
| 你做的事 | 结果 |
|---|---|
| 在页面修改这三个参数的默认值/描述 | ❌ 下次构建会被流水线覆盖回去 |
| 在页面新增第 4 个参数 | ❌ 下次构建会消失 (properties() 用完整列表覆盖) |
想改参数的可选值(比如加个 sit 环境) |
✅ 要改共享库的 GetParameters.groovy |
10. Jenkins
10.1 Job 配置详解(手工部分)
General
| 配置项 | 建议值 | 说明 |
|---|---|---|
| 描述 | 可空 | --- |
| 不允许并发构建 | 勾选 | ⚠️ 重要:避免两个构建同时改 GitOps 仓库导致 push 冲突 |
| 丢弃旧的构建 | 策略 = Log Rotation ,最大个数 = 15 | 与流水线里的 buildDiscarder 一致 |
参数化构建过程
这三个参数由流水线自动生成,不要手工改(见 9.3)。这里只列出它们的实际配置,便于理解:
| # | 名称 | 插件 | 关键配置 |
|---|---|---|---|
| 1 | Git_Branch_Name_Parameter |
Git 参数 | 参数类型 = 分支或标签 ;默认值 = master |
| 2 | Release_Env_Type_Parameter |
Active Choices Reactive Parameter | Choice Type = Single Select ;Referenced parameters = Git_Branch_Name_Parameter;Enable filters: Filter starts at = 1 |
| 3 | Dest_Cluster_Name_Parameter |
Active Choices Reactive Parameter | Choice Type = Check Boxes ;Referenced parameters = Release_Env_Type_Parameter;Filter starts at = 1 |
Triggers
| 触发器 | 建议 |
|---|---|
| Build when a change is pushed to GitLab | ✅ 启用。Webhook URL:http://jenkins.yunwdh.cn/project/<Job名> |
| 定时构建 | 按需 |
| 轮询 SCM | ❌ 建议关闭(用 webhook 更实时、更省资源) |
| 其他工程构建后触发 | 按需 |
这是唯一必须手工维护的触发配置 。在 GitLab 仓库的
Settings → Webhooks配同一个 URL,勾选 Push events / Merge request events。
流水线
| 配置项 | 值 |
|---|---|
| 定义 | Pipeline script from SCM |
| SCM | Git |
| Repository URL | http://gitlab.yunwdh.cn/slp/vue |
| Credentials | slp/****** (gitlab-credentials) |
| Branches to build | */master |
| 脚本路径 | Jenkinsfile |
| 轻量级检出 | 勾选 |
注意 :Job 的 SCM 与
Jenkinsfile里的map.gitUrl是两个独立配置。
Job 的 SCM 决定用哪个仓库的 Jenkinsfile 跑流水线 (分支固定
*/master)
map.gitUrl决定运行时拉哪个仓库的代码、以及分支参数的来源(可切换分支) 两者通常配成同一个仓库。
10.2 插件清单(必需)
| 插件 | 代码中的依据 | 缺失时的表现 |
|---|---|---|
| Kubernetes | agent { kubernetes { ... } }、libraryResource |
No such DSL method 'kubernetes' |
| Pipeline: Declarative | pipeline { } |
语法不识别 |
| Pipeline: Basic Steps | sh/echo/writeFile/fileExists/dir/timeout/retry/sleep/error/wrap |
各种 DSL 缺失 |
| Pipeline: SCM Step | checkout scmGit(...) |
No such DSL method 'scmGit' |
| Git + Git Client | SCM 配置与 git 操作 | --- |
| Git Parameter | gitParameter(name: 'Git_Branch_Name_Parameter', ...) |
分支参数不显示 |
| Active Choices(旧名 uno-choice) | CascadeChoiceParameter、GroovyScript |
后两个参数不显示 |
| AnsiColor | ansiColor('xterm') { ... } |
彩色日志失效 |
| Build User Vars | BUILD_USER_ID、BUILD_USER、wrap([$class:'BuildUser']) |
取不到构建人 → prod 部署被权限门拦下 |
| Docker Pipeline | docker.withRegistry / docker.build |
镜像构建阶段失败 |
| Credentials Binding | withCredentials |
凭证注入失败 |
| Timestamper | timestamps() |
No such DSL method |
| Pipeline: Stage View | 阶段视图 UI | 仅影响展示 |
| 组件 | 当前环境版本 |
|---|---|
| Jenkins | 2.555.3 |
| Pod 内 Git CLI | 2.47.3 |
10.3 Kubernetes 云配置
流水线里写的是 cloud 'kubernetes',必须在 Jenkins 全局配置里存在同名云。
路径 :系统管理 → 云 → 新建云 → Kubernetes
| 配置项 | 值 | 说明 |
|---|---|---|
| 名称 | kubernetes |
★ 必须与代码完全一致 |
| Kubernetes 地址 | 构建集群 API Server | 如 https://<apiserver>:6443 |
| Kubernetes 命名空间 | jenkins |
Pod 创建位置 |
| 凭据 | 具备创建 Pod 权限的凭证 | ServiceAccount Token / kubeconfig |
| Jenkins 地址 | http://jenkins.yunwdh.cn |
★ JNLP 容器回连 Jenkins 用,最容易配错 |
⚠️
Jenkins 地址填错的表现 :Pod 一直Pending或is offline。
RBAC 要求 (Jenkins 凭证在 jenkins 命名空间需要):
rules:
- apiGroups: [""]
resources: ["pods", "pods/exec", "pods/log", "pods/portforward",
"secrets", "configmaps", "persistentvolumeclaims", "events", "services"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
10.4 凭证清单(★ 重点)
流水线共需要 6 个凭证:
| # | 用途 | 代码变量 | map 参数 |
本环境实际值 | 类型 | 来源 / 所需权限 |
|---|---|---|---|---|---|---|
| 1 | 拉业务代码 + 推 GitOps 仓库 | gitCredId |
gitCredId |
gitlab-credentials (用户名 slp) |
Username with password | GitLab 账号 。需对 slp/gitops 读写 (写是给 SedYaml push 用),对 slp/vue、共享库有读权限 |
| 2 | Docker 登录 / 推拉镜像 | imgRegCredId |
imgRegCredId |
harbor-credentials (用户名 admin) |
Username with password | Harbor 账号 。需对 dev/test/prod 有 push + pull。建议改用机器人账号 |
| 3 | ArgoCD 操作 | argocdCerdId |
argocdCerdId |
0ee5a2bf-ba0d-41c7-84af-d07475cb7eb2(默认 UUID) |
Username with password | ArgoCD 账号 (用户名 admin)。需能 proj create / app create / app sync |
| 4 | 加载共享库 | --- | --- | gitlab-credentials(复用 #1) |
同上 | 在 Jenkinsfile 的 library(...) 里引用 |
| 5 | 连接构建集群(K8s Cloud) | --- | --- | 自定义 | Secret text / kubeconfig | 构建集群的 ServiceAccount Token |
| 6 | GitLab Connection(webhook) | --- | --- | 自定义 | GitLab API token | GitLab 个人访问令牌(api scope) |
凭证 ID 有"可读名"和"UUID"两种写法 。代码对
gitCredId/imgRegCredId/argocdCerdId都提供了 UUID 兜底 → 说明旧环境用 UUID 命名。新环境建议统一用可读名 并在map中显式传入。
创建步骤(以 #1 为例):
系统管理 → 凭据 → 系统 → 全局凭据 → 添加凭据
类型:Username with password
Username: slp
Password: <GitLab 密码或 Access Token>
ID: gitlab-credentials ← ★ ID 必须与 Jenkinsfile 里写的一致
Description: GitLab 代码仓库凭据
验证三个凭证:
| 凭证 | 验证方法 |
|---|---|
gitlab-credentials |
GitClone 阶段能 checkout;SedYaml 阶段能 push |
harbor-credentials |
ImageBuild 阶段出现 Login Succeeded |
| ArgoCD 凭证 | DeployK8s 阶段出现 'admin:login' logged in successfully |
10.5 安全配置(prod 权限门的前提)
路径 :系统管理 → 全局安全配置
| 配置项 | 建议值 | 原因 |
|---|---|---|
| 安全域 | Jenkins 专用用户数据库 | 需要能识别构建用户 |
| 授权策略 | 矩阵授权策略 | 需要能给用户管理员权限 |
| 匿名用户权限 | 只读或更小 | 避免匿名触发 |
再创建管理员用户:系统管理 → 管理用户 → 新建用户,在授权矩阵中勾选 Administer。
⚠️ 不要图省事去开
allowProdWithoutUserIdentity=true------ 那等于取消 prod 权限管控。
10.6 网络连通性
Jenkins 控制器 需要能访问:构建集群 API Server(6443)、gitlab.yunwdh.cn(80)。
Agent Pod 内的容器需要能访问:
| 目标 | 端口 | 用在哪 |
|---|---|---|
gitlab.yunwdh.cn |
80 | checkout 代码、拉/推 gitops |
*-harbor.yunwdh.cn |
80 | docker login / push |
argocd.yunwdh.cn |
443 | argocd login / app sync |
registry.cn-chengdu.aliyuncs.com |
443 | 拉取构建基础镜像 |
| Jenkins 控制器 | 50000 或 80/443 | jnlp 回连 |
curl -sI http://gitlab.yunwdh.cn
curl -s http://bj-harbor.yunwdh.cn/v2/ # 期望 401
curl -skI https://argocd.yunwdh.cn
11. ArgoCD
11.1 安装与基础配置
| 项 | 值 | 说明 |
|---|---|---|
| 命名空间 | argo-cd |
⚠️ 代码中硬编码 :ApplicationSet 的 metadata.namespace、父 App 的 --dest-namespace 都是 argo-cd |
| 访问域名 | argocd.yunwdh.cn |
⚠️ 代码中硬编码 :argocd login argocd.yunwdh.cn |
| 访问方式 | HTTPS(代码用 --insecure,说明是自签证书) |
|
| gRPC | 需支持 --grpc-web(代码中每条 argocd 命令都带此参数) |
⚠️ 如果安装到非
argo-cd命名空间,ApplicationSet 与父 Application 都无法工作,必须同步改共享库代码。
ArgoCD 必须能访问 http://gitlab.yunwdh.cn/slp/gitops.git(父 App 与子 App 的 source 都指向它)。
11.2 添加目标集群(★ 核心)
为什么必须打标签
template/argocd/applicationset.yaml 用的是 cluster 生成器 ,靠集群 Secret 上的标签筛选:
generators:
- clusters:
selector:
matchLabels:
cluster-name: targetClusterEnv # 会被 sed 替换成实际集群名(如 sz-prod-01)
→ 所以集群 Secret 必须带 cluster-name: <集群名> 标签 ,且值要与 Jenkins 参数里的集群名逐字符一致。
前置条件
kubectl config get-contexts # 看本机有哪些 context
kubectl --context=kubernetes-admin@kubernetes get nodes # 确认能连上目标集群
# 注:本项目三个集群的 master 上,context 名都叫 kubernetes-admin@kubernetes(无法区分)
方式一:CLI 添加(推荐)
# 登录 ArgoCD
argocd login argocd.yunwdh.cn --username admin --insecure --grpc-web
# 添加集群
# 本项目三个集群的 master 上 context 名都是 kubernetes-admin@kubernetes,无法区分,
# 所以用 KUBECONFIG 环境变量分别指向各集群的 kubeconfig 文件
KUBECONFIG=~/.kube/config-bj-dev argocd cluster add kubernetes-admin@kubernetes --name bj-dev-01 -y
KUBECONFIG=~/.kube/config-cq-test argocd cluster add kubernetes-admin@kubernetes --name cq-test-01 -y
KUBECONFIG=~/.kube/config-sz-prod argocd cluster add kubernetes-admin@kubernetes --name sz-prod-01 -y
# 顺便打标签(部分 CLI 版本支持 --label,若报错就用下面的方式二)
KUBECONFIG=~/.kube/config-sz-prod argocd cluster add kubernetes-admin@kubernetes \
--name sz-prod-01 --label cluster-name=sz-prod-01 -y
argocd cluster add会自动在目标集群创建:
ServiceAccount
argocd-manager(kube-system命名空间)ClusterRole / ClusterRoleBinding
argocd-manager-role(cluster-admin 权限 ) 并在 ArgoCD 的argo-cd命名空间创建集群 Secret。
方式二:直接给集群 Secret 打标签(通用,不受 CLI 版本影响)
# ① 找到集群 Secret 名
kubectl -n argo-cd get secret -l argocd.argoproj.io/secret-type=cluster
# ② 打标签(集群 Secret 名形如 cluster-<一串字符>,用上一步查到的实际名字)
kubectl -n argo-cd label secret cluster-xxxxxxxx cluster-name=sz-prod-01
# 或直接 patch
kubectl -n argo-cd patch secret cluster-xxxxxxxx --type=merge \
-p '{"metadata":{"labels":{"cluster-name":"sz-prod-01"}}}'
方式三:声明式创建集群 Secret
apiVersion: v1
kind: Secret
metadata:
name: cluster-sz-prod-01
namespace: argo-cd
labels:
argocd.argoproj.io/secret-type: cluster
cluster-name: sz-prod-01 # ★ 关键标签
stringData:
name: sz-prod-01
server: https://192.168.10.112:6443
config: |
{
"tlsClientConfig": { "insecure": true },
"bearerToken": "<ServiceAccount Token>",
"namespace": "kube-system"
}
需要添加的集群清单
| 集群名 | API Server | 环境 | 必须的标签 |
|---|---|---|---|
bj-dev-01 |
https://192.168.10.107:6443 |
dev | cluster-name: bj-dev-01 |
cq-test-01 |
https://192.168.10.110:6443 |
test | cluster-name: cq-test-01 |
sz-prod-01 |
https://192.168.10.112:6443 |
prod | cluster-name: sz-prod-01 |
⚠️ 集群名必须与
GetParameters.groovy里destClusterNameGroovyScript的取值完全一致 , 否则:Jenkins 选的集群名 →targetClusterEnv替换值 → 标签匹配不上 → 子 Application 永远不生成。
验证
argocd cluster list
期望输出:
SERVER NAME VERSION STATUS MESSAGE
https://192.168.10.107:6443 bj-dev-01 v1.31.9 Successful
https://192.168.10.110:6443 cq-test-01 v1.31.9 Successful
https://192.168.10.112:6443 sz-prod-01 Unknown Cluster has no applications and is not being monitored.
https://kubernetes.default.svc in-cluster v1.31.9 Successful
✅
Unknown / Cluster has no applications and is not being monitored是正常的 ------ 意思是"当前没有应用指向这个集群,ArgoCD 不做主动探测"。有应用之后会变成Successful。
补充验证(不依赖 argocd CLI,最可靠):
# ① 集群 API 是否真的通
curl -k -s https://192.168.10.112:6443/version # 期望返回 gitVersion JSON
# ② 标签是否打上
kubectl -n argo-cd get secret -l argocd.argoproj.io/secret-type=cluster \
-o custom-columns=NAME:.metadata.name,LABELS:.metadata.labels
⚠️ 集群不可达时的表现 :
argocd cluster list显示Failed ... connection refused。 此时任何同步都会失败 ;更麻烦的是------如果之前有 Application 正在删除,会永远卡在application is deleting(因为级联删除需要能连上目标集群去删资源)。处理办法见 17.3。
11.3 AppProject 与 Application(不用手工建)
Deploy.groovy 会自动完成:
# Project 不存在则创建
argocd proj create <项目名> --description <项目名> -d '*,*' -s '*' --allow-cluster-resource '*/*'
# 父 Application 不存在则创建
argocd app create --project default --name <项目名> \
--repo http://gitlab.yunwdh.cn/slp/gitops.git --path argocd/<项目名> \
--dest-server https://kubernetes.default.svc --dest-namespace argo-cd \
--revision master --sync-policy automated
| 对象 | 名称 | 是否自动创建 |
|---|---|---|
| AppProject | <项目名> |
✅ 自动 |
| 父 Application | <项目名> |
✅ 自动 |
| ApplicationSet | <项目>-<应用>-<环境>-<集群> |
✅ 由流水线写 GitOps 仓库后,父 App 同步生成 |
| 子 Application | <项目>-<环境>-<集群>-<应用> |
✅ 由 ApplicationSet 生成 |
⚠️ AppProject 一旦创建就不要手工删(虽然能重建,但会造成短暂中断)。
11.4 权限模型
| 层级 | 由谁创建 | 作用 |
|---|---|---|
| Cluster Secret | 运维手工添加 | 告诉 ArgoCD 有哪些集群可用 |
| AppProject | 流水线自动 | 限定该项目能用哪些仓库/集群/命名空间 |
| Application / ApplicationSet | 流水线 + ApplicationSet 控制器 | 具体部署 |
12. Harbor 镜像仓库
12.1 需要几个 Harbor
镜像地址由两处独立逻辑决定,这是整套系统最容易出问题的地方:
| 环节 | 代码位置 | 逻辑 |
|---|---|---|
| 推送镜像 | Pipeline_GitOps_Run.groovy |
def registryServer = 'bj-harbor.yunwdh.cn' ------ 硬编码 bj,与所选环境和集群无关 |
| 拉取镜像 | SedYaml.groovy(由上方传入 imageServer) |
按集群名前缀:sz→sz-harbor、cq→cq-harbor、bj/其他→bj-harbor |
结果对照:
| 部署目标 | 镜像推到 | YAML 里写的 | 是否一致 |
|---|---|---|---|
dev / bj-dev-01 |
bj-harbor.../dev/... |
bj-harbor.../dev/... |
✅ 一致 |
test / cq-test-01 |
bj-harbor.../test/... |
cq-harbor.../test/... |
❌ 不一致 |
prod / sz-prod-01 |
bj-harbor.../prod/... |
sz-harbor.../prod/... |
❌ 不一致 |
不以 sz/cq/bj 开头的集群(如 gz-test-01) |
bj-harbor... |
bj-harbor... |
✅ 一致(走 default) |
这不一定是错的 ------ "统一推到主仓库 + 各站点从本地仓库拉"是标准的镜像分发架构(上传快、拉取快)。 但前提是 bj 与 cq/sz 之间有共享存储或复制规则 ,否则跨地域集群会
ImagePullBackOff。 详见 12.5 与第 18 章隐患 #3。
12.2 域名与 TLS
| 域名 | 用途 |
|---|---|
bj-harbor.yunwdh.cn |
主接收仓库(所有镜像都推这里) |
cq-harbor.yunwdh.cn |
cq 集群拉取 |
sz-harbor.yunwdh.cn |
sz 集群拉取 |
curl -s http://bj-harbor.yunwdh.cn/v2/ # 期望返回 401(说明 Harbor 活着)
curl -s http://cq-harbor.yunwdh.cn/v2/
curl -s http://sz-harbor.yunwdh.cn/v2/
⚠️ 如果通过 HTTP (非 443)访问,构建节点的 Docker 必须配置
insecure-registries:
// /etc/docker/daemon.json
{
"insecure-registries": [
"bj-harbor.yunwdh.cn",
"cq-harbor.yunwdh.cn",
"sz-harbor.yunwdh.cn"
]
}
systemctl restart docker && docker info | grep -A5 "Insecure Registries"
不配会报:
http: server gave HTTP response to HTTPS client症状对照 :connection refused= 服务没起;HTTP response to HTTPS client= 协议/端口配错。
12.3 Harbor 项目(Project)
镜像路径是:
${registryServer}/${params.Release_Env_Type_Parameter}/${projectName}/${JOB_BASE_NAME}:${tag}
│ │ │ │
bj-harbor... dev/test/prod slp-vue-demo slp-vue-demo
→ Harbor 上的项目名 = 环境名,因此必须创建:
| Harbor 项目 | 用途 | 建议 |
|---|---|---|
dev |
开发环境镜像 | 私有 |
test |
测试环境镜像 | 私有 |
prod |
生产环境镜像 | 私有 + 不可变 tag + 保留策略 |
创建方式 :Harbor UI → 项目 → 新建项目 → 名称填 dev / test / prod
或 API:
curl -u admin:<密码> -X POST "https://bj-harbor.yunwdh.cn/api/v2.0/projects" \
-H "Content-Type: application/json" \
-d '{"project_name":"prod","metadata":{"public":"false"}}'
⚠️ 不预先创建项目 ,
docker push会报denied: requested access to the resource is denied。
12.4 推送账号与权限
| 账号 | 类型 | 权限 |
|---|---|---|
admin |
系统管理员 | 全权限(当前环境在用) |
| 推荐:机器人账号 | Robot Account | 限定 dev/test/prod 三项目的 push + pull,可轮换、可审计 |
Harbor UI → 项目 dev/test/prod → 机器人账号 → 新建机器人账号
名称:jenkins
权限:勾选 push + pull
→ 生成 name(如 robot$jenkins)+ secret,二者即为 Jenkins 凭证的用户名/密码
用机器人账号时,Jenkins 凭证的 Username 要填完整名 (含
robot$前缀)。
验证推送权限:
docker login -u <账号> -p <密码> https://bj-harbor.yunwdh.cn
docker pull registry.cn-chengdu.aliyuncs.com/qiuyl01/nginx:alpine
docker tag registry.cn-chengdu.aliyuncs.com/qiuyl01/nginx:alpine bj-harbor.yunwdh.cn/prod/test-push:v1
docker push bj-harbor.yunwdh.cn/prod/test-push:v1
docker rmi bj-harbor.yunwdh.cn/prod/test-push:v1
12.5 多 Harbor 的镜像同步方案(★ 关键)
因为 push 固定到 bj-harbor,而 test/prod 的 YAML 指向 cq-harbor / sz-harbor,必须解决"镜像如何在多个 Harbor 上都可见"。三种方案,任选其一:
方案 A:共享存储后端(当前环境的做法,零配置)
原理 :三个 Harbor 域名指向同一套 Harbor 服务或同一份存储后端 ,所以推到 bj-harbor 的镜像在 sz-harbor 也能拉到。
| 优点 | 缺点 |
|---|---|
| 零配置、零延迟、无一致性风险 | 跨地域拉取要走广域网,慢;一个实例故障影响所有环境 |
验证方法:
# 推到一个域名,从另一个域名拉
docker tag bj-harbor.yunwdh.cn/prod/slp-vue-demo/slp-vue-demo:v1 \
sz-harbor.yunwdh.cn/prod/slp-vue-demo/slp-vue-demo:v1
docker pull sz-harbor.yunwdh.cn/prod/slp-vue-demo/slp-vue-demo:v1
判定是哪种情况:
# ① 三个环境是不是同一个实例
curl -u admin:<密码> -s http://bj-harbor.yunwdh.cn/api/v2.0/systeminfo
curl -u admin:<密码> -s http://sz-harbor.yunwdh.cn/api/v2.0/systeminfo
# 对比 harbor_version / registry_url
# ② 有没有配复制规则
curl -u admin:<密码> -s "http://bj-harbor.yunwdh.cn/api/v2.0/replication/policies"
# 或 Harbor UI → 系统管理 → 复制
# ③ 交叉验证:在 bj 推一个独特 tag,立刻去 sz 查
curl -u admin:<密码> -s "https://sz-harbor.yunwdh.cn/api/v2.0/projects/prod/repositories/slp-vue-demo%2Fslp-vue-demo/artifacts?q=tags%3D<tag>"
方案 B:Harbor 复制规则(跨地域推荐)
前提 :两个 Harbor 是独立实例(各自存储),网络互通。
① 在源 Harbor(bj-harbor)上注册目标仓库
Harbor UI → 系统管理 → 仓库管理 → 新建目标
提供者:Harbor
名称:sz-harbor
目标 URL:https://sz-harbor.yunwdh.cn
访问 ID:<sz-harbor 的账号>
访问密码:<密码>
验证远程证书:勾选(正式证书时)
→ 测试连接 → 通过后保存
② 创建复制规则
Harbor UI → 系统管理 → 复制 → 新建复制规则
名称:bj-to-sz-prod
复制模式:Push-based ← 推送型(源主动推)
源资源过滤器:
名称:prod ← 只同步 prod 项目
标签:** ← 所有 tag
资源类型:镜像
目标仓库:sz-harbor
触发模式:
○ 事件驱动(Event Based) ← 有 push 动作时自动触发(推荐)
○ 定时(Scheduled) ← 如 cron "0 0 2 * * *"
○ 手动(Manual)
覆盖:✔ 勾选(同 tag 覆盖)
③ 为每个「源项目 × 目标仓库」建一条规则
| 规则名 | 源项目 | 目标 |
|---|---|---|
bj-to-cq-test |
test |
cq-harbor |
bj-to-sz-prod |
prod |
sz-harbor |
dev项目不需要复制(dev 集群的 YAML 本来就指向 bj-harbor)。
| 优点 | 缺点 |
|---|---|
| 跨地域拉取快、各站点自治 | 需配置与运维;存在同步延迟;需处理同步失败告警 |
拉取型复制(Pull-based) :反向配置,消费端定时去源端拉。适合"按需拉取"场景,但不保证推送后立即可用 。 本系统是「CI 推完就要部署」,推荐推送型 + 事件驱动。
方案 C:统一镜像地址(根治,需改代码)
把 Pipeline_GitOps_Run.groovy 里的 registryServer 改成与 SedYaml 相同的路由逻辑,使"推到哪儿、YAML 就写哪儿"。
| 优点 | 缺点 |
|---|---|
| 语义正确、无需复制 | ⚠️ 会让 push 变成跨地域上传 (构建节点在 bj,部署在 sz 时要把镜像传到 sz),更慢 |
结论 :只在构建节点分布在不同区域 时才推荐方案 C。当前架构(统一推 bj + 复制/共享)更优,只是要把它显式化。
12.6 各目标命名空间的拉取凭证(★ 极易遗漏)
template/kustomize/base/deployment.yaml 里有:
spec:
imagePullSecrets:
- name: harbor-secret
→ 每一个部署目标命名空间里都必须存在名为 harbor-secret 的 docker-registry Secret ,否则 Pod 会 ImagePullBackOff。
kubectl -n <命名空间> create secret docker-registry harbor-secret \
--docker-server=<该集群对应的 Harbor 域名> \
--docker-username=<账号> \
--docker-password=<密码> \
--docker-email=admin.zcyw@qq.com
| 集群 | 命名空间 | --docker-server |
|---|---|---|
| bj-dev-01 | slp-vue-demo |
bj-harbor.yunwdh.cn |
| cq-test-01 | slp-vue-demo |
cq-harbor.yunwdh.cn |
| sz-prod-01 | slp-vue-demo |
sz-harbor.yunwdh.cn |
| 各集群 | common-k8s-resource |
同集群 |
⚠️ 命名空间不需要预先创建 (ApplicationSet 里有
CreateNamespace=true,ArgoCD 会自动建), 但 Secret 不会自动创建,必须提前放好或在命名空间创建后立即补。
验证:
kubectl -n slp-vue-demo get secret harbor-secret
# 期望:harbor-secret kubernetes.io/dockerconfigjson 数据 1
更优雅的做法 :把
harbor-secret纳入 GitOps 管理(放进common-k8s-resource), 但 Secret 明文入库不安全,需配合 Sealed Secrets / External Secrets / SOPS。
12.7 镜像保留策略建议
| 环境 | 建议 |
|---|---|
dev |
只保留最近 10~20 个 tag,节省空间 |
test |
保留最近 30 个 |
prod |
保留全部 (或至少 1 年),并开启不可变 tag(防误删/覆盖) |
配置路径:Harbor UI → 项目 → 策略 → 保留策略 / 标签不可变
13. 目标集群 / DNS / 基础镜像
13.1 目标业务集群需要具备什么
| 检查项 | 说明 | 验证命令 |
|---|---|---|
| 集群可达 | ArgoCD 能访问其 API Server(6443) | curl -k https://<ip>:6443/version |
harbor-secret |
每个目标命名空间要有 docker-registry Secret | kubectl -n <ns> get secret harbor-secret |
| 默认 IngressClass | ingress-patch.yaml 没有 ingressClassName 也没有 annotations,完全依赖默认 IngressClass |
kubectl get ingressclass(找带 (default) 的) |
| Ingress Controller | 需已部署且正常运行 | kubectl get svc -A | grep -i ingress |
| DNS 解析 | *-<环境>.yunwdh.cn 必须解析到 Ingress 入口 |
nslookup slp-vue-demo-prod.yunwdh.cn |
| StorageClass(如需) | 应用若有 PVC 需求 | kubectl get sc |
⚠️ 实测 cq-test-01 集群的默认 IngressClass 是
nginx✅(kubectl get ingress -n slp-vue-demo的 CLASS 列显示nginx)。
关于 Ingress 的 ADDRESS 为空
裸金属 / NodePort 方式部署的 ingress-nginx,其 Service 是:
NAME TYPE EXTERNAL-IP PORT(S)
ingress-nginx-controller LoadBalancer <pending> 80:30327/TCP,443:32323/TCP
EXTERNAL-IP = <pending> 说明没有云厂商 LB ,因此 Ingress 的 ADDRESS 列会空着 ------ 这是正常的,不是故障 。 关键是 Ingress 的 CLASS 列有值(说明控制器已接管)。
实测访问方式(cq-test-01):
curl -s -H "Host: slp-vue-demo-test.yunwdh.cn" http://192.168.10.111 | grep app123
说明
192.168.10.111上有一个统一入口 (前置代理 / VIP)转发到了 ingress。 纯 NodePort 场景则是<节点IP>:30327。
13.2 DNS 与网络汇总
| 域名 | 用途 | 谁需要访问 |
|---|---|---|
gitlab.yunwdh.cn |
代码仓库 | Jenkins 控制器、Agent Pod 内所有容器 |
jenkins.yunwdh.cn |
Jenkins 入口 | GitLab(webhook)、jnlp 容器 |
argocd.yunwdh.cn |
ArgoCD | cd-deploy 容器 |
bj-harbor.yunwdh.cn |
Harbor(主) | 构建节点的 Docker |
cq-harbor.yunwdh.cn |
Harbor | cq-test-01 的 kubelet |
sz-harbor.yunwdh.cn |
Harbor | sz-prod-01 的 kubelet |
registry.cn-chengdu.aliyuncs.com |
阿里云镜像服务 | ci-build(拉构建基础镜像) |
*-<环境>.yunwdh.cn |
业务应用域名 | 终端用户浏览器 |
# 一次性检查全部
for h in gitlab.yunwdh.cn jenkins.yunwdh.cn argocd.yunwdh.cn \
bj-harbor.yunwdh.cn cq-harbor.yunwdh.cn sz-harbor.yunwdh.cn; do
echo -n "$h -> "; nslookup $h | awk '/^Address: /{print $2}' | tail -1
done
业务域名的 DNS 规划
每个接入的应用都会自动产生 <应用名>-<环境>.yunwdh.cn,所以:
| 方案 | 说明 |
|---|---|
| 推荐:泛解析 | 配 *.yunwdh.cn(或按环境配 *.test.yunwdh.cn / *.prod.yunwdh.cn)指向对应集群的 ingress 入口,以后新应用接入不用每次找网管 |
| 逐条添加 | 每接一个应用加一条 A 记录,规范但麻烦 |
⚠️ 不同环境的域名必须指向各自集群的 ingress 入口,不能都指一个集群。
13.3 构建阶段的基础镜像
DockerBuild.groovy 生成的 Dockerfile 依赖这些来自阿里云镜像服务的基础镜像,构建节点必须能拉取(或提前同步到内网):
| 镜像 | 用途 |
|---|---|
registry.cn-chengdu.aliyuncs.com/qiuyl01/node:22.23.1-alpine |
Node 22 构建阶段 |
registry.cn-chengdu.aliyuncs.com/qiuyl01/node:23.11.1-alpine |
Node 23 构建阶段 |
registry.cn-chengdu.aliyuncs.com/qiuyl01/node:24.16.0-alpine |
Node 24 构建阶段(也是未知版本的兜底) |
registry.cn-chengdu.aliyuncs.com/qiuyl01/nginx:alpine |
前端运行阶段 |
registry.cn-chengdu.aliyuncs.com/qiuyl01/maven:3.8.6-openjdk-8 |
Java 构建阶段 |
registry.cn-chengdu.aliyuncs.com/qiuyl01/eclipse-temurin:8-jre |
Java 运行阶段 |
registry.cn-chengdu.aliyuncs.com/qiuyl01/busybox:latest |
Pod 的 init 容器与 ci-build |
registry.cn-chengdu.aliyuncs.com/qiuyl01/inbound-agent:3355.v388858a_47b_33-22 |
Pod 的 jnlp |
registry.cn-chengdu.aliyuncs.com/qiuyl01/centos:centos7.9.2009-git |
Pod 的 cd-deploy |
内网环境建议 :把这些镜像一次性 pull + tag + push 到内网 Harbor,再改 DockerBuild.groovy 和 Pod YAML 里的地址(需要改共享库代码)。
14. 部署前置准备检查清单
按「组件」组织,每个组件给出检查项 + 验证命令。建议从上到下逐项完成 ------ 任何一项缺失都会在流水线某个阶段直接失败。
14.1 总览
| # | 组件 | 必须完成的事 | 验证方式 |
|---|---|---|---|
| 1 | K8s 构建集群 | jenkins 命名空间、nodejs-package PVC(RWX 20Gi)、节点上预置 Docker / argocd CLI / Node / Maven / Go |
kubectl -n jenkins get pvc nodejs-package |
| 2 | Jenkins | 插件齐全、K8s 云配置、6 个凭证、启用安全认证 + 管理员、创建 Job | 空跑一次流水线 |
| 3 | GitLab | 三个仓库存在;账号对三者有读写权限;webhook 指向 Jenkins | git ls-remote 三个地址 |
| 4 | Harbor | 实例可达、dev/test/prod 三个项目、推送账号、HTTP/insecure-registries |
docker login + docker push |
| 5 | 目标集群 | harbor-secret(docker-registry 类型)、默认 IngressClass |
kubectl -n <ns> get secret harbor-secret |
| 6 | ArgoCD | 安装到 argo-cd、三个集群已添加并打上 cluster-name 标签、能访问 GitLab |
argocd cluster list |
| 7 | DNS / 网络 | gitlab / jenkins / argocd / *-harbor .yunwdh.cn 解析;构建节点能访问三者;ArgoCD 能访问目标集群 6443 |
curl / nslookup |
14.2 Kubernetes 构建集群
# ① 命名空间
kubectl create namespace jenkins
# ② Node.js 运行时 PVC(★ 必须手动创建,必须 RWX)
kubectl apply -f Pvc-CICD-Base-Build-Env.yaml
kubectl -n jenkins get pvc nodejs-package # 必须为 Bound
# ③ 宿主机路径(每台可能跑构建的节点)
ls -l /var/run/docker.sock /usr/bin/docker /usr/local/bin/argocd
ls -d /root/.m2 /root/go/pkg
# ④ Node 运行时放入 PVC(目录结构需符合 nvm 规范)
# /root/.nvm/versions/node/v22.23.1/bin/node
# ⑤ Docker insecure-registries
docker info | grep -A5 "Insecure Registries"
⚠️ hostPath 要求 Pod 被调度到"预置了这些路径的节点" 。 建议 :给这类节点打标签(如
role=jenkins-build),并在 Pod 模板里加nodeSelector限定调度范围。
14.3 GitLab
| 检查项 | 要求 |
|---|---|
ops/devops/zcyw-jenkins-shared-library |
存在,Jenkins 账号有读权限 |
slp/gitops |
存在,Jenkins 账号有 Developer 及以上(需要 push) |
slp/<业务仓库> |
存在,Jenkins 账号有读权限 |
GitOps 仓库的 template/ |
10 个模板文件齐全(见 7.1),缺一不可 |
| Webhook | (可选)http://jenkins.yunwdh.cn/project/<Job名> |
git ls-remote http://<用户名>:<密码>@gitlab.yunwdh.cn/ops/devops/zcyw-jenkins-shared-library.git
git ls-remote http://<用户名>:<密码>@gitlab.yunwdh.cn/slp/gitops.git
git ls-remote http://<用户名>:<密码>@gitlab.yunwdh.cn/slp/vue.git
14.4 Harbor
# ① 三个域名可达
curl -s http://bj-harbor.yunwdh.cn/v2/ # 期望 401
curl -s http://cq-harbor.yunwdh.cn/v2/
curl -s http://sz-harbor.yunwdh.cn/v2/
# ② dev / test / prod 三个项目已创建(Harbor UI 或 API)
# ③ 推送权限验证
docker login -u admin -p '<密码>' https://bj-harbor.yunwdh.cn
# 期望 Login Succeeded
# ④ 镜像同步方案已确定(共享存储 / 复制规则 / 统一地址)
curl -u admin:<密码> -s "http://bj-harbor.yunwdh.cn/api/v2.0/replication/policies"
14.5 ArgoCD
argocd login argocd.yunwdh.cn --username admin --insecure --grpc-web
# 添加集群
argocd cluster add <context> --name bj-dev-01 -y
argocd cluster add <context> --name cq-test-01 -y
argocd cluster add <context> --name sz-prod-01 -y
# 打标签(三选一,见 11.2)
kubectl -n argo-cd get secret -l argocd.argoproj.io/secret-type=cluster
kubectl -n argo-cd label secret <secret名> cluster-name=<集群名>
# 验证
argocd cluster list
kubectl -n argo-cd get secret -l argocd.argoproj.io/secret-type=cluster \
-o custom-columns=NAME:.metadata.name,LABELS:.metadata.labels
14.6 目标集群
# 每个集群 × 每个将部署的命名空间
kubectl -n slp-vue-demo create secret docker-registry harbor-secret \
--docker-server=<对应Harbor> --docker-username=<账号> --docker-password=<密码>
kubectl -n slp-vue-demo get secret harbor-secret
kubectl get ingressclass
kubectl get svc -A | grep -i ingress
14.7 一句话自检流程
能在 Jenkins 上跑通一次构建(6 个阶段全绿)
= 构建集群 ✅ + Jenkins 插件/云/凭证 ✅ + GitLab 权限 ✅ + Harbor 推送 ✅
+ GitOps 模板 ✅ + ArgoCD 集群/label ✅
构建成功但 Pod ImagePullBackOff
= 缺 harbor-secret,或该集群对应的 Harbor 拿不到镜像(见 12.5)
构建成功、Pod Running,但域名访问不通
= DNS 未解析 / 默认 IngressClass 缺失 / 入口地址不对(见 13.1、17.4)
第四篇 · 运维与排障
15. 首次部署操作步骤(SOP)
每步都有 ✅ 验证点,验证不过不要往下走。
步骤 1 · 准备 GitOps 仓库
git clone http://gitlab.yunwdh.cn/slp/gitops.git && cd gitops
ls template/
✅ 验证 :template/ 下 10 个模板文件齐全(见 7.1)。
步骤 2 · 准备 Harbor
curl -s http://bj-harbor.yunwdh.cn/v2/ # 期望 401
curl -s http://cq-harbor.yunwdh.cn/v2/
curl -s http://sz-harbor.yunwdh.cn/v2/
docker login -u admin -p '<密码>' https://bj-harbor.yunwdh.cn
✅ 验证 :Login Succeeded;Harbor UI 中能看到 dev / test / prod 三个项目。
步骤 3 · 准备构建集群
kubectl -n jenkins get pvc nodejs-package # Bound
ls -l /var/run/docker.sock /usr/bin/docker /usr/local/bin/argocd
docker info | grep -A3 "Insecure Registries"
✅ 验证 :PVC 为 Bound;hostPath 全部存在。
步骤 4 · 准备 Jenkins
| 子步骤 | 操作 |
|---|---|
| 4.1 | 安装插件(见 10.2) |
| 4.2 | 配置 Kubernetes 云,名称为 kubernetes ,Jenkins 地址填 http://jenkins.yunwdh.cn |
| 4.3 | 创建 3 个流水线凭证:gitlab-credentials / harbor-credentials / ArgoCD 凭证 |
| 4.4 | 启用安全认证 + 创建管理员用户 |
| 4.5 | 新建 Pipeline Job,名称 = 项目名(建在根目录) |
| 4.6 | Job 配置:Pipeline script from SCM → Git → http://gitlab.yunwdh.cn/slp/vue → 凭证 gitlab-credentials → 分支 */master → 脚本路径 Jenkinsfile |
| 4.7 | 勾选「不允许并发构建」,丢弃策略设为保留 15 次 |
| 4.8 | (可选)配置 GitLab webhook |
✅ 验证 :系统管理 → 云 测试连接成功。
步骤 5 · 准备 ArgoCD
argocd login argocd.yunwdh.cn --username admin --insecure --grpc-web
argocd cluster add <bj-dev-01的context> --name bj-dev-01
argocd cluster add <cq-test-01的context> --name cq-test-01
argocd cluster add <sz-prod-01的context> --name sz-prod-01
kubectl -n argo-cd get secret -l argocd.argoproj.io/secret-type=cluster
kubectl -n argo-cd label secret <secret名> cluster-name=<集群名>
argocd cluster list
✅ 验证 :三个集群都出现,label 与 Jenkins 参数中的集群名完全一致。
步骤 6 · 在目标集群创建 harbor-secret
kubectl --context=<目标集群> -n slp-vue-demo create secret docker-registry harbor-secret \
--docker-server=<该集群对应Harbor> --docker-username=<账号> --docker-password=<密码>
命名空间不存在时先
kubectl create namespace slp-vue-demo(或等首次部署由 ArgoCD 自动创建后立即补)。
✅ 验证 :kubectl -n slp-vue-demo get secret harbor-secret。
步骤 7 · 首次构建
Jenkins → <Job名> → Build with Parameters
Git_Branch_Name_Parameter = master
Release_Env_Type_Parameter = <目标环境>
Dest_Cluster_Name_Parameter = <目标集群>(只勾一个)
逐阶段观察日志:
| 阶段 | 期望日志 |
|---|---|
| PreRunCheck | >>>用户: xxx, 管理员权限: true<<<、>>>具有prod环境构建权限<<< |
| GitClone | commitHash: xxxxxxxx,代码获取成功 |
| ImageBuild | Login Succeeded → Successfully built → digest: sha256:... |
| SedYaml | >>>部署新服务<<< → xxxx..xxxx master -> master |
| DeployK8s | 'admin:login' logged in successfully → Sync Status: Synced to HEAD (...) → Phase: Succeeded |
| Post | >>>构建成功!<<< |
步骤 8 · 验证部署结果
# ArgoCD 侧
argocd app list | grep <项目名>
argocd app get <项目>-<环境>-<集群>-<应用> # Health Status 应为 Healthy
# 集群侧
kubectl -n argo-cd get application,applicationset | grep <项目名>
kubectl -n <项目名> get deploy,pod,svc,ingress
kubectl -n <项目名> get endpoints <应用名> # ★ 必须不是 <none>
kubectl -n <项目名> describe ingress <应用名> # 看 Rules 和 Events
# 业务侧
curl -s -o /dev/null -w "HTTP %{http_code}\n" -H "Host: <应用>-<环境>.yunwdh.cn" http://<ingress入口IP>
| 检查项 | 期望 |
|---|---|
| 父 Application | Synced / Healthy |
| 子 Application | Synced / Healthy |
| Deployment | READY 1/1 |
| Service | 存在,且 endpoints 不是 <none> |
| Ingress | 存在,CLASS 有值,Rules 指向正确的 Service:Port |
| Pod | Running,无 ImagePullBackOff |
| HTTP | 200 |
16. 接入指南:新项目 / 新环境 / 新集群
16.1 新增一个项目
第一步:业务仓库准备文件
| 文件 | 必需性 |
|---|---|
Jenkinsfile |
✅ 必需 |
package.json / pom.xml |
✅ 按技术栈 |
nginx.conf |
前端项目必需 |
Dockerfile |
可选(也可用 customDockerfile) |
Jenkinsfile 模板:
#!groovy
library(
identifier: "zcyw-jenkins-shared-library@master", retriever: modernSCM([
$class: "GitSCMSource",
remote: "http://gitlab.yunwdh.cn/ops/devops/zcyw-jenkins-shared-library.git",
credentialsId: 'gitlab-credentials'
])
)
def map = [:]
map.put('gitUrl', 'http://gitlab.yunwdh.cn/<组>/<业务仓库>')
map.put('gitCredId', 'gitlab-credentials')
map.put('imgRegCredId', 'harbor-credentials')
map.put('buildCmd', 'npm install && npm run build:env_type') // env_type 会自动替换
map.put('nodeVersion', '22')
map.put('portNumber', '80')
map.put('compOutputPath','dist')
Pipeline_GitOps_Run(map)
第二步:在 Jenkins 创建 Job
-
Job 名 = 项目名(会同时成为 K8s 命名空间、GitOps 目录名、ArgoCD Project/父 App 名)
-
建在根目录,不要分组
-
配置见 10.1
第三步:确认 Harbor 项目存在
Harbor 项目名 = 环境名(dev / test / prod),通常复用已有项目,无需为每个业务单独建。
第四步:确认 ArgoCD 有目标集群
集群必须已注册且打了 cluster-name 标签(见 11.2)。
第五步:在目标集群准备命名空间与 harbor-secret
kubectl create namespace <新项目名>
kubectl -n <新项目名> create secret docker-registry harbor-secret \
--docker-server=<对应Harbor> --docker-username=<账号> --docker-password=<密码>
命名空间也可以交给 ArgoCD 自动创建(
CreateNamespace=true),但 Secret 必须手工补。
第六步:触发首次构建
选 master / 目标环境 / 目标集群。
无需手工写任何 YAML ------ 流水线会走「部署新服务」分支,从 template/ 自动生成:
-
argocd/<项目>/<项目>-<应用>-<环境>-<集群>-applicationset.yaml -
<项目>/<应用>/base/(Deployment + Service + kustomization) -
<项目>/<应用>/overlays/<环境>/(kustomization + ingress-patch) -
<项目>/<应用>/applicationset-config/<集群>/config.json -
以及
common-k8s-resource的对应一套
16.2 新增一个环境(已有项目,加新环境)
前提:该环境对应的集群已在 ArgoCD 中注册并打好标签。
操作:直接用新环境参数触发一次构建即可。
Release_Env_Type_Parameter = <新环境>
Dest_Cluster_Name_Parameter = <新环境对应的集群>
流水线会:
-
走「部署新环境」分支 → 生成
overlays/<新环境>/ -
生成
applicationset-config/<集群>/config.json -
生成
<项目>-<应用>-<新环境>-<集群>-applicationset.yaml(兜底逻辑保证一定存在) -
同步父 App → ApplicationSet 生效 → 生成子 App → 同步部署
⚠️ 新增的环境名必须在
GetParameters.groovy的releaseEnvTypeGroovyScript里 。 当前环境列表写死了["dev","test","prod"],如果要加sit/uat,必须改共享库代码 ,并把新集群加进destClusterNameGroovyScript。
16.3 新增一个集群
| 步骤 | 操作 |
|---|---|
| 1 | 在 ArgoCD 添加集群:argocd cluster add <context> --name <集群名> |
| 2 | 给集群 Secret 打标签 :cluster-name: <集群名> |
| 3 | 在共享库 GetParameters.groovy 的 destClusterNameGroovyScript 里加上这个集群名 |
| 4 | 目标集群创建 harbor-secret(各命名空间) |
| 5 | 按需配置 Harbor 同步(若集群名以 sz/cq 开头,YAML 会指向对应 Harbor,见 12.5) |
| 6 | 在 Jenkins 用新集群参数触发一次构建 |
⚠️ 集群名前缀有语义 :
sz*→ sz-harbor,cq*→ cq-harbor,其他 → bj-harbor。 命名新集群时要注意这一点,否则 YAML 里的镜像地址会和预期不符。
17. 排障手册
17.1 按阶段定位
| 阶段 | 报错关键字 | 原因 | 处理 |
|---|---|---|---|
| 全程 | Pod 一直 Pending / is offline |
构建集群资源不足 / 无匹配节点 / hostPath 不存在 / Jenkins 地址配错 | kubectl -n jenkins describe pod <pod> 看 Events;检查 14.2 的路径 |
| 全程 | No such DSL method 'xxx' |
Jenkins 缺插件 | 对照 10.2 装插件 |
| PreRunCheck | 权限错误: prod 环境要求可识别的 Jenkins 管理员,但 Jenkins 中未找到用户: anonymous |
Jenkins 未启用安全认证 | 启用安全域 + 授权策略,用管理员账号构建(见 10.5) |
| PreRunCheck | 错误: 只允许选择一个目标集群 |
集群参数勾了多个 | 只勾一个 |
| PreRunCheck | 错误: 未勾选发布的目标集群 |
没勾集群 | 勾一个 |
| GitClone | Couldn't find any revision to build |
分支不存在或凭证无权限 | 检查分支名与 gitlab-credentials 权限 |
| GitClone | Authentication failed |
凭证密码过期 | 更新 Jenkins 凭证 |
| ImageBuild | docker login failed + connection refused |
Harbor 服务不可达 | curl http://<harbor>/v2/ 验证;找运维 |
| ImageBuild | Login Succeeded 后 denied: requested access to the resource is denied |
Harbor 项目不存在 / 账号无 push 权限 | 创建 dev/test/prod 项目;检查账号权限 |
| ImageBuild | http: server gave HTTP response to HTTPS client |
Docker 未配 insecure-registries | 配 /etc/docker/daemon.json 后重启 Docker |
| ImageBuild | MissingMethodException: No signature of method ... goBuild |
Go / gcc / php / 兜底项目的构建方法未实现 | 改用 dockerFile 或 customDockerfile |
| ImageBuild | COPY failed: file not found ... dist |
compOutputPath 写错或有 ./ 前缀 |
改成 dist(不带 ./) |
| SedYaml | sed: no input files |
模板里占位符被写死,grep 匹配为空 |
恢复模板里的占位符(见 7.2 的警告) |
| SedYaml | git push ... 403 |
gitlab-credentials 对 slp/gitops 无写权限 |
提升为 Developer 及以上 |
| SedYaml | ! [rejected] ... (fetch first) |
并发构建导致 GitOps 仓库冲突 | Job 勾选「不允许并发构建」 |
| DeployK8s | permission denied |
ArgoCD 凭证权限不足 / app 名查询不到 | 检查 ArgoCD 账号权限;对照 17.2 的干扰项 |
| DeployK8s | application is deleting(重试多次) |
ArgoCD 里有卡在删除的 Application | 见 17.3 |
| DeployK8s | FailedPrecondition desc = application is deleting |
同上 | 同上 |
| DeployK8s | cluster "https://...:6443" is not accessible |
目标集群不可达 | curl -k https://<ip>:6443/version |
| DeployK8s | 子 App 一直不生成 | 集群 label 与参数名不匹配 | kubectl -n argo-cd get secret -l argocd.argoproj.io/secret-type=cluster -o yaml 检查 cluster-name |
| DeployK8s | ComparisonError / kustomize 报错 |
kustomization.yaml 有非法字段 |
见 17.5 |
| 部署后 | Pod ImagePullBackOff |
命名空间缺 harbor-secret,或镜像没推到 YAML 指向的 Harbor |
建 Secret;或配 Harbor 同步(见 12.5、12.6) |
| 部署后 | 域名访问 404 / 502 / 503 | Ingress 未创建 / 无默认 IngressClass / Service 无 endpoints | 见 17.4 |
17.2 按现象定位(学习用)
现象:Jenkins 日志里出现 PermissionDenied desc = permission denied
先在日志里找这一行:
+ argocd app get slp-vue-demo-prod-slp-vue-demo | grep Server | awk '{print $2}'
time="..." level=fatal msg="rpc error: code = PermissionDenied desc = permission denied"
判断方法 :看
argocd app get后面的 app 名 。 如果名字是<项目>-<环境>-<应用>(只有三段,缺少集群段 ),那这是已知的残留查询 (共享库已修复,见 18.3), 它不影响构建结果,只是日志噪音 ------ 修好后不会再出现。 如果名字是完整四段,那才是真的权限/app 不存在问题。
现象:构建成功但 ArgoCD 里子是 Unknown 状态
application.argoproj.io/slp-vue-demo-test-cq-test-01-slp-vue-demo Unknown Healthy
SYNC STATUS |
含义 |
|---|---|
Synced |
已同步 ✅ |
OutOfSync |
有差异,等待自动同步或手工 sync |
Unknown |
还没完成对比 。常见原因:刚创建、集群刚接入、或对比报错(看 argocd app get 的 Conditions) |
argocd app get <app名> # 看 Conditions 有没有 ComparisonError
现象:Health Status: Progressing 但构建已成功
这是正常的 ------ 同步成功返回时 Deployment 可能还在滚动更新。
argocd app get <app名> # 稍后再看是否为 Healthy
kubectl -n <ns> get pod -w # 观察 Pod 状态
kubectl -n <ns> describe pod <pod> | tail -30 # 看 Events
17.3 Application 卡在 application is deleting
判断
kubectl -n argo-cd get application <名字> \
-o jsonpath='{.metadata.deletionTimestamp}{"\n"}{.metadata.finalizers}{"\n"}'
# 期望:第一行有时间戳(非空),第二行含 resources-finalizer.argocd.argoproj.io
kubectl -n argo-cd get applications -o custom-columns=NAME:.metadata.name,DELETING:.metadata.deletionTimestamp
kubectl -n argo-cd get applicationsets -o custom-columns=NAME:.metadata.name,DELETING:.metadata.deletionTimestamp
常见原因
目标集群不可达 → 子 App 无法删除目标集群里的资源 → 整条级联删除链冻结。
级联链长这样:
父 Application
└─ ApplicationSet
└─ 子 Application(要连目标集群删资源) ← 卡在这里,上面全冻住
解除(自下而上摘 finalizer)
# ① 子 Application
kubectl -n argo-cd patch application <子App名> --type=merge -p '{"metadata":{"finalizers":null}}'
# ② ApplicationSet
kubectl -n argo-cd patch applicationset <名> --type=merge -p '{"metadata":{"finalizers":null}}'
# ③ 父 Application
kubectl -n argo-cd patch application <父App名> --type=merge -p '{"metadata":{"finalizers":null}}'
⚠️ 摘 finalizer 会跳过资源清理 ,目标集群里的资源变成"孤儿",需另行手工清理。 ⚠️
AppProject不要删。
验证清空
kubectl -n argo-cd get application,applicationset | grep <项目名> # 期望无输出
kubectl -n argo-cd get appproject <项目名> # 期望有一行
实操经验 :摘掉最底层的 finalizer 后,上层会自动 走完删除。所以后面的命令可能报
NotFound------ 那是好事,说明链条解开了。
预防
| 措施 | 说明 |
|---|---|
| 集群保持可达 | 目标集群挂了就必然卡删除 |
| 不在部署期间删 Application | 删除会触发级联清理,风险高 |
17.4 域名访问不通
按顺序排查:
# ① Ingress 是否存在、CLASS 是否有值(以 test 环境为例)
kubectl -n slp-vue-demo get ingress
kubectl -n slp-vue-demo describe ingress slp-vue-demo
# ② Service 是否有 endpoints(没有就会 503)
kubectl -n slp-vue-demo get endpoints slp-vue-demo
# ③ Pod 是否 Ready
kubectl -n slp-vue-demo get pod
# ④ 集群是否有默认 IngressClass
kubectl get ingressclass
# ⑤ 绕过 DNS,直接用 Host 头打入口(cq-test-01 的入口实测为 192.168.10.111)
curl -s -H "Host: slp-vue-demo-test.yunwdh.cn" http://192.168.10.111
| 现象 | 原因 | 处理 |
|---|---|---|
curl 立即返回空 |
DNS 解析失败 | nslookup slp-vue-demo-test.yunwdh.cn;找网管加记录 |
curl 挂住(需 Ctrl-C) |
DNS 解析到了不通的地址 | curl -v -m 5 slp-vue-demo-test.yunwdh.cn 看 Trying <IP>;修正 DNS |
HTTP 404 |
Ingress 规则没匹配上 | 检查 Host 拼写;检查 CLASS |
HTTP 503 |
Service 没有 endpoints | 检查 Service 的 selector 与 Pod 标签是否一致 |
HTTP 502 |
后端端口不对 / 应用未监听 | 检查 Service targetPort 与容器 containerPort |
| 用 Host 头打 IP 通、域名不通 | 纯 DNS 问题 | 加 DNS 记录(推荐泛解析,见 13.2) |
实测参考(cq-test-01):
kubectl -n slp-vue-demo get svc,pod,ingress # service/slp-vue-demo ClusterIP 10.96.3.171 80/TCP # ingress CLASS=nginx HOSTS=slp-vue-demo-test.yunwdh.cn kubectl -n slp-vue-demo get endpoints slp-vue-demo # ENDPOINTS 100.102.164.248:80 ← 有值就对了 curl -s -H "Host: slp-vue-demo-test.yunwdh.cn" http://192.168.10.111 | grep app123 # <div id="app123"></div> ← 成功
17.5 kustomize build / ComparisonError
kustomize 对 kustomization.yaml 启用了 DisallowUnknownFields() ------ 写了不存在的字段会直接解析失败。
常见错误:
error: invalid Kustomization: json: unknown field "imagePullSecrets"
原因 :imagePullSecrets 不是 kustomization.yaml 的合法顶层字段,它属于 Deployment 的 spec.template.spec。
# ✅ 对:放在 Deployment 里
spec:
template:
spec:
imagePullSecrets:
- name: harbor-secret
# ❌ 错:放在 kustomization.yaml 顶层
kind: Kustomization
namespace: xxx
imagePullSecrets: # ← 非法字段,会报错
- name: harbor-secret
其它常见 kustomize 错误:
| 报错 | 原因 |
|---|---|
no such file or directory |
resources 里写的文件路径不存在 |
cycle detected |
overlay 与 base 互相引用 |
field imageTagNameEnv not found |
占位符没被 sed 替换(模板占位符写错或 sed 漏了) |
17.6 GitOps 仓库状态异常
# 本地与线上比对
git ls-remote http://gitlab.yunwdh.cn/slp/gitops.git refs/heads/master
# 看目录结构
git -C <克隆目录> ls-tree -r --name-only HEAD
| 现象 | 说明 | 处理 |
|---|---|---|
| 每次构建后提交数 +1 | 完全正常(流水线自动提交) | 无需处理 |
业务目录只剩 template/ |
有人执行过 reset | 下次构建会走「部署新服务」完整重建 |
template/ 不见了 |
严重:脚手架源头丢了 | 立刻从历史恢复(见下) |
| 同一个应用出现两套 ApplicationSet | 旧命名(三段)未清理 | 删掉旧的那份,避免两个 ApplicationSet 争抢同一 Application |
恢复被误删的模板:
git log --diff-filter=D --oneline -- template/ # 找到删除模板的提交
git checkout <删除前的提交> -- template/ # 恢复
git commit -m "revert: 恢复 template 目录"
git push
17.7 排查工具箱
# ========== Jenkins / 构建集群 ==========
kubectl -n jenkins get pod # Agent Pod 状态(名字形如 slp-vue-demo-47-xxxxx)
kubectl -n jenkins describe pod slp-vue-demo-47-xxxxx # Pod 启动失败原因
kubectl -n jenkins logs slp-vue-demo-47-xxxxx -c ci-build # 指定容器日志
# ========== GitOps 仓库 ==========
cd /tmp/gitops && git log --oneline -10
git diff HEAD~1 HEAD # 上一次构建改了什么
# ========== ArgoCD(以 test / cq-test-01 为例)==========
argocd app list | grep slp-vue-demo
argocd app get slp-vue-demo-test-cq-test-01-slp-vue-demo # Sync/Health、Conditions
argocd app diff slp-vue-demo-test-cq-test-01-slp-vue-demo # 期望 vs 实际的差异 ★ 很好用
argocd app manifests slp-vue-demo-test-cq-test-01-slp-vue-demo # 查看将 apply 的清单 ★ 很好用
argocd app history slp-vue-demo-test-cq-test-01-slp-vue-demo # 部署历史
argocd cluster list # 集群可达性
# ========== 目标集群(以 test / cq-test-01 为例)==========
kubectl -n slp-vue-demo get deploy,pod,svc,ingress,endpoints
kubectl -n slp-vue-demo describe pod slp-vue-demo-7fd9785c8d-k2wws | tail -30
kubectl -n slp-vue-demo logs deploy/slp-vue-demo --tail=50
kubectl -n slp-vue-demo get events --sort-by=.lastTimestamp | tail -20
# ========== Harbor ==========
curl -s http://bj-harbor.yunwdh.cn/v2/ # 401 = 活着
curl -u admin:'<密码>' -s "http://bj-harbor.yunwdh.cn/api/v2.0/projects/prod/repositories"
docker manifest inspect sz-harbor.yunwdh.cn/prod/slp-vue-demo/slp-vue-demo:202609200731-46faaaa4-v47
18. 已知隐患与改进建议
按严重程度 排序。前 4 条建议优先处理。 标注 ✅ 的表示已修复。
18.1 隐患清单
| # | 严重度 | 位置 | 问题 | 影响 | 建议 |
|---|---|---|---|---|---|
| 1 | 🔴 高 | DockerBuild.groovy |
goBuild / gccBuild / phpBuild / netBuild 完全没有实现,但流水线会调用它们 |
Go / gcc / php / 兜底项目必然抛 MissingMethodException |
补齐方法,或改为报出明确错误;Go 项目暂用 customDockerfile |
| 2 | 🟠 中 | Pod-CICD-Base-Build-Env.yaml |
ci-build 容器 privileged: true + 挂载 docker.sock + init 容器 chmod 777 |
容器逃逸风险,等于把宿主机 root 交给构建任务 | 改用 Kaniko / BuildKit rootless / 独立 DinD;限制触发构建的人员范围 |
| 3 | 🟠 中 | Pipeline_GitOps_Run.groovy |
隐性依赖 :推送固定 bj-harbor,拉取按集群前缀路由(sz*→sz-harbor、cq*→cq-harbor) |
这本身是合理的"中心推送 + 边缘拉取"架构 ,但前提是 bj → cq/sz 存在共享存储或复制规则 ,而该前提在代码/仓库中无任何声明。风险:新增集群静默不同步、prod 跨地域依赖 bj-harbor、排查时容易去错误的 Harbor 找镜像 | 保留架构 ,但把它显式化:① 确认是共享存储还是复制规则;② 对复制任务加监控告警;③ 新增 sz*/cq* 集群时纳入接入检查清单。仅当在 cq/sz 也部署 Jenkins 构建集群时才需改代码 |
| 4 | 🟠 中 | Pipeline_GitOps_Run.groovy:46 |
awk -F'/' '{print $NR}' 取的是第一段而非最后一段 |
Job 名带 / 分组时 appNamespace 取错 → 命名空间/目录名全错 |
改成 awk -F'/' '{print $NF}';或强制 Job 建在根目录(当前做法) |
| 5 | 🟠 中 | GetParameters.groovy |
环境列表 dev/release/master/other 四个分支返回完全相同的列表 |
级联"限制"形同虚设,任何分支都能选 prod | 按分支返回不同列表,或明确注释"仅靠权限门控制" |
| 6 | 🟠 中 | GetParameters.groovy |
Fallback Script 里的集群名(cd-gx-az01-biz-priv-dev-01)与正式脚本(bj-dev-01)完全不一致 |
正式脚本异常时才暴露,排查困难 | 统一为一致的集群名 |
| 7 | 🟠 中 | GetParameters.groovy |
Dest_Cluster_Name_Parameter 声明 PT_CHECKBOX(多选),却用报错强制单选 |
交互与校验矛盾 | 改为 PT_SINGLE_SELECT |
| 8 | 🟠 中 | SedYaml.groovy 推送段 |
git push http://${GIT_USERNAME}:${GIT_PASSWORD}@... 把凭证拼进 URL |
会出现在进程参数 / git remote 里(Jenkins 会打码但仍是隐患) |
改用凭据 helper 或 GIT_ASKPASS |
| 9 | 🟡 低 | Deploy.groovy |
sonApplicationServer != destCluster 比较的是集群 server URL 与集群名 ,恒为真 |
判断失效(当前行为恰好是想要的完整流程,无实际故障) | 改为对比 URL,或直接去掉该条件 |
| 10 | 🟡 低 | gitops applicationset-config/*/config.json |
文件被生成,但 ApplicationSet 模板并未引用 | 冗余;易让人误以为改了它就生效 | 明确用途(如供外部平台读取)或移除生成逻辑 |
| 11 | 🟡 低 | slp/vue: index.html |
没有 <script> 标签 去加载 main.js |
App.vue / main.js 永远不执行,页面空白 |
补 <script type="module" src="/src/main.js"></script> |
| 12 | 🟡 低 | gitops template/kustomize/overlays/env/ingress-patch.yaml |
Ingress 没有 ingressClassName 也没有 annotations |
依赖集群存在默认 IngressClass;集群没配就访问不通 | 显式补 ingressClassName: nginx(或多集群场景改用 patches) |
| 13 | 🟡 低 | 共享库重试配置 | SedYaml retry(8)、GitClone retry(6) 偏多 |
确定性错误下白等(DeployK8s 已从 10 次降到 2 次 × 10 秒) |
按需下调 |
| 14 | 🟡 低 | Common.groovy |
大量变量(projectName、Release_Env_Type_Parameter、BUILD_USER_ID)依赖 Pipeline 全局绑定隐式传入 Groovy 类 |
耦合强、IDE 无法静态检查,改错变量名会静默失效 | 改为显式传参,或集中定义常量类 |
| 15 | 🟡 低 | PreRunCheck.groovy |
allowProdWithoutUserIdentity 开关可绕过 prod 权限管控 |
误开启后任何人都能发生产 | 仅在内网确认无需认证时临时使用;启用认证后务必删除该行 |
18.2 已修复项(记录在案)
| 项 | 原问题 | 修复方式 | 提交 |
|---|---|---|---|
| ✅ A | common-k8s-resource 的 overlay 模板把 namespace 写死为 common-k8s-resource(而非占位符 serviceNameEnv),导致 grep 空匹配 → sed: no input files,首次往已有项目加新环境必然失败 |
恢复占位符 serviceNameEnv(替换结果不变,不影响已有环境) |
gitops 7444ddf |
| ✅ B | template/kustomize/overlays/env/kustomization.yaml 未引用 ingress-patch.yaml → Ingress 从未被渲染创建 |
overlay 的 resources 增加 ingress-patch.yaml |
gitops 61ed55b |
| ✅ C | template/kustomize/base/ 缺少 Service 资源 → Ingress 的后端 Service 不存在 |
新增 base/service.yaml 并在 base/kustomization.yaml 引用 |
gitops 61ed55b |
| ✅ D | d44880f 曾把 imagePullSecrets 写在 kustomization.yaml 顶层 (非法字段,kustomize 会报 unknown field) |
已改到 Deployment 的 spec.template.spec.imagePullSecrets |
gitops(后续提交) |
| ✅ E | Deploy.groovy 查询子 Application 时缺少集群段 ,导致每次构建固定刷出 PermissionDenied |
补上 ${params.Dest_Cluster_Name_Parameter} |
共享库 8f5ec95 |
| ✅ F | 权限门在"无法识别构建用户"时打印 null(颜色 map 缺 yellow),用户看不到原因 |
补 yellow 颜色 + handleUnidentifiedUser() 统一处理并给出解决办法 |
共享库 04ec55e、d6dd630 |
| ✅ G | 一个应用只能绑定一个集群(ApplicationSet 不按环境区分),换环境就失败 | ApplicationSet 改为按「应用+环境+集群」命名 + 独立的兜底校验 | 共享库 04ec55e、7b924be |
| ✅ H | Agent retries 导致阶段失败时整条流水线重跑、阶段视图出现空列 |
移除 agent retries,DeployK8s 重试降为 2 次 × 10 秒 |
共享库 67e98df |
18.3 关于「日志里出现 PermissionDenied」
判断方法 :看 argocd app get 后面的 app 名。
| app 名形态 | 结论 |
|---|---|
<项目>-<环境>-<应用>(三段,缺集群段) |
已知的历史残留查询,不影响构建,已修复(见 18.2 E) |
<项目>-<环境>-<集群>-<应用>(完整四段) |
真的有问题:检查 ArgoCD 账号权限,或该 app 是否正在删除 |
18.4 建议的处理优先级
| 优先级 | 处理项 | 说明 |
|---|---|---|
| P0 功能正确性 | #1 补齐构建方法、#12 显式 IngressClass | #1 影响 Go/php 等项目能否构建;#12 影响域名能否访问 |
| P1 可用性 | #4 awk 修正、#5/#6/#7 参数脚本整理 |
不影响当前运行,但埋着坑 |
| P2 安全加固 | #2 Pod 特权降级、#8 凭证不进 URL、#15 权限门 | 安全相关,建议排期 |
| P3 整洁性 | #3 显式化 Harbor 依赖、#9~#11、#13~#14 | 可读性与可维护性 |
附录
附录 A · 术语表
| 术语 | 全称 / 英文 | 一句话解释 |
|---|---|---|
| CI | Continuous Integration | 持续集成:代码提交后自动构建、测试、产出制品 |
| CD | Continuous Delivery / Deployment | 持续交付/部署:把制品自动部署到环境 |
| Jenkinsfile | --- | 放在业务仓库根目录的流水线入口脚本,声明 library(...) + map |
| 共享库 | Shared Library | 把流水线的公共逻辑抽到独立 Git 仓库,供所有项目复用 |
vars/ |
--- | 共享库目录,文件名 = Jenkins 全局方法名 (如 vars/MyStep.groovy → MyStep()) |
src/ |
--- | 共享库目录,普通 Groovy 类,需 new org.devops.XXX() 使用 |
call(Map) |
--- | 共享库的隐式调用入口,所以能写 Pipeline_GitOps_Run(map) |
map |
--- | 业务仓库传给共享库的参数集合,声明"差异" |
properties() |
--- | Pipeline 步骤,把参数定义持久化写入 Job 配置 |
podTemplate / kubernetes |
--- | Jenkins agent 类型:在 K8s 上动态创建 Pod 跑构建 |
jnlp |
Jenkins Remoting Agent | Pod 里回连 Jenkins 的容器,名称是插件写死的 |
emptyDir |
--- | Pod 内容器共享的临时卷,Pod 删除即消失 |
hostPath |
--- | 把宿主机上的文件/目录挂进容器 |
| PVC | PersistentVolumeClaim | K8s 存储声明,这里用来存放预装的 Node 运行时 |
| RWX | ReadWriteMany | 多节点同时读写,多节点并发构建的 PVC 必须用它 |
| Kustomize | --- | K8s 原生的配置定制工具,用 base + overlay 避免 YAML 重复 |
kustomization.yaml |
--- | Kustomize 的入口文件,会严格校验字段,写错字段直接报错 |
| base | --- | Kustomize 公共部分(所有环境都用) |
| overlay | --- | Kustomize 环境差异部分(引用 base + 写差异) |
images 替换器 |
--- | Kustomize 功能:把 base 里的锚点名替换成真实镜像地址 + tag |
| GitOps | --- | 用 Git 仓库作为"集群期望状态"的单一事实来源,由工具自动对齐 |
| Application | ArgoCD Application | 一条部署指令:仓库+路径 → 集群+命名空间 |
| ApplicationSet | ArgoCD ApplicationSet | Application 生成器,靠集群标签批量生成 |
| AppProject | ArgoCD AppProject | 权限边界:限定可用的仓库/集群/命名空间 |
| cluster 生成器 | clusters generator | ApplicationSet 的一种生成器,按集群 Secret 的标签筛选集群 |
| cluster Secret | --- | ArgoCD 里代表一个被管理集群的 Secret,argocd cluster add 会创建它 |
selfHeal |
--- | ArgoCD 自动纠偏:集群里被手工改动会被改回 Git 里的状态 |
prune |
--- | ArgoCD 自动清理:Git 里删掉的资源,同步时也从集群删掉 |
targetRevision: HEAD |
--- | ArgoCD 跟踪仓库的最新提交 |
| Harbor | --- | 私有 Docker 镜像仓库;本项目里"项目名 = 环境名" |
| Robot Account | --- | Harbor 机器人账号,可限定项目与权限、可轮换 |
insecure-registries |
--- | Docker 配置项,允许用 HTTP 访问镜像仓库 |
harbor-secret |
--- | K8s 里 kubernetes.io/dockerconfigjson 类型的 Secret,供 Pod 拉镜像 |
imagePullSecrets |
--- | Deployment 字段,指定用哪个 Secret 拉私有仓库镜像 |
| Ingress | --- | K8s 的七层入口规则:域名 + 路径 → Service |
| IngressClass | --- | 指定由哪个 Ingress 控制器处理;集群最好配一个默认的 |
| ClusterIP | --- | Service 类型:只在集群内部可访问 |
| NodePort | --- | Service 类型:在每个节点上开一个高位端口暴露服务 |
| Endpoints | --- | Service 背后的真实 Pod 地址列表;为空说明 selector 没匹配上 |
| commit8 | --- | 业务仓库 commit 哈希的前 8 位,写进镜像 tag 用于溯源 |
附录 B · 学习路径与动手实验
B.1 建议学习路径(约 1~2 天)
| 阶段 | 目标 | 看哪些章节 | 产出 |
|---|---|---|---|
| 第 1 步 | 建立整体印象 | 第 1 章(问题背景)→ 第 1.4 节(一张图) | 能说出"Jenkins/ArgoCD/GitLab/Harbor 各管什么" |
| 第 2 步 | 理解五个核心概念 | 第 2 章 | 能解释 GitOps、Kustomize、ApplicationSet 分别解决什么问题 |
| 第 3 步 | 跟着一次构建走一遍 | 第 3 章(对照真实构建日志) | 能说出 6 个阶段各做什么 |
| 第 4 步 | 分清"谁在哪配" | 第 9 章(配置归属) | 知道哪些要手工、哪些是代码生成 |
| 第 5 步 | 读代码 | 第 6、7、8 章 | 能定位"改某个行为要去哪个文件" |
| 第 6 步 | 动手 | 附录 B.2 实验 | 亲眼看到流水线的行为 |
B.2 动手实验(每个实验 5~15 分钟)
建议在 dev / bj-dev-01 环境做,不影响生产。
实验 1 · 观察"改代码 → 新镜像 → 新 Pod"的完整链路
# ① 改业务仓库的 index.html(改 title 或 div 内容,肉眼可见)
# ② 提交推送
# ③ Jenkins 触发构建
# ④ 观察镜像 tag 变了(commit 段跟着变)
# ⑤ 观察 ArgoCD 子 App 的 Sync Revision 变了
# ⑥ 观察目标集群的 Pod 被重建
学会:镜像 tag 的三个组成(日期 / commit8 / BUILD_ID)、ArgoCD 如何跟着 GitOps 仓库走。
实验 2 · 用 argocd app diff / manifests 看"期望 vs 实际"
argocd app diff <子App名> # 差异
argocd app manifests <子App名> # 将 apply 的完整清单(能看到 kustomize 渲染结果)
学会:Kustomize 的 base + overlay 是怎么合成最终 YAML 的。
实验 3 · 验证 selfHeal(自动纠偏)
# 在目标集群手工改副本数(ArgoCD 会把 Deployment 改回去)
kubectl -n <ns> scale deploy <应用名> --replicas=3
kubectl -n <ns> get deploy <应用名> -w # 观察副本数被改回 1
学会:GitOps 的"Git 是唯一事实来源"到底意味着什么。
实验 4 · 验证 CreateNamespace=true
kubectl delete ns <某个测试命名空间> # 删掉命名空间
argocd app sync <子App名> # 手工同步
kubectl get ns # 命名空间被自动重建
学会 :ApplicationSet 里的 syncOptions: [CreateNamespace=true] 的作用。
实验 5 · 亲手制造一个 kustomize 错误
# 在 gitops 仓库某个 overlay 的 kustomization.yaml 顶层加一行:
imagePullSecrets:
- name: harbor-secret
# 推送,然后在 ArgoCD UI / argocd app get 里看 ComparisonError
学会:kustomize 的严格字段校验;以及"错在 GitOps 仓库,报错在 ArgoCD"的排查路径。
实验 6 · 观察「三种生成场景」的日志差异
| 操作 | 会走哪个分支 | 日志 |
|---|---|---|
| 新建一个项目 + 首次构建 | 部署新服务 | >>>部署新服务<<< |
| 已有项目,换一个新环境构建 | 部署新环境 | >>>部署新环境<<< |
| 已有项目和环境,重新构建 | 服务更新 | >>>已部署过服务或环境,执行服务更新镜像<<< |
学会 :为什么改了 template/ 后已有环境不会自动更新。
实验 7 · 本地渲染 Kustomize
# 需要本机有 kubectl 或 kustomize
kubectl kustomize /tmp/gitops/slp-vue-demo/slp-vue-demo/overlays/test
学会:ArgoCD 在"对比"阶段到底做了什么。
实验 8 · 读一次完整的构建日志
# 从 Jenkins 打开一次成功的构建 → Console Output
# 对照第 3.2 节的时间线表,逐个标记 6 个阶段
学会:日志 → 阶段 → 代码位置的对应关系,这是排障的基本功。
附录 C · Jenkins 凭证与账号速查
地址、仓库、集群、域名等真实值统一见开头的 「本项目实例」 章节,此处不再重复;本附录只列 Jenkins 凭证。
C.1 Jenkins 凭证
| 凭证 ID | 用户名 | 用途 |
|---|---|---|
gitlab-credentials |
slp |
拉代码 + 推 GitOps 仓库 + 加载共享库 |
harbor-credentials |
admin |
Harbor 登录 / 推拉镜像 |
0ee5a2bf-ba0d-41c7-84af-d07475cb7eb2 |
admin |
ArgoCD 操作(代码里的默认值) |
代码里三个凭证的兜底 UUID :
gitCredId=a3dea81a-e05d-47db-827e-e4effb10cd6f|imgRegCredId=54b99cbd-eb0e-4602-bd72-eb70b9c28454|argocdCerdId=0ee5a2bf-ba0d-41c7-84af-d07475cb7eb2
附录 D · 部署前一页纸检查清单
【K8s 构建集群】
[ ] 1. jenkins 命名空间存在
[ ] 2. PVC nodejs-package = Bound(RWX 20Gi)
[ ] 3. 构建节点有 /usr/local/bin/argocd、/usr/bin/docker、/var/run/docker.sock
[ ] 4. 构建节点有 /root/.m2、/root/go/pkg
[ ] 5. 构建节点 Docker 配了 insecure-registries(三个 harbor)
【Jenkins】
[ ] 6. 必需插件齐全(Kubernetes / Git Parameter / Active Choices / AnsiColor /
Build User Vars / Docker Pipeline / Credentials Binding / Timestamper ...)
[ ] 7. Kubernetes 云名称 = kubernetes,Jenkins 地址 = http://jenkins.yunwdh.cn
[ ] 8. 凭证三个就绪:gitlab-credentials / harbor-credentials / ArgoCD 凭证
[ ] 9. 已启用安全认证(安全域 + 授权策略)
[ ] 10. 有管理员用户
[ ] 11. Job 建在根目录,名称 = 项目名
[ ] 12. Job 的 SCM 指向业务仓库,脚本路径 = Jenkinsfile,分支 = */master
[ ] 13. 勾选「不允许并发构建」
【GitLab】
[ ] 14. 三个仓库存在
[ ] 15. Jenkins 账号对 slp/gitops 有 Developer 及以上权限(需要 push)
[ ] 16. GitOps 仓库 template/ 下 10 个模板文件齐全
[ ] 17. (可选)Webhook 指向 http://jenkins.yunwdh.cn/project/slp-vue-demo
【Harbor】
[ ] 18. 三个 Harbor 域名可达(curl /v2/ 返回 401)
[ ] 19. dev / test / prod 三个项目已创建
[ ] 20. 推送账号有 push 权限(docker login + push 验证通过)
[ ] 21. 镜像同步方案已确定并生效(共享存储 / 复制规则 / 统一地址)
【ArgoCD】
[ ] 22. 安装在 argo-cd 命名空间
[ ] 23. 三个集群已添加
[ ] 24. 集群 Secret 上的 cluster-name 标签与集群名一致
[ ] 25. ArgoCD 能访问 http://gitlab.yunwdh.cn/slp/gitops.git
【目标集群】
[ ] 26. 每个命名空间有 harbor-secret(docker-registry 类型)
[ ] 27. 有默认 IngressClass + Ingress Controller 在跑
【DNS】
[ ] 28. gitlab / jenkins / argocd / {bj,cq,sz}-harbor .yunwdh.cn 全部可解析
[ ] 29. slp-vue-demo-<环境>.yunwdh.cn 解析到对应集群的 ingress 入口
附录 E · 变更操作手册
"我想改 X,该去哪改?"
| 我想改什么 | 去哪儿改 | 生效时机 | 影响面 |
|---|---|---|---|
| 编译命令、端口、Node 版本、产物目录 | 业务仓库 Jenkinsfile 的 map |
下次构建 | 仅该项目 |
| 用自带/内联 Dockerfile | 业务仓库 Jenkinsfile 加 dockerFile / customDockerfile |
下次构建 | 仅该项目 |
| 环境 / 集群的可选项 | 共享库 GetParameters.groovy |
下次构建 | 所有项目 |
| 分支与环境的级联规则 | 共享库 GetParameters.groovy |
下次构建 | 所有项目 |
| prod 权限门规则 | 共享库 PreRunCheck.groovy |
下次构建 | 所有项目 |
| 镜像怎么打 / 基础镜像版本 | 共享库 DockerBuild.groovy |
下次构建 | 所有项目 |
| 镜像推到哪个 Harbor | 共享库 Pipeline_GitOps_Run.groovy 的 registryServer |
下次构建 | 所有项目 |
| 新项目生成什么 YAML | gitops template/(配合共享库 SedYaml.groovy) |
仅对"新服务/新环境"生效;已有环境需直接改生成文件 | 新接入项目 |
| 已存在应用的集群里长什么样 | gitops 仓库 里已生成的 base/ / overlays/ |
ArgoCD 自动同步(3 分钟内) | 该应用的该环境 |
| ArgoCD 怎么建/同步应用 | 共享库 Deploy.groovy |
下次构建 | 所有项目 |
| 谁能发生产 / 怎么触发 / Job 名 | Jenkins 页面 | 立即 | 该项目 |
| 集群注册与标签 | ArgoCD(argocd cluster add / kubectl label secret) |
立即 | 该集群 |
| Harbor 项目与账号 | Harbor 页面 | 立即 | --- |
| 各命名空间的拉取凭证 | 目标集群 kubectl create secret |
立即 | 该命名空间 |
| 业务域名解析 | DNS 服务 | 生效时间看 TTL | 该域名 |
E.1 一条铁律
改
template/≠ 已有环境会变。 模板只在「部署新服务」/「部署新环境」两个分支被复制。 要让已有应用生效,必须直接改 gitops 里已生成的文件(这次修 Service / Ingress 就是这么做的)。
E.2 改完怎么验证
| 改动位置 | 验证方式 |
|---|---|
map / Jenkinsfile |
触发一次构建,看日志里的 buildCmd is : |
| 共享库 | 触发一次构建(共享库 @master 每次都会拉最新) |
gitops template/ |
新建一个测试项目走一次「部署新服务」验证 |
| gitops 已生成文件 | argocd app diff <子App名>;或 argocd app manifests <子App名> |
| Jenkins 页面 | 立即生效 |
| ArgoCD 集群标签 | kubectl -n argo-cd get secret -l argocd.argoproj.io/secret-type=cluster -o custom-columns=NAME:.metadata.name,LABELS:.metadata.labels |
E.3 回滚怎么做
| 场景 | 操作 |
|---|---|
| 回滚业务代码 | 业务仓库 git revert → 重建 → 新镜像 tag |
| 回滚部署配置 | gitops 仓库 git revert <提交> → push → ArgoCD 自动同步 |
| 快速回滚镜像版本 | 直接改 gitops 里 overlays/<环境>/kustomization.yaml 的 newTag 为旧 tag → push |
| 回滚 ArgoCD 对象 | 用 argocd app history <app> 查看历史,或 git revert GitOps 仓库 |
回滚的优势正是 GitOps 的核心价值 :所有变更都是 Git 提交,
git revert就等于回滚部署。
附录 F · 后续需要完善的功能与存在的不足
集中列出这套流水线还需要完善的功能 、存在的不足 ,以及本文档尚未覆盖的内容,便于后续排期。
F.1 本文档尚未覆盖的内容
| # | 缺口 | 说明 | 建议 |
|---|---|---|---|
| 1 | 「一个项目多个应用」的场景 | 当前 appNamespace = JOB_NAME 的第一段,所以 1 个 Jenkins Job = 1 个项目 = 1 个应用(同时也是 K8s 命名空间) 。若想让一个项目下挂多个应用(如 web + api 共用一个命名空间),需要改共享库(用 map 显式传 projectName)。另外 Job 若建在分组目录下会因 awk 的坑(隐患 #4)取错名字 |
补充一章"多应用共享项目"的设计方案 |
| 2 | Jenkins/ArgoCD/Harbor 的从零安装步骤 | 本文档假定这三个组件已经就绪,只讲了"配置与对接" | 单独写一份《基础组件安装手册》 |
| 3 | 构建日志的逐行注释版 | 第 3.2 节只给了"关键字 → 阶段"的对应,没有把一份完整日志贴出来逐行讲解 | 附一份成功构建的完整日志 + 行内注释 |
| 4 | 告警与监控方案 | 完全没有涉及(构建失败通知、Harbor 复制失败告警、ArgoCD 应用不健康告警) | 接入 Jenkins 邮件/企微通知 + ArgoCD Notifications + Harbor 复制告警 |
| 5 | Secret 管理的进阶方案 | 现在 harbor-secret 靠手工创建,Secret 无法安全入库 |
引入 Sealed Secrets / External Secrets Operator / SOPS |
| 6 | 备份与灾备 | 未涉及 GitLab / Harbor / ArgoCD 的数据备份 | 明确备份周期与恢复演练 |
| 7 | 多租户与权限细化 | 未涉及"不同业务组看到不同 Job / 只能发自己的环境" | GitLab 组权限 + Jenkins 视图 + ArgoCD Projects 三者配合 |
| 8 | 非前端/Java 技术栈的完整示例 | 只有 Vue 的端到端例子,Java 只讲了原理 | 补一个 Java(Maven)的完整示例 |
F.2 需要完善的功能与改进项
| 优先级 | 改进项 | 对应隐患 | 价值 |
|---|---|---|---|
| 高 | 补齐 goBuild / gccBuild / phpBuild / netBuild,或改为抛出明确错误 |
#1 | 让这些技术栈可用 / 报错可读 |
| 高 | ingress-patch.yaml 显式写 ingressClassName |
#12 | 不依赖集群默认 IngressClass,换集群不踩坑 |
| 高 | 把 Harbor 的"中心推送 + 边缘拉取"依赖显式化(文档 + 复制监控 + 接入清单) | #3 | 避免新增集群时静默失败;prod 不隐式依赖 bj-harbor |
| 中 | Pod 特权降级(Kaniko / BuildKit rootless / DinD) | #2 | 消除容器逃逸风险 |
| 中 | Git 推送不再把凭证拼进 URL(改用 GIT_ASKPASS) |
#8 | 消除凭证泄露面 |
| 中 | 整理 GetParameters.groovy(环境列表分分支、fallback 统一、改单选) |
#5 #6 #7 | 消除误导与矛盾 |
| 中 | appNamespace 的 awk 改为取最后一段 |
#4 | 支持 Job 分组命名 |
| 中 | 补充 DockerBuild 的镜像/版本可配置化(不再硬编码阿里云地址) |
--- | 便于内网化 |
| 低 | 构建失败 / 复制失败 / 应用不健康的告警接入 | --- | 缩短故障发现时间 |
| 低 | Harbor 保留策略 + prod 不可变 tag | 12.7 | 节省空间、防误删 |
| 低 | 共享库加静态检查(CI 里跑 groovy lint) | #14 | 减少"改错变量名静默失效" |
| 低 | 显式化 registryServer 的选择逻辑 |
#3 | 代码可读性 |
文档结束