概述
背景介绍
团队构建CI/CD体系的过程,往往是一个逐步演进的旅程。最初,我们依赖手动操作:登录服务器、拉取代码、执行编译、重启服务。随后,我们转向了Jenkins的Freestyle Job,通过配置一系列Shell脚本来实现自动化。然而,随着服务数量的增长,为每个服务维护独立的Job变得异常繁琐和痛苦。
一个关键的转折点发生在上线事故后。在回滚过程中,我们才发现之前的构建产物已无处可寻,配置也混乱不堪,无人能清晰复现上一个版本的确切部署流程。这次经历促使我们下定决心,使用Pipeline彻底重构了整个CI/CD流程。
Pipeline as Code带来的优势是显而易见的:版本控制、代码审查、逻辑复用以及可测试性。本文将梳理我们在这一演进过程中积累的经验与最佳实践。
技术特点
Pipeline vs Freestyle Job对比:
| 对比项 |
Freestyle |
Pipeline |
| 配置方式 |
Web界面配置 |
Jenkinsfile代码 |
| 版本控制 |
无 |
随代码库版本控制 |
| 复杂流程 |
难以实现 |
原生支持 |
| 并行执行 |
需要插件 |
原生支持 |
| 代码复用 |
复制粘贴 |
Shared Library |
| 可维护性 |
差 |
好 |
Declarative vs Scripted Pipeline示例:
// Declarative Pipeline(推荐)
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'mvn clean package'
}
}
}
}
// Scripted Pipeline(灵活但复杂)
node {
stage('Build') {
sh 'mvn clean package'
}
}
推荐使用Declarative Pipeline,其语法结构更清晰,并内置了语法验证机制。
适用场景
- 场景一:多服务微服务架构的CI/CD流程。
- 场景二:包含多阶段、多环境的复杂构建与部署流程。
- 场景三:需要严格代码审查的发布流程。
- 场景四:多团队或项目需要共享CI/CD基础设施。
环境要求
| 组件 |
版本要求 |
说明 |
| Jenkins |
2.400+ |
LTS长期支持版本 |
| Pipeline插件 |
最新 |
核心插件 |
| Git |
2.0+ |
代码版本管理 |
| Docker |
20.10+ |
容器化构建 |
| Kubernetes |
1.20+ |
可选,用于K8s部署 |
详细步骤
基础Pipeline结构
最小化Jenkinsfile
pipeline {
agent any
environment {
APP_NAME = 'my-app'
VERSION = "${env.BUILD_NUMBER}"
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
}
}
stage('Test') {
steps {
sh 'mvn test'
}
post {
always {
junit '**/target/surefire-reports/*.xml'
}
}
}
stage('Deploy') {
steps {
sh './deploy.sh'
}
}
}
post {
success {
echo 'Pipeline succeeded!'
}
failure {
echo 'Pipeline failed!'
}
}
}
常用指令说明
pipeline {
// 1. 指定运行节点
agent {
label 'linux' // 指定标签
// 或 docker { image 'maven:3.8-jdk-11' } // Docker容器
// 或 kubernetes { yaml '...' } // K8s Pod
}
// 2. 环境变量
environment {
DOCKER_REGISTRY = 'registry.example.com'
DOCKER_CREDENTIALS = credentials('docker-cred')
}
// 3. 参数化构建
parameters {
string(name:'BRANCH', defaultValue:'main', description:'分支名')
choice(name:'ENV', choices: ['dev', 'staging', 'prod'], description:'环境')
booleanParam(name:'SKIP_TESTS', defaultValue:false, description:'跳过测试')
}
// 4. 触发器
triggers {
cron('H 2 * * *') // 定时构建
pollSCM('H/5 * * * *') // 轮询SCM
githubPush() // GitHub Webhook
}
// 5. 选项
options {
timeout(time:30, unit:'MINUTES')
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr:'10'))
timestamps()
}
stages { /* 阶段定义 */ }
post { /* 后置处理 */ }
}
多阶段流水线
完整的CI/CD流程示例
pipeline {
agent any
environment {
DOCKER_REGISTRY = 'registry.example.com'
APP_NAME = 'my-app'
VERSION = "${env.BUILD_NUMBER}-${env.GIT_COMMIT?.take(7)}"
}
stages {
stage('Checkout') {
steps {
checkout scm
script {
env.GIT_COMMIT = sh(returnStdout:true, script:'git rev-parse HEAD').trim()
env.GIT_BRANCH = sh(returnStdout:true, script:'git rev-parse --abbrev-ref HEAD').trim()
}
}
}
stage('Build') { steps { sh 'mvn clean package -DskipTests -Drevision=${VERSION}' } }
stage('Unit Test') {
steps { sh 'mvn test' }
post { always { junit '**/target/surefire-reports/*.xml' } }
}
stage('Code Analysis') {
steps {
withSonarQubeEnv('SonarQube') {
sh 'mvn sonar:sonar'
}
}
}
stage('Build Docker Image') {
steps {
script {
docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-credentials') {
def image = docker.build("${DOCKER_REGISTRY}/${APP_NAME}:${VERSION}")
image.push()
image.push('latest')
}
}
}
}
stage('Deploy to Dev') {
when { branch 'develop' }
steps {
sh "kubectl set image deployment/${APP_NAME} ${APP_NAME}=${DOCKER_REGISTRY}/${APP_NAME}:${VERSION} -n dev"
}
}
stage('Deploy to Staging') {
when { branch 'main' }
steps {
sh "kubectl set image deployment/${APP_NAME} ${APP_NAME}=${DOCKER_REGISTRY}/${APP_NAME}:${VERSION} -n staging"
}
}
stage('Deploy to Production') {
when { branch 'main' }
input {
message "Deploy to production?"
ok "Yes, deploy it!"
submitter "admin,ops"
}
steps {
sh "kubectl set image deployment/${APP_NAME} ${APP_NAME}=${DOCKER_REGISTRY}/${APP_NAME}:${VERSION} -n production"
}
}
}
post {
success { slackSend(color:'good', message:"✅ ${APP_NAME} ${VERSION} deployed successfully") }
failure { slackSend(color:'danger', message:"❌ ${APP_NAME} build failed: ${env.BUILD_URL}") }
}
}
并行执行
stage('Parallel Tests') {
parallel {
stage('Unit Tests') { steps { sh 'mvn test' } }
stage('Integration Tests') { steps { sh 'mvn verify -Pintegration' } }
stage('E2E Tests') { steps { sh 'npm run e2e' } }
}
}
矩阵构建
stage('Build Matrix') {
matrix {
axes {
axis { name 'PLATFORM'; values 'linux', 'windows', 'mac' }
axis { name 'JDK'; values '11', '17', '21' }
}
excludes {
exclude {
axis { name 'PLATFORM'; values 'mac' }
axis { name 'JDK'; values '11' }
}
}
stages {
stage('Build') {
steps {
echo "Building on ${PLATFORM} with JDK ${JDK}"
sh "./build.sh --platform=${PLATFORM} --jdk=${JDK}"
}
}
}
}
}
Shared Library(共享库)
将多个项目共用的逻辑抽取到Shared Library中,可以有效减少代码重复。
目录结构
jenkins-shared-library/
├── vars/ # 全局变量/函数
│ ├── buildMaven.groovy
│ ├── deployToK8s.groovy
│ └── notifySlack.groovy
├── src/ # 类定义
│ └── com/example/Docker.groovy
├── resources/ # 资源文件
│ └── templates/deployment.yaml
└── Jenkinsfile # 库自身测试
定义共享步骤示例
// vars/buildMaven.groovy
def call(Map config = [:]) {
def mavenVersion = config.mavenVersion ?: '3.8'
def jdkVersion = config.jdkVersion ?: '11'
def skipTests = config.skipTests ?: false
pipeline {
agent { docker { image "maven:${mavenVersion}-jdk-${jdkVersion}" } }
stages {
stage('Build') {
steps { sh "mvn clean package ${skipTests ? '-DskipTests' : ''}" }
}
stage('Test') {
when { expression { !skipTests } }
steps { sh 'mvn test' }
post { always { junit '**/target/surefire-reports/*.xml' } }
}
}
}
}
// vars/deployToK8s.groovy
def call(Map config) {
def namespace = config.namespace
def deployment = config.deployment
def image = config.image
def kubeconfig = config.kubeconfig ?: 'kubeconfig'
withCredentials([file(credentialsId: kubeconfig, variable:'KUBECONFIG')]) {
sh """
kubectl set image deployment/${deployment} ${deployment}=${image} -n ${namespace} --kubeconfig=\$KUBECONFIG
kubectl rollout status deployment/${deployment} -n ${namespace} --timeout=300s --kubeconfig=\$KUBECONFIG
"""
}
}
使用Shared Library
@Library('my-shared-library') _
// 方式一:直接调用封装好的完整Pipeline
buildMaven(mavenVersion:'3.9', jdkVersion:'17', skipTests:false)
// 方式二:在自定义Pipeline中调用共享函数
pipeline {
agent any
stages {
stage('Build') { steps { sh 'mvn clean package' } }
stage('Deploy') {
steps {
deployToK8s(
namespace: 'production',
deployment: 'my-app',
image: "registry.example.com/my-app:${env.BUILD_NUMBER}"
)
}
}
}
}
凭据管理
安全地管理和使用密码、密钥等敏感信息。
使用Jenkins凭据
pipeline {
environment {
// 用户名密码类型凭据,会自动注入 DOCKER_CREDS_USR 和 DOCKER_CREDS_PSW 变量
DOCKER_CREDS = credentials('docker-credentials')
SSH_KEY = credentials('ssh-private-key') // 密钥文件
API_TOKEN = credentials('api-token') // 密文
}
stages {
stage('Docker Login') {
steps {
sh 'echo $DOCKER_CREDS_PSW | docker login -u $DOCKER_CREDS_USR --password-stdin'
}
}
stage('Deploy via SSH') {
steps {
withCredentials([sshUserPrivateKey(credentialsId:'ssh-key', keyFileVariable:'SSH_KEY')]) {
sh 'ssh -i $SSH_KEY user@server "docker pull myapp:latest && docker-compose up -d"'
}
}
}
}
}
高级特性
动态Agent
在不同阶段使用不同类型的Agent执行环境。
pipeline {
agent none // 不指定全局agent
stages {
stage('Build') {
agent { docker { image 'maven:3.8-jdk-11'; args '-v $HOME/.m2:/root/.m2' } }
steps {
sh 'mvn clean package'
stash includes:'target/*.jar', name:'app' // 暂存构建产物
}
}
stage('Build Docker') {
agent { label 'docker' }
steps {
unstash 'app' // 取出上阶段产物
sh 'docker build -t myapp:latest .'
}
}
stage('Deploy') {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: kubectl
image: bitnami/kubectl:latest
command: ['sleep', 'infinity']
'''
}
}
steps {
container('kubectl') {
sh 'kubectl apply -f deployment.yaml'
}
}
}
}
}
错误处理与重试
stage('Deploy') {
steps {
retry(3) { // 失败自动重试3次
sh './deploy.sh'
}
}
}
stage('Smoke Test') {
steps {
timeout(time:5, unit:'MINUTES') { // 超时控制
waitUntil { // 等待直到条件满足
script {
def response = sh(script:'curl -s -o /dev/null -w "%{http_code}" http://myapp/health', returnStdout:true).trim()
return response == '200'
}
}
}
}
}
post {
failure {
script {
if (env.BRANCH_NAME == 'main') {
sh 'kubectl rollout undo deployment/myapp' // 自动回滚
}
}
}
}
示例代码与配置
完整的微服务Pipeline示例
此示例展示了一个在Kubernetes环境中运行,包含代码质量检查、多环境部署和审批流程的完整微服务Pipeline。
@Library('my-shared-library@main') _
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: maven
image: maven:3.9-eclipse-temurin-17
command: ['sleep', 'infinity']
volumeMounts:
- name: maven-cache
mountPath: /root/.m2
- name: docker
image: docker:24-dind
securityContext:
privileged: true
- name: kubectl
image: bitnami/kubectl:1.28
command: ['sleep', 'infinity']
volumes:
- name: maven-cache
persistentVolumeClaim:
claimName: maven-cache
'''
}
}
environment {
DOCKER_REGISTRY = 'registry.example.com'
APP_NAME = 'order-service'
}
parameters {
choice(name:'DEPLOY_ENV', choices: ['dev', 'staging', 'prod'], description:'部署环境')
booleanParam(name:'SKIP_TESTS', defaultValue:false, description:'跳过测试')
booleanParam(name:'FORCE_DEPLOY', defaultValue:false, description:'强制部署(跳过审批)')
}
options {
timeout(time:30, unit:'MINUTES')
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr:'20'))
timestamps()
}
stages {
stage('Checkout') {
steps {
checkout scm
script {
env.GIT_COMMIT_SHORT = sh(returnStdout:true, script:'git rev-parse --short HEAD').trim()
env.VERSION = "${env.BUILD_NUMBER}-${env.GIT_COMMIT_SHORT}"
env.IMAGE_TAG = "${DOCKER_REGISTRY}/${APP_NAME}:${VERSION}"
}
}
}
stage('Build') {
steps { container('maven') { sh 'mvn clean package -DskipTests -Drevision=${VERSION}' } }
}
stage('Test') {
when { expression { !params.SKIP_TESTS } }
parallel {
stage('Unit Tests') {
steps { container('maven') { sh 'mvn test' } }
post { always { junit '**/target/surefire-reports/*.xml' } }
}
stage('Integration Tests') {
steps { container('maven') { sh 'mvn verify -Pintegration' } }
}
}
}
stage('Code Quality') {
when { expression { !params.SKIP_TESTS } }
steps {
container('maven') {
withSonarQubeEnv('SonarQube') {
sh 'mvn sonar:sonar'
}
}
}
}
stage('Quality Gate') {
when { expression { !params.SKIP_TESTS } }
steps {
timeout(time:5, unit:'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
stage('Build & Push Image') {
steps {
container('docker') {
withCredentials([usernamePassword(credentialsId:'docker-cred', usernameVariable:'USER', passwordVariable:'PASS')]) {
sh '''
echo $PASS | docker login ${DOCKER_REGISTRY} -u $USER --password-stdin
docker build -t ${IMAGE_TAG} .
docker push ${IMAGE_TAG}
docker tag ${IMAGE_TAG} ${DOCKER_REGISTRY}/${APP_NAME}:latest
docker push ${DOCKER_REGISTRY}/${APP_NAME}:latest
'''
}
}
}
}
stage('Deploy to Dev') {
when { expression { params.DEPLOY_ENV == 'dev' } }
steps {
container('kubectl') {
deployToK8s(namespace:'dev', deployment: APP_NAME, image: IMAGE_TAG, kubeconfig:'kubeconfig-dev')
}
}
}
stage('Deploy to Production') {
when { expression { params.DEPLOY_ENV == 'prod' } }
stages {
stage('Approval') {
when { expression { !params.FORCE_DEPLOY } }
steps {
script {
def approval = input(
message: "Deploy ${APP_NAME}:${VERSION} to production?",
ok: 'Deploy',
submitter: 'ops,admin',
parameters: [ choice(name:'STRATEGY', choices: ['rolling', 'canary'], description:'部署策略') ]
)
env.DEPLOY_STRATEGY = approval.STRATEGY
}
}
}
stage('Full Rollout') {
steps {
container('kubectl') {
deployToK8s(namespace:'production', deployment: APP_NAME, image: IMAGE_TAG, kubeconfig:'kubeconfig-prod')
}
}
}
}
}
}
post {
success { notifySlack(status:'SUCCESS', channel:'#deployments') }
failure {
notifySlack(status:'FAILURE', channel:'#deployments')
script {
if (params.DEPLOY_ENV == 'prod') {
emailext(
to: 'ops@example.com',
subject: "❌ ${APP_NAME} Production Deploy Failed",
body: "Build ${env.BUILD_URL} failed. Please check immediately."
)
}
}
}
always { cleanWs() }
}
}
实际应用案例
案例一:构建时间优化
问题:Maven构建每次下载依赖,耗时长达15分钟。
解决方案:在Kubernetes Pod中使用PersistentVolumeClaim (PVC) 缓存本地Maven仓库。
# maven-cache-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: maven-cache
namespace: jenkins
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
在Pipeline的agent配置中挂载该PVC:
agent {
kubernetes {
yaml '''
spec:
containers:
- name: maven
volumeMounts:
- name: maven-cache
mountPath: /root/.m2
volumes:
- name: maven-cache
persistentVolumeClaim:
claimName: maven-cache
'''
}
}
效果:构建时间从15分钟降至3分钟。
案例二:多环境配置管理
问题:不同环境(开发、测试、生产)配置混杂,易出错。
解决方案:使用独立的YAML配置文件,配合参数化构建动态读取。
stage('Deploy') {
steps {
script {
def envConfig = readYaml(file: "config/${params.ENV}.yaml")
sh """
kubectl set image deployment/${APP_NAME} ${APP_NAME}=${IMAGE_TAG} -n ${envConfig.namespace}
kubectl set env deployment/${APP_NAME} DB_HOST=${envConfig.database.host} -n ${envConfig.namespace}
"""
}
}
}
最佳实践与注意事项
最佳实践
- 设计原则:
- 快速失败:将易失败的步骤(如代码检查、单元测试)前置。
- 并行执行:将无依赖关系的任务(如不同类型的测试)并行化。
- 缓存复用:对构建依赖(Maven、NPM)和Docker镜像层进行缓存。
- 最小权限:凭据按需使用,避免全局暴露。
- 可重复性:确保相同的代码输入总能产生相同的输出。
- 目录结构规范:
project/
├── Jenkinsfile # Pipeline定义
├── jenkins/
│ ├── scripts/ # 构建/部署脚本
│ └── config/ # 多环境配置
├── Dockerfile
└── src/
- 版本管理:为容器化构建的镜像定义清晰的版本标签。
environment {
VERSION = "${env.BUILD_NUMBER}-${env.GIT_COMMIT?.take(7)}-${new Date().format('yyyyMMdd')}"
}
注意事项与安全
- 常见错误:
- 凭据泄露:避免在日志中打印密码。始终使用
withCredentials包装敏感操作。
- 构建缓慢:未使用依赖缓存。务必配置Maven、NPM等缓存。
- 并发冲突:多个构建同时操作同一分支。使用
disableConcurrentBuilds()选项。
- 安全建议:
// 错误做法:密码可能出现在日志中
sh "docker login -u admin -p ${PASSWORD}"
// 正确做法:使用凭据管理
withCredentials([usernamePassword(credentialsId:'docker-cred', usernameVariable:'USER', passwordVariable:'PASS')]) {
sh 'echo $PASS | docker login -u $USER --password-stdin'
}
故障排查与监控
总结
技术要点回顾
- Pipeline as Code:将流水线定义为代码(Jenkinsfile),实现版本控制、协作审查和复用。
- Shared Library:抽象通用步骤,提升代码复用率和维护性。
- 安全的凭据管理:杜绝硬编码,利用Jenkins凭据管理或外部Vault集成。
- 并行化与矩阵构建:充分利用资源,显著缩短流水线执行时间。
- 清晰的审批与流程控制:通过
input步骤和条件判断(when),为生产环境部署等关键操作设置卡点。
核心价值
通过采纳上述Pipeline最佳实践,团队能够构建出高效、可靠、安全且易于维护的自动化部署流水线,为持续集成与持续交付奠定坚实基础。