Featured image of post Jenkins 流水线:Java 微服务从代码提交到金丝雀发布的完整 CI/CD 实战

Jenkins 流水线:Java 微服务从代码提交到金丝雀发布的完整 CI/CD 实战

Java Web 微服务系列 · 第 9 篇 · Jenkins 流水线 / 完整 CI/CD
阅读时长:约 90 分钟
本文写于 2026 年 6 月
配套版本:Jenkins 2.452.x LTS / Kubernetes 1.29 / Argo Rollouts 1.7 / Helm 3.x
前置阅读:本系列第 5 篇「Nacos 服务中心与配置中心」(同 Java Web 微服务系列)
配套参考:站内 K8s 容器编排专题(多个分章节文章)

引子:2020 年那个凌晨 3 点的上线翻车

2020 年 11 月 11 日,距离双 11 大促开场还有 6 小时

下午 5 点 30 分,我正准备关电脑去会场盯盘,钉钉群里弹出一条消息:「@我 order-service 修复一个 Nacos 配置 bug,紧急上线,11 点前必须完成」。

我看了一下 commit:改动 1 个 yaml 配置,3 行变更。看上去毫无风险

接下来 90 分钟里,我们 5 个人上演了一出"上线翻车连续剧":

  • 17:42 — 申请 30 分钟上线窗口(运维审批通过)
  • 17:55 — ssh 到 1 号生产机,手动 scp 上传 app-order-service.jar错误 1:5 台机器我一台一台 scp,忘了第 3 台
  • 18:10 — 3 号机没启动新 jar,但 Nacos 配置中心已经推下去,3 号机用旧 jar 读新配置,启动直接失败
  • 18:12 — 3 号机 Pod 持续重启,5 秒一次,Hystrix 触发熔断,订单查询接口返回 502
  • 18:15 — 用户侧开始出现「订单提交失败」提示,监控告警开始轰炸
  • 18:18 — 我和另两个 SRE 一边回滚 3 号机一边改 4 号机,越改越乱
  • 18:30 — 第 4 号机配置文件错位(错误 2:复制粘贴时路径写错),订单服务彻底雪崩
  • 18:35 — 大促预热开始,雪崩被监控捕获,公司副总裁电话打到我手机上

最终回滚 + 重启完成时间:18:52。大促预热 20 分钟完全不可用

直接损失

  • 大促预热阶段订单流失估算 80 万+
  • 副总裁级别投诉 1 次
  • 我和团队全员通宵复盘
  • 后续两周所有"小修改"必须走"完整测试 + 灰度"

复盘会上我写了一段话,后来被打印出来贴在工位上:

不是代码问题,是流程问题。 我们缺的不是更强的程序员,是一条把’代码提交’和’生产部署’彻底打通的自动化流水线——让人为操作降到 0、让 5 台机器同步生效、让回滚一键完成、让配置错误在 CI 阶段就被拦截。」

那次之后,我花了三个月从 0 到 1 搭起了团队的 Jenkins 流水线。后来三年里,团队从 5 个微服务长到 80 个,从 5 台机器扩到 200+ Pod,从手动上线变成全自动化

这篇文章就是那三个月里踩过的所有坑 + 后续三年里迭代出的所有最佳实践的完整复盘。目标是让你照着做一遍,就能在你团队里落地一套生产可用的 Jenkins + Docker + K8s + 蓝绿/金丝雀的完整 CI/CD 链路。

文章分 8 章 + 1 个收尾,从架构总览到本地可跑通 demo,从 Jenkinsfile 模板到 Argo Rollouts 金丝雀发布,约 16500 字,建议收藏后分章节阅读。


一、Jenkins 体系总览

1.1 Master/Agent 架构:为什么单点 Jenkins 撑不住 200+ 微服务

2021 年初,我第一次把团队 5 个微服务跑上 Jenkins,那时还是单机 Master:Jenkins 装在一台 8C16G 的虚拟机上,所有构建任务都在 Master 本地跑。5 个微服务的时候一切正常。

半年后,团队扩张到 30 个微服务,每天 200+ 次构建。问题开始集中爆发:

  • 构建排队:早高峰 9-10 点,平均 15 个 job 在排队,单个 job 平均等 20 分钟——开发者提交代码到拿到构建结果要 1 小时
  • 插件 OOM:Pipeline + Docker Pipeline + Kubernetes CLI + Helm + 十几个 Sonar/Nexus/Email 插件一起跑,Master JVM 经常 OOM,每周重启 2-3 次
  • 单点故障:Jenkins 升级插件、清理 workspace、重启服务时,整个团队的开发节奏被打断
  • 磁盘撑爆:Maven 仓库 + Docker 镜像缓存 + Sonar 报告 + 构建产物,3 个月吃掉 800GB 磁盘

痛定思痛,我把 Jenkins 拆成了 Master/Agent 架构——这是今天所有中大规模团队的标准做法。

Master 只负责调度,Agent 真正干活

  • Master:保存 Job 配置、Pipeline 脚本、构建历史、凭据;不执行任何构建任务;只通过 JNLP 协议把构建任务分发给 Agent
  • Agent:真正执行 Shell、Docker、Maven、Kubectl 的工作节点;可以是物理机、虚拟机、Docker 容器、K8s Pod
  • 通信协议:JNLP(推荐,HTTP 长连接,跨公网可穿透防火墙)或 SSH(适合内网)

我当时的方案是把 3 类 Agent 拆开,每类干一件事:

Agent 类型镜像用途资源
maven agentmaven:3.9-eclipse-temurin-17Java 编译、单元测试、生成 jar/war4C8G
docker agentdocker:24-dindDocker in Docker,构建镜像、推 Harbor4C8G
k8s agentjenkins/inbound-agent:latest跑 K8s 部署任务、用完即销毁2C4G

这种架构的好处是横向扩展——构建量翻倍时,加 3 个 maven agent 就够了,不用动 Master。

1.2 Pipeline as Code:从 UI 点击到 Jenkinsfile 提交

2018 年之前我用的还是 Jenkins「老模式」:在 Jenkins Web UI 上点 New Item → 配置 Git 仓库地址 → 配置 Build Steps → 配置 Post-build Actions → Save。

这种模式有 4 个致命问题

  1. 配置散落:30 个微服务 × 30 个 Job = 900 个 Job 配置,散在 Jenkins 数据库里,没人能说清楚哪个 Job 在跑什么命令
  2. 改不动:想统一加一个 SonarQube 扫描步骤?30 个 Job 全部手动改一遍,改完还要截图存档证明
  3. 不可审计:谁在什么时间改了 Job 配置?Jenkins 不记录——出问题时只能看 audit log 里的"管理员在 X 时间编辑了 Y Job"
  4. 不可回滚:配错了?只能手动重配,没有 diff 没有 revert

2018 年 Jenkins 推出 Pipeline as Code(也称 Jenkinsfile),所有这一切问题被一次性解决

  • 配置即代码:Job 配置不再是 UI 点击,而是写在一个叫 Jenkinsfile 的文本文件里,和业务代码一起存在 Git 仓库
  • PR review 流程:Jenkinsfile 改了 → 走 PR → 同事 review → 合并,和业务代码一样的代码审查流程
  • 版本可追溯:每次改 Jenkinsfile 都有 commit 历史,回滚一行 git revert 就好
  • 可复用:Jenkinsfile 里有公共逻辑(build、test、deploy)?抽成 Shared Library,10 个微服务共用同一段代码

我自己从 2018 年开始用 Jenkinsfile,到 2020 年把所有 freestyle job 全部迁完。这一步是流水线从"能用"到"好用"的关键分水岭。

1.3 声明式 vs 脚本式:怎么选?

Jenkinsfile 写起来有两种风格:

  • 声明式(Declarative):语法限制严格,结构清晰,像填表
  • 脚本式(Scripted):完整 Groovy 语法,自由度高,像写程序

90% 的场景用声明式就够了。我团队 80 个微服务,75 个用声明式,只有 5 个复杂动态场景(按需选环境、动态生成 Job)用脚本式。

6 维度对比表

维度声明式 (Declarative)脚本式 (Scripted)
语法限制严格,结构固定自由,完整 Groovy
学习曲线低(半小时上手)高(需会 Groovy)
复用性library 调 Shared LibraryGroovy Class 自己封装
IDE 支持主流 IDE 都有插件弱,需要懂 Groovy
错误提示友好(语法错误立即提示)一般(运行时才知道)
适用规模中小团队 / 80% 场景复杂动态 / 大型平台
推荐度⭐⭐⭐⭐⭐⭐⭐

声明式的骨架长这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
pipeline {
    agent { label 'maven' }            // 在哪个 agent 上跑
    options {                           // 全局选项
        timeout(time: 30, unit: 'MINUTES')
        disableConcurrentBuilds()
    }
    parameters {                        // 手动参数
        string(name: 'BRANCH', defaultValue: 'main')
    }
    stages {                            // 阶段
        stage('Build') { steps { ... } }
        stage('Test')  { steps { ... } }
    }
    post {                              // 后置动作
        always { echo 'cleanup' }
        failure { mail to: 'team@xxx.com' }
    }
}

pipeline / agent / stages / steps / post 是声明式的5 大顶层指令,写错一个 Jenkins 立刻提示。脚本式你写 node { stage { ... } },语法错误要到运行时才报。

1.4 关键插件清单:20 个就够,多了就是债务

Jenkins 插件生态庞大,官方仓库 1800+ 插件。我见过最夸张的团队装 200+ 插件——Jenkins 启动 5 分钟,OOM 每月 3 次

经验法则:20 个插件以内,撑起 80 个微服务。

我团队的生产可用最小集(按重要性排序):

类别插件必装用途
核心PipelineJenkinsfile 支持
核心Git拉代码
核心Credentials Binding凭据注入
核心Workspace Cleanup清理工作区
核心Timestamper日志加时间戳
SCMGitHub / GitLabWebhook + 状态回调
SCMGeneric Webhook Trigger通用 Webhook 触发器
构建Docker PipelineJenkinsfile 里写 docker.build
构建Kubernetes CLIkubectl apply
构建HelmHelm Chart 部署
质量SonarQube Scanner代码扫描
质量JUnit测试报告聚合
制品Nexus Artifact Uploader推 Maven 包到 Nexus
制品Warnings NG代码告警聚合
通知Email-ext邮件通知
通知DingTalk钉钉通知
UIBlue Ocean可视化流水线(可选)
运维Configuration as CodeJenkins 配置 yaml 化
运维Matrix Authorization权限矩阵
运维Role-based Authorization角色权限

注意:

  • 装过的插件必须用 JCasC(Configuration as Code)写入 yaml,否则重启就丢
  • 插件升级前先在测试 Jenkins 验证——很多插件升级有 breaking change(比如 Docker Pipeline 1.x → 2.x 改了 API)
  • 半年审一次插件清单,用不到的立即 disable,不常用的 uninstall

第一章总结:Jenkins Master/Agent 架构是规模化的基础,Pipeline as Code 是工程化的关键,声明式 Pipeline 是 90% 场景的最优解,20 个插件覆盖 80% 用法

下一章我们把这些原则落到本地——用 docker-compose 起一套完整可跑通的 Jenkins + k3d K8s 演示环境。


二、高可用 Jenkins 集群搭建

2.1 本地环境拓扑:Jenkins + k3d 一键起

看 100 篇文章不如跑 1 遍」——这是我在团队内部推广 Jenkins 时的口头禅。

文章里写的 docker-compose.yml、k3d 启动脚本、Jenkinsfile 模板、Helm Chart 都可以直接复制使用。读者需要做的就是跟着敲命令,在自己电脑上把整套环境跑起来。

本节给出完整可跑通的本地演示环境拓扑:

架构关键点

  • Jenkins Master 跑在宿主机 Docker 上(不是 k3d 内)——避免 K8s 故障时 Jenkins 也挂
  • 3 类 Agent 跑在 k3d 之外(也可以跑在 k3d 内作为 Pod,本章用更简单的「固定容器」方案)
  • k3d 集群跑在宿主机 Docker 里,共享 docker.sock,Jenkins 构建的镜像能被 K8s 看到
  • Harbor 跑在 k3d 内(用 NodePort 30002 暴露)

资源占用

  • 最低:8C16G,能跑但卡
  • 推荐:16C32G,这是流畅跑 Jenkins + k3d 的甜点
  • 极致:32C64G,可同时跑 SonarQube + Nexus + 多个项目

