
先说一个我上周刚处理完的真实场景。有个朋友的项目组“自动化”搞了半年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加JenkinsJenkins直接装在虚拟机或容器里构建时用宿主机的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: - TZAsia/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构建时动态选择分支、TagDocker Pipeline在流水线中构建并推送镜像Kubernetes动态创建K8s Pod作为AgentGeneric Webhook Trigger接收GitLab等系统的WebhookCredentials Binding安全引用账号密码、TokenBlue 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 挂载宿主socketDoS与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_HOSTtcp://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场景写法本机socketunix:///var/run/docker.sock本机TCPtcp://127.0.0.1:2375远程Docker机器tcp://192.168.1.100:2375启用了TLS的远程Dockerhttps://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 -DskipTestsfalse } } 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: gitgitee.com:example/order-service.git, credentialsId: gitlab-deploy-keycredentialsId对应你在Jenkins里配置的凭证ID我下面会专门讲。4.2 环境变量全家桶内置变量、自定义变量与参数化Jenkins有大量内置环境变量在Pipeline里可以直接用env.xxx读取。记住最常用的几个就够起步了变量含义env.BUILD_NUMBER当前构建号env.JOB_NAME任务名称env.WORKSPACE构建工作目录env.BUILD_URL本次构建的完整URLenv.GIT_COMMIT当前构建对应的Git提交SHAenv.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对接Kubernetes5.1 动态Agent的价值与资源模型先明确一个问题为什么不用一台固定机器当构建机一台4C8G的虚拟机闲着也是闲着但一半时间CPU是空的。任务多了又开始排队高峰期一个构建等半小时。动态Agent的思路是按需创建、用完销毁。在K8s里Jenkins通过Kubernetes插件调用API Server为每个构建任务动态创建一个PodPod跑完自动删除。这个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然后生成一个TokenJenkins用它访问APIkubectl create token jenkins -n jenkins-agent把Token复制出来在Jenkins里添加一个Secret text类型的凭证。第二步填写Cloud配置。关键项如下配置项建议值说明Namek8s-cloudCloud名称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一直PendingJenkins那边看到“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-serviceSpring Boot应用代码放GitLab构建产物是Docker镜像镜像仓库用Harbor部署目标是K8s集群。整条链路是开发提交代码到GitLab的main分支GitLab通过Webhook通知JenkinsJenkins动态起一个K8s Pod作为构建AgentAgent依次执行Maven打包、构建镜像、推送Harbor、再调kubectl更新Deployment镜像版本最后做健康检查。全部成功之后发邮件通知失败也发邮件。阶段产物执行环境拉代码源码动态Agent工作目录Maven打包order-service.jarmaven容器构建镜像镜像Docker守护进程DoS方案推送Harbor远程镜像Docker守护进程更新DeploymentK8s资源更新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: gitgitee.com:example/order-service.git, credentialsId: gitlab-deploy-key } } stage(Maven打包与单元测试) { steps { container(maven) { sh mvn clean package -DskipTestsfalse } } 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} --timeout300s } } } 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: devexample.com } failure { emailext subject: [CICD] ${JOB_NAME} 构建失败, body: 构建号${BUILD_NUMBER}\n请查看日志${BUILD_URL}, to: devexample.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等待滚动更新完成。--timeout300s是一种保护机制如果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?tokenorder-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-revision2有个细节我在生产环境吃过亏回滚之后Deployment的实际镜像tag和最后一次Jenkins构建的tag会不一致。这时候如果你只信任“最后一个tag就是线上版本”后续排查会非常痛苦。所以维护一份“环境版本对照表”是低成本高收益的习惯最直接的办法是把回滚动作也记录成一条构建记录或者用kubectl annotate给Deployment补一个备注。6.4 落地之后的高频维护点整套链路跑通只是开始我最后列几个维护要点。首先是磁盘。Jenkins的/var/jenkins_home会随构建次数快速膨胀尤其是保留了Artifact的历史构建。在Job配置里设置“丢弃旧构建”限制保存最近30天或者最近100次构建。我在生产环境见过Jenkins_home占用几百G导致容器起不来的情况这不是罕见个例。其次是时区。容器默认UTC时间日志里的构建时间和钉钉通知时间对不上会让人崩溃。记得在docker-compose里设置TZAsia/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这个东西方案可以裁剪但链条必须完整。链条一旦断了那又回到了“手动上线四十分钟”的老路。