Jenkins Pipeline 进阶
大约 13 分钟约 3760 字
Jenkins Pipeline 进阶
简介
Jenkins Pipeline 进阶涵盖 Shared Libraries、多分支流水线、参数化构建、并行阶段和制品管理等高级特性。掌握这些能力后,可以将复杂的 CI/CD 流程编写为可维护、可复用的声明式代码,支撑大型团队的高效交付。
在 CI/CD 实践中,Jenkins Pipeline 的核心价值在于"流水线即代码"(Pipeline as Code)的理念。通过将构建、测试、部署流程声明为代码文件(Jenkinsfile),团队可以像管理应用代码一样管理 CI/CD 流程——进行代码审查、版本控制、灰度发布和回滚。本文将深入讲解 Jenkins Pipeline 的高级用法,帮助团队建立标准化、可维护的交付流水线。
特点
Pipeline 基础概念
声明式 vs 脚本式 Pipeline
// 声明式 Pipeline(推荐)— 结构清晰,语法约束强
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'make build'
}
}
}
}
// 脚本式 Pipeline — 灵活但难以维护
node {
stage('Build') {
sh 'make build'
}
}Pipeline 执行环境配置
pipeline {
agent {
docker {
image 'golang:1.21'
label 'docker'
args '--memory 4g --cpus 2'
}
}
// 全局环境变量
environment {
GO111MODULE = 'on'
CGO_ENABLED = '0'
GOPROXY = 'https://goproxy.cn,direct'
}
stages {
stage('Setup') {
steps {
sh 'go version'
sh 'go env'
}
}
}
}// 使用 Kubernetes Pod 模板(推荐用于 K8s 环境)
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: golang
image: golang:1.21
command: ['cat']
tty: true
- name: docker
image: docker:24
command: ['cat']
tty: true
volumeMounts:
- name: docker-sock
mountPath: /var/run/docker.sock
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
'''
defaultContainer 'golang'
}
}
stages {
stage('Build') {
steps {
container('golang') {
sh 'go build -o app ./cmd/server'
}
}
}
}
}实现
声明式 Pipeline 与并行阶段
// Jenkinsfile — 完整的声明式 Pipeline 示例
pipeline {
agent any
// 参数化构建
parameters {
choice(name: 'ENV', choices: ['staging', 'production'], description: '部署环境')
string(name: 'IMAGE_TAG', defaultValue: 'latest', description: '镜像标签')
booleanParam(name: 'RUN_E2E', defaultValue: true, description: '是否执行E2E测试')
text(name: 'DEPLOY_NOTES', defaultValue: '', description: '部署说明')
}
// 全局环境变量
environment {
REGISTRY = 'registry.example.com'
IMAGE_NAME = 'myapp'
FULL_IMAGE = "${REGISTRY}/${IMAGE_NAME}"
}
// 触发条件
triggers {
// 每 5 分钟轮询 SCM 变更
pollSCM('H/5 * * * *')
// 或者使用 Webhook 触发(推荐)
}
// 全局选项
options {
// 构建超时
timeout(time: 1, unit: 'HOURS')
// 保留最近 20 次构建
buildDiscarder(logRotator(numToKeepStr: '20'))
// 不允许并发构建
disableConcurrentBuilds()
// 时间戳输出
timestamps()
// ANSI 颜色输出
ansiColor('xterm')
}
stages {
stage('Checkout') {
steps {
checkout scm
sh 'git log --oneline -5'
}
}
stage('Build') {
steps {
sh "docker build -t ${FULL_IMAGE}:${IMAGE_TAG} ."
}
}
stage('Test') {
// 并行执行多个测试阶段
parallel {
stage('Unit Tests') {
steps {
sh 'go test ./... -v --coverprofile=coverage.out -race'
}
post {
always {
junit 'report.xml'
}
}
}
stage('Lint & Security') {
steps {
sh 'golangci-lint run ./...'
sh 'gosec ./...'
}
}
stage('Integration Tests') {
steps {
sh 'docker compose -f docker-compose.test.yml up --abort-on-container-exit'
}
}
}
}
stage('Push Image') {
steps {
withCredentials([usernamePassword(
credentialsId: 'docker-registry',
usernameVariable: 'REGISTRY_USER',
passwordVariable: 'REGISTRY_PASS'
)]) {
sh """
echo ${REGISTRY_PASS} | docker login ${REGISTRY} -u ${REGISTRY_USER} --password-stdin
docker push ${FULL_IMAGE}:${IMAGE_TAG}
docker tag ${FULL_IMAGE}:${IMAGE_TAG} ${FULL_IMAGE}:build-${BUILD_NUMBER}
docker push ${FULL_IMAGE}:build-${BUILD_NUMBER}
"""
}
}
}
stage('Deploy') {
when {
expression { params.ENV == 'production' }
}
steps {
input message: '确认发布到生产环境?', ok: '发布'
sh """
kubectl set image deployment/myapp myapp=${FULL_IMAGE}:${IMAGE_TAG} -n production
kubectl rollout status deployment/myapp -n production --timeout=300s
"""
}
}
stage('Smoke Test') {
when {
expression { params.ENV == 'production' }
}
steps {
sh '''
# 等待服务就绪
sleep 10
# 执行冒烟测试
curl -sf http://myapp.example.com/health || exit 1
curl -sf http://myapp.example.com/api/v1/status || exit 1
'''
}
}
}
post {
failure {
notifySlack('FAILED')
// 部署失败自动回滚
sh 'kubectl rollout undo deployment/myapp -n production || true'
}
success {
notifySlack('SUCCESS')
}
always {
cleanWs()
}
}
}Shared Library 复用公共逻辑
# Shared Library 仓库结构
shared-library/
├── src/ # Groovy 类库(复杂逻辑)
│ └── org/
│ └── devops/
│ ├── Docker.groovy
│ ├── KubeDeploy.groovy
│ └── Notifier.groovy
├── vars/ # 全局变量(Pipeline 中直接调用)
│ ├── standardPipeline.groovy
│ ├── dockerBuild.groovy
│ ├── helmDeploy.groovy
│ ├── notifySlack.groovy
│ └── sonarScan.groovy
└── resources/ # 资源文件(模板、脚本等)
├── docker/
│ └── Dockerfile.template
└── k8s/
└── deployment.yaml.template// vars/standardPipeline.groovy — 标准流水线模板
def call(Map config = [:]) {
pipeline {
agent { label config.agentLabel ?: 'docker' }
environment {
REGISTRY = config.registry ?: 'registry.example.com'
IMAGE = "${REGISTRY}/${config.repoName}"
}
options {
timeout(time: config.timeout ?: 1, unit: 'HOURS')
buildDiscarder(logRotator(numToKeepStr: '20'))
disableConcurrentBuilds()
timestamps()
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build & Push') {
steps {
dockerBuild(
image: "${IMAGE}",
tag: env.BUILD_NUMBER,
dockerfile: config.dockerfile ?: 'Dockerfile'
)
}
}
stage('Test') {
when {
not {
branch 'main'
}
}
steps {
sh config.testCommand ?: 'make test'
}
}
stage('SonarQube Scan') {
when {
anyOf {
branch 'main'
branch 'develop'
}
}
steps {
sonarScan(
projectKey: config.sonarProjectKey ?: config.repoName,
qualityGate: true
)
}
}
stage('Deploy to K8s') {
when {
branch config.deployBranch ?: 'main'
}
steps {
input message: "确认部署 ${config.repoName} 到 ${config.namespace}?", ok: '部署'
helmDeploy(
releaseName: config.releaseName,
chartPath: "./charts/${config.chartName}",
namespace: config.namespace,
imageTag: env.BUILD_NUMBER,
values: config.helmValues ?: [:]
)
}
}
}
post {
failure {
notifySlack(
channel: config.slackChannel ?: '#ci-cd',
status: 'FAILED',
projectName: config.repoName
)
}
success {
notifySlack(
channel: config.slackChannel ?: '#ci-cd',
status: 'SUCCESS',
projectName: config.repoName
)
}
}
}
}// vars/dockerBuild.groovy — Docker 构建推送
def call(Map config = [:]) {
withCredentials([usernamePassword(
credentialsId: config.credentialsId ?: 'docker-registry',
usernameVariable: 'REGISTRY_USER',
passwordVariable: 'REGISTRY_PASS'
)]) {
sh """
echo ${REGISTRY_PASS} | docker login ${config.registry ?: 'registry.example.com'} \
-u ${REGISTRY_USER} --password-stdin
docker build \
-t ${config.image}:${config.tag} \
-t ${config.image}:latest \
-f ${config.dockerfile ?: 'Dockerfile'} \
--build-arg BUILDKIT_INLINE_CACHE=1 \
.
docker push ${config.image}:${config.tag}
docker push ${config.image}:latest
"""
}
}// vars/helmDeploy.groovy — Helm 部署
def call(Map config = [:]) {
def valuesArgs = ''
if (config.values) {
config.values.each { key, value ->
valuesArgs += " --set ${key}=${value}"
}
}
sh """
helm upgrade --install ${config.releaseName} ${config.chartPath} \
--namespace ${config.namespace} \
--create-namespace \
--set image.tag=${config.imageTag} \
--wait \
--timeout 300s \
${valuesArgs} \
--history-max 10
"""
}// vars/notifySlack.groovy — Slack 通知
def call(Map config = [:]) {
def status = config.status ?: 'UNKNOWN'
def color = status == 'SUCCESS' ? '#36a64f' : (status == 'FAILED' ? '#ff0000' : '#ffaa00')
def emoji = status == 'SUCCESS' ? '✅' : (status == 'FAILED' ? '❌' : '⚠️')
slackSend(
channel: config.channel ?: '#ci-cd',
color: color,
message: "${emoji} *${config.projectName ?: env.JOB_NAME}* - 构建 #${env.BUILD_NUMBER} ${status}\n" +
"分支: ${env.BRANCH_NAME}\n" +
"触发人: ${env.BUILD_CAUSE ? '定时触发' : currentBuild.description ?: '手动触发'}\n" +
"耗时: ${currentBuild.durationString}\n" +
"<${env.BUILD_URL}|查看详情>"
)
}// 项目中使用共享库
// Jenkinsfile
@Library('my-shared-lib') _
standardPipeline(
repoName: 'web-api',
releaseName: 'web-api',
chartName: 'web-api',
namespace: 'production',
deployBranch: 'main',
slackChannel: '#team-web-api',
testCommand: 'make test-integration'
)Shared Library 高级用法
// src/org/devops/KubeDeploy.groovy — 复杂部署逻辑
package org.devops
class KubeDeploy implements Serializable {
def steps
def config
KubeDeploy(steps, Map config = [:]) {
this.steps = steps
this.config = config
}
def deploy() {
steps.withCredentials([steps.file(
credentialsId: config.kubeconfig ?: 'kubeconfig',
variable: 'KUBECONFIG'
)]) {
steps.sh """
export KUBECONFIG=${steps.env.KUBECONFIG}
kubectl set image deployment/${config.deployment} \
${config.container}=${config.image}:${config.tag} \
-n ${config.namespace}
kubectl rollout status deployment/${config.deployment} \
-n ${config.namespace} \
--timeout=${config.timeout ?: '300'}s
"""
}
}
def rollback() {
steps.withCredentials([steps.file(
credentialsId: config.kubeconfig ?: 'kubeconfig',
variable: 'KUBECONFIG'
)]) {
steps.sh """
export KUBECONFIG=${steps.env.KUBECONFIG}
kubectl rollout undo deployment/${config.deployment} -n ${config.namespace}
"""
}
}
}// 在 Pipeline 中使用类
@Library('my-shared-lib') _
def deployer = new org.devops.KubeDeploy(this, [
deployment: 'myapp',
container: 'myapp',
image: 'registry.example.com/myapp',
tag: env.BUILD_NUMBER,
namespace: 'production'
])
try {
deployer.deploy()
} catch (Exception e) {
echo "部署失败: ${e.message}"
deployer.rollback()
throw e
}多分支流水线与 PR 构建
// Jenkinsfile — 支持多分支自动发现
pipeline {
agent any
triggers {
// Webhook 触发(推荐,由 Git 平台推送事件)
// GitHub: 安装 GitHub Integration Plugin
// GitLab: 安装 GitLab Plugin
}
stages {
stage('Checkout') {
steps {
checkout scm
// 显示分支信息
sh 'echo "Branch: ${BRANCH_NAME}"'
sh 'echo "Change ID: ${CHANGE_ID}"'
sh 'echo "Change Target: ${CHANGE_TARGET}"'
sh 'echo "Change Title: ${CHANGE_TITLE}"'
}
}
stage('Build') {
steps {
sh 'make build'
}
}
stage('Test') {
when {
anyOf {
branch 'main'
branch 'develop'
changeRequest()
}
}
steps {
sh 'make test'
}
}
stage('Code Quality') {
when {
anyOf {
branch 'main'
changeRequest()
}
}
steps {
sh 'make lint'
// SonarQube 扫描
withSonarQubeEnv('sonar-server') {
sh 'sonar-scanner'
}
// 等待质量门禁结果
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
stage('Build Image') {
when {
anyOf {
branch 'main'
branch 'develop'
changeRequest()
}
}
steps {
sh """
docker build -t registry.example.com/app:${BUILD_NUMBER} .
"""
}
}
stage('Deploy Staging') {
when {
branch 'develop'
}
steps {
sh 'make deploy ENV=staging'
}
}
stage('Deploy Production') {
when {
branch 'main'
}
steps {
// 人工审批
input message: '确认发布到生产环境?', ok: '发布'
timeout(time: 30, unit: 'MINUTES')
sh 'make deploy ENV=production'
}
}
stage('PR Comment') {
when {
changeRequest()
}
steps {
script {
// 在 PR 中添加评论(需要 GitHub/GitLab Token)
def prComment = """
## CI/CD 构建结果 #${BUILD_NUMBER}
**状态**: ${currentBuild.result ?: 'SUCCESS'}
**分支**: ${BRANCH_NAME} -> ${CHANGE_TARGET}
**耗时**: ${currentBuild.durationString}
@${CHANGE_AUTHOR} 请查看构建结果。
"""
echo "PR Comment: ${prComment}"
}
}
}
}
}制品管理与归档
pipeline {
agent any
stages {
stage('Package') {
steps {
sh 'make package'
// 归档制品(可在 Jenkins UI 中下载)
archiveArtifacts artifacts: 'dist/*.tar.gz', fingerprint: true
// 保存制品到 stash(跨节点传递)
stash name: 'artifact', includes: 'dist/*.tar.gz'
}
}
stage('Publish to Nexus') {
steps {
unstash 'artifact'
sh '''
TAG=$(git describe --tags --always)
curl -u admin:password --upload-file dist/app.tar.gz \
"https://nexus.example.com/repository/maven-releases/com/example/app/${TAG}/app-${TAG}.tar.gz"
'''
}
}
stage('Build & Push Docker Image') {
steps {
script {
def tag = sh(returnStdout: true, script: 'git describe --tags --always').trim()
docker.build("registry.example.com/app:${tag}")
docker.withRegistry('https://registry.example.com', 'docker-registry') {
docker.image("registry.example.com/app:${tag}").push()
docker.image("registry.example.com/app:${tag}").push('latest')
}
}
}
}
}
post {
always {
// 清理工作空间
cleanWs()
// 代码质量报告
recordIssues(tools: [go(), golangciLint()])
}
success {
// 发布构建信息
publishHTML(target: [
allowMissing: false,
alwaysLinkToLastBuild: false,
keepAll: true,
reportDir: 'coverage',
reportFiles: 'index.html',
reportName: 'Coverage Report'
])
}
}
}凭证管理与安全
// 使用 Jenkins Credentials 管理敏感信息
pipeline {
agent any
stages {
stage('Deploy') {
steps {
// 用户名密码凭证
withCredentials([usernamePassword(
credentialsId: 'docker-registry',
usernameVariable: 'REGISTRY_USER',
passwordVariable: 'REGISTRY_PASS'
)]) {
sh 'docker login -u $REGISTRY_USER -p $REGISTRY_PASS registry.example.com'
}
// SSH 密钥凭证
withCredentials([sshUserPrivateKey(
credentialsId: 'deploy-key',
keyFileVariable: 'SSH_KEY',
usernameVariable: 'SSH_USER'
)]) {
sh "ssh -i $SSH_KEY ${SSH_USER}@deploy.example.com 'docker pull ...'"
}
// Secret Text 凭证
withCredentials([string(
credentialsId: 'slack-webhook',
variable: 'SLACK_WEBHOOK'
)]) {
sh "curl -X POST $SLACK_WEBHOOK -d '{\"text\":\"Build done\"}'"
}
// Secret File 凭证
withCredentials([file(
credentialsId: 'kubeconfig',
variable: 'KUBECONFIG'
)]) {
sh 'kubectl --kubeconfig $KUBECONFIG get pods'
}
}
}
}
}通知与报告
// Slack 通知(推荐使用 Shared Library 封装)
pipeline {
agent any
post {
always {
// 使用 Jenkins 内置的 Slack 插件
slackSend(
channel: '#ci-cd',
color: currentBuild.result == 'SUCCESS' ? 'good' : 'danger',
message: "${currentBuild.result}: ${env.JOB_NAME} #${env.BUILD_NUMBER} (${env.BRANCH_NAME})"
)
// 邮件通知
mail(
to: 'team@example.com',
subject: "构建 ${currentBuild.result}: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "查看: ${env.BUILD_URL}"
)
}
}
}错误处理与重试
pipeline {
agent any
stages {
stage('Deploy') {
steps {
// 重试机制
retry(3) {
sh 'kubectl apply -f k8s/'
}
}
}
stage('Smoke Test') {
steps {
script {
// 自定义超时和重试
timeout(time: 5, unit: 'MINUTES') {
waitUntil {
def result = sh(
returnStdout: true,
script: 'curl -sf http://myapp.example.com/health || echo "FAIL"'
).trim()
return (result == 'OK')
}
}
}
}
}
stage('Rollback on Failure') {
steps {
script {
try {
sh 'make deploy ENV=production'
sh 'make smoke-test'
} catch (Exception e) {
echo "部署失败,执行回滚: ${e.message}"
sh 'make rollback'
currentBuild.result = 'UNSTABLE'
throw e
}
}
}
}
}
}Jenkinsfile 模板与最佳实践
// 推荐的 Jenkinsfile 目录结构
// Jenkinsfile(项目根目录)
@Library('my-shared-lib') _
// 从配置文件读取参数
def config = readYaml(file: '.jenkins/pipeline.yaml')
standardPipeline(
repoName: config.project.name,
releaseName: config.project.name,
chartName: config.project.name,
namespace: config.deploy.namespace,
deployBranch: config.deploy.branch,
slackChannel: config.notify.channel
)# .jenkins/pipeline.yaml — 流水线配置文件
project:
name: web-api
deploy:
branch: main
namespace: production
notify:
channel: '#team-web-api'
test:
command: make test-integration
sonar:
projectKey: web-api# Jenkins 全局配置 — Shared Library
# Manage Jenkins -> Configure System -> Global Pipeline Libraries
# Name: my-shared-lib
# Default Version: main
# Repository URL: https://github.com/example/shared-library.git
# Credentials: github-token
# 或者通过 Jenkins Configuration as Code (JCasC) 配置# jenkins.yaml (JCasC 配置)
jenkins:
cascDefaults:
globalConfigFiles:
- mavenSettings:
id: maven-settings
name: Maven Settings
content: |
<settings>...</settings>
unclassified:
globalLibraries:
libraries:
- name: my-shared-lib
defaultVersion: main
retriever:
modernSCM:
scm:
git:
remote: https://github.com/example/shared-library.git
credentialsId: github-token优点
缺点
总结
Jenkins Pipeline 进阶的核心是将构建逻辑从 UI 配置迁移到代码管理,通过 Shared Libraries 和声明式语法实现可维护、可复用的 CI/CD 流程。大型团队应建立统一的 Jenkinsfile 模板和共享库,新项目基于模板快速启动。同时关注 Jenkins 本身的运维——JCasC 配置管理、Agent 弹性伸缩、插件版本控制,确保 CI/CD 平台的稳定性。
关键知识点
- 声明式 Pipeline 的 when 指令支持 branch、environment、expression、changeRequest 等条件
- parallel 块内的阶段同时执行,但同一 parallel 内的阶段之间不能有依赖关系
- Shared Library 存放在独立 Git 仓库的 vars/ 目录下,文件名即为函数名
- stash/unstash 用于跨阶段、跨节点传递构建产物
- withCredentials 用于安全使用敏感信息,禁止在 Pipeline 中硬编码密码
- input 指令必须配合 timeout 使用,否则流水线可能无限等待
- options 块中的 timeout、disableConcurrentBuilds 是生产环境的必备配置
项目落地视角
- 建立团队统一的 Shared Library,标准化构建、测试、部署流程
- 为每个项目创建多分支流水线,自动发现分支并触发构建
- 将敏感凭证(Registry 密码、K8s token)存储在 Jenkins Credentials 中,禁止硬编码
- 使用 JCasC 管理 Jenkins 全局配置,将 Jenkins 本身的配置也纳入版本控制
- 为生产部署配置 input 审批和自动回滚机制
- 监控 Jenkins 构建队列长度和 Agent 资源使用率
常见误区
- 在 Shared Library 中写入过多的业务逻辑,导致库本身难以维护
- Pipeline 中使用 blocking input 但没有设置 timeout,导致流水线永久挂起
- 并行阶段中共享同一工作空间导致文件冲突
- 在 Pipeline 中硬编码密码、Token 等敏感信息
- 不使用 stash/unstash,直接依赖工作空间文件传递,导致跨节点失败
- Shared Library 不做版本控制,导致不同项目使用不同版本的库代码
- Jenkins 插件不锁定版本,自动更新导致构建失败
进阶路线
- 学习 Jenkins Templating Engine(JTE),进一步抽象流水线模板
- 掌握 Jenkins Configuration as Code(JCasC),实现 Jenkins 本身的配置管理
- 评估 GitHub Actions / GitLab CI 等云原生 CI/CD 平台作为 Jenkins 的替代方案
- 学习 Jenkins Agent 的 Kubernetes 弹性伸缩(Kubernetes Plugin)
- 研究 Jenkins 与 ArgoCD/Flux 的混合 CI/CD 模式
适用场景
- 大型团队需要标准化的构建流程和共享库管理
- 复杂的多阶段发布流程需要条件判断和人工审批
- 多分支开发模式下需要自动发现分支并触发构建
- 需要集成多种工具链(SonarQube、Nexus、Kubernetes、Docker)的 CI/CD 流程
- 对 CI/CD 平台有高度定制化需求
落地建议
- 将 Jenkinsfile 和 Shared Library 纳入代码审查流程,任何变更必须经过 Review
- 为生产部署配置 input 审批和 timeout 限制,防止流水线无限等待
- 监控 Jenkins 构建成功率和平均耗时,将流水线性能纳入可观测性体系
- 使用 JCasC 管理 Jenkins 配置,避免手动 UI 配置导致的环境漂移
- 定期清理旧的构建记录和镜像,避免磁盘空间不足
- 为 Jenkins Master 配置高可用(如 Active-Active 模式)
排错清单
- 流水线语法错误:使用 Jenkins 的 Pipeline Syntax 生成器辅助编写
- Shared Library 加载失败:检查库的 Git 地址、分支和凭证配置
- 并行阶段资源竞争:使用 agent none + 指定节点,或使用 lock 资源互斥
- 凭证使用失败:检查 Credentials ID 是否正确,变量名是否拼写正确
- Docker 构建失败:检查 Docker daemon 是否运行,镜像仓库凭证是否有效
- 构建队列堆积:检查 Agent 节点数量和标签配置,考虑启用弹性伸缩
- Webhook 不触发:检查 Git 平台和 Jenkins 之间的网络连通性
复盘问题
- 当前团队的 Jenkinsfile 是否有统一的模板和规范?
- Shared Library 的变更是否经过充分测试?是否有回滚机制?
- 流水线的平均执行时间是否在可接受范围?哪个阶段是瓶颈?
- Jenkins 本身的配置是否纳入了版本控制?
- 团队是否考虑过迁移到云原生 CI/CD 平台(如 GitHub Actions)?
