Jenkins Pipeline:搭建你的第一条 CI/CD 流水线
最近有个读者私信我:“哥,我们公司发版还是手动
mvn package完用 FTP 传服务器,再 SSH 上去kill -9重启,发一次版熬到凌晨两点,还总出错。有没有救?”我说:“兄弟,你这哪是发版,这是渡劫啊。”
今天咱们就来聊聊,怎么用 Jenkins Pipeline 把这套"人肉 CI/CD"给自动化了。读完这篇文章,你至少能搭出一条像模像样的流水线,告别凌晨两点的人工操作。
一、问题引入:那些年,我们手动发过的版
先回忆一下传统发版的"标准流程":
- 开发在本地打好包(
mvn package或npm run build) - 打开 FTP/WinSCP,把 jar 包或 dist 文件夹拖到服务器上
- SSH 登录服务器,找到旧进程,
kill -9干掉 - 启动新服务,祈祷不要报错
- 打开浏览器手动点两下,验证"应该没问题"
- 如果有问题,手忙脚乱回滚…
这套流程的问题,咱们心里都门儿清:
- 容易出错:人不是机器,步骤一多就容易漏。jar 包传错了目录?配置文件忘了覆盖?服务启动参数写岔了?随便一个失误就是生产事故。
- 不可追溯:谁发的版?什么时候发的?发了什么版本?全凭微信群里的"我发完了"和运维小哥的记忆力。
- 效率低下:发一次版少则半小时,多则几个小时。要是多环境(测试、预发、生产)各来一遍,一晚上就没了。
- 回滚困难:一旦出问题,想回滚到上一个版本?不好意思,你可能连上个版本的包都找不到。
说白了,发版这件事,就不该靠人手动去做。机器能干的活,为什么要让人熬夜干?
二、方案分析:为什么选 Jenkins Pipeline?
解决这个问题的方案其实不少,咱们简单盘一盘:
| 方案 | 优点 | 缺点 |
|---|---|---|
| GitLab CI/CD | 和代码仓库深度集成,配置简单 | 对 Jenkins 生态已有的插件和存量项目迁移成本高 |
| GitHub Actions | 云端即用,社区 Action 丰富 | 企业内网或私有部署场景下不太方便 |
| Jenkins + Pipeline | 生态最成熟,插件丰富,自由度高,企业用得最多 | 界面有点复古,配置稍显复杂 |
如果你的公司已经在用 Jenkins,或者你想学一套"通用性最强、找工作最吃香"的 CI/CD 方案,那Jenkins Pipeline几乎是不二之选。
而且 Pipeline 相比传统的 Jenkins Job(在 Web 界面上点点点配置),有个巨大的优势:Pipeline as Code。整个流水线写在一个Jenkinsfile里,跟代码一起放在 Git 仓库里。版本可控、可 review、可复用,谁改了流水线一目了然。
Jenkins Pipeline 有两种语法:
- Scripted Pipeline:基于 Groovy 脚本,灵活度高,但写起来像代码,对新手不太友好。
- Declarative Pipeline:声明式语法,结构清晰,像写 YAML 一样规整,推荐新手和大多数场景使用。
我的建议:除非你有特别复杂的动态逻辑,否则一律用 Declarative。好读、好维护、团队协作成本低。
三、实现过程:Step by Step 搭一条流水线
好了,说了这么多,咱们开始动手。先看一下这条流水线的完整流程:
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ 拉取 │ -> │ 编译 │ -> │ 单元测试│ -> │ 构建镜像│ -> │ 推送镜像│ -> │ 部署应用│ │ 代码 │ │ 打包 │ │ │ │ │ │ 仓库 │ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘这就是一条标准的 CI/CD 流水线。下面咱们逐个 stage 拆解。
3.1 前置准备:Jenkins 和核心插件
Jenkins 的安装就不手把手教了,网上教程一大把。不管你是用 Docker 起一台,还是在服务器上直接装,核心思路都一样。
装完之后,记得装这几个必备插件:
- Pipeline:没有它,Jenkinsfile 跑不起来。
- Git:从 Git 仓库拉代码。
- Docker Pipeline:在 Pipeline 里执行 Docker 命令。
- Kubernetes CLI(可选):如果你要部署到 K8s,这个插件能帮你方便地操作
kubectl。
3.2 编写 Jenkinsfile(Declarative 语法)
下面是一份精简但可直接用的 Jenkinsfile 模板。我加了详细注释,你可以直接复制到项目里改改参数就能跑。
pipeline{// 指定在哪台 agent 上运行,any 表示任意可用节点agent any// 定义环境变量,方便后面复用environment{IMAGE_NAME="myapp"IMAGE_TAG="${BUILD_NUMBER}"// 用 Jenkins 构建号做镜像标签REGISTRY="registry.example.com"KUBE_CONFIG=credentials('kubeconfig-id')// 从 Jenkins 凭据里取 kubeconfig}stages{// Stage 1: 拉取代码stage('Checkout'){steps{// 从 Git 仓库拉代码,分支参数化,默认 mastergit branch:'${BRANCH_NAME}',url:'https://github.com/your-repo.git'echo"代码拉取完成,当前分支:${env.BRANCH_NAME}"}}// Stage 2: 编译打包stage('Build'){steps{// 以 Maven 项目为例,执行编译并跳过测试(测试单独跑)sh'mvn clean package -DskipTests'echo"编译完成,产物在 target/ 目录下"}}// Stage 3: 单元测试stage('Test'){steps{sh'mvn test'}post{always{// 收集测试报告,Jenkins 界面上能看到junit'target/surefire-reports/*.xml'}}}// Stage 4: 构建 Docker 镜像stage('Build Docker Image'){steps{script{// 用当前目录的 Dockerfile 构建镜像docker.build("${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}")}}}// Stage 5: 推送镜像到仓库stage('Push Image'){steps{script{// 登录镜像仓库并推送docker.withRegistry("https://${REGISTRY}",'registry-credentials-id'){docker.image("${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}").push()// 同时推一个 latest 标签,方便快速拉取docker.image("${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}").push('latest')}}}}// Stage 6: 部署到 K8sstage('Deploy'){steps{// 用 sed 替换 deployment.yaml 里的镜像标签sh""" sed -i 's|IMAGE_TAG|${IMAGE_TAG}|g' k8s/deployment.yaml kubectl --kubeconfig=${KUBE_CONFIG}apply -f k8s/ """echo"部署完成,镜像版本:${IMAGE_TAG}"}}}// 流水线跑完后的收尾post{success{echo"🎉 流水线执行成功!可以去喝杯咖啡了。"}failure{echo"❌ 流水线挂了,快去查日志吧..."}always{// 清理工作区,避免磁盘爆掉cleanWs()}}}关键点解读:
agent any:让 Jenkins 自己挑一台空闲的节点跑任务。如果你的编译节点有标签(比如label 'maven'),可以改成agent { label 'maven' }。environment:把镜像名、仓库地址等抽成变量,后面改起来方便。post里的always:不管成功失败都执行,适合放清理、发通知这类操作。cleanWs():很多新手会漏掉这步,结果 Jenkins 服务器磁盘被旧构建占满,别问我怎么知道的。
3.3 蓝绿部署 vs 滚动更新:部署的时候怎么保证不断服?
代码编译完了、镜像也推了,最后一步部署可不能简单粗暴地kill -9重启。生产环境讲究的是零停机发布。这里介绍两个最常见的思路:
滚动更新(Rolling Update)
这是 K8s 的默认策略。简单说就是:先起几个新 Pod,等它们健康了,再逐个干掉旧 Pod。整个过程中,服务的总实例数基本保持不变,用户无感知。
优点是简单、省资源;缺点是如果新版本有问题,回滚需要时间,而且更新期间新旧版本会共存一小段时间。
蓝绿部署(Blue-Green Deployment)
同时维护两套环境:蓝色(当前线上版本)和绿色(新版本)。部署时,先把绿色环境全部启动并验证通过,然后把流量一次性从蓝色切到绿色。如果出问题,秒切回蓝色。
优点是回滚快、风险低;缺点是需要双倍资源。
对于刚入门 CI/CD 的同学,我的建议是:先用 K8s 自带的滚动更新,配置简单、成本低。等团队成熟了,再考虑上蓝绿部署或金丝雀发布。
四、踩坑记录:我踩过的那些 Jenkins 的坑
理论很美好,实操起来总有些意想不到的坑。这里分享两个我实实在在踩过的,希望你别重蹈覆辙。
坑 1:Jenkins 容器里执行不了 Docker 命令
很多同学习惯用 Docker 跑 Jenkins,结果在 Pipeline 里一执行docker build,报错:
docker: not found或者权限不足:
Got permission denied while trying to connect to the Docker daemon socket原因:Jenkins 容器本身没有 Docker 环境,或者容器里的jenkins用户没有权限访问宿主机的 Docker socket。
解决方案:
启动 Jenkins 容器时,把宿主机的 Docker socket 挂载进去:
dockerrun-d\-v/var/run/docker.sock:/var/run/docker.sock\-v/usr/bin/docker:/usr/bin/docker\jenkins/jenkins:lts同时,给jenkins用户加到docker组:
# 进入 Jenkins 容器usermod-aGdockerjenkins注意:挂载 Docker socket 有一定安全风险,但在内网 CI 场景下基本够用。如果要求严格,可以考虑用 Docker in Docker(DinD),配置会复杂一些。
坑 2:Pipeline 里的环境变量传不到 Shell 脚本里
我在environment里定义了一个变量:
environment{MY_VAR="hello"}然后在sh步骤里用$MY_VAR,结果为空。
原因:Declarative Pipeline 的sh步骤里,Groovy 变量和 Shell 环境变量的作用域是两套系统。直接用$MY_VAR有时候能行,有时候不行,取决于 Jenkins 版本和上下文。
解决方案:用双引号包裹sh,并显式用${env.MY_VAR}或${MY_VAR}插值:
sh""" echo${env.MY_VAR}echo${MY_VAR}"""如果你用的是单引号sh '...',Groovy 不会进行变量插值,Shell 里自然拿不到值。这个细节坑了我整整一个下午。
坑 3:构建缓存导致依赖没更新( bonus 坑)
有时候明明改了pom.xml里的依赖版本,Jenkins 构建出来的包还是老的。
原因:Maven 的本地仓库缓存(~/.m2/repository)或者 Docker 的 layer 缓存没清。
解决方案:
- Maven 构建加
-U参数强制更新快照依赖:mvn clean package -U - Docker 构建加
--no-cache参数,或者在 Dockerfile 里把pom.xml和源码分层 COPY,避免不必要的缓存
五、验证与总结
按照上面的 Jenkinsfile 配置好流水线之后,你可以这样做验证:
- 在代码仓库里提交一个改动,推送到 Git。
- 打开 Jenkins,点击"立即构建",或者配置 Webhook 让它自动触发。
- 看着流水线一个 stage 一个 stage 地变绿,最后显示"🎉 流水线执行成功!"。
- 登录 K8s Dashboard 或服务器,确认新版本已经跑起来了。
这时候你会发现,发版从原来的人工两小时,变成了点一下按钮(甚至自动触发)的十分钟。而且全程有日志、有版本、可追溯,出了问题也知道从哪查。
六、写在最后
CI/CD 这件事,说起来高大上,本质上就是把"人重复做的事"交给机器去做。Jenkins Pipeline 只是工具之一,更重要的是流水线思维——代码提交后应该自动编译、自动测试、自动部署,人只负责写代码和做决策。
当然,Jenkins 也不是完美的。界面老旧、插件兼容性偶尔抽风、Groovy 语法对 Java 程序员还算友好但对前端同学可能有点劝退。如果你已经在用 GitLab 或 GitHub,也可以试试它们原生的 CI/CD。工具没有绝对的好坏,适合团队现状的才是最好的。
最后,想问问你:
- 你们公司现在是怎么发版的?还是手动 FTP 吗?
- 你在搭 Jenkins Pipeline 的时候踩过哪些坑?
- 有没有比 Jenkins 更好用的 CI/CD 工具推荐?
欢迎在评论区交流,咱们一起进步!
如果这篇文章对你有帮助,点个赞或者转发给还在手动发版的兄弟,救救他吧。