作者:没有四次元口袋的蓝胖
日期:2026-10-10
标签:Jenkins, Pipeline, Jenkinsfile, CICD实战
Jenkins知识梳理(2)
写在前面
上一篇我们把 Jenkins 装好了,也通了 GitLab 集成链路。这篇来写真正干活的部分------Jenkinsfile。
Jenkins Pipeline 是 Jenkins 2.0 之后推出的核心能力,用代码(而不是 Web UI 点击)来定义整个构建、测试、部署流程。这个理念叫 Pipeline as Code------构建流程本身也应该被版本化管理。
本文以一个真实的 Spring Boot 项目为例,从零写一个完整的 Jenkinsfile,覆盖拉代码、编译、测试、打包、部署全流程,并逐步优化到生产可用的水平。
一、Pipeline 基础概念
1.1 Declarative vs Scripted Pipeline
Jenkins 有两种 Pipeline 语法:
| 类型 | 语法 | 特点 | 推荐度 |
|---|---|---|---|
| Declarative | 结构化语法,更严格 | 易读、有校验、支持 Blue Ocean 可视化 | ⭐⭐⭐⭐⭐ 推荐 |
| Scripted | Groovy 脚本,更灵活 | 灵活但易写乱、无语法校验 | ⭐⭐ 复杂场景备选 |
本文全部使用 Declarative Pipeline。
1.2 Pipeline 的基本结构
groovy
pipeline {
agent any // 在哪执行
environment { // 环境变量
APP_NAME = 'my-app'
}
tools { // 工具配置
maven 'Maven3.8'
jdk 'JDK17'
}
options { // Pipeline 级别选项
timeout(time: 30, unit: 'MINUTES')
timestamps() // 日志显示时间戳
buildDiscarder(logRotator(numToKeepStr: '20'))
}
parameters { // 参数化构建
choice(name: 'DEPLOY_ENV', choices: ['staging', 'production'], description: '部署环境')
}
stages { // 阶段(按顺序执行)
stage('Build') { ... }
stage('Test') { ... }
stage('Deploy') { ... }
}
post { // 后置动作(无论成功失败都执行)
success { ... }
failure { ... }
always { ... }
}
}
1.3 核心语法元素
| 元素 | 说明 | 示例 |
|---|---|---|
agent |
指定执行节点 | agent any / agent { label 'java' } / agent { docker { image 'maven:3.8' } } |
stages |
所有阶段的容器 | 包含多个 stage |
stage |
一个逻辑阶段 | stage('Build') { steps { ... } } |
steps |
阶段内的具体步骤 | sh 'mvn clean compile' |
post |
后置动作 | success {} / failure {} / always {} |
environment |
环境变量 | DB_URL = 'jdbc:mysql://...' |
when |
条件执行 | when { branch 'main' } |
parallel |
并行执行 | 同一 stage 内多个子 stage 并行 |
input |
人工审批 | input { message '确认部署?' } |
二、从零写一个完整的 Jenkinsfile
2.1 项目结构
my-spring-boot-project/
├── Jenkinsfile ← Pipeline 定义文件
├── src/
│ ├── main/java/
│ └── test/java/
├── pom.xml
└── docker/
└── Dockerfile ← 构建 Docker 镜像
2.2 第一版:基础构建流程
先写一个能跑通的最小 Pipeline:
groovy
pipeline {
agent any
tools {
maven 'Maven3.8'
jdk 'JDK17'
}
stages {
stage('拉取代码') {
steps {
echo '=== 开始拉取代码 ==='
checkout scm // 自动从 SCM 配置中拉取(Pipeline from SCM 模式)
}
}
stage('编译') {
steps {
echo '=== 开始编译 ==='
sh 'mvn clean compile -q'
}
}
stage('单元测试') {
steps {
echo '=== 运行单元测试 ==='
sh 'mvn test'
}
}
stage('打包') {
steps {
echo '=== 开始打包 ==='
sh 'mvn package -DskipTests -q'
}
}
}
post {
success {
echo '✅ 构建成功!'
}
failure {
echo '❌ 构建失败,请检查日志'
}
}
}
这版能跑通,但还远远不够。 接下来逐步优化。
三、进阶优化
3.1 添加缓存加速
每次构建都重新下载 Maven 依赖太慢了,加缓存:
groovy
pipeline {
agent any
tools {
maven 'Maven3.8'
jdk 'JDK17'
}
environment {
MAVEN_REPO = "${WORKSPACE}/.m2/repository"
}
stages {
stage('编译') {
steps {
sh "mvn clean compile -Dmaven.repo.local=${MAVEN_REPO} -q"
}
}
stage('单元测试') {
steps {
sh "mvn test -Dmaven.repo.local=${MAVEN_REPO}"
}
post {
success {
junit 'target/surefire-reports/TEST-*.xml' // 收集测试报告
}
}
}
stage('打包') {
steps {
sh "mvn package -DskipTests -Dmaven.repo.local=${MAVEN_REPO} -q"
}
post {
success {
archiveArtifacts artifacts: 'target/*.jar',
fingerprint: true
}
}
}
}
}
3.2 添加代码质量检查
集成 SonarQube 做代码扫描:
groovy
stage('代码质量检查') {
steps {
echo '=== SonarQube 扫描 ==='
withSonarQubeEnv('SonarQube-Server') {
sh "mvn sonar:sonar \
-Dsonar.projectKey=${env.JOB_NAME} \
-Dsonar.projectName=${env.JOB_NAME} \
-Dmaven.repo.local=${MAVEN_REPO}"
}
}
}
3.3 添加 Docker 镜像构建
groovy
stage('构建 Docker 镜像') {
steps {
echo '=== 构建 Docker 镜像 ==='
script {
def imageTag = "${env.BUILD_NUMBER}-${env.GIT_COMMIT?.take(8) ?: 'unknown'}"
def imageName = "registry.company.com/myapp/${env.JOB_NAME}"
sh """
docker build \
--build-arg JAR_FILE=target/*.jar \
-t ${imageName}:${imageTag} \
-t ${imageName}:latest \
-f docker/Dockerfile .
"""
// 推送到镜像仓库
docker.withRegistry('https://registry.company.com', 'registry-credentials') {
sh "docker push ${imageName}:${imageTag}"
sh "docker push ${imageName}:latest"
}
}
}
}
3.4 添加部署步骤
groovy
stage('部署到测试环境') {
when {
branch 'main' // 只在 main 分支部署
}
steps {
echo '=== 部署到测试环境 ==='
sshagent(['deploy-ssh-key']) {
sh """
ssh deploy@test-server << 'EOF'
cd /opt/myapp
docker pull registry.company.com/myapp/${JOB_NAME}:latest
docker-compose up -d
echo "部署完成"
EOF
"""
}
}
}
stage('部署到生产环境') {
when {
branch 'main'
expression { params.DEPLOY_ENV == 'production' }
}
input {
message '确认部署到生产环境?'
ok '确认部署'
submitter 'admin,tech-lead' // 只有这些人能审批
}
steps {
echo '=== 部署到生产环境 ==='
sshagent(['deploy-ssh-key']) {
sh """
ssh deploy@prod-server << 'EOF'
cd /opt/myapp
docker pull registry.company.com/myapp/${JOB_NAME}:latest
docker-compose up -d
echo "生产部署完成"
EOF
"""
}
}
}
四、完整生产级 Jenkinsfile
把上面的优化整合成一个完整版本:
groovy
pipeline {
agent {
label 'java' // 指定在有 java 标签的 Agent 上执行
}
tools {
maven 'Maven3.8'
jdk 'JDK17'
}
environment {
APP_NAME = 'my-spring-app'
MAVEN_REPO = "${WORKSPACE}/.m2/repository"
REGISTRY_URL = 'registry.company.com'
IMAGE_NAME = "${REGISTRY_URL}/myapp/${APP_NAME}"
IMAGE_TAG = "${BUILD_NUMBER}-${GIT_COMMIT?.take(8) ?: 'latest'}"
}
options {
timeout(time: 30, unit: 'MINUTES')
timestamps()
buildDiscarder(logRotator(numToKeepStr: '20'))
disableConcurrentBuilds() // 禁止并发构建
}
parameters {
choice(name: 'DEPLOY_ENV',
choices: ['staging', 'production'],
description: '选择部署环境')
booleanParam(name: 'SKIP_TEST', defaultValue: false, description: '是否跳过测试')
}
stages {
stage('拉取代码') {
steps {
echo "📦 拉取代码 - 分支: ${env.GIT_BRANCH}, 提交: ${env.GIT_COMMIT?.take(8)}"
checkout scm
}
}
stage('编译') {
steps {
echo '🔨 编译项目'
sh "mvn clean compile -Dmaven.repo.local=${MAVEN_REPO} -q"
}
}
stage('单元测试') {
when {
expression { return !params.SKIP_TEST }
}
steps {
echo '🧪 运行单元测试'
sh "mvn test -Dmaven.repo.local=${MAVEN_REPO}"
}
post {
success {
junit allowEmptyResults: true,
testResults: 'target/surefire-reports/TEST-*.xml'
}
}
}
stage('代码质量检查') {
when {
expression { return !params.SKIP_TEST }
}
steps {
echo '🔍 SonarQube 代码扫描'
withSonarQubeEnv('SonarQube-Server') {
sh """
mvn sonar:sonar \
-Dsonar.projectKey=${APP_NAME} \
-Dmaven.repo.local=${MAVEN_REPO}
"""
}
}
}
stage('打包') {
steps {
echo '📦 打包构建'
sh "mvn package -DskipTests -Dmaven.repo.local=${MAVEN_REPO} -q"
}
post {
success {
archiveArtifacts artifacts: 'target/*.jar',
fingerprint: true,
allowEmptyArchive: false
}
}
}
stage('构建 Docker 镜像') {
steps {
echo "🐳 构建镜像: ${IMAGE_NAME}:${IMAGE_TAG}"
sh """
docker build \
--build-arg JAR_FILE=target/*.jar \
-t ${IMAGE_NAME}:${IMAGE_TAG} \
-t ${IMAGE_NAME}:latest \
-f docker/Dockerfile .
"""
}
}
stage('推送镜像') {
steps {
echo "📤 推送镜像到 ${REGISTRY_URL}"
docker.withRegistry("https://${REGISTRY_URL}", 'registry-credentials') {
sh "docker push ${IMAGE_NAME}:${IMAGE_TAG}"
sh "docker push ${IMAGE_NAME}:latest"
}
}
}
stage('并行测试与检查') {
parallel {
stage('集成测试') {
steps {
echo '🧪 运行集成测试'
sh "mvn verify -Dmaven.repo.local=${MAVEN_REPO} -Pintegration-test"
}
}
stage('安全扫描') {
steps {
echo '🔒 安全扫描'
sh 'echo "执行安全扫描..."'
}
}
}
}
stage('部署到测试环境') {
when {
allOf {
branch 'main'
expression { return params.DEPLOY_ENV == 'staging' }
}
}
steps {
echo '🚀 部署到测试环境'
sshagent(['deploy-ssh-key']) {
sh """
ssh deploy@test-server 'cd /opt/${APP_NAME} && \
docker pull ${IMAGE_NAME}:${IMAGE_TAG} && \
IMAGE_TAG=${IMAGE_TAG} docker-compose up -d'
"""
}
}
post {
success {
echo "✅ 测试环境部署完成: http://test.example.com"
}
}
}
stage('部署到生产环境') {
when {
allOf {
branch 'main'
expression { return params.DEPLOY_ENV == 'production' }
}
}
input {
message "确认部署 ${APP_NAME}:${IMAGE_TAG} 到生产环境?"
ok '确认部署'
submitter 'admin,tech-lead'
}
steps {
echo '🚀 部署到生产环境'
sshagent(['deploy-ssh-key']) {
sh """
ssh deploy@prod-server 'cd /opt/${APP_NAME} && \
docker pull ${IMAGE_NAME}:${IMAGE_TAG} && \
IMAGE_TAG=${IMAGE_TAG} docker-compose up -d && \
echo "生产部署完成"'
"""
}
}
}
}
post {
success {
echo "✅ Pipeline 执行成功!"
emailext subject: "✅ [${JOB_NAME}] #${BUILD_NUMBER} 构建成功",
body: """
<h3>构建成功</h3>
<ul>
<li>项目:${JOB_NAME}</li>
<li>分支:${GIT_BRANCH}</li>
<li>提交:${GIT_COMMIT?.take(8)}</li>
<li>镜像:${IMAGE_NAME}:${IMAGE_TAG}</li>
<li>环境:${params.DEPLOY_ENV}</li>
<li><a href="${BUILD_URL}">查看详情</a></li>
</ul>
""",
to: 'team@company.com',
mime type: 'text/html'
}
failure {
echo "❌ Pipeline 执行失败!"
emailext subject: "❌ [${JOB_NAME}] #${BUILD_NUMBER} 构建失败",
body: "构建失败,请尽快排查!<br>日志:${BUILD_URL}console",
to: 'team@company.com',
mime type: 'text/html'
}
aborted {
echo "⚠️ Pipeline 被中止"
}
always {
echo "🧹 清理工作区"
cleanWs()
}
}
}
五、Pipeline 高级技巧
5.1 共享库(Shared Library)
当多个项目有相似的 Pipeline 逻辑时,可以抽取公共代码到共享库:
shared-library/
├── src/
│ └── com/company/
│ └── Utils.groovy
└── vars/
├── buildJavaApp.groovy
├── deployApp.groovy
└── notifyTeam.groovy
在 Pipeline 中引用共享库:
groovy
@Library('my-shared-library') _
pipeline {
agent any
stages {
stage('Build') {
steps {
buildJavaApp(jdkVersion: '17', mavenVersion: '3.8')
}
}
stage('Deploy') {
steps {
deployApp(env: 'staging', imageTag: env.IMAGE_TAG)
}
}
}
post {
always {
notifyTeam()
}
}
}
5.2 参数化构建
让每次构建可以传入不同参数:
groovy
parameters {
choice(name: 'DEPLOY_ENV',
choices: ['dev', 'staging', 'production'],
description: '部署环境')
string(name: 'VERSION', defaultValue: '', description: '指定版本号')
booleanParam(name: 'SKIP_TEST', defaultValue: false, description: '跳过测试')
}
在 Jenkins 页面点击 "Build with Parameters" 即可选择参数。
5.3 条件执行(when)
groovy
stage('Deploy') {
when {
anyOf {
branch 'main'
branch 'release/*'
}
not {
changelog '.*\\[skip-ci\\].*' // 提交信息含 [skip-ci] 则跳过
}
}
steps {
sh './deploy.sh'
}
}
常用 when 条件:
| 条件 | 说明 |
|---|---|
branch 'main' |
只在 main 分支执行 |
tag 'v*' |
只在 v 开头的 Tag 时执行 |
expression { ... } |
自定义 Groovy 表达式 |
changeset '**/*.java' |
有 Java 文件变更时执行 |
environment name: 'DEPLOY', value: 'true' |
环境变量匹配时执行 |
allOf { ... } |
所有条件都满足 |
anyOf { ... } |
任一条件满足 |
not { ... } |
条件取反 |
5.4 并行执行(Parallel)
同一个 stage 内可以并行执行多个子 stage:
groovy
stage('并行检查') {
parallel {
stage('单元测试') {
steps { sh 'mvn test' }
}
stage('代码扫描') {
steps { sh 'mvn sonar:sonar' }
}
stage('安全审计') {
steps { sh './security-scan.sh' }
}
}
}
5.5 人工审批(Input)
生产部署前加一个确认环节:
groovy
stage('生产部署确认') {
input {
message '确认部署到生产环境?'
ok '确认部署'
submitter 'admin,tech-lead'
parameters {
string(name: 'DEPLOY_NOTE', defaultValue: '', description: '部署备注')
}
}
steps {
echo "部署备注: ${params.DEPLOY_NOTE}"
sh './deploy-prod.sh'
}
}
5.6 错误处理(try-catch)
groovy
stage('Deploy') {
steps {
script {
try {
sh './deploy.sh'
} catch (Exception e) {
echo "部署失败: ${e.message}"
sh './rollback.sh' // 自动回滚
currentBuild.result = 'FAILURE'
throw e // 重新抛出,让 Pipeline 标记为失败
}
}
}
}
六、构建 19 次,#19 终于成功------调试之路
实际项目中,Pipeline 很少能一次跑通。分享一个真实的调试过程(项目共构建 19 次,第 19 次才完全成功):
6.1 常见失败原因与解决
| 构建次数 | 失败原因 | 解决方案 |
|---|---|---|
| #1 | Maven 依赖下载超时 | 配置 Maven 私服(Nexus)或本地缓存 |
| #2 | JDK 版本不匹配(用了 JDK 8 编译 JDK 17 的代码) | 修改 tools 配置指定 JDK17 |
| #3 | 单元测试失败(Mock 配置不对) | 修复测试用例 |
| #4-5 | SSH 连接远程服务器失败 | 检查凭据配置和网络连通性 |
| #6 | Docker build 上下文太大(没写 .dockerignore) | 添加 .dockerignore |
| #7 | 镜像推送到 Registry 认证失败 | 检查 registry-credentials 凭据 |
| #8-10 | Docker Compose 启动失败(端口冲突/环境变量缺失) | 调整 docker-compose.yml |
| #11-15 | 各种细节调整(路径、权限、日志级别等) | 逐步修复 |
| #16-18 | 优化 Pipeline(添加缓存、并行、通知) | 持续迭代 |
| #19 | ✅ 全部通过! | 🎉 |
6.2 调试技巧
groovy
// 1. 添加详细日志
echo "当前分支: ${env.GIT_BRANCH}"
echo "当前提交: ${env.GIT_COMMIT}"
echo "工作目录: ${env.WORKSPACE}"
// 2. 打印环境变量(排查变量引用问题)
stage('Debug') {
steps {
sh 'env | sort'
}
}
// 3. 使用 try-catch 定位失败步骤
script {
try {
sh 'some-command'
} catch (e) {
echo "命令执行失败: ${e.message}"
sh 'cat /tmp/debug.log' // 打印调试日志
throw e
}
}
// 4. 临时允许失败(先跑通其他步骤)
stage('Flaky Test') {
steps {
sh 'mvn test -Dtest=FlakyTest'
}
post {
unstable {
echo '测试不稳定,暂不阻塞构建'
}
}
}
七、Jenkinsfile 最佳实践
7.1 编写规范
| 原则 | 说明 |
|---|---|
| Jenkinsfile 放在仓库根目录 | 和代码一起版本管理 |
| Stage 命名清晰 | 用中文或有意义的英文,让 Blue Ocean 可视化好看 |
| 敏感信息走凭据 | 绝不硬编码密码、Token |
| 合理设置超时 | 避免构建卡死占用资源 |
| 添加注释 | 复杂逻辑写清楚注释 |
| 限制构建历史 | 用 buildDiscarder 避免磁盘占满 |
7.2 安全最佳实践
groovy
// ❌ 错误:硬编码密码
sh 'mysql -u root -p123456 -h db-server mydb < init.sql'
// ✅ 正确:使用凭据
withCredentials([usernamePassword(credentialsId: 'db-cred',
usernameVariable: 'DB_USER',
passwordVariable: 'DB_PASS')]) {
sh "mysql -u ${DB_USER} -p${DB_PASS} -h db-server mydb < init.sql"
}
7.3 性能优化
| 优化项 | 方法 |
|---|---|
| Maven 缓存 | 使用本地仓库路径 -Dmaven.repo.local |
| 并行执行 | parallel {} 让无依赖的 stage 同时跑 |
| Docker 层缓存 | 合理安排 Dockerfile 指令顺序 |
| 跳过不必要的步骤 | when {} 条件控制 |
| 限制并发构建 | disableConcurrentBuilds() |
八、Dockerfile 配套示例
配合 Jenkinsfile 中的 Docker 构建步骤:
dockerfile
# docker/Dockerfile
FROM eclipse-temurin:17-jre-alpine
LABEL maintainer="team@company.com"
# 时区设置
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 创建应用目录
WORKDIR /opt/app
# 构建参数
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
# JVM 参数(可通过环境变量覆盖)
ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC"
ENV SERVER_PORT=8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:${SERVER_PORT}/actuator/health || exit 1
EXPOSE ${SERVER_PORT}
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar app.jar"]
思维导图速览
Jenkins Pipeline 实战
│
├── Pipeline 基础
│ ├── Declarative(推荐) vs Scripted
│ ├── 基本结构:agent → stages → post
│ ├── 核心元素:stage/steps/environment/when/parallel
│ └── Jenkinsfile 放在仓库根目录
│
├── 构建流程(实战链路)
│ ├── 拉取代码(checkout scm)
│ ├── 编译(mvn compile)
│ ├── 单元测试(mvn test + junit 报告)
│ ├── 代码质量(SonarQube 扫描)
│ ├── 打包(mvn package)
│ ├── Docker 构建(docker build)
│ ├── 推送镜像(docker push)
│ └── 部署(SSH → docker-compose)
│
├── 高级技巧
│ ├── 共享库(Shared Library)复用逻辑
│ ├── 参数化构建(choice/string/booleanParam)
│ ├── 条件执行(when:branch/tag/changeset/expression)
│ ├── 并行执行(parallel 子 stage)
│ ├── 人工审批(input + submitter)
│ └── 错误处理(try-catch + 自动回滚)
│
├── 调试经验
│ ├── 添加 echo 日志
│ ├── 打印环境变量排查
│ ├── try-catch 定位失败步骤
│ ├── allow_failure 临时跳过
│ └── 构建19次才成功是常态,别灰心
│
├── 最佳实践
│ ├── Pipeline as Code(Jenkinsfile 版本管理)
│ ├── 敏感信息走 Credentials
│ ├── 合理设置超时和构建历史
│ └── 性能优化(缓存/并行/跳过/限并发)
│
└── 配套 Dockerfile
├── 基础镜像选择(JRE Alpine 更轻量)
├── 时区设置
├── JVM 参数可配置
└── 健康检查(HEALTHCHECK)
写在最后
Jenkins Pipeline 的核心思想就一个------Pipeline as Code。
构建流程不是 Web UI 上的点点点,而是写在 Jenkinsfile 里的代码。这意味着:
- 构建流程可以被 Review(和代码一起 Review)
- 构建流程可以被版本管理(改坏了能回退)
- 构建流程可以被复用(共享库机制)
面试中怎么聊 Jenkins?
- "我配置了 Jenkins CI/CD 流水线,Pipeline as Code,用 Jenkinsfile 管理构建流程"
- "集成了 GitLab Webhook,代码 push 后自动触发构建"
- "Pipeline 覆盖编译→测试→代码扫描→打包→Docker 构建→部署全流程"
- "生产部署前有人工审批环节,支持一键回滚"
- "项目迭代了 19 个版本,从 #1 到 #19 逐步跑通了整条 GitLab + Jenkins 集成链路"
到这里,Git + GitLab + Jenkins 三个模块就全部完成了。从版本控制到协作平台到自动化流水线,一套完整的 DevOps 工具链。