先说一个我上周刚处理完的真实场景。有个朋友的项目组“自动化”搞了半年,Jenkins装了两套,代码也能定时构建,但每次上线依然是开发本地打包、传到跳板机、手动登录服务器重启进程,四十分钟起步,还经常搞错版本。他问我:Jenkins到底能不能做到全自动部署?我说能,但你现在的用法只是把以前的编译动作搬到了服务器上,没有形成一条完整的CICD链路。链路才是CICD的灵魂。
这篇博文我就以Jenkins为主线,从一条真正的自动化部署链路出发,把环境搭建、初始化配置、容器内构建Docker镜像、Pipeline脚本编写、对接Kubernetes做动态构建节点,一直到最终的Git提交触发滚动更新,完整过一遍。内容偏实战,适合刚接触CICD的后端开发、想在公司内部落地自动化部署的运维同学,也适合那种“Jenkins装了但不知道下一步怎么弄”的人。
1. CICD链路全景:Jenkins到底替我们做了什么
1.1 一条合格流水线上的五个环节
很多人对CICD的理解停留在“自动打包”这一步,其实完整链路至少包含五个环节:触发、拉代码、构建、制品流转、部署验证。
触发是源头。代码推送到分支、提交MR、打tag,都应该能自动唤起构建,而不是让工程师跑去Jenkins页面点“立即构建”。拉代码是把指定分支的源码拉到构建节点的Workspace里。构建则是真正的体力活——跑单元测试、编译、打包,Java项目用Maven,前端用npm,这一步产出的是“制品”。制品流转在容器化时代基本等于“构建镜像并推送到镜像仓库”,传统项目则是归档jar包或者war包到统一的存储。最后一步是部署与验证:把制品更新到目标环境,调用健康检查接口确认服务没挂,再通过钉钉、邮件把结果扔给对应的人。
这五个环节在Jenkins里对应的就是流水线的阶段(Stage)。我后面会在实战部分给出一份完整Jenkinsfile,先记住这个概念。
1.2 两套主流架构:单机Docker版与Kubernetes版
我在不同公司见过两套最常见的落地架构。
第一套是轻量版:GitLab加Jenkins,Jenkins直接装在虚拟机或容器里,构建时用宿主机的Docker来打镜像,部署阶段通过SSH登录目标机器执行docker run或者docker compose restart。这套架构够用两三个项目的团队,维护成本低,问题在于并发一高就排队,环境多了以后脚本会逐渐走向“谁也改不动”。
第二套是生产版:GitLab、Jenkins、Harbor镜像仓库、Kubernetes集群。Jenkins本身可以部署在K8s里,也通过Kubernetes插件动态创建Pod作为执行构建的临时Agent,每跑一个任务就起一个Pod,跑完自动销毁。部署阶段直接用kubectl更新Deployment镜像版本。这套的好处是资源利用率高、扩容简单,CI和CD都能在一条流水线里串完。
本文重点讲第二套架构,因为容器化之后这套是主流,而且很多热搜词都指向这个方向,比如“jenkins容器内使用docker命令”、“jenkins 2.541.3配置kubernetes”。但每一阶段我也会指出轻量版怎么改,两套思路是共通的。
1.3 Jenkins在其中的角色与边界
一个常见的误解是:“Jenkins能构建镜像所以它自带Docker”“Jenkins能部署所以它自带kubectl”。其实不是。Jenkins是个调度中枢,它负责安排工作、传递参数、记录日志、统一权限,但真正干活的永远是Agent环境里的工具链。
这一点直接解释了为什么很多人“照着教程装了Jenkins还是跑不通”——因为教程里的Agent有Maven有Docker有kubectl,而你的Agent里可能什么都没有。理解这个边界之后,配置的时候就会习惯性问自己一句:我这个Agent环境里到底装了什么工具?
2. 装出一个好用的Jenkins:版本、初始化和国内插件源
2.1 用Docker Compose拉起2.541.3 LTS
新版本Jenkins要求JDK17,基础镜像我长期用的是jenkins/jenkins:2.541.3-lts-jdk17这个tag。LTS版本追求稳定,工作中我绝不会追新功能而主动上weekly版,身边因为插件兼容问题把生产Jenkins搞挂的案例太多了。
下面这套docker-compose是我在测试环境常用的配置:
version: "3" services: jenkins: image: jenkins/jenkins:2.541.3-lts-jdk17 container_name: jenkins restart: always user: root ports: - "8080:8080" - "50000:50000" volumes: - /data/jenkins_home:/var/jenkins_home - /var/run/docker.sock:/var/run/docker.sock - /usr/bin/docker:/usr/bin/docker environment: - TZ=Asia/Shanghai注意那个user: root。这里我做了个取舍:Jenkins容器内很多操作都涉及文件权限和socket访问,非root用户下写/var/jenkins_home以及访问docker.sock都要额外授权,测试环境用root省掉一批权限报错。生产环境如果安全要求严格,可以去掉root,再把jenkins用户加入docker组并调整挂载目录属主。二选一,不建议中间态。
启动之后先查日志拿初始密码:
docker logs jenkins | grep -A 2 "initialAdminPassword"默认密码在容器内路径是/var/jenkins_home/secrets/initialAdminPassword,日志里也会打出来。这里建议顺手把8080端口的访问加一层nginx鉴权,否则公司内网随便谁都能上去点点点。
端口50000是Jenkins的Agent通信端口(JNLP协议),如果后面对接了动态Agent、或者在外面加了独立构建机,这个端口必须保持可达。忘了开这个端口是很多人配置完新节点后一直“离线”的头号原因。
2.2 插件下载太慢的根治办法
Jenkins装完第一步永远是装插件,而插件下载慢是真老大难问题。多数情况下不是网速问题,是默认更新中心地址在国外。
根治方法:把Update Site换成国内镜像源。入口在“系统管理→插件管理→高级”,把“更新站点”URL从官方地址:
https://updates.jenkins.io/update-center.json换成清华源:
https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json提交之后建议重启一次Jenkins,然后回到“可选插件”页签,你会发现页面加载速度和插件列表刷新速度都明显好于之前。注意一点,升级Jenkins版本后镜像源有时候会短暂报错,因为更新中心的版本映射和部署版本不一致,清一下缓存或者手动验证一下json是否返回200就能判断。
如果公司内网完全隔离、下载不了任何外网资源,那就走离线方案:在一台能联网的机器上把插件包(.hpi或.jpi文件)下载好,然后在“插件管理→高级→上传插件”手动上传。这种方案有个坑,插件之间是有依赖关系的,比如装Kubernetes插件之前得先把Pipeline、Credentials、Docker Commons这些基础插件装齐,否则上传报错提示缺依赖,你只能一个接一个手动补。真实环境里我建议先离线装一个“基础全家桶”,再根据项目需要增量补充。
2.3 汉化与一张必备插件清单
汉化很简单,插件搜索“Localization: Chinese (Simplified)”,安装后重启界面就变中文。不过实操中我反而建议:看不懂的英文关键词别乱翻译,很多配置项翻译后更容易混淆。我见过同事把“丢弃旧构建”当成“删除构建历史”理解,结果误操作清掉了全部构建记录。所以掌握英文原文和中文含义同时对照,比无脑依赖汉化包靠谱。
下面是我每次新装Jenkins都会装的插件清单,按用途分好了:
| 插件 | 作用 |
|---|---|
| Git | 拉取Git仓库代码 |
| Pipeline | 声明式流水线支持 |
| Git Parameter | 构建时动态选择分支、Tag |
| Docker Pipeline | 在流水线中构建并推送镜像 |
| Kubernetes | 动态创建K8s Pod作为Agent |
| Generic Webhook Trigger | 接收GitLab等系统的Webhook |
| Credentials Binding | 安全引用账号密码、Token |
| Blue Ocean | 可视化查看流水线执行情况 |
| HTML Publisher | 输出测试报告 |
| Email Extension | 邮件通知 |
装这些就够了。Docker Pipeline和Kubernetes是本文后面两个核心功能的核心依赖,我会在对应章节展开讲。
3. 容器内构建Docker镜像:sock挂载、DinD与URI配置
3.1 为什么Jenkins容器里执行docker十有八九报错
构建镜像这个环节,你必须让某个构建环境里能执行docker build。但Jenkins容器本身默认没有Docker CLI,更没有一个可用的Docker守护进程。
第一次用容器方式部署Jenkins的人,在Pipeline里写sh 'docker build -t myapp:1.0 .'的时候,大概率会碰到两种报错。
第一种:
sh: docker: command not found这是容器里没有Docker客户端。解决方式有两个:一是在镜像里把Docker CLI装进去,二是像我上面docker-compose里那样,把宿主机的docker二进制文件挂载进容器:
- /usr/bin/docker:/usr/bin/docker第二种:
docker: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?这是客户端找到了,但连接不上守护进程。因为容器里的docker客户端默认找容器内的/var/run/docker.sock,而容器里没有这个文件,你需要把宿主机的socket挂载进去:
- /var/run/docker.sock:/var/run/docker.sock还有一个高频变种:用了root用户没事,用jenkins用户执行时报Permission denied。这个问题的根源是socket的属主是root:docker,而jenkins用户不在宿主机的docker组。我早期踩过这坑,最后直接root用户了事。
3.2 挂载宿主socket(DoS)与DinD的取舍
把宿主机的/var/run/docker.sock挂进Jenkins容器,让容器里的docker客户端操作宿主机Docker守护进程,这套方案叫Docker outside of Docker,缩写DoS,听起来绕但概念很简单。镜像构建任务本质上发生在宿主机上,Jenkins容器只是发号施令。
DoS的优点:构建速度块,基础镜像层可以直接复用宿主机的缓存;配置简单,一条挂载命令解决。缺点也明显:拿到docker.sock基本等于拿到宿主机的root权限,一旦Jenkins被攻破,整个宿主机沦陷。另外宿主机Docker版本升级后,容器里挂载的旧版本docker CLI偶尔会和新daemon通信出诡异问题。
另一套方案是DinD,即再起一个docker:dind容器作为专用的Docker守护进程:
docker-dind: image: docker:dind container_name: docker-dind privileged: true environment: - DOCKER_TLS_CERTDIR="" volumes: - /data/dind-data:/var/lib/dockerJenkins侧的构建命令通过TCP协议访问这个dind容器,配置DOCKER_HOST环境变量:
DOCKER_HOST=tcp://docker-dind:2375DinD的好处是隔离性更好,多个团队可以各用各的daemon,互不污染镜像缓存;缺点是第一次构建要全部重新拉基础镜像,慢,且要看护的组件多了一个。我的建议很明确:测试环境、小团队用DoS最省心;生产环境、多租户场景用DinD;如果已经全面容器化并且有K8s,动态Agent里的镜像构建可以优先考虑挂载sock或者在单独Pod里走DinD,这是个权衡题,没有标准答案。
3.3 Docker Host URI:一个看似简单却频繁翻车的配置项
搜索“jenkins new cloud docker host uri root”的同学,大概率是在配置Docker Cloud或Kubernetes Cloud时卡住了。
Docker Host URI的作用是告诉Jenkins“去连哪个Docker守护进程”。它的正确写法必须是带协议前缀的URL:
| 场景 | 写法 |
|---|---|
| 本机socket | unix:///var/run/docker.sock |
| 本机TCP | tcp://127.0.0.1:2375 |
| 远程Docker机器 | tcp://192.168.1.100:2375 |
| 启用了TLS的远程Docker | https://192.168.1.100:2376 |
我见过的最常见错误,是有人只填了ip加端口,比如192.168.1.100:2375,漏了tcp://前缀,Jenkins解析URL直接失败,报错信息五花八门,最后翻日志才定位到是URL格式问题。
第二个坑:远程Docker默认根本没有监听2375端口。如果你确定要在远程机器上用Docker做动态构建节点,得先在那台机器上修改daemon.json:
{ "hosts": ["tcp://0.0.0.0:2375", "unix:///var/run/docker.sock"] }改完重启dockerd。注意这样会把Docker的2375端口暴露出去,在公司内网可以,暴露公网等于给别人一个root入口,千万别这么干。
第三个坑就是那个root。在New Cloud配置页面里,Docker Host URI旁边有时候会要求填写证书路径,最常见的是填写/root/.docker,这是Docker客户端默认的证书目录。如果URI用的是https而我们没配客户端证书,连接会报证书验签失败。很多人照着网上的截图填了root路径但证书文件根本没放进去,自然连不上。如果是http方案(测试内网),把TLS校验关掉、URI改成tcp就能绕过这个坑。
镜像构建涉及拉基础镜像慢的问题,顺手说一句:如果用的是Docker守护进程是宿主机上的,直接改宿主机的/etc/docker/daemon.json,配置registry-mirrors国内镜像加速;如果用的是DinD,那dind容器要挂载对应的daemon.json配置,或者容器启动时通过环境变量传入。改谁的环境,就得在谁的daemon上配,这个归属关系别搞混。同理,如果你的构建是跑在K8s动态Agent里,那实际执行docker命令的还是Pod所在的节点宿主机,加速器配置也要落在对应节点上。
4. Pipeline基本功:拉代码、环境变量与凭证管理
4.1 最小Pipeline结构:声明式脚本怎么起步
Pipeline是Jenkins 2.x之后最核心的能力。它把原来的“界面点按钮、填参数”变成了Jenkinsfile脚本,Jenkinsfile随着代码仓库走,谁改了谁负责,可审计、可回滚。
一个最简的声明式Pipeline长这样:
pipeline { agent any stages { stage('拉取代码') { steps { echo '准备拉取代码' checkout scm } } stage('构建') { steps { sh 'mvn clean package -DskipTests=false' } } stage('打印结果') { steps { echo "构建号:${env.BUILD_NUMBER}" sh 'pwd && ls -la target/' } } } }agent any指任意一个可用的Agent节点上执行。如果你用了Kubernetes动态Agent,这里会换成agent { label 'k8s-agent' },让任务自动调度到动态创建的Pod里。
stage是阶段划分,steps里是具体动作。sh 'xxxx'是执行Shell命令,这是Pipeline里使用频率最高的命令之一。注意sh后面如果用双引号,Groovy会做变量插值,$变量会被解析;用单引号则不解析,直接传给Shell。这是新人最容易踩的区分点。
关于拉代码,如果你的Jenkins任务配置为“Pipeline script from SCM”,Jenkins会先自动把Jenkinsfile所在仓库拉下来并执行,所以仓库代码和Jenkinsfile在同一个仓库时,第一阶段的checkout scm是必要的,否则后面构建用的还是上一次的旧代码。
如果代码在另一个仓库,更明确的写法是:
git branch: 'main', url: 'git@gitee.com:example/order-service.git', credentialsId: 'gitlab-deploy-key'credentialsId对应你在Jenkins里配置的凭证ID,我下面会专门讲。
4.2 环境变量全家桶:内置变量、自定义变量与参数化
Jenkins有大量内置环境变量,在Pipeline里可以直接用env.xxx读取。记住最常用的几个就够起步了:
| 变量 | 含义 |
|---|---|
| env.BUILD_NUMBER | 当前构建号 |
| env.JOB_NAME | 任务名称 |
| env.WORKSPACE | 构建工作目录 |
| env.BUILD_URL | 本次构建的完整URL |
| env.GIT_COMMIT | 当前构建对应的Git提交SHA |
| env.GIT_BRANCH | 当前构建对应的Git分支 |
引用方式有个细节:在双引号字符串里,$BUILD_NUMBER和${env.BUILD_NUMBER}都能生效;在单引号字符串里不会。所以拼命令时我习惯统一写成双引号,然后天然完成变量替换。
自定义环境变量和参数化构建是真实项目里跑不掉的:
pipeline { agent any parameters { string(name: 'TAG', defaultValue: 'latest', description: '镜像标签') choice(name: 'ENV', choices: ['dev', 'staging', 'prod'], description: '目标环境') } environment { DOCKER_REGISTRY = 'harbor.example.com' IMAGE_NAME = 'order-service' } stages { stage('构建镜像') { steps { sh "docker build -t ${DOCKER_REGISTRY}/${IMAGE_NAME}:${params.TAG} ." } } } }parameters里的值用params.xxx读取,environment里的值可以像普通变量那样直接引用。参数化之后同一套Pipeline可以复用到不同环境,只需在构建时选择就行。
还遇到过一种情况:需要把环境变量从一个阶段传给另一个阶段。解决办法是用脚本块临时变量,或者写到文件里再读取。简单场景写在script块里最方便:
script { env.IMAGE_TAG = "${params.TAG}-${BUILD_NUMBER}" }这段执行后,后面的阶段都能引用env.IMAGE_TAG。
4.3 凭证的正确打开方式
明文密码写在Jenkinsfile里是绝对的禁忌。Jenkins的Credentials系统就是为此设计的。
在“系统管理→凭证→系统→全局凭证”里添加,常见类型有:
- Username with password:用于GitLab、Harbor的账号密码登录
- SSH Username with private key:用于Git SSH拉取、目标服务器部署
- Secret text:用于Token、API Key
创建时会得到一个ID,这个ID是你在Jenkinsfile里引用它的唯一依据。拿账号密码类型举例:
withCredentials([usernamePassword( credentialsId: 'harbor-credentials', usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASS' )]) { sh 'docker login harbor.example.com -u $HARBOR_USER -p $HARBOR_PASS' }注意,在withCredentials块里,Shell命令里的变量要用$VAR而不是${VAR},因为前者让Jenkins把凭证值注入Shell环境,后者可能被Groovy先解析成空值。
我的习惯:每个项目单独建一套凭证,不搞一个“万能账号”全仓库复用。因为凭证泄露之后,影响面能控制在一个项目内。轮换凭证时也只需要改一个ID对应的内容,不用翻代码。
5. 动态Agent实践:Jenkins 2.541.3对接Kubernetes
5.1 动态Agent的价值与资源模型
先明确一个问题:为什么不用一台固定机器当构建机?一台4C8G的虚拟机闲着也是闲着,但一半时间CPU是空的。任务多了又开始排队,高峰期一个构建等半小时。动态Agent的思路是:按需创建、用完销毁。
在K8s里,Jenkins通过Kubernetes插件调用API Server,为每个构建任务动态创建一个Pod,Pod跑完自动删除。这个Pod就是构建Agent,里面至少包含一个jnlp容器(负责和Jenkins Master通信、监听指令),还可以按需追加工具容器,比如一个装Maven的容器、一个装了kubectl的容器。
| 对比项 | 固定Agent | 动态Agent |
|---|---|---|
| 资源利用率 | 低,闲时浪费 | 高,按需使用 |
| 环境隔离 | 差,工具链互相污染 | 好,每个Pod独立 |
| 弹性扩容 | 难,要手动加机器 | 容易,Pod自动扩容 |
| 故障恢复 | 慢 | 快,异常重建 |
5.2 从Cloud配置到Pod模板的完整操作
在Jenkins 2.541.3上,入口是“系统管理→节点管理→Clouds→New Cloud→Kubernetes”。老版本可能在“系统管理→Kubernetes”,界面上位置不太一样,核心配置项不变。
第一步:准备Kubernetes侧权限。创建一个专用的命名空间和服务账号,我给一段最小RBAC配置:
apiVersion: v1 kind: ServiceAccount metadata: name: jenkins namespace: jenkins-agent --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: jenkins-agent rules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch", "create", "delete"] - apiGroups: ["apps"] resources: ["deployments"] verbs: ["get", "list", "watch", "update", "patch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: jenkins-agent-binding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: jenkins-agent subjects: - kind: ServiceAccount name: jenkins namespace: jenkins-agent然后生成一个Token,Jenkins用它访问API:
kubectl create token jenkins -n jenkins-agent把Token复制出来,在Jenkins里添加一个Secret text类型的凭证。
第二步:填写Cloud配置。关键项如下:
| 配置项 | 建议值 | 说明 |
|---|---|---|
| Name | k8s-cloud | Cloud名称 |
| Kubernetes地址 | https://kubernetes.default.svc | 或外网API Server地址 |
| 命名空间 | jenkins-agent | 动态Pod创建的命名空间 |
| 凭证 | 选择刚才的Secret text | 用Token访问API |
| 连接超时 | 10秒 | 避免假死挂起 |
Kubernetes地址这一项是个经典报错源。Jenkins部署在集群内时用https://kubernetes.default.svc最稳;如果Jenkins在集群外,填API Server的对外地址,比如https://10.0.0.10:6443,端口一定不要漏。
第三步:配置Pod Template。在Cloud配置里点“Pod Templates→Add”,模板名随意,标签(Label)很关键,因为Pipeline里就是靠label来调度到这个模板。模板内容分两部分:jnlp主容器,通常用:
jenkins/inbound-agent:4.13-3-jdk17它是Agent的主容器,没有它Pod无法和Master建立通信。如果你需要Maven构建环境,再添加一个容器:
maven:3.9.6-eclipse-temurin-17用container('maven')就能在Pipeline里切换到该容器执行命令:
stage('Maven构建') { steps { container('maven') { sh 'mvn clean package' } } }如果后面部署阶段要用kubectl,同理加一个装了kubectl的容器,或者在主容器里预装。
5.3 常见错误:连接失败、Pending与镜像拉取
先说我见过占用率最高的报错:Cloud配置完成后测试连接,一直Connect timed out。这类问题九成出在Kubernetes地址不通或者网络策略拦截。先在Jenkins容器里手动访问一下API地址:
curl -k https://kubernetes.default.svc/version返回JSON说明网络通,然后检查Token权限;如果直接超时,先排查网络而不是检查配置。
第二个高频场景:任务触发了,但Pod一直Pending,Jenkins那边看到“Scheduling failed”。去集群侧看:
kubectl get pods -n jenkins-agent kubectl describe pod <pod-name>通常原因是节点资源不足、标签选择器不准或者有污点不调度。改Pod Template里的资源请求,或者给模板指定nodeSelector都能解决。
第三个场景:动态Pod创建成功,但jnlp镜像拉取失败。如果Jenkins和K8s在同一个内网环境,镜像仓库访问比较慢,建议先把agent镜像手动拉取到所有节点,或者配置好imagePullSecret,让Pod能从私有仓库拉镜像。否则每次动态起Pod都要磨蹭半天。
最后提醒一个细节:不要指望动态Agent里的jnlp主容器既做构建又做部署。我一般把一次完整的构建拆成多个阶段,每个阶段指定合适的容器,Maven构建用maven容器,镜像构建用挂载了socket的Docker容器,部署用kubectl容器。容器职责单一,Pipeline的可读性和稳定性都会好很多。
6. 全流程实战:从Git提交到K8s滚动更新
6.1 实战项目背景和链路设计
为了演示完整效果,我设计一个贴近真实的小项目:order-service,Spring Boot应用,代码放GitLab,构建产物是Docker镜像,镜像仓库用Harbor,部署目标是K8s集群。
整条链路是:开发提交代码到GitLab的main分支,GitLab通过Webhook通知Jenkins,Jenkins动态起一个K8s Pod作为构建Agent,Agent依次执行Maven打包、构建镜像、推送Harbor、再调kubectl更新Deployment镜像版本,最后做健康检查。全部成功之后发邮件通知,失败也发邮件。
| 阶段 | 产物 | 执行环境 |
|---|---|---|
| 拉代码 | 源码 | 动态Agent工作目录 |
| Maven打包 | order-service.jar | maven容器 |
| 构建镜像 | 镜像 | Docker守护进程(DoS方案) |
| 推送Harbor | 远程镜像 | Docker守护进程 |
| 更新Deployment | K8s资源更新 | kubectl容器 |
| 健康检查 | HTTP状态码 | kubectl容器或curl容器 |
6.2 一起拆解完整Jenkinsfile
下面这份Jenkinsfile我删除了业务无关部分,保留了主干,你可以直接改参数套用到自己的项目:
pipeline { agent { label 'k8s-agent' } environment { DOCKER_REGISTRY = 'harbor.example.com' IMAGE_NAME = 'order-service' NAMESPACE = 'production' DEPLOYMENT = 'order-service' } parameters { string(name: 'BRANCH', defaultValue: 'main', description: '构建分支') choice(name: 'ENV', choices: ['dev', 'staging', 'prod'], description: '目标环境') } stages { stage('拉取代码') { steps { git branch: "${params.BRANCH}", url: 'git@gitee.com:example/order-service.git', credentialsId: 'gitlab-deploy-key' } } stage('Maven打包与单元测试') { steps { container('maven') { sh 'mvn clean package -DskipTests=false' } } post { failure { error '单元测试失败,终止流水线' } } } stage('构建并推送镜像') { steps { script { docker.withRegistry("https://${DOCKER_REGISTRY}", 'harbor-credentials') { def image = docker.build("${DOCKER_REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${params.BRANCH}-${BUILD_NUMBER}") image.push() } } } } stage('更新Kubernetes') { steps { container('kubectl') { sh """ kubectl set image deployment/${DEPLOYMENT} \ ${DEPLOYMENT}=${DOCKER_REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${params.BRANCH}-${BUILD_NUMBER} \ -n ${NAMESPACE} --record kubectl rollout status deployment/${DEPLOYMENT} -n ${NAMESPACE} --timeout=300s """ } } } stage('健康检查') { steps { container('kubectl') { sh "curl -sf http://order-service.example.com/actuator/health || exit 1" } } } } post { success { emailext subject: "[CICD] ${JOB_NAME} 构建成功", body: "构建号:${BUILD_NUMBER}\n构建地址:${BUILD_URL}", to: 'dev@example.com' } failure { emailext subject: "[CICD] ${JOB_NAME} 构建失败", body: "构建号:${BUILD_NUMBER}\n请查看日志:${BUILD_URL}", to: 'dev@example.com' } } }逐段解释一下设计理由。
拉代码阶段用了git命令加credentialsId,而不是简化的checkout scm,是因为代码仓库和Jenkinsfile并不在同一个仓库。credentialsId对应GitLab的SSH Key凭证,密钥单独管理,不进代码库。
Maven阶段用container('maven')切到Pod里的Maven容器。这里如果你在Pod Template里没加maven容器,直接执行sh 'mvn'会报command not found,回到Agent工具链的问题——必须有这个容器才能执行命令。测试失败用post块错误退出,避免后续继续构建一个坏制品。
镜像构建这段是全文最核心的精华。docker.build("${DOCKER_REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${params.BRANCH}-${BUILD_NUMBER}")会读取构建Agent工作目录下的Dockerfile,执行镜像构建。docker.withRegistry第一个参数是镜像仓库地址,第二个参数是Harbor的凭证ID。整个镜像命名用“分支-构建号”做tag,保证了每次构建的镜像版本全局唯一,也方便从镜像tag反查构建来源。tag里不建议用latest,生产环境“latest到底是什么”这个问题迟早会坑人。
部署阶段用kubectl set image直接把Deployment的镜像地址替换成新构建出来的镜像tag,再rollout status等待滚动更新完成。--timeout=300s是一种保护机制,如果5分钟没完成滚动,任务失败并触发通知,避免流水线无限挂死。
健康检查阶段用curl打应用的健康检查接口。这一步很多人会省,但我强烈建议保留。部署成功不等于服务正常,接口状态码才是最终证据。
6.3 Webhook触发、部署验证与回滚
要达成“提交代码自动部署”的效果,Webhook是关键。我用Generic Webhook Trigger插件,因为它的兼容性比特定平台的Webhook插件好很多。
配置方法:在Job的“构建触发器”里勾选Generic Webhook Trigger,然后设置一个Token,比如:
order-service-webhook-token触发URL就变成了:
http://jenkins.example.com/generic-webhook-trigger/invoke?token=order-service-webhook-token把这条URL填到GitLab的“设置→Webhooks”,触发事件勾选Push events和Tag push events,保存后推一次代码测试。
要注意:Webhook的Token是明文的,URL泄露后别人可以反复触发构建来消耗资源。建议在Generic Webhook Trigger配置里加上分支过滤,只响应main分支,减少无谓的并发构建。
部署验证除了Pipeline里的健康检查,我还习惯补两个手工确认动作。一个是kubectl get pods查看Pod状态和RESTARTS列,确认没有崩溃重启;另一个是kubectl logs查看应用日志是否正常打印启动完成信息。这两个动作可以把很多“接口返回200但业务实际上没就绪”的问题提前捞出来。
回滚预案在K8s场景下非常简单。因为每次更新Deployment都用了--record,所以执行:
kubectl rollout undo deployment/order-service -n production就能回滚到上一个版本的镜像。如果你要回滚到更早的版本,先用history看版本号,再用--to-revision指定版本:
kubectl rollout history deployment/order-service -n production kubectl rollout undo deployment/order-service -n production --to-revision=2有个细节我在生产环境吃过亏:回滚之后,Deployment的实际镜像tag和最后一次Jenkins构建的tag会不一致。这时候如果你只信任“最后一个tag就是线上版本”,后续排查会非常痛苦。所以维护一份“环境版本对照表”是低成本高收益的习惯,最直接的办法是把回滚动作也记录成一条构建记录,或者用kubectl annotate给Deployment补一个备注。
6.4 落地之后的高频维护点
整套链路跑通只是开始,我最后列几个维护要点。
首先是磁盘。Jenkins的/var/jenkins_home会随构建次数快速膨胀,尤其是保留了Artifact的历史构建。在Job配置里设置“丢弃旧构建”,限制保存最近30天或者最近100次构建。我在生产环境见过Jenkins_home占用几百G导致容器起不来的情况,这不是罕见个例。
其次是时区。容器默认UTC时间,日志里的构建时间和钉钉通知时间对不上会让人崩溃。记得在docker-compose里设置TZ=Asia/Shanghai。
再次是凭证轮换。GitLab密钥、Harbor密码、K8s Token都是有生命周期的。建议在日历里建一个季度提醒,每季度轮换一次CI专用凭证,轮换后跑一遍关键Job确认没断。
最后是“配置即代码”。Jenkins系统里的全局配置很难自动同步,但你至少要把所有Jenkinsfile提交到Git仓库。这样任何改动都走MR评审,出了问题能快速找回上一个可用版本。
至于Jenkins自身的备份,我的建议是至少每周打一次/var/jenkins_home的快照,恢复的时候整个目录覆盖回去就行,在2.541.3版本上我实测过,这套方式最直接有效。
上面这套流程是这几年我反复实践后的最终形态:GitLab触发、Jenkins调度、Harbor存镜像、K8s滚动部署。它足够覆盖绝大多数中小团队的自动化上线需求。如果你只是两三个人小项目,把K8s动态Agent换成一台装了Docker和Maven的虚拟机,把部署命令从kubectl set image改成ssh加docker run,链路骨架完全一样。CICD这个东西,方案可以裁剪,但链条必须完整。链条一旦断了,那又回到了“手动上线四十分钟”的老路。