2.2 docker-compose.yml:Jenkins Master + 持久化

完整 docker-compose.yml(生产可用 + 本地 demo 两相宜):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# docker-compose.yml
# 启动:docker compose up -d
# 查看日志:docker compose logs -f jenkins
version: "3.8"

services:
  jenkins:
    image: jenkins/jenkins:2.452.3-lts-jdk17
    container_name: jenkins-master
    restart: unless-stopped
    user: root                                    # 容器内用 root,方便调 docker
    ports:
      - "8080:8080"                               # Web UI
      - "50000:50000"                             # Agent JNLP
    volumes:
      - jenkins_data:/var/jenkins_home           # 配置持久化
      - /var/run/docker.sock:/var/run/docker.sock # 共享宿主机 docker daemon
      - /usr/bin/docker:/usr/bin/docker:ro        # docker CLI(容器内能用 docker 命令)
    environment:
      - JAVA_OPTS=-Xms2g -Xmx4g -Djava.awt.headless=true
      - JENKINS_OPTS=--httpPort=8080
    networks:
      - jenkins-net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/login"]
      interval: 30s
      timeout: 10s
      retries: 10

  # 固定 maven agent(也可改成动态 K8s pod agent)
  maven-agent:
    image: jenkins/inbound-agent:3192.v713e3def_8641-1
    container_name: jenkins-maven-agent
    depends_on:
      jenkins:
        condition: service_healthy
    environment:
      - JENKINS_URL=http://jenkins:8080
      - JENKINS_SECRET=<从 Master UI 拷过来>
      - JENKINS_AGENT_NAME=maven-agent-1
    networks:
      - jenkins-net

  docker-agent:
    image: docker:24-dind
    container_name: jenkins-docker-agent
    privileged: true                              # DIND 必须
    environment:
      - DOCKER_TLS_CERTDIR=
    volumes:
      - docker_data:/var/lib/docker
    networks:
      - jenkins-net

volumes:
  jenkins_data:
  docker_data:

networks:
  jenkins-net:
    driver: bridge

关键配置解析

  • /var/run/docker.sock 挂载:让 Master 容器能直接调用宿主机 docker daemon,好处是构建的镜像在宿主机 docker 里可见,k3d 也能用
  • user: root:容器内 Jenkins 进程用 root 跑,方便调 docker(生产环境用非 root 需要更多配置)
  • JAVA_OPTS=-Xms2g -Xmx4g:JVM 堆 4G,够 100 个并发 job,超过这个量级需要排查 OOM
  • JNLP 端口 50000:Agent 通过这个端口和 Master 建长连接,必须开且不能被防火墙挡

首次启动

1
2
3
4
docker compose up -d
docker compose logs -f jenkins | grep -A 1 "initial"
# 看到一行:Please use the following password to proceed to installation:
# 类似:2a3b4c5d6e7f8g9h0i1j2k3l4m5n6o7p

把这个密码复制到 http://localhost:8080,第一次进入会走 Setup Wizard:

  1. 安装推荐插件(Install suggested plugins)——包含 Git、Pipeline、Credentials Binding 等
  2. 创建第一个管理员用户(admin /
  3. 保存并完成

进入后必须做的 3 件事

  1. Configure Global Security → 启用 Agent → Master 访问控制(JNLP 协议需要)
  2. Manage Plugins → 搜索并安装 Docker Pipeline / Kubernetes CLI / Helm / DingTalk / SonarQube Scanner
  3. Manage Nodes and Clouds → New Node → 复制 JNLP 秘钥,填到上面 maven-agentJENKINS_SECRET

2.3 k3d 一键起本地 K8s

k3dk3s(轻量 K8s)的 docker 包装器——5 秒拉起一个 K8s 集群,内存只占 300MB。比 kind(K8s in Docker)更轻,比 minikube 启动快 10 倍。

安装 k3d

1
2
3
4
5
6
7
8
# macOS
brew install k3d k3s

# Linux
curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash

# Windows(WSL2)
wsl -e bash -c "curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash"

启动集群

1
2
3
4
5
6
7
8
k3d cluster create jenkins-demo \
  --image rancher/k3s:v1.29.4-k3s1 \
  --port "80:80@loadbalancer" \
  --port "443:443@loadbalancer" \
  --port "30000-30100:30000-30100@loadbalancer" \
  --agents 3 \
  --volume "/tmp/k3d-shared:/tmp/shared@all" \
  --k3s-arg "--disable=traefik@server:0"

参数解析

  • --image rancher/k3s:v1.29.4-k3s1:K8s 版本固定(避免每次启动版本漂移)
  • --port "80:80@loadbalancer":把容器内 80 端口映射到宿主机 80
  • --port "30000-30100:30000-30100":NodePort 范围,本地访问业务服务用
  • --agents 3:3 个 worker node(默认 0,建议至少 2 个才能演示多 Pod 部署
  • --volume:共享目录,让 K8s 内的 Pod 能看到宿主机文件(重要!Jenkins 构建的镜像在这里 push 之后 K8s 才能 pull)
  • --k3s-arg "--disable=traefik@server:0":禁用默认的 Traefik Ingress(我们用 Nginx Ingress 自己装)

验证集群

1
2
3
4
5
kubectl get nodes
# 预期看到 4 个 node(1 server + 3 agent),全部 Ready

kubectl get pods -A
# kube-system 下有 coredns、metrics-server 等基础组件

Windows WSL2 用户注意:如果你用 WSL2 + Docker Desktop,/var/run/docker.sock 在 WSL2 内的路径是 //var/run/docker.sock(注意双斜杠),docker-compose.yml 里的挂载可能要改成:

1
2
volumes:
  - //var/run/docker.sock:/var/run/docker.sock

2.4 三类 Agent:maven / docker / k8s

Agent 设计的第一原则:「一类活一台 Agent」——别让一个 Agent 干所有事,原因有三:

  1. 资源隔离:Java 构建要 4C8G,Docker 构建也要 4C8G,两个一起跑会抢内存 OOM
  2. 环境隔离:Maven Agent 装 JDK + Maven,不要装 Docker CLI(污染 PATH);Docker Agent 装 Docker CLI + dind,不要装 JDK
  3. 故障隔离:某类 Agent 出问题(磁盘满、插件崩),不影响其他类型

我的标准方案(80 个微服务团队实战):

Agent 类型镜像资源用途标签
maven agentmaven:3.9-eclipse-temurin-174C8G编译、单测、Sonar 扫描、生成 jarmavenlinux
docker agentdocker:24-dind4C8Gdocker build、docker push 镜像dockerdind
k8s agentjenkins/inbound-agent:latest-jdk172C4Gkubectl、helm、argocd 操作k8slinux

Jenkinsfile 里怎么选

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
pipeline {
    agent none  // 顶层不指定 agent
    stages {
        stage('Build') {
            agent { label 'maven' }   // 拉 maven agent
            steps { sh 'mvn package' }
        }
        stage('Docker Build') {
            agent { label 'docker' }  // 拉 docker agent
            steps { sh 'docker build -t app:${TAG} .' }
        }
        stage('Deploy') {
            agent { label 'k8s' }     // 拉 k8s agent
            steps { sh 'kubectl apply -f deployment.yaml' }
        }
    }
}

进阶:用 K8s Pod 作为 Agent(动态伸缩)

如果构建量波动大(白天多、夜里少),用固定 Agent 不划算。Jenkins 支持「K8s Pod Agent」——每次构建拉一个新 Pod,构建完自动销毁。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
agent {
    kubernetes {
        label 'maven-pod'
        yaml '''
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: maven
    image: maven:3.9-eclipse-temurin-17
    command: ['cat']
    tty: true
    resources:
      requests: { cpu: '1', memory: '2Gi' }
      limits:   { cpu: '2', memory: '4Gi' }
'''
    }
}

这种方案需要先装 Kubernetes 插件,并在 Jenkins 全局配置里配 K8s Cloud:

  • Jenkins URL:https://kubernetes.default(Jenkins 跑在 K8s 内的方式)
  • 或:https://<k3d-api-server>:6443(Jenkins 跑在 K8s 外的 k3d 集群方式)
  • Credentials:ServiceAccount Token 或 Kubeconfig

两种方案对比

维度固定 AgentK8s Pod Agent
启动速度0 秒(常驻)20-30 秒(拉镜像)
资源利用率24/7 占资源用完即销毁
镜像缓存常驻,命中率高每次重新拉(除非配 PV)
适用规模小到中(<50 并发)中到大(100+ 并发)
复杂度中(要配 PVC、网络)

我的建议:30 个微服务以内用固定 Agent,简单稳定;超过 50 个用 K8s Pod Agent,节省资源

2.5 凭据管理 + SSH 互通

Jenkins 凭据是最容易出安全问题的环节——90% 的「Jenkins 被黑」事故根因都是凭据管理不当。

4 类核心凭据

凭据类型用途存哪里
Username/PasswordDocker Registry、SonarQube、邮件账号Jenkins 内置凭据库
SSH KeyGit 仓库、目标机器Jenkins 内置凭据库
Secret TextAPI Token、Webhook URLJenkins 内置凭据库
KubeconfigK8s 集群访问Jenkins 内置凭据库

Jenkinsfile 里的标准用法(不要在 Jenkinsfile 写明文密码!):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// ❌ 错误写法
sh 'docker login -u admin -p Harbor12345 harbor.xxx.com'

// ✅ 正确写法
withCredentials([usernamePassword(
    credentialsId: 'harbor-creds',
    usernameVariable: 'HARBOR_USER',
    passwordVariable: 'HARBOR_PASS'
)]) {
    sh "docker login -u ${HARBOR_USER} -p ${HARBOR_PASS} harbor.xxx.com"
}

多环境凭据切换(生产 / 预发 / 开发):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def credMap = [
    dev:     'k8s-dev-creds',
    staging: 'k8s-stg-creds',
    prod:    'k8s-prod-creds'
]
def credId = credMap[params.ENV]

withCredentials([file(credentialsId: credId, variable: 'KUBECONFIG')]) {
    sh "kubectl --kubeconfig=${KUBECONFIG} apply -f deployment.yaml"
}

凭据管理 5 大铁律

  1. 不在 Jenkinsfile 写明文——这是 1.0 级别的红线
  2. 凭据 ID 命名规范——<服务>-<环境>-<类型>,比如 harbor-prod-user
  3. 凭据权限最小化——Matrix Authorization 插件配「谁能用哪些凭据」
  4. 定期轮换——90 天换一次 Harbor/数据库密码
  5. 审计日志开启——谁在什么时间用了什么凭据,全部记录

第二章总结:Jenkins Master 跑在 docker-compose 里、Agent 按 maven/docker/k8s 三类拆分、k3d 一键起本地 K8s 集群、凭据用 withCredentials 注入——这套组合拳能让你在 1 小时内本地跑通完整演示环境。

下一章我们写一份生产可用的 Jenkinsfile——6 个 stage、150+ 行,覆盖 Build/Test/Sonar/Package/Docker/Deploy 全流程。

2.6 完整启动脚本:一键起所有服务

把上面的 docker-compose 和 k3d 命令封装成一个 shell 脚本,方便反复重置本地环境:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/bin/bash
# scripts/start-demo-env.sh
# 一键起 Jenkins + k3d + 必要工具
set -e

echo "==> 1. 启动 Jenkins Master + Agents"
docker compose up -d

echo "==> 2. 等待 Jenkins 就绪"
for i in {1..30}; do
  if curl -sf http://localhost:8080/login >/dev/null 2>&1; then
    echo "    Jenkins ready"
    break
  fi
  echo "    等待 Jenkins 启动... ${i}/30"
  sleep 5
done

echo "==> 3. 启动 k3d 集群"
k3d cluster create jenkins-demo \
  --image rancher/k3s:v1.29.4-k3s1 \
  --port "80:80@loadbalancer" \
  --port "443:443@loadbalancer" \
  --port "30000-30100:30000-30100@loadbalancer" \
  --agents 3 \
  --volume "/tmp/k3d-shared:/tmp/shared@all" \
  --k3s-arg "--disable=traefik@server:0" \
  --wait

echo "==> 4. 安装 Argo Rollouts"
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/download/v1.7.0/install.yaml

echo "==> 5. 安装 Prometheus(轻量版)"
kubectl apply -f https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/v0.72.0/bundle.yaml

echo "==> 6. 安装 Harbor(轻量)"
helm repo add harbor https://helm.goharbor.io
helm install harbor harbor/harbor \
  --namespace harbor --create-namespace \
  --set expose.type=nodePort \
  --set expose.nodePort.ports.http.nodePort=30002 \
  --set persistence.enabled=false \
  --set core.persistence.enabled=false \
  --set database.persistence.enabled=false \
  --set redis.persistence.enabled=false \
  --set jobservice.persistence.enabled=false \
  --set registry.persistence.enabled=false \
  --set trivy.persistence.enabled=false

echo "==> 7. 验证"
echo "    Jenkins:    http://localhost:8080"
echo "    k3d kubeconfig: k3d kubeconfig write jenkins-demo"
echo "    Harbor:     http://localhost:30002 (admin/Harbor12345)"
echo ""
echo "全部就绪!"

跑完这个脚本,你的本地就有

  • Jenkins Master(http://localhost:8080)
  • k3d K8s 集群(4 节点)
  • Argo Rollouts Controller
  • Prometheus Operator
  • Harbor 镜像仓库

接下来就可以在 Jenkins UI 上建 Job、配 Webhook、写 Jenkinsfile 跑流水线了。

2.7 常见坑位与排查清单

3 年里我们团队在 Jenkins + K8s 集成上踩过的高频 10 个坑,按出现频率排序:

#坑位现象解决方案
1docker.sock 权限拒绝permission denied while trying to connect to the Docker daemon socketMaster 用 user: root 跑,或在宿主机 chmod 666 /var/run/docker.sock
2k3d 集群内存爆4G 内存跑 Jenkins + k3d + Harbor,宿主机卡死宿主机至少 16G;Harbor 用 --set persistence.enabled=false
3Agent 离线JNLP 连不上,Agent 显示 offline检查 Master 50000 端口是否开放;JNLP 秘钥是否过期
4镜像推送后 K8s 拉不到ImagePullBackOffk3d 集群和 docker 共享 /tmp/k3d-shared 目录;或用 k3d image import 导入
5Maven 依赖下载慢第一次构建 5 分钟全在 download挂载 .m2 目录到 host:volumes: ['~/.m2:/root/.m2']
6Pipeline 步骤超时30 分钟还没跑完options { timeout(time: 60, unit: 'MINUTES') } 加大
7Jenkins OOMjava.lang.OutOfMemoryError调大 JAVA_OPTS=-Xmx4g;或加 Master 节点
8插件升级后 build 失败Docker Pipeline 1.x → 2.x 改了 API锁插件版本;先在测试 Jenkins 验证再升级
9Webhook 不触发push 代码 Jenkins 没反应检查 GitHub Webhook 状态码;Jenkins Manage Webhooks 日志
10并发构建冲突同一 Job 跑两次互相干扰options { disableConcurrentBuilds() } 串行

调试工具

  • Jenkins 构建日志:直接看 console output
  • Agent 状态:Manage Nodes and Clouds → 点 Agent 名 → Log
  • K8s 状态:kubectl get pods -Akubectl describe pod <name>
  • 镜像拉取:docker exec -it k3d-jenkins-demo-agent-0 crictl images
  • 网络连通:docker exec -it jenkins-master ping k3d-jenkins-demo-server-0

2.8 Jenkins 配置即代码:JCasC 拯救「文档缺失」

我刚接手团队 Jenkins 时最大的痛苦是——没人知道生产 Jenkins 的 30 个 Job 是怎么配的。某天 Jenkins 挂了重装,所有 Job 全部丢光,只能从 Jenkinsfile 重新跑。

2020 年我引入 JCasC(Configuration as Code) 插件,把所有「在 Jenkins UI 上配的东西全部 yaml 化,和 Jenkinsfile 一样存进 Git:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# casc/jenkins.yaml
jenkins:
  systemMessage: "团队 CI/CD 中心 - powered by Jenkins 2.452"
  numExecutors: 0                    # Master 不跑任务
  mode: EXCLUSIVE                    # 一个时间只跑一个 Job(防 OOM)
  securityRealm:
    local:
      users:
        - id: "admin"
          name: "Admin"
          password: "${ADMIN_PASSWORD}"   # 从环境变量读
  authorizationStrategy:
    loggedInUsersCanDoAnything:
      allowAnonymousRead: false
  remotingSecurity:
    enabled: true

credentials:
  system:
    domainCredentials:
      - credentials:
          - usernamePassword:
              scope: GLOBAL
              id: "harbor-creds"
              username: "admin"
              password: "${HARBOR_PASSWORD}"
              description: "Harbor 镜像仓库"
          - file:
              scope: GLOBAL
              id: "k8s-prod-kubeconfig"
              fileName: "kubeconfig"
              secretBytes:
                "${K8S_PROD_KUBECONFIG_BASE64}"

unclassified:
  location:
    url: "https://jenkins.xxx.com/"
    adminAddress: "devops@xxx.com"
  tool:
    jdk:
      installations:
        - name: "jdk17"
          properties:
            - installSource:
                installers:
                  - adoptOpenJdk:
                      id: "jdk-17.0.10+7"

启动时加载:

1
2
3
docker run -e CASC_JENKINS_CONFIG=/var/jenkins_home/casc/jenkins.yaml \
  -v $PWD/casc:/var/jenkins_home/casc:ro \
  jenkins/jenkins:2.452.3-lts-jdk17

JCasC 的 3 大价值

  1. 环境可重建:Jenkins 挂了重装,30 秒自动恢复所有配置(凭据除外,敏感信息走环境变量 / Vault)
  2. 变更可审计:所有配置改动走 PR review,谁在什么时间改了 systemMessage 一目了然
  3. 多环境一致:dev / staging / prod Jenkins 用同一份 yaml + 不同环境变量,不会出现「测试配了生产没配」的坑

2.9 备份策略:每日 3-2-1 法则

Jenkins 数据分两类,备份策略完全不同

配置数据(小,KB-MB 级):

  • JCasC yaml(已在 Git 里,天然多副本
  • Job 配置(如果用 Jenkinsfile 形式,已在 Git 里;如果是 freestyle,必须定期导出
  • 凭据 ID 清单(凭据值本身不能导出,只能记「有哪些凭据」
  • 全局安全配置、插件清单

运行时数据(大,GB 级):

  • jenkins_data 卷(workspace、构建历史、归档制品、用户内容)
  • 数据库(如果用外部 DB)

我的 3-2-1 备份方案

数据备份方式频率保留期
JCasC yamlGit(主仓 + GitLab mirror)实时永久
JenkinsfileGit(业务仓)实时永久
freestyle Job 配置Job History 插件 + 定时导出 yaml每日90 天
jenkins_data 卷restic 加密备份到 S3 / 阿里云 OSS每日 03:0030 天
关键构建产物推 Nexus / Harbor实时按 artifact 策略
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 备份脚本(简化版)
#!/bin/bash
BACKUP_DIR=/backup/jenkins/$(date +%Y%m%d)
mkdir -p $BACKUP_DIR

# 1. 备份 JCasC(已在 Git 里,这里只备份运行时配置)
docker exec jenkins-master bash -c \
  "java -jar /var/jenkins_home/war/WEB-INF/jenkins-cli.jar \
   -s http://localhost:8080 get-job my-job" > $BACKUP_DIR/my-job.xml

# 2. 备份插件清单
docker exec jenkins-master cat /var/jenkins_home/plugins.txt > $BACKUP_DIR/plugins.txt

# 3. 备份 jenkins_data(restic 增量)
restic backup /var/lib/docker/volumes/jenkins_data

# 4. 上传 S3
aws s3 sync $BACKUP_DIR s3://my-backup-bucket/jenkins/$BACKUP_DIR

灾备演练:每季度做一次「Jenkins 全毁 → 1 小时内恢复」的演练,不演练的备份等于没备份


三、Jenkinsfile 完整模板

3.1 项目结构假设 + Pipeline 总体设计

为了一篇文章讲透 Jenkinsfile,我假设你的项目是这种结构(绝大多数 Java 微服务团队都是这个形态):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
my-microservice/                  # Spring Boot 3.x 多模块 Maven 项目
├── pom.xml                       # 父 POM
├── common-lib/                   # 公共库
│   ├── pom.xml
│   └── src/main/java/...
├── order-service/                # 订单服务
│   ├── pom.xml
│   ├── Dockerfile
│   └── src/
├── user-service/                 # 用户服务
│   ├── pom.xml
│   ├── Dockerfile
│   └── src/
├── Jenkinsfile                   # 我们的主角
└── sonar-project.properties      # SonarQube 配置

Pipeline 6 阶段总体设计

关键设计决策

  1. agent none + 每个 stage 单独指定 agent——Build/Test 在 maven agent,Docker 在 docker agent,Deploy 在 k8s agent
  2. 每个 stage 必须可独立触发——when { expression { ... } } 条件跳过不需要的 stage
  3. post 4 大场景——always 清理、success 通知、failure 告警 + 日志归档、aborted 通知
  4. 重试机制——网络类操作(git push、docker push)retry(3) 包一层
  5. 超时——整体 Pipeline 30 分钟,单 stage 10 分钟,超时自动 fail

3.2 完整 Jenkinsfile 全文(150+ 行可直接复制)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
// Jenkinsfile - 适用于 Spring Boot 多模块微服务
// 用法:放在项目根目录,Jenkins 创建 Pipeline Job 时选 "Pipeline script from SCM"

pipeline {
    agent none  // 顶层不指定,由各 stage 决定

    options {
        // 整体超时:30 分钟
        timeout(time: 30, unit: 'MINUTES')
        // 禁止并发构建(同一 Job 同一时间只能跑 1 个)
        disableConcurrentBuilds()
        // 构建历史保留:30 天最多 50 次
        buildDiscarder(logRotator(numToKeepStr: '50', daysToKeepStr: '30'))
        // 日志加时间戳
        timestamps()
        // ansi 颜色
        ansiColor('xterm')
    }

    parameters {
        // 手动参数
        choice(name: 'ENV', choices: ['dev', 'staging', 'prod'],
               description: '部署环境')
        string(name: 'BRANCH', defaultValue: 'main',
               description: '代码分支')
        string(name: 'IMAGE_TAG', defaultValue: '',
               description: '镜像 tag(留空用 commit SHA)')
        booleanParam(name: 'SKIP_TEST', defaultValue: false,
               description: '跳过测试')
        booleanParam(name: 'FORCE_DEPLOY', defaultValue: false,
               description: '强制部署(跳过健康检查)')
    }

    environment {
        // 全局环境变量
        APP_NAME       = 'order-service'
        HARBOR_PROJECT = 'myteam'
        K8S_NAMESPACE  = "${params.ENV}"
        GIT_COMMIT     = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
        IMAGE_TAG      = "${params.IMAGE_TAG?.trim() ? params.IMAGE_TAG : env.GIT_COMMIT}"
        IMAGE_NAME     = "harbor.xxx.com/${HARBOR_PROJECT}/${APP_NAME}"
        DOCKER_IMAGE   = "${IMAGE_NAME}:${IMAGE_TAG}"
    }

    stages {
        // ===== 阶段 1:Checkout =====
        stage('Checkout') {
            agent { label 'maven' }
            steps {
                checkout scm
                sh 'git status'
                sh 'git log -1 --oneline'
            }
        }

        // ===== 阶段 2:Build =====
        stage('Build') {
            agent { label 'maven' }
            steps {
                sh 'mvn -B -V -T 4 clean compile'
            }
            post {
                failure {
                    echo 'Build 失败,请检查代码编译错误'
                }
            }
        }

        // ===== 阶段 3:Test =====
        stage('Test') {
            agent { label 'maven' }
            when {
                expression { return !params.SKIP_TEST }
            }
            steps {
                sh 'mvn -B test'
                // 归档测试报告
                junit '**/target/surefire-reports/*.xml'
                // 代码覆盖率
                jacoco(execPattern: '**/target/jacoco.exec',
                       minimumLineCoverage: '0.60')
            }
        }

        // ===== 阶段 4:Code Scan =====
        stage('Code Scan') {
            agent { label 'maven' }
            steps {
                withSonarQubeEnv('sonarqube-server') {
                    sh 'mvn sonar:sonar -Dsonar.projectKey=${APP_NAME}'
                }
                // 等待 Quality Gate 结果(10 分钟超时)
                timeout(time: 10, unit: 'MINUTES') {
                    waitForQualityGate abortPipeline: true
                }
            }
        }

        // ===== 阶段 5:Package =====
        stage('Package') {
            agent { label 'maven' }
            steps {
                sh 'mvn -B -DskipTests clean package'
                // 归档 jar
                archiveArtifacts artifacts: '**/target/*.jar',
                                 fingerprint: true,
                                 allowEmptyArchive: false
            }
        }

        // ===== 阶段 6:Docker Build =====
        stage('Docker Build') {
            agent { label 'docker' }
            steps {
                sh """
                    docker build \
                        --build-arg JAR_FILE=${APP_NAME}/target/*.jar \
                        -t ${DOCKER_IMAGE} \
                        -t ${IMAGE_NAME}:latest \
                        .
                """
            }
        }

        // ===== 阶段 7:Push to Harbor =====
        stage('Push') {
            agent { label 'docker' }
            steps {
                withCredentials([usernamePassword(
                    credentialsId: 'harbor-creds',
                    usernameVariable: 'HARBOR_USER',
                    passwordVariable: 'HARBOR_PASS'
                )]) {
                    sh """
                        docker login -u ${HARBOR_USER} -p ${HARBOR_PASS} harbor.xxx.com
                        retry 3 docker push ${DOCKER_IMAGE}
                        docker push ${IMAGE_NAME}:latest
                    """
                }
            }
        }

        // ===== 阶段 8:Deploy =====
        stage('Deploy') {
            agent { label 'k8s' }
            when {
                expression { return params.ENV != '' }
            }
            steps {
                script {
                    def credMap = [
                        dev:     'k8s-dev-creds',
                        staging: 'k8s-stg-creds',
                        prod:    'k8s-prod-creds'
                    ]
                    withCredentials([file(
                        credentialsId: credMap[params.ENV],
                        variable: 'KUBECONFIG'
                    )]) {
                        sh """
                            kubectl --kubeconfig=${KUBECONFIG} \
                                set image deployment/${APP_NAME} \
                                ${APP_NAME}=${DOCKER_IMAGE} \
                                -n ${K8S_NAMESPACE}

                            kubectl --kubeconfig=${KUBECONFIG} \
                                rollout status deployment/${APP_NAME} \
                                -n ${K8S_NAMESPACE} \
                                --timeout=5m
                        """
                    }
                }
            }
        }
    }

    // ===== 构建后操作 =====
    post {
        // 始终执行
        always {
            echo "构建结束:${currentBuild.currentResult}"
            // 清理 workspace
            cleanWs()
        }
        // 成功
        success {
            script {
                def msg = """
                    ✅ 构建成功
                    Job: ${env.JOB_NAME} #${env.BUILD_NUMBER}
                    分支: ${params.BRANCH}
                    环境: ${params.ENV}
                    镜像: ${DOCKER_IMAGE}
                    提交: ${env.GIT_COMMIT}
                    链接: ${env.BUILD_URL}
                """.stripIndent()
                dingtalk(
                    type: 'MARKDOWN',
                    title: '✅ Jenkins 构建成功',
                    text: msg,
                    at: []
                )
            }
        }
        // 失败
        failure {
            script {
                // 归档失败日志
                archiveArtifacts artifacts: '**/build.log',
                                 allowEmptyArchive: true
                // 钉钉告警
                dingtalk(
                    type: 'MARKDOWN',
                    title: '❌ Jenkins 构建失败',
                    text: """
                        ❌ 构建失败
                        Job: ${env.JOB_NAME} #${env.BUILD_NUMBER}
                        分支: ${params.BRANCH}
                        提交: ${env.GIT_COMMIT}
                        链接: ${env.BUILD_URL}
                        失败阶段: ${env.STAGE_NAME ?: '未知'}
                    """.stripIndent()
                )
            }
        }
        // 终止
        aborted {
            echo '构建被人工终止'
        }
    }
}

这段 Jenkinsfile 包含 8 个 stage、150+ 行,是生产可用的最小完整版本。复制过去改 4 个变量就能用:

  • APP_NAME:你的服务名
  • HARBOR_PROJECT:Harbor 项目名
  • harbor.xxx.com:Harbor 地址
  • sonarqube-server:SonarQube 配置名(在 Jenkins 全局配置里配)

3.3 Checkout 阶段详解

checkout scm 是声明式 Pipeline 里最简洁也最强大的一行——它会自动用 Job 配置的 SCM 拉代码。

多仓库场景(order-service 依赖 common-lib):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
stage('Checkout') {
    agent { label 'maven' }
    steps {
        checkout([
            $class: 'GitSCM',
            branches: [[name: params.BRANCH]],
            userRemoteConfigs: [[
                url: 'git@github.com:myteam/order-service.git',
                credentialsId: 'github-ssh-key'
            ]],
            extensions: [
                // 拉 common-lib 子模块
                [$class: 'SubmoduleOption',
                 recursiveSubmodules: true,
                 parentCredentials: true],
                // 浅克隆(只拉最近 50 个 commit,加速)
                [$class: 'CloneOption',
                 depth: 50, shallow: true, noTags: false]
            ]
        ])
    }
}

关键参数

  • credentialsId:用 SSH key 凭据,别用 https 用户名密码(每次 build 都会触发 2FA)
  • submodule:如果项目用 Git submodule 拉公共库,必须 recursiveSubmodules: true
  • shallow: true:浅克隆,从 5 分钟缩到 30 秒
  • LFS:大文件用 Git LFS 时要额外配

3.4 Build + Test 阶段详解

Maven 加速 5 个技巧

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 1. 批模式:-B 减少 Jenkins 日志噪音
sh 'mvn -B clean package'

// 2. 版本校验:-V 强制用父 POM 版本(防 dep version 漂移)
sh 'mvn -B -V clean package'

// 3. 并行构建:-T 4(4 线程并行)
sh 'mvn -B -T 4 clean package'

// 4. 跳过测试:-DskipTests
sh 'mvn -B -DskipTests package'

// 5. 增量编译:am(also-make)只编译依赖模块
sh 'mvn -B -pl order-service -am clean package'

测试报告

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
stage('Test') {
    agent { label 'maven' }
    steps {
        sh 'mvn -B test'
        // JUnit 报告聚合
        junit allowEmptyResults: true,
              testResults: '**/target/surefire-reports/*.xml'

        // 覆盖率门槛
        jacoco(execPattern: '**/target/jacoco.exec',
               minimumLineCoverage: '0.60',
               minimumBranchCoverage: '0.50')
    }
}

minimumLineCoverage: '0.60' 是个强约束——单测覆盖率低于 60% 直接 fail。我团队的策略是 60% 起步、季度提升 5%,3 年从 60% 干到 78%。

Maven 本地仓库缓存

如果 Agent 是容器,每次构建都重新下载依赖。解决方法:

1
2
3
4
5
# docker-compose.yml 给 maven agent 挂 .m2
services:
  maven-agent:
    volumes:
      - ~/.m2:/root/.m2  # 共享宿主机 .m2 目录

这样 100 个微服务用同一份 .m2,首次 5 分钟,后续 30 秒

3.5 SonarQube + Docker 阶段详解

SonarQube 集成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
stage('Code Scan') {
    agent { label 'maven' }
    steps {
        // 1. 注入 SonarQube 配置(从 Jenkins 全局配置里拿 token)
        withSonarQubeEnv('sonarqube-server') {
            sh 'mvn sonar:sonar -Dsonar.projectKey=order-service'
        }
        // 2. 阻塞等结果(10 分钟超时)
        timeout(time: 10, unit: 'MINUTES') {
            // 3. Quality Gate 不通过 abortPipeline: true
            waitForQualityGate abortPipeline: true
        }
    }
}

sonar-project.properties(放项目根目录):

1
2
3
4
5
6
7
8
9
sonar.projectKey=order-service
sonar.projectName=Order Service
sonar.projectVersion=1.0.0
sonar.sources=order-service/src/main/java
sonar.tests=order-service/src/test/java
sonar.java.binaries=order-service/target/classes
sonar.java.coveragePlugin=jacoco
sonar.coverage.jacoco.xmlReportPaths=order-service/target/site/jacoco/jacoco.xml
sonar.exclusions=**/generated/**,**/proto/**

Quality Gate 规则推荐

  • 新代码覆盖率 ≥ 80%
  • 新代码 0 个 Blocker/Critical 漏洞
  • 代码异味 < 50
  • 重复率 < 3%

Docker 构建 + 推送

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
stage('Docker Build') {
    agent { label 'docker' }
    steps {
        sh """
            docker build \
                --build-arg JAR_FILE=order-service/target/*.jar \
                -t ${DOCKER_IMAGE} \
                -t ${IMAGE_NAME}:latest \
                .
        """
    }
}

stage('Push') {
    agent { label 'docker' }
    steps {
        withCredentials([usernamePassword(
            credentialsId: 'harbor-creds',
            usernameVariable: 'HARBOR_USER',
            passwordVariable: 'HARBOR_PASS'
        )]) {
            sh """
                docker login -u ${HARBOR_USER} -p ${HARBOR_PASS} harbor.xxx.com
                docker push ${DOCKER_IMAGE}
                docker push ${IMAGE_NAME}:latest
            """
        }
    }
}

3 个优化点

  1. 多 tag 同时打:commit SHA + latest,让 K8s 既能精确回滚到某次 commit,也能用 latest 拉最新
  2. --build-arg 注入 jar:多模块项目镜像只装当前服务的 jar,不装别的
  3. Harbor 项目隔离:每个团队一个 Harbor project,用 RBAC 控制谁能 push/pull

3.6 Deploy 阶段详解 + 金丝雀

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
stage('Deploy') {
    agent { label 'k8s' }
    when {
        expression { return params.ENV != '' }
    }
    steps {
        script {
            // 凭据切换(多环境)
            def credMap = [
                dev:     'k8s-dev-creds',
                staging: 'k8s-stg-creds',
                prod:    'k8s-prod-creds'
            ]
            withCredentials([file(
                credentialsId: credMap[params.ENV],
                variable: 'KUBECONFIG'
            )]) {
                sh """
                    # 1. 替换镜像版本
                    kubectl --kubeconfig=\${KUBECONFIG} \\
                        set image deployment/${APP_NAME} \\
                        ${APP_NAME}=${DOCKER_IMAGE} \\
                        -n ${K8S_NAMESPACE}

                    # 2. 等待滚动更新完成
                    kubectl --kubeconfig=\${KUBECONFIG} \\
                        rollout status deployment/${APP_NAME} \\
                        -n ${K8S_NAMESPACE} \\
                        --timeout=5m
                """
            }
        }
    }
}

注意

  • set image原地更新,会触发 K8s 滚动更新(默认 25% maxSurge / 25% maxUnavailable)
  • 如果你用的是 Argo Rollouts(推荐),改用 kubectl argo rollouts set image,后面第七章详讲
  • 镜像 tag 必须是固定值(commit SHA),不要用 latest——latest 会导致 K8s 永远拿最新镜像,回滚失败

3.7 完整流水线时序图


第三章总结:一份 150+ 行的 Jenkinsfile 覆盖 8 个 stage、4 类 stage agent、6 种 stage 后置动作——这是 Java 微服务 CI/CD 的最小完整骨架

下一章我们把这个 Jenkinsfile 接到多环境分支策略——develop / staging / release / master 的 GitFlow 怎么映射到 Jenkins Job。


四、多环境 + GitFlow 集成

4.1 GitFlow 分支策略:dev / staging / release / master

Jenkinsfile 写好后,下一步是让它在不同分支跑不同的逻辑——这就是「GitFlow + Jenkins」的精髓。

我们团队用的 GitFlow(2020 年版简化版,去掉了一些繁琐环节):

6 类分支各自对应一个环境 + 触发器

分支环境触发器凭据通知人
feature/*devPR pushdev k8s作者
developdevpushdev k8s全员
stagingstagingpushstaging k8sQA 团队
release/*stagingpushstaging k8sQA + 研发负责人
masterprodpushprod k8s全员 + 总监
hotfix/*prodpushprod k8s全员 + 总监

核心规则

  1. feature → develop:开发者 fork feature 分支开发,合到 develop 自动跑 dev 流水线
  2. develop → staging:每周末从 develop 切 staging 分支,QA 在 staging 环境做集成测试
  3. staging → release → master:QA 通过后切 release 分支,再合 master + 打 tag,自动触发 prod 流水线
  4. hotfix → master:生产事故从 master 拉 hotfix 分支,修完直接合 master,同时 cherry-pick 回 develop

4.2 Webhook 配置:从 git push 到 Jenkins 触发

GitHub Webhook(Settings → Webhooks → Add):

1
2
3
Payload URL:  http://jenkins.xxx.com/generic-webhook-trigger/invoke?token=order-service-token
Content type: application/json
Events:       push, pull_request

GitLab Webhook(Settings → Webhooks):

1
2
URL:    http://jenkins.xxx.com/project/order-service
Events: Push events, Merge request events

Jenkins 端配 Generic Webhook Trigger

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Jenkinsfile 顶部
triggers {
    GenericTrigger(
        // token 唯一标识这个 job
        token: 'order-service-token',
        // 打印 payload 到控制台(调试用)
        printPostContent: true,
        // 解析 push 事件
        regexpFilterText: '$ref',
        regexpFilterExpression: '^refs/heads/(.*)$',
        // 提取分支名
        causeString: 'Triggered by push on branch',
        // 参数化注入
        genericVariables: [
            [key: 'BRANCH_FROM_PAYLOAD', value: '$.ref'],
        ]
    )
}

通用 Webhook 的 5 大优势(vs 老版 GitHub/GitLab 插件):

  1. 支持任意 Git 平台:GitHub / GitLab / Gitee / 自建 Git,配置都一样
  2. 支持任意事件:push / pull_request / tag / release
  3. 支持复杂过滤:只触发特定分支、特定文件变更
  4. 支持参数化注入:把 webhook payload 里的 commit、author、message 注入到 Job 参数
  5. 调试友好printPostContent: true 把整个 payload 打到控制台

4.3 Parameterized Build + 多环境凭据切换

3 个核心参数

1
2
3
4
5
6
7
8
parameters {
    choice(name: 'ENV', choices: ['dev', 'staging', 'prod'],
           description: '部署环境')
    string(name: 'BRANCH', defaultValue: 'main',
           description: '代码分支')
    string(name: 'IMAGE_TAG', defaultValue: '',
           description: '镜像 tag(留空用 commit SHA)')
}

多环境凭据切换(最常见的坑):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
stage('Deploy') {
    steps {
        script {
            def credMap = [
                dev:     'k8s-dev-creds',
                staging: 'k8s-stg-creds',
                prod:    'k8s-prod-creds'
            ]
            def credId = credMap[params.ENV]
            if (!credId) {
                error("未知环境:${params.ENV}")
            }
            withCredentials([file(
                credentialsId: credId,
                variable: 'KUBECONFIG'
            )]) {
                sh "kubectl --kubeconfig=${KUBECONFIG} apply -f deploy/"
            }
        }
    }
}

凭据 ID 必须用 Map 管理——不要在每个 stage 写 if (params.ENV == 'prod') 这种硬编码。

4.4 通知 + 审批流:钉钉机器人

钉钉自定义机器人配置

  1. 钉钉群 → 群设置 → 智能群助手 → 添加机器人 → 自定义
  2. 复制 Webhook URL(含签名
  3. Jenkins 凭据库添加:dingtalk-webhook,类型 Secret text,值是 Webhook URL

Jenkinsfile 里发钉钉

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
post {
    success {
        script {
            withCredentials([string(
                credentialsId: 'dingtalk-webhook',
                variable: 'WEBHOOK'
            )]) {
                sh """
                    curl -X POST ${WEBHOOK} \
                        -H 'Content-Type: application/json' \
                        -d '{
                            "msgtype": "markdown",
                            "markdown": {
                                "title": "✅ 构建成功",
                                "text": "## ✅ ${env.JOB_NAME} #${env.BUILD_NUMBER}\n\n**环境**: ${params.ENV}\n**分支**: ${params.BRANCH}\n**提交**: ${env.GIT_COMMIT}\n\n[查看构建](${env.BUILD_URL})"
                            }
                        }'
                """
            }
        }
    }
    failure {
        // 失败也发,加 @ 负责人
        ...
    }
}

生产部署的「5 分钟审批缓冲」(用 input 步骤):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
stage('Deploy to Prod') {
    when {
        expression { return params.ENV == 'prod' }
    }
    steps {
        script {
            // 1. 部署到 prod
            deployToProd()
            // 2. 等 5 分钟(缓冲回滚)
            echo '等待 5 分钟观察监控...'
            sleep 300
            // 3. 人工确认
            timeout(time: 10, unit: 'MINUTES') {
                input(
                    message: '生产部署后 5 分钟观察期已到,是否确认发布?',
                    ok: '确认(继续)',
                    submitter: 'tech-lead,devops',
                    submitterParameter: 'APPROVER'
                )
            }
            // 4. 记录审批人
            echo "Prod 部署由 ${APPROVER} 确认"
        }
    }
}

submitter: 'tech-lead,devops' 表示只有技术负责人或运维才能点确认,其他人在 Jenkins 上看不到「确认」按钮

双保险

  • 技术层input 步骤人工确认
  • 流程层:生产部署走单独的 Job(不是 dev/staging 同一个 Job),且需要 tech-lead 才能触发

第四章总结:GitFlow 分支映射到 Jenkins Job,Webhook 自动触发,参数化构建 + 多环境凭据切换,钉钉通知 + 生产审批流——这套组合拳让「代码提交 → 生产部署」全过程可追溯、可审计、可回滚

下一章我们把 Jenkinsfile 里的可复用逻辑抽成 Shared Library——80 个微服务共用同一份构建代码。


五、共享库封装:把可复用 Stage 抽成库

5.1 共享库目录结构

到 2021 年,团队里有 80 个微服务,每个微服务的 Jenkinsfile 都长得差不多——都是 Build/Test/Sonar/Package/Docker/Push/Deploy 7 步。

问题是「差不多但不完全一样」:

  • 30% 是 Spring Boot,10% 是 Node.js,5% 是 Python
  • 不同服务 Docker 基础镜像不同(JDK 17 / JDK 21 / JDK 11)
  • 不同服务 K8s namespace 不同
  • 不同服务 SonarQube projectKey 不同

一开始我每个服务的 Jenkinsfile 复制粘贴一遍。结果:

  • 80 个 Jenkinsfile 改了 80 次
  • 改错一个地方,80 个服务出 bug
  • 新人入职配一个服务的 Jenkinsfile 要 2 小时

2022 年我引入 Shared Library(共享库)——把 Jenkinsfile 里的可复用逻辑抽到一个独立 Git 仓库,80 个微服务共用。

目录结构(Jenkins 规定):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
jenkins-shared-library/                 # 独立 Git 仓库
├── vars/                               # 【声明式直接调用的方法】
   ├── buildMaven.groovy               # 文件名 = 方法名
   ├── buildNode.groovy
   ├── buildPython.groovy
   ├── dockerBuild.groovy
   ├── dockerPush.groovy
   ├── deployK8s.groovy
   ├── deployArgoRollout.groovy
   ├── notifyDingTalk.groovy
   └── sonarScan.groovy
├── src/                                # 【普通 Groovy 类】
   └── com/myteam/ci/
       ├── Utils.groovy                # 工具类
       ├── DockerUtils.groovy
       └── K8sUtils.groovy
├── resources/                          # 【静态资源】
   ├── com/myteam/ci/
      └── Dockerfile-template.txt     # Dockerfile 模板
   └── sonar/
       └── sonar-template.properties
├── test/                               # 【单元测试】
   └── com/myteam/ci/
       └── UtilsTest.groovy
├── README.md
└── CHANGELOG.md

关键约定

  • vars/ 下的 .groovy 文件文件名 = 方法名,声明式 Pipeline 里直接调用
  • src/ 下的类不会被声明式直接调用,需要 importdef utils = new com.myteam.ci.Utils()
  • resources/ 下的文件运行时可通过 libraryResource 'path/to/file' 读取

5.2 完整 vars/ 脚本

vars/buildMaven.groovy(构建 Java 服务):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// vars/buildMaven.groovy
import com.myteam.ci.Utils

def call(Map args) {
    // 参数校验
    if (!args.projectName) {
        error "buildMaven 必须传 projectName 参数"
    }
    def projectName = args.projectName
    def goals = args.goals ?: 'clean package'
    def skipTests = args.skipTests ?: false
    def useJacoco = args.useJacoco ?: true

    // 打印分隔
    Utils.printBanner("Maven Build: ${projectName}")

    // 1. 编译
    sh "mvn -B -V -T 4 -pl ${projectName} -am clean compile"

    // 2. 测试
    if (!skipTests) {
        sh "mvn -B -pl ${projectName} -am test"
        junit allowEmptyResults: true,
              testResults: "**/${projectName}/target/surefire-reports/*.xml"
        if (useJacoco) {
            jacoco(execPattern: "**/${projectName}/target/jacoco.exec",
                   minimumLineCoverage: '0.60')
        }
    }

    // 3. 打包
    sh "mvn -B -pl ${projectName} -am -DskipTests clean package"
    archiveArtifacts artifacts: "${projectName}/target/*.jar",
                     fingerprint: true
}

vars/deployK8s.groovy(部署到 K8s):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// vars/deployK8s.groovy
import com.myteam.ci.K8sUtils

def call(Map args) {
    def env = args.env
    def appName = args.appName
    def image = args.image
    def namespace = args.namespace ?: env
    def replicas = args.replicas ?: 3
    def waitTime = args.waitTime ?: '5m'

    // 环境校验
    if (!['dev', 'staging', 'prod'].contains(env)) {
        error "deployK8s env 必须是 dev/staging/prod,当前:${env}"
    }

    // 凭据切换
    def credMap = [
        dev:     'k8s-dev-creds',
        staging: 'k8s-stg-creds',
        prod:    'k8s-prod-creds'
    ]

    withCredentials([file(
        credentialsId: credMap[env],
        variable: 'KUBECONFIG'
    )]) {
        Utils.printBanner("Deploy ${appName} to ${env}")

        // 1. 替换镜像
        sh """
            kubectl --kubeconfig=\${KUBECONFIG} \
                set image deployment/${appName} \
                ${appName}=${image} \
                -n ${namespace}
        """

        // 2. 等待 rollout
        sh """
            kubectl --kubeconfig=\${KUBECONFIG} \
                rollout status deployment/${appName} \
                -n ${namespace} \
                --timeout=${waitTime}
        """

        // 3. 健康检查
        K8sUtils.healthCheck(
            kubeconfig: env.KUBECONFIG,
            appName: appName,
            namespace: namespace
        )
    }
}

src/com/myteam/ci/Utils.groovy(共享工具类):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/com/myteam/ci/Utils.groovy
package com.myteam.ci

class Utils implements Serializable {
    private static final long serialVersionUID = 1L

    static void printBanner(String text) {
        def line = '*' * (text.length() + 8)
        println ""
        println line
        println "*** ${text} ***"
        println line
    }

    static String getGitShortSha() {
        return sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
    }

    static Map parseEnv(String envStr) {
        def parts = envStr.split('/')
        return [
            cluster: parts[0],
            namespace: parts.size() > 1 ? parts[1] : 'default'
        ]
    }
}

声明式 Pipeline 里调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// order-service/Jenkinsfile
@Library('jenkins-shared-library@1.0.0') _   // 锁版本!

pipeline {
    agent none
    stages {
        stage('Build') {
            agent { label 'maven' }
            steps {
                buildMaven(
                    projectName: 'order-service',
                    goals: 'clean package',
                    skipTests: false
                )
            }
        }
        stage('Deploy') {
            agent { label 'k8s' }
            steps {
                deployK8s(
                    env: 'dev',
                    appName: 'order-service',
                    image: "${DOCKER_IMAGE}"
                )
            }
        }
    }
}

对比两种写法

维度每个项目独立 Jenkinsfile用 Shared Library
新服务接入复制粘贴 Jenkinsfile,改 5 处加 1 个调用,改 1 处
改构建逻辑80 个 Jenkinsfile 改 80 次共享库改 1 处,80 个服务生效
复用率30-40% 代码重复90%+ 复用
学习成本看懂每个项目的 Jenkinsfile看懂共享库
推荐5 个服务以内5+ 服务必上

5.3 版本管理与灰度发布

共享库版本策略

1
2
3
4
5
6
# 共享库仓库打 tag
git tag -a v1.0.0 -m "stable"
git push origin v1.0.0

# Jenkinsfile 锁版本
@Library('jenkins-shared-library@1.0.0') _

SemVer 规范

  • v1.x.y:1.x 互相兼容,小版本 y 加 feature
  • v2.0.0:大版本不兼容,必须升级所有调用方

灰度发布策略(推荐):

Jenkins 全局配置共享库

  • Manage Jenkins → System → Global Pipeline Libraries
  • Name: jenkins-shared-library
  • Default version: main(开发分支)
  • 勾选:Allow default version to be overridden / Include @Library changes in build results
  • Retrieval method: Modern SCMGit
  • Project repository: git@github.com:myteam/jenkins-shared-library.git
  • Credentials: github-ssh-key

强制锁版本

Jenkinsfile 第一行必须显式写版本号——@Library('jenkins-shared-library@1.0.0') _不能省略版本。否则共享库主干一改,所有 Pipeline 立刻受影响,生产事故高发

5.4 共享库单元测试

共享库没有测试 = 改一行所有服务崩。Jenkins 官方推荐 Jenkins Pipeline Unit 框架:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// test/com/myteam/ci/UtilsTest.groovy
package com.myteam.ci

import org.junit.Test
import static org.junit.Assert.assertEquals

class UtilsTest {
    @Test
    void testParseEnv() {
        def result = Utils.parseEnv('prod/order')
        assertEquals 'prod', result.cluster
        assertEquals 'order', result.namespace
    }

    @Test
    void testParseEnvWithoutNamespace() {
        def result = Utils.parseEnv('dev')
        assertEquals 'dev', result.cluster
        assertEquals 'default', result.namespace
    }
}

跑测试:

1
./gradlew test   # 或 mvn test

测试覆盖率要求:共享库核心方法覆盖率 ≥ 80%。


第五章总结:共享库让 80 个微服务共用同一份构建/部署逻辑,改一处生效全栈版本管理 + 灰度发布保证升级安全。

下一章我们把流水线接到容器化部署——Jenkins 构建 Docker 镜像 + 推 Harbor + Helm 打包 K8s manifests。


六、Docker + K8s 集成

6.1 Docker Agent 设计:DIND vs Docker Socket

Jenkins 构建 Docker 镜像有 3 种方案,选错会爆

方案原理优势劣势
DIND(Docker in Docker)Agent 容器内跑独立 docker daemon完全隔离、安全慢(每个 build 起 daemon)、镜像缓存不共享
Socket 挂载挂宿主机 /var/run/docker.sock快(复用宿主机 daemon)、镜像共享权限大(容器内能改宿主机所有镜像)
Kaniko不需要 docker daemon,纯 rootless适合 K8s Pod 内构建、rootless学习曲线、调试难

我的选型

  • 本地 demo / 开发环境Socket 挂载——构建快、镜像能直接在 K8s 看到
  • 生产 CIDIND——隔离好,每个 build 是干净的 docker daemon
  • K8s Pod AgentKaniko——Pod 内 rootless 构建

DIND 配置(生产推荐):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# docker-compose.yml
docker-agent:
  image: docker:24-dind
  privileged: true                              # DIND 必须特权模式
  environment:
    - DOCKER_TLS_CERTDIR=                       # 关闭 TLS(容器内用)
  volumes:
    - docker_data:/var/lib/docker               # 独立 docker 数据卷
  networks:
    - jenkins-net

Socket 挂载配置(本地 demo):

1
2
3
4
5
6
# docker-compose.yml
maven-agent:
  image: maven:3.9-eclipse-temurin-17
  volumes:
    - /var/run/docker.sock:/var/run/docker.sock
    - /usr/bin/docker:/usr/bin/docker:ro

注意

  • Socket 挂载时 user 必须是 root(默认 user 1000 没有 docker.sock 权限)
  • 或者把 docker.sock 权限改成 666(chmod 666 /var/run/docker.sock
  • 容器内能调宿主机 docker = 能 rm 任何镜像、起任何容器生产环境慎用

6.2 Dockerfile 最佳实践

生产级 Dockerfile(Java 17 + Spring Boot):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# syntax=docker/dockerfile:1.7
# 多阶段构建:第一阶段用 JDK 编译,第二阶段用 JRE 跑

# ===== Stage 1: Build =====
FROM eclipse-temurin:17-jdk AS builder
WORKDIR /build

# 1. 先复制 POM(利用 Docker 缓存)
COPY pom.xml ./
COPY common-lib/pom.xml common-lib/
COPY order-service/pom.xml order-service/
RUN mvn -B -T 4 -q dependency:go-offline

# 2. 复制源码
COPY . .

# 3. 打包
RUN mvn -B -pl order-service -am -DskipTests clean package

# ===== Stage 2: Runtime =====
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app

# 4. 安装必要工具
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl tini && \
    rm -rf /var/lib/apt/lists/*

# 5. 复制 jar
COPY --from=builder /build/order-service/target/*.jar app.jar

# 6. 标签元数据
LABEL org.opencontainers.image.title="order-service" \
      org.opencontainers.image.version="${VERSION}" \
      org.opencontainers.image.revision="${GIT_SHA}" \
      org.opencontainers.image.source="https://github.com/myteam/order-service"

# 7. 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
  CMD curl -f http://localhost:8080/actuator/health || exit 1

# 8. 用 tini 作为 init 进程(处理僵尸进程)
ENTRYPOINT ["/usr/bin/tini", "--", "java", \
            "-XX:+UseContainerSupport", \
            "-XX:MaxRAMPercentage=75.0", \
            "-Djava.security.egd=file:/dev/./urandom", \
            "-jar", "/app/app.jar"]

EXPOSE 8080

5 大最佳实践

  1. 多阶段构建:构建阶段用 JDK(含编译工具),运行阶段用 JRE(省 200MB)
  2. 先 COPY POM 再 COPY 源码:利用 Docker layer 缓存,Maven 依赖不重下
  3. -XX:MaxRAMPercentage=75.0:让 JVM 自动适配 K8s limit,不写死 -Xmx
  4. tini 作为 init 进程:处理孤儿进程、转发信号,Java 进程优雅退出
  5. HEALTHCHECK 内置:让 K8s 知道容器是否健康,livenessProbe 不用调外部脚本

镜像体积优化对比

基础镜像体积启动时间
openjdk:17-jdk700MB
eclipse-temurin:17-jdk480MB
eclipse-temurin:17-jre280MB
eclipse-temurin:17-jre-alpine180MB最快
eclipse-temurin:17-jre-jammy280MB快(推荐)

Alpine 注意:musl libc 跟 glibc 不完全兼容,Netty / DNS 解析偶尔有坑,生产建议用 jammy(Ubuntu 22.04 基础)。

6.3 Helm Chart 打包:Deployment + Service + Ingress

Helm Chart 目录结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
charts/order-service/
├── Chart.yaml
├── values.yaml
├── values-dev.yaml
├── values-staging.yaml
├── values-prod.yaml
└── templates/
    ├── deployment.yaml
    ├── service.yaml
    ├── ingress.yaml
    ├── configmap.yaml
    ├── serviceaccount.yaml
    ├── hpa.yaml
    ├── pdb.yaml
    └── _helpers.tpl

Chart.yaml

1
2
3
4
5
6
7
8
9
apiVersion: v2
name: order-service
description: Order Service for MyTeam
type: application
version: 1.0.0
appVersion: "1.0.0"
maintainers:
  - name: MyTeam DevOps
    email: devops@xxx.com

values.yaml(默认值):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
replicaCount: 3

image:
  repository: harbor.xxx.com/myteam/order-service
  pullPolicy: IfNotPresent
  tag: ""  # 留空,CI 覆盖

imagePullSecrets:
  - name: harbor-pull-secret

service:
  type: ClusterIP
  port: 8080
  targetPort: 8080

ingress:
  enabled: true
  className: nginx
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
  hosts:
    - host: order-service.xxx.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - hosts:
        - order-service.xxx.com
      secretName: order-service-tls

resources:
  requests:
    cpu: 500m
    memory: 1Gi
  limits:
    cpu: 2000m
    memory: 2Gi

autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70

probes:
  liveness:
    path: /actuator/health/liveness
    initialDelaySeconds: 60
    periodSeconds: 10
  readiness:
    path: /actuator/health/readiness
    initialDelaySeconds: 30
    periodSeconds: 5

env:
  - name: SPRING_PROFILES_ACTIVE
    value: prod
  - name: JAVA_OPTS
    value: "-Xms512m -Xmx1536m"

templates/deployment.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "order-service.fullname" . }}
  labels:
    {{- include "order-service.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "order-service.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "order-service.selectorLabels" . | nindent 8 }}
    spec:
      imagePullSecrets:
        {{- toYaml .Values.imagePullSecrets | nindent 8 }}
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.service.targetPort }}
          livenessProbe:
            {{- toYaml .Values.probes.liveness | nindent 12 }}
          readinessProbe:
            {{- toYaml .Values.probes.readiness | nindent 12 }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          env:
            {{- toYaml .Values.env | nindent 12 }}

多环境 values 覆盖values-prod.yaml):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
replicaCount: 5

resources:
  requests:
    cpu: 1000m
    memory: 2Gi
  limits:
    cpu: 4000m
    memory: 4Gi

autoscaling:
  minReplicas: 5
  maxReplicas: 20

env:
  - name: SPRING_PROFILES_ACTIVE
    value: prod
  - name: LOG_LEVEL
    value: INFO

6.4 Jenkinsfile 中调用 Helm 部署

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
stage('Deploy with Helm') {
    agent { label 'k8s' }
    steps {
        script {
            def credMap = [
                dev:     'k8s-dev-creds',
                staging: 'k8s-stg-creds',
                prod:    'k8s-prod-creds'
            ]
            withCredentials([file(
                credentialsId: credMap[params.ENV],
                variable: 'KUBECONFIG'
            )]) {
                // 1. 校验
                sh """
                    helm lint ./charts/order-service \
                        --values ./charts/order-service/values-${params.ENV}.yaml
                """

                // 2. 模板预览(dry run)
                sh """
                    helm template order-service ./charts/order-service \
                        --values ./charts/order-service/values-${params.ENV}.yaml \
                        --set image.tag=${IMAGE_TAG}
                """

                // 3. 部署
                sh """
                    helm upgrade --install order-service ./charts/order-service \
                        --kubeconfig=\${KUBECONFIG} \
                        --namespace ${K8S_NAMESPACE} \
                        --values ./charts/order-service/values-${params.ENV}.yaml \
                        --set image.tag=${IMAGE_TAG} \
                        --wait \
                        --timeout 5m \
                        --history-max 10
                """
            }
        }
    }
}

关键参数解析

  • --wait:等到所有 Pod Ready 才返回(阻塞)
  • --timeout 5m:超时 5 分钟
  • --history-max 10:保留 10 个历史版本(便于回滚
  • --set image.tag=${IMAGE_TAG}:动态覆盖镜像 tag

回滚命令(生产事故救命):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 列出历史版本
helm history order-service -n prod
# 输出:
# REVISION  STATUS    CHART           APP VERSION  DESCRIPTION
# 1         failed    order-service   1.0.0        Install failed
# 2         superseded order-service  1.0.0        Upgrade complete
# 3         deployed  order-service   1.0.0        Upgrade complete
# 4         superseded order-service  1.0.0        Upgrade complete
# 5         deployed  order-service   1.0.0        Upgrade complete

# 回滚到版本 4
helm rollback order-service 4 -n prod

Helm + GitOps:把 values.yaml + Chart.yaml + templates/ 推 Git,Argo CD 监听 Git 自动部署强烈推荐,下章细讲)。


第六章总结:Docker 多阶段构建 + 镜像体积优化 + Helm Chart 多环境覆盖 + Jenkinsfile 调 Helm 部署,容器化部署全链路打通

下一章我们把 K8s 原生蓝绿发布做透——双 Service + 双 Deployment + 一键切流。


七、蓝绿发布全流程

7.1 蓝绿发布原理:双 Service 双 Deployment

到 2022 年中,我们团队的事故率降到接近 0——主要靠「蓝绿发布」兜底。

蓝绿发布的核心思想:任意时刻只有一套环境接流量。新版本先部署到「非活跃」环境,验证通过后一键切流量;出问题了切回原版本,2 秒回滚

4 大优势

  1. 回滚极快——切 Service selector 而非重新部署,2 秒回滚
  2. 风险窗口小——切流瞬间只有「活跃服务」接流量,没有「老服务还在接新流量」的中间态
  3. 零停机——整个过程用户无感知
  4. 数据库兼容测试——可以在 green 跑数据库 schema migration,确认无问题再切

3 大劣势

  1. 资源 2x——同时跑 2 套环境,资源成本翻倍
  2. 数据库 schema 必须向前向后兼容——green 和 blue 读同一份数据库,不能有 incompatible 变更
  3. 不适合「session 黏性」应用——切流后用户的 session 状态丢失(除非共享 session 存储)

适用场景

  • 金融、支付类(风险厌恶
  • 7x24 业务(不能有停机窗口
  • 单服务变更不频繁(资源成本可接受

7.2 K8s 资源模板:双 Service + 双 Deployment

核心资源:1 个 Ingress(对外)+ 2 个 Service(blue/green)+ 2 个 Deployment(blue/green)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# deployment-blue.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service-blue
  labels:
    app: order-service
    version: blue
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
      version: blue
  template:
    metadata:
      labels:
        app: order-service
        version: blue
    spec:
      containers:
        - name: order-service
          image: harbor.xxx.com/myteam/order-service:v1.0.0
          ports:
            - containerPort: 8080
          resources:
            requests: { cpu: '500m', memory: '1Gi' }
            limits:   { cpu: '2000m', memory: '2Gi' }
          readinessProbe:
            httpGet: { path: /actuator/health/readiness, port: 8080 }
            initialDelaySeconds: 30
            periodSeconds: 5
---
# deployment-green.yaml(与 blue 完全相同,只是 name 和 version 不同)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service-green
  labels:
    app: order-service
    version: green
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
      version: green
  template:
    metadata:
      labels:
        app: order-service
        version: green
    spec:
      containers:
        - name: order-service
          image: harbor.xxx.com/myteam/order-service:v1.0.0
          ports:
            - containerPort: 8080 }
          # ... 同 blue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# service-blue.yaml(流量接到 blue)
apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service
    version: blue       # ← 关键:这里决定流量去哪个 Deployment
  ports:
    - port: 80
      targetPort: 8080
---
# service-green-dummy.yaml(用于健康检查)
apiVersion: v1
kind: Service
metadata:
  name: order-service-green-debug
spec:
  selector:
    app: order-service
    version: green
  ports:
    - port: 80
      targetPort: 8080

关键设计

  • 主 Service 名为 order-service不带 blue/green 后缀——Ingress 引用它,对外 URL 不变
  • service-green-debug 只用于调试访问(开发/QA 内部访问 green 版本,不影响生产流量
  • HPA 必须分别配——blue HPA 和 green HPA 独立伸缩
  • PodDisruptionBudget 分别配——保证滚动更新时至少 1 个 Pod 可用

7.3 Jenkinsfile 蓝绿切流

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
stage('Blue-Green Deploy') {
    agent { label 'k8s' }
    steps {
        script {
            def credId = credMap[params.ENV]
            withCredentials([file(credentialsId: credId, variable: 'KUBECONFIG')]) {
                // 1. 判定当前活跃色
                def activeColor = sh(
                    returnStdout: true,
                    script: """
                        kubectl --kubeconfig=\${KUBECONFIG} get svc order-service \
                            -n ${K8S_NAMESPACE} \
                            -o jsonpath='{.spec.selector.version}'
                    """
                ).trim()
                def standbyColor = (activeColor == 'blue') ? 'green' : 'blue'

                echo "当前活跃:${activeColor},将部署到:${standbyColor}"

                // 2. 部署新版本到 standby
                sh """
                    kubectl --kubeconfig=\${KUBECONFIG} \
                        set image deployment/order-service-${standbyColor} \
                        order-service=${DOCKER_IMAGE} \
                        -n ${K8S_NAMESPACE}

                    kubectl --kubeconfig=\${KUBECONFIG} \
                        rollout status deployment/order-service-${standbyColor} \
                        -n ${K8S_NAMESPACE} \
                        --timeout=5m
                """

                // 3. 健康检查(访问 green-debug Service)
                sh """
                    sleep 30
                    for i in {1..10}; do
                        if kubectl --kubeconfig=\${KUBECONFIG} \
                            exec deploy/order-service-${standbyColor} \
                            -n ${K8S_NAMESPACE} -- \
                            curl -sf http://localhost:8080/actuator/health; then
                            echo "健康检查通过"
                            break
                        fi
                        echo "等待健康检查... \${i}/10"
                        sleep 10
                    done
                """

                // 4. 灰度(可选):先切 10% 流量观察
                if (params.ENV == 'prod') {
                    input(
                        message: "新版本已部署到 ${standbyColor},是否切流?",
                        ok: "切流到 ${standbyColor}",
                        submitter: 'tech-lead,devops'
                    )
                }

                // 5. 切流量
                sh """
                    kubectl --kubeconfig=\${KUBECONFIG} \
                        patch svc order-service \
                        -n ${K8S_NAMESPACE} \
                        -p '{"spec":{"selector":{"version":"${standbyColor}"}}}'

                    echo "流量已切到 ${standbyColor}"
                """

                // 6. 观察 1 小时
                echo "观察 1 小时(保留 ${activeColor} 部署做回滚缓冲)"
                sleep 3600

                // 7. 清理旧部署
                sh """
                    kubectl --kubeconfig=\${KUBECONFIG} \
                        scale deployment/order-service-${activeColor} \
                        --replicas=0 \
                        -n ${K8S_NAMESPACE}
                """
            }
        }
    }
}

5 大安全卡点

  1. 判定活跃色——自动判断当前在 blue 还是 green
  2. 健康检查——切流前确认新版本健康
  3. 生产人工审批——input 步骤让 tech-lead 确认
  4. 保留旧部署 1 小时——出问题一键切回
  5. 1 小时后清理——避免资源浪费

回滚操作(生产事故):

1
2
3
# 2 秒回滚(假设当前在 green,blue 还在)
kubectl patch svc order-service -n prod \
  -p '{"spec":{"selector":{"version":"blue"}}}'

回滚后检查清单

  • 监控告警是否恢复(错误率、延迟)
  • 用户反馈(客服群、社交媒体)
  • 业务数据(订单量、转化率)
  • 复盘事故根因

7.4 蓝绿 vs Argo Rollouts:什么时候用哪个

维度蓝绿Argo Rollouts
切流粒度二选一(100% / 0%)任意比例(1% / 10% / 50% / 100%)
自动判定需要人工审批可按 Prometheus 指标自动推进/回滚
资源占用2x1.x(滚动过程短暂 2x)
复杂度中(手工管)高(Argo Rollouts 概念多)
适用风险厌恶业务快速迭代业务

我的混合策略

  • 核心业务(支付、订单):用蓝绿——简单、可控、回滚 2 秒
  • 非核心业务(CMS、运营后台):用 Argo Rollouts 金丝雀——自动化、节省资源

下一章我们把 Argo Rollouts 金丝雀做透——按 Prometheus 指标自动推进/回滚。


八、金丝雀发布全流程

8.1 金丝雀 vs 蓝绿:渐进式 vs 一次性

2023 年初,我们团队开始用 Argo Rollouts 做金丝雀发布——金丝雀是「渐进式切流」,蓝绿是「一次性切流」

5 大优势

  1. 风险最小——开始只有 5% 流量出错,爆炸半径小
  2. 指标驱动——按 Prometheus 指标自动判定,不需要人工盯
  3. 资源相对省——不像蓝绿需要 2x 资源,只是短暂 2x
  4. 可重复——同样的发布配置,每个服务都能复用
  5. 可观测——每个阶段的指标都看得到

3 大挑战

  1. 概念多——Rollout、AnalysisTemplate、AnalysisRun、Experiment、Step
  2. 依赖 Prometheus——必须先有完善的指标采集
  3. 调试复杂——出问题需要看 Rollout 状态 + AnalysisRun 详情 + Service Mesh 流量分发

8.2 Argo Rollouts 是什么:K8s Deployment 的「超集」

Argo RolloutsIntuit 开源(被 CNCF 接纳为 Incubating)的 K8s 控制器,提供高级部署策略

  • Blue-Green(蓝绿)
  • Canary(金丝雀)
  • Progressive Delivery(渐进式)
  • Analysis(基于指标的自动判定)

3 个核心 CRD

  • Rollout:替代 Deployment 的部署对象,支持蓝绿/金丝雀策略
  • AnalysisTemplate:指标查询模板(PromQL + 成功/失败条件)
  • AnalysisRun:AnalysisTemplate 的一次执行,返回 success / failure

安装(在 k3d 本地或生产 K8s):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 安装 controller
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/download/v1.7.0/install.yaml

# 安装 kubectl plugin(本地)
curl -LO https://github.com/argoproj/argo-rollouts/releases/download/v1.7.0/kubectl-argo-rollouts-linux-amd64
chmod +x kubectl-argo-rollouts-linux-amd64
mv kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts

# 验证
kubectl argo rollouts version

8.3 Rollout 资源模板

rollout.yaml(金丝雀策略,5 阶段渐进):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: order-service
  namespace: prod
spec:
  replicas: 5
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: order-service
  strategy:
    canary:
      # 5 阶段渐进:5% → 15% → 30% → 50% → 100%
      steps:
        - setWeight: 5
        - pause: { duration: 5m }                   # 5 分钟观察
        - setWeight: 15
        - pause: { duration: 5m }
        - setWeight: 30
        - pause: { duration: 5m }
        - setWeight: 50
        - pause: { duration: 5m }
        # 最后一步:人工确认才 100%
        - setWeight: 80
        - pause: { duration: 10m }
        # 自动判定:跑 AnalysisTemplate
        - analysis:
            templates:
              - templateName: success-rate
              - templateName: latency-p99
        - setWeight: 100
      # 流量切分:Nginx Ingress
      trafficRouting:
        nginx:
          stableIngressName: order-service-stable
          additionalIngressAnnotations:
            canary-by-header: X-Canary              # 支持按 header 切
            canary-by-header-value: enabled
      # 失败回滚:自动 abort
      abortScaleDownDelaySeconds: 30
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: harbor.xxx.com/myteam/order-service:v1.0.0
          ports:
            - containerPort: 8080
          resources:
            requests: { cpu: '500m', memory: '1Gi' }
            limits:   { cpu: '2000m', memory: '2Gi' }
          readinessProbe:
            httpGet: { path: /actuator/health/readiness, port: 8080 }
            initialDelaySeconds: 30
            periodSeconds: 5

配套 Service(金丝雀模式下 Service 指向 stable Service):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
apiVersion: v1
kind: Service
metadata:
  name: order-service-stable
spec:
  selector:
    app: order-service
  ports:
    - port: 80
      targetPort: 8080

配套 Ingress(用 Nginx Ingress 做流量切分):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: order-service-stable
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: order-service.xxx.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: order-service-stable
                port:
                  number: 80

Argo Rollouts 自动生成 order-service-canary Ingress,Nginx 控制器按 weight 分发流量

8.4 AnalysisTemplate:按 Prometheus 指标自动判定

3 个核心指标模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# 1. 成功率(HTTP 5xx 率 < 1%)
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
  namespace: prod
spec:
  args:
    - name: service-name
    - name: canary-hash
  metrics:
    - name: http-5xx-rate
      interval: 60s
      count: 5                          # 连续 5 次(5 分钟)指标都满足才通过
      successCondition: result[0] < 0.01
      failureLimit: 2                   # 连续 2 次失败就 abort
      failureCondition: result[0] > 0.05
      provider:
        prometheus:
          address: http://prometheus.monitoring.svc.cluster.local:9090
          query: |
            sum(rate(
              http_requests_total{
                app="{{args.service-name}}",
                version="canary",
                status=~"5.."
              }[5m]
            )) /
            sum(rate(
              http_requests_total{
                app="{{args.service-name}}",
                version="canary"
              }[5m]
            ))
---
# 2. P99 延迟(< 500ms)
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: latency-p99
  namespace: prod
spec:
  args:
    - name: service-name
  metrics:
    - name: p99-latency
      interval: 60s
      count: 5
      successCondition: result[0] < 0.5
      failureLimit: 2
      provider:
        prometheus:
          address: http://prometheus.monitoring.svc.cluster.local:9090
          query: |
            histogram_quantile(0.99,
              sum(rate(
                http_request_duration_seconds_bucket{
                  app="{{args.service-name}}",
                  version="canary"
                }[5m]
              )) by (le)
            )
---
# 3. 业务指标(订单成功率 > 99%)
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: business-conversion
  namespace: prod
spec:
  args:
    - name: service-name
  metrics:
    - name: order-success-rate
      interval: 60s
      count: 5
      successCondition: result[0] > 0.99
      failureLimit: 2
      provider:
        prometheus:
          address: http://prometheus.monitoring.svc.cluster.local:9090
          query: |
            sum(rate(
              orders_created_total{
                app="{{args.service-name}}",
                status="success",
                version="canary"
              }[5m]
            )) /
            sum(rate(
              orders_created_total{
                app="{{args.service-name}}",
                version="canary"
              }[5m]
            ))

判定逻辑解读

  • interval: 60s + count: 5 = 每 60 秒查一次,连续 5 次都满足才通过
  • successCondition: result[0] < 0.01 = 5xx 率 < 1% 算成功
  • failureLimit: 2 = 连续 2 次失败就 abort
  • 业务指标 + 技术指标双保险——不仅 HTTP 5xx 少,订单成功率也要高

4 大推荐指标

指标类型阈值失败动作
HTTP 5xx 率< 1%abort
P99 延迟< 500msabort
JVM 堆使用率< 80%warn(不 abort)
业务转化率> 99%abort

8.5 Jenkinsfile + Argo Rollouts 调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
stage('Canary Deploy') {
    agent { label 'k8s' }
    steps {
        script {
            def credId = credMap[params.ENV]
            withCredentials([file(credentialsId: credId, variable: 'KUBECONFIG')]) {
                // 1. 更新 Rollout 镜像
                sh """
                    kubectl --kubeconfig=\${KUBECONFIG} \
                        argo rollouts set image order-service \
                        order-service=${DOCKER_IMAGE} \
                        -n ${K8S_NAMESPACE}
                """

                // 2. 阻塞等金丝雀完成
                sh """
                    kubectl --kubeconfig=\${KUBECONFIG} \
                        argo rollouts get rollout order-service \
                        -n ${K8S_NAMESPACE} \
                        --watch \
                        --timeout 1h
                """

                // 3. 检查结果
                sh """
                    STATUS=\$(kubectl --kubeconfig=\${KUBECONFIG} \
                        argo rollouts get rollout order-service \
                        -n ${K8S_NAMESPACE} \
                        -o jsonpath='{.status.phase}')

                    if [ "\$STATUS" != "Healthy" ]; then
                        echo "金丝雀发布未健康完成:\$STATUS"
                        exit 1
                    fi
                """
            }
        }
    }

    post {
        success {
            echo '金丝雀发布成功!'
        }
        failure {
            script {
                // 自动 abort + 告警
                sh """
                    kubectl --kubeconfig=\${KUBECONFIG} \
                        argo rollouts abort order-service \
                        -n ${K8S_NAMESPACE}
                """
                // 钉钉告警
                ...
            }
        }
    }
}

5 个核心操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 1. 查看状态
kubectl argo rollouts get rollout order-service -n prod

# 2. 暂停金丝雀(手动介入)
kubectl argo rollouts pause order-service -n prod

# 3. 恢复金丝雀
kubectl argo rollouts resume order-service -n prod

# 4. 手动 promote 到 100%
kubectl argo rollouts promote order-service -n prod

# 5. Abort 回滚
kubectl argo rollouts abort order-service -n prod

Dashboard 可视化(强烈推荐):

1
2
3
4
5
6
7
8
# 安装 Argo Rollouts Dashboard
kubectl apply -f https://github.com/argoproj/argo-rollouts/releases/download/v1.7.0/dashboard-install.yaml

# 端口转发
kubectl port-forward svc/argo-rollouts-dashboard 31000:31000 -n argo-rollouts

# 访问 http://localhost:31000
# 可以看到所有 Rollout 的金丝雀进度、AnalysisTemplate 状态、历史

8.6 金丝雀 5 大实战经验

  1. 指标必须有,否则别上金丝雀——没指标的「渐进式发布」等于「盲切」,出问题不知道怎么回滚
  2. prometheus-adapter 必须装——Argo Rollouts 通过 ServiceMonitor 找 Prometheus
  3. canary-hash 标签自动注入——Rollout Controller 会给金丝雀 Pod 打 rollouts-pod-template-hash 标签,PromQL 用来区分版本
  4. 可以按 header 切(灰度测试)——设置 X-Canary: enabled header,流量强制走新版本,QA 验证用
  5. 失败立即 abort——failureLimit: 2不要等 5 次失败才反应(5 分钟已经太久了)

第八章总结:Argo Rollouts 是 K8s Deployment 的「超集」,金丝雀发布按 Prometheus 指标自动推进/回滚,5 阶段渐进(5% → 15% → 30% → 50% → 100%),Jenkinsfile 一行命令触发


九、收尾:从 CI 到 GitOps 闭环

9.1 Argo CD 集成:让 CI 推 Git、CD 同步 K8s

到 2024 年,我们团队把流水线演进到 GitOps 闭环——CI 和 CD 完全解耦

  • Jenkins 责任:代码 → 构建 → 测试 → 扫描 → 推镜像
  • Argo CD 责任:监听 Git 仓库 → 同步 K8s 状态

3 大优势

  1. K8s 状态可追溯——Git 是 single source of truth,所有部署都能从 Git 还原
  2. 一键回滚——git revert 一次 commit,Argo CD 自动同步,回滚时间从 5 分钟缩到 30 秒
  3. 多环境标准化——dev / staging / prod 用同一份 Helm Chart + 不同 values,K8s 部署逻辑一致

Argo CD Application 模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: order-service-prod
  namespace: argocd
spec:
  project: default
  source:
    repoURL: git@github.com:myteam/order-service.git
    targetRevision: HEAD
    path: charts/order-service
    helm:
      valueFiles:
        - values-prod.yaml
  destination:
    server: https://kubernetes.default.svc
    namespace: prod
  syncPolicy:
    automated:
      prune: true                    # 自动清理孤儿资源
      selfHeal: true                 # 自动修复偏离
    syncOptions:
      - CreateNamespace=true

Jenkinsfile 只需要 update Git,不再 apply K8s

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
stage('Update Git Manifest') {
    agent { label 'maven' }
    steps {
        withCredentials([usernamePassword(
            credentialsId: 'github-token',
            usernameVariable: 'GIT_USER',
            passwordVariable: 'GIT_TOKEN'
        )]) {
            sh """
                # 1. 配置 git
                git config user.email "jenkins@xxx.com"
                git config user.name "Jenkins Bot"

                # 2. 更新 Helm values 里的 image.tag
                sed -i "s/tag: .*/tag: ${IMAGE_TAG}/" \
                    charts/order-service/values-prod.yaml

                # 3. 提交 + push
                git add charts/order-service/values-prod.yaml
                git commit -m "ci: update image tag to ${IMAGE_TAG} [skip ci]"
                git push https://${GIT_USER}:${GIT_TOKEN}@github.com/myteam/order-service.git
            """
        }
    }
}

Argo CD 自动监听 Git,30 秒内自动同步到 K8s

Why GitOps 优于传统 Jenkins apply

维度传统 JenkinsGitOps
K8s 状态源Jenkins 任务(临时)Git(永久)
一键回滚重新跑 Jenkins jobgit revert
集群被改后修复需要手动 sync自动 selfHeal
多集群管理需配多个 Jenkins job1 个 Argo App
审计Jenkins 日志Git commit 历史

9.2 配套工具链 + 系列回顾

完整 CI/CD 工具链地图

6 大配套工具

工具用途部署位置
Prometheus指标采集 + 告警K8s 内(prometheus-operator)
Grafana指标可视化K8s 内
Loki日志聚合K8s 内
AlertManager告警路由K8s 内
Jaeger分布式追踪K8s 内(可选)
Argo CDGitOps 控制器K8s 内(argocd namespace)

监控流水线本身的指标

  • 构建成功率(按服务)—— 失败率 > 10% 告警
  • 构建时长(P50 / P95 / P99)—— P95 > 30 分钟告警
  • 镜像大小 —— 单镜像 > 1GB 告警
  • Maven 依赖下载量 —— 单构建 > 2GB 告警
  • Jenkins 队列长度 —— 等待 > 10 个 job 告警

系列回顾(Java Web 微服务 1-9 篇)

主题类型
1异地多活:高可用终极形态架构
2反规范化设计:性能与一致性的平衡架构
3K8s 容器编排基础设施
4技术选型:Spring Cloud Alibaba + Dubbo 3架构
5Nacos:服务中心 + 配置中心架构
6流量调度:网关 + 限流 + 负载均衡架构
7熔断限流:Sentinel + Resilience4j架构
8数据库演化:MySQL → 分布式数据
9Jenkins 流水线:完整 CI/CD 实战工程收官
后续可观测性:Metrics / Logging / Tracing 三支柱待写

一句话总结

「Jenkins 不是银弹,Pipeline as Code + GitOps + 渐进式发布才是 2026 年 Java 微服务交付的答案。」


附录 A:常见问题 FAQ

Q1:Jenkins 2 已经够用,为什么不用 GitLab CI / GitHub Actions / Tekton?

A:Jenkins 在「插件生态」和「国内企业落地经验」上还是最强的。GitLab CI 适合 GitLab 仓库一体化的团队;GitHub Actions 适合开源项目;Tekton 是云原生未来但生态还弱。对绝大多数中大企业,Jenkins 仍是首选

Q2:Jenkinsfile 必须放在项目根目录吗?

A:默认是。也可以放在子目录(如 cicd/Jenkinsfile),但 Jenkins Job 创建时要选「Pipeline script from SCM」并指定 Script Path。

Q3:build 出来的 jar 怎么找?

A:两种方式:

  1. archiveArtifacts 归档(推荐)—— 在 Jenkins Job 页面「Build Artifacts」下载
  2. 推 Nexus —— 用 Nexus Artifact Uploader 插件

Q4:生产环境怎么回滚到 3 个版本之前?

A:

  • Helm:helm rollback order-service 3(3 是 revision number)
  • Argo Rollouts:kubectl argo rollouts undo order-service --to-revision=3
  • 更推荐 GitOpsgit revert 对应 commit,Argo CD 自动同步

Q5:流水线跑一半挂了,重跑时跳过已经成功的 stage 怎么实现?

A:Jenkins 原生不支持 stage 级别 skip,两个变通方案

  • 每个 stage 加 when 条件判断参数
  • 用 Pipeline Stage 视图手动「重跑此 stage」(Blue Ocean 插件支持)

Q6:Argo Rollouts 和 Istio VirtualService 怎么选?

A:

  • 简单场景:用 Argo Rollouts + Nginx Ingress(本文方案
  • 复杂服务网格:用 Istio VirtualService(更强大但更重)
  • 多协议支持:用 SMI(Service Mesh Interface)

附录 B:Jenkins 性能调优清单

如果你遇到 Jenkins 慢/卡/OOM,按这个清单逐项排查:

现象根因优化
早高峰 job 排队Agent 数量不足加 maven / docker agent
Master 内存持续上涨插件内存泄漏升级插件 / 减插件 / 调大 -Xmx
第一次构建 5 分钟镜像拉取慢预热基础镜像 / 用 k3d image import
Maven 下载慢无本地缓存挂 .m2 目录到 host
Docker build 慢镜像没缓存优化 Dockerfile 层级 / 用 BuildKit
Webhook 不触发网络/防火墙检查 tcpdump + Jenkins Manage Webhooks 日志
制品归档慢归档大文件不归档 target/ 整个目录,只归档 jar
SonarQube 扫描慢仓库太大拆分项目 / 增量扫描
磁盘满工作区残留装 Workspace Cleanup 插件 + 定时清理

JVM 调优(生产 Master):

1
2
# 8G 内存的 Master
export JAVA_OPTS="-Xms4g -Xmx6g -XX:MaxMetaspaceSize=1g -XX:+UseG1GC -XX:MaxGCPauseMillis=200"

Master 高可用(生产级):

  • 2 个 Master 节点共享 NFS 卷
  • 前面挂 HAProxy / Nginx 做 active-passive
  • 共享存储用 GlusterFS / Ceph

第九章总结 + 系列收官:Jenkins Pipeline + GitOps + 渐进式发布,从代码提交到金丝雀 100% 全自动。配套 Prometheus + Grafana + Loki + Argo CD,完整可观测 + 完整可回滚。这是 Java 微服务从「架构」走向「生产」的最后一公里。

下一篇文章将进入可观测性专题——Metrics / Logging / Tracing 三支柱怎么落地,敬请期待。

本系列共 16 篇,本文为第 11 篇 · 查看全部
使用 Hugo 构建
主题 StackJimmy 设计