概述
Pipeline as Code 是 Jenkins 从"拖拽式配置"走向"代码化"的分水岭。把流水线定义写在 Jenkinsfile 中,纳入 Git 版本控制,意味着每次流水线变更都有 diff 可审查、有历史可追溯、有分支可回滚。从 Jenkinsfile 语法到生产级流水线设计,详细梳理 Pipeline as Code 的核心实践。
一、Jenkinsfile 语法
1.1 声明式 vs 脚本式
Jenkins Pipeline 有两种语法风格:
| 维度 | 声明式(Declarative) | 脚本式(Scripted) |
|---|---|---|
| 语法 | 结构化 DSL | Groovy 代码 |
| 可读性 | 高,接近配置文件 | 低,需要 Groovy 知识 |
| 灵活性 | 受限于 DSL 约束 | 完全自由 |
| 输入验证 | 内置 post、when 等结构 | 需手动实现 |
| 推荐场景 | 标准流水线、团队协作 | 复杂逻辑、条件分支多 |
// === 声明式 Pipeline ===
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'make build'
}
}
}
}
// === 脚本式 Pipeline ===
node {
stage('Build') {
sh 'make build'
}
}
实践建议:优先用声明式,仅在声明式无法表达的复杂逻辑处用
script {}块嵌入脚本式代码。
1.2 声明式 Pipeline 结构
pipeline {
// === Agent:执行环境 ===
agent {
label 'linux && docker'
// 或使用 Docker
// docker { image 'golang:1.22' }
// 或 K8s
// kubernetes { ... }
}
// === 环境变量 ===
environment {
APP_NAME = 'myapp'
VERSION = "${env.BUILD_ID}"
DOCKER_REGISTRY = 'registry.example.com'
// 从 Credentials 读取
DOCKER_CREDENTIALS = credentials('docker-registry')
// 从配置文件读取
DEPLOY_CONFIG = 'deploy-config'
}
// === 选项 ===
options {
timeout(time: 30, unit: 'MINUTES')
timestamps()
buildDiscarder(logRotator(numToKeepStr: '20'))
disableConcurrentBuilds()
retry(3)
ansiColor('xterm')
}
// === 触发器 ===
triggers {
cron('H 2 * * *') // 每天凌晨 2 点
pollSCM('H/5 * * * *') // 每 5 分钟检查 SCM
upstream(upstreamProjects: 'base-library', threshold: hudson.model.Result.SUCCESS)
}
// === 参数 ===
parameters {
choice(name: 'ENVIRONMENT', choices: ['staging', 'production'], description: '部署环境')
string(name: 'VERSION', defaultValue: '', description: '部署版本(留空则用最新)')
booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: '跳过测试')
password(name: 'DEPLOY_TOKEN', defaultValue: '', description: '部署令牌')
}
// === 阶段 ===
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'make build'
}
}
stage('Test') {
when {
expression { !params.SKIP_TESTS }
}
steps {
sh 'make test'
}
}
}
// === 后置处理 ===
post {
always {
junit 'reports/**/*.xml'
archiveArtifacts artifacts: 'dist/**', fingerprint: true
cleanWs()
}
success {
echo 'Pipeline 执行成功'
}
failure {
echo 'Pipeline 执行失败'
// 发送通知
emailext(
subject: "构建失败: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "请检查: ${env.BUILD_URL}",
to: 'team@example.com'
)
}
unstable {
echo '构建不稳定'
}
changed {
echo '构建状态发生变化'
}
}
}
二、多分支流水线
2.1 配置多分支流水线
多分支流水线自动发现 Git 仓库中的分支和 PR,为每个分支创建独立的 Jenkins Job:
// Jenkinsfile - 多分支流水线适配
pipeline {
agent any
environment {
// 根据分支名动态设置环境
BRANCH_NAME = "${env.BRANCH_NAME ?: 'unknown'}"
IS_MAIN = "${env.BRANCH_NAME == 'main'}"
IS_PR = "${env.CHANGE_ID != null}"
}
stages {
stage('Detect Environment') {
steps {
script {
echo "分支: ${env.BRANCH_NAME}"
echo "是否 PR: ${env.CHANGE_ID != null}"
if (env.CHANGE_ID) {
echo "PR 编号: ${env.CHANGE_ID}"
echo "PR 目标分支: ${env.CHANGE_TARGET}"
}
}
}
}
stage('Build') {
steps {
sh 'make build'
}
}
stage('Test') {
steps {
sh 'make test'
}
}
stage('Deploy') {
when {
anyOf {
branch 'main'
branch 'release/*'
}
expression { env.CHANGE_ID == null } // 非 PR
}
steps {
script {
if (env.BRANCH_NAME == 'main') {
echo '部署到 staging'
sh 'make deploy-staging'
} else if (env.BRANCH_NAME?.startsWith('release/')) {
input message: '确认部署到生产?', ok: '部署'
echo '部署到 production'
sh 'make deploy-production'
}
}
}
}
}
}
2.2 分支策略
main → 自动部署 staging
release/* → 人工确认后部署 production
develop → 构建并测试,不部署
feature/* → 构建(快速验证)
PR → 构建 + 测试 + 代码质量检查
// 分支特定的 when 条件
stage('Deploy Staging') {
when {
branch 'main'
}
steps {
sh 'make deploy-staging'
}
}
stage('Deploy Production') {
when {
branch 'release/*'
beforeInput true // 在 input 之前评估 when
}
input {
message "确认部署到生产环境?"
ok "部署"
submitter "release-managers"
}
steps {
sh 'make deploy-production'
}
}
stage('PR Check') {
when {
changeRequest target: 'main'
}
steps {
sh 'make lint security-scan'
}
}
三、共享库
3.1 创建共享库
当多个项目有相似的流水线逻辑时,提取为共享库统一维护:
jenkins-shared-library/
├── vars/
│ ├── standardPipeline.groovy # 全局变量(可直接调用)
│ ├── deploy.groovy
│ ├── notify.groovy
│ └── buildDocker.groovy
├── src/
│ └── com/
│ └── example/
│ ├── Deployer.groovy # 类库
│ └── Config.groovy
└── resources/
└── templates/
└── deployment.yaml # 资源文件
// vars/standardPipeline.groovy
// 标准流水线模板,各项目复用
def call(Map config = [:]) {
pipeline {
agent {
label config.agent ?: 'linux'
}
environment {
APP_NAME = config.appName ?: error('appName is required')
DOCKER_REGISTRY = config.registry ?: 'registry.example.com'
}
options {
timeout(time: config.timeout ?: 30, unit: 'MINUTES')
timestamps()
buildDiscarder(logRotator(numToKeepStr: '20'))
disableConcurrentBuilds()
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh config.buildCmd ?: 'make build'
}
}
stage('Test') {
steps {
sh config.testCmd ?: 'make test'
}
post {
always {
junit testResults: config.testResults ?: 'reports/**/*.xml',
allowEmptyResults: true
}
}
}
stage('Code Quality') {
when {
expression { config.qualityCheck != false }
}
steps {
sh config.qualityCmd ?: 'make lint'
}
}
stage('Build Image') {
when {
expression { config.dockerBuild != false }
}
steps {
script {
buildDocker(
image: "${env.APP_NAME}",
tag: "${env.BUILD_NUMBER}"
)
}
}
}
stage('Deploy') {
when {
anyOf {
branch config.deployBranch ?: 'main'
expression { env.BRANCH_NAME?.startsWith('release/') }
}
}
steps {
script {
deploy(
appName: env.APP_NAME,
version: env.BUILD_NUMBER,
env: env.BRANCH_NAME == 'main' ? 'staging' : 'production'
)
}
}
}
}
post {
failure {
notify.slack(
channel: config.slackChannel ?: '#alerts',
message: "构建失败: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
}
}
}
3.2 使用共享库
// 项目 Jenkinsfile - 只需几行
@Library('jenkins-shared-library@v1.2') _
standardPipeline(
appName: 'payment-service',
agent: 'golang',
buildCmd: 'go build -o bin/app ./cmd/',
testCmd: 'go test -v -race ./...',
qualityCmd: 'golangci-lint run',
dockerBuild: true,
deployBranch: 'main',
slackChannel: '#payments'
)
3.3 共享库工具函数
// vars/buildDocker.groovy
def call(Map params) {
def image = params.image
def tag = params.tag
def dockerfile = params.dockerfile ?: 'Dockerfile'
def context = params.context ?: '.'
sh """
docker build -t ${image}:${tag} \
-f ${dockerfile} \
--build-arg VERSION=${tag} \
${context}
"""
// 推送镜像
if (params.push != false) {
withCredentials([usernamePassword(
credentialsId: 'docker-registry',
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS'
)]) {
sh """
echo \$DOCKER_PASS | docker login registry.example.com -u \$DOCKER_USER --password-stdin
docker push ${image}:${tag}
docker tag ${image}:${tag} ${image}:latest
docker push ${image}:latest
docker logout registry.example.com
"""
}
}
}
// vars/notify.groovy
def slack(Map params) {
def color = params.color ?: 'danger'
def channel = params.channel ?: '#general'
def message = params.message ?: 'No message'
slackSend(
channel: channel,
color: color,
message: message
)
}
def dingtalk(Map params) {
def message = params.message ?: 'No message'
def mobiles = params.mobiles ?: []
dingtalk(
robot: 'jenkins-robot',
type: 'MARKDOWN',
title: 'Jenkins 通知',
text: message,
at: mobiles
)
}
def email(Map params) {
emailext(
subject: params.subject ?: "Jenkins: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: params.body ?: "请检查: ${env.BUILD_URL}",
to: params.to ?: 'team@example.com',
mimeType: 'text/html',
attachmentsPattern: params.attachments ?: ''
)
}
四、并行阶段
4.1 并行执行测试
pipeline {
agent any
stages {
stage('Test') {
parallel {
stage('Unit Tests') {
steps {
sh 'go test -v -short ./... 2>&1 | tee reports/unit-tests.xml'
}
post {
always {
junit 'reports/unit-tests.xml'
}
}
}
stage('Integration Tests') {
steps {
sh 'make test-integration'
}
}
stage('Lint') {
steps {
sh 'golangci-lint run --out-format=junit > reports/lint.xml'
}
post {
always {
junit 'reports/lint.xml'
}
}
}
stage('Security Scan') {
steps {
sh 'trivy fs --severity HIGH,CRITICAL .'
}
}
stage('License Check') {
steps {
sh 'go-licenses check ./...'
}
}
}
}
}
}
4.2 矩阵构建
pipeline {
agent any
stages {
stage('Matrix Build') {
matrix {
axes {
axis {
name 'GO_VERSION'
values '1.21', '1.22', '1.23'
}
axis {
name 'GOOS'
values 'linux', 'darwin', 'windows'
}
axis {
name 'GOARCH'
values 'amd64', 'arm64'
}
}
excludes {
// 排除不支持的组合
exclude {
axis {
name 'GOOS'
values 'windows'
}
axis {
name 'GOARCH'
values 'arm64'
}
}
}
agent {
label "golang-${GO_VERSION}"
}
stages {
stage('Build') {
steps {
sh """
GOOS=${GOOS} GOARCH=${GOARCH} \
go build -o bin/app-${GOOS}-${GOARCH} ./cmd/
"""
}
}
stage('Test') {
when {
expression { GOOS == 'linux' && GOARCH == 'amd64' }
}
steps {
sh 'go test -v ./...'
}
}
}
}
}
}
}
五、条件触发
5.1 when 条件详解
pipeline {
agent any
stages {
// 基于分支
stage('Deploy Staging') {
when {
branch 'main'
}
steps { sh 'make deploy-staging' }
}
// 基于 PR
stage('PR Validation') {
when {
changeRequest target: 'main', branch: 'feature/*'
}
steps { sh 'make validate' }
}
// 基于表达式
stage('Production Deploy') {
when {
expression {
env.BRANCH_NAME ==~ /release\/.*/ &&
env.BUILD_NUMBER?.toInteger() > 0
}
}
steps { sh 'make deploy-production' }
}
// 基于变更文件路径
stage('Build Frontend') {
when {
changeset 'frontend/**'
}
steps { sh 'cd frontend && npm run build' }
}
stage('Build Backend') {
when {
changeset 'backend/**'
}
steps { sh 'cd backend && go build' }
}
// 基于提交信息
stage('Skip on [skip ci]') {
when {
expression {
!env.GIT_COMMIT_MESSAGE?.contains('[skip ci]')
}
}
steps { echo 'Running because no skip ci' }
}
// 组合条件
stage('Complex Condition') {
when {
allOf {
branch 'main'
changeset 'deploy/**'
expression { params.FORCE_DEPLOY || env.GIT_MESSAGE?.contains('[deploy]') }
}
}
steps { sh 'make deploy' }
}
// 排除条件
stage('Except Docs') {
when {
not {
changeset 'docs/**'
}
}
steps { sh 'make build' }
}
}
}
5.2 获取提交信息
stage('Get Commit Info') {
steps {
script {
// 获取最近提交
def commitMsg = sh(
script: 'git log -1 --pretty=%B',
returnStdout: true
).trim()
def commitAuthor = sh(
script: 'git log -1 --pretty=%an',
returnStdout: true
).trim()
def commitHash = sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
// 设置环境变量供后续使用
env.GIT_MESSAGE = commitMsg
env.GIT_AUTHOR = commitAuthor
env.GIT_HASH = commitHash
echo "Commit: ${commitHash} by ${commitAuthor}"
echo "Message: ${commitMsg}"
// 判断是否包含特定标记
if (commitMsg.contains('[skip deploy]')) {
echo '跳过部署阶段'
env.SKIP_DEPLOY = 'true'
}
}
}
}
六、制品管理
6.1 制品上传与下载
pipeline {
agent any
environment {
ARTIFACT_NAME = "myapp-${env.BUILD_NUMBER}.tar.gz"
}
stages {
stage('Package') {
steps {
sh """
tar czf ${ARTIFACT_NAME} \
bin/ \
configs/ \
migrations/
"""
}
}
stage('Upload Artifact') {
steps {
// 方案一:使用 Artifactory 插件
rtUpload(
serverId: 'artifactory',
spec: '''{
"files": [
{
"pattern": "*.tar.gz",
"target": "libs-release-local/myapp/${BUILD_NUMBER}/"
}
]
}'''
)
// 方案二:使用 Nexus
nexusArtifactUploader(
nexusVersion: 'nexus3',
protocol: 'https',
nexusUrl: 'nexus.example.com',
groupId: 'com.example',
version: "${env.BUILD_NUMBER}",
repository: 'releases',
credentialsId: 'nexus-credentials',
artifacts: [
[artifactId: 'myapp',
classifier: '',
file: "${ARTIFACT_NAME}",
type: 'tar.gz']
]
)
// 方案三:使用 AWS S3
withAWS(credentials: 'aws-credentials', region: 'cn-north-1') {
s3Upload(
bucket: 'my-artifacts',
path: "myapp/${env.BUILD_NUMBER}/",
includePathPattern: '**/*.tar.gz'
)
}
}
}
stage('Download Artifact') {
steps {
script {
// 从制品库下载
rtDownload(
serverId: 'artifactory',
spec: '''{
"files": [{
"pattern": "libs-release-local/myapp/${BUILD_NUMBER}/*.tar.gz",
"target": "downloads/"
}]
}'''
)
}
}
}
}
}
6.2 制品版本管理
// vars/publishArtifact.groovy
def call(Map params) {
def appName = params.appName
def version = params.version
def files = params.files ?: []
def repository = params.repository ?: 'releases'
// 判断是 SNAPSHOT 还是 Release
def isRelease = !version.contains('SNAPSHOT') && !version.contains('dev')
if (!isRelease) {
repository = 'snapshots'
}
echo "上传制品到 ${repository}: ${appName}:${version}"
files.each { file ->
echo " 上传: ${file}"
}
// 构建制品元数据
def metadata = [
appName: appName,
version: version,
buildNumber: env.BUILD_NUMBER,
buildUrl: env.BUILD_URL,
gitCommit: env.GIT_COMMIT,
timestamp: new Date().format("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone('UTC'))
]
// 写入元数据文件
writeJSON file: 'artifact-metadata.json', json: metadata, pretty: 2
// 上传元数据
files << 'artifact-metadata.json'
return metadata
}
七、与 Kubernetes 集成
7.1 K8s Pod Agent
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: golang
image: golang:1.22-alpine
command: ['sleep', 'infinity']
volumeMounts:
- mountPath: /go/pkg
name: gopkg
- mountPath: /root/.cache/go-build
name: gobuild
resources:
requests:
memory: 1Gi
cpu: 500m
limits:
memory: 2Gi
cpu: 1000m
- name: docker
image: docker:24
command: ['sleep', 'infinity']
securityContext:
privileged: true
volumeMounts:
- mountPath: /var/run/docker.sock
name: docker-sock
- name: kubectl
image: bitnami/kubectl:latest
command: ['sleep', 'infinity']
volumes:
- name: gopkg
emptyDir: {}
- name: gobuild
emptyDir: {}
- name: docker-sock
hostPath:
path: /var/run/docker.sock
'''
defaultContainer: 'golang'
}
}
stages {
stage('Build') {
steps {
container('golang') {
sh 'go build -o bin/app ./cmd/'
}
}
}
stage('Docker Build') {
steps {
container('docker') {
sh """
docker build -t myapp:${env.BUILD_NUMBER} .
docker tag myapp:${env.BUILD_NUMBER} registry.example.com/myapp:${env.BUILD_NUMBER}
docker push registry.example.com/myapp:${env.BUILD_NUMBER}
"""
}
}
}
stage('Deploy') {
steps {
container('kubectl') {
sh """
kubectl set image deployment/myapp \
app=registry.example.com/myapp:${env.BUILD_NUMBER} \
-n production
kubectl rollout status deployment/myapp -n production --timeout=300s
"""
}
}
}
}
}
7.2 动态 Agent 模板
// 根据项目类型选择不同的 Pod 模板
def getPodTemplate(String projectType) {
def templates = [
golang: '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: golang
image: golang:1.22-alpine
command: ['sleep', 'infinity']
resources:
requests: { memory: 1Gi, cpu: 500m }
limits: { memory: 2Gi, cpu: 2000m }
- name: docker
image: docker:24
securityContext: { privileged: true }
volumeMounts:
- mountPath: /var/run/docker.sock
name: docker-sock
volumes:
- name: docker-sock
hostPath: { path: /var/run/docker.sock }
''',
nodejs: '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: node
image: node:20-alpine
command: ['sleep', 'infinity']
resources:
requests: { memory: 2Gi, cpu: 500m }
limits: { memory: 4Gi, cpu: 2000m }
- name: docker
image: docker:24
securityContext: { privileged: true }
volumeMounts:
- mountPath: /var/run/docker.sock
name: docker-sock
volumes:
- name: docker-sock
hostPath: { path: /var/run/docker.sock }
''',
python: '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: python
image: python:3.12-slim
command: ['sleep', 'infinity']
resources:
requests: { memory: 1Gi, cpu: 500m }
limits: { memory: 2Gi, cpu: 1000m }
'''
]
return templates[projectType] ?: templates.golang
}
pipeline {
agent none
stages {
stage('Build & Test') {
steps {
script {
def template = getPodTemplate('golang')
podTemplate(yaml: template) {
node(POD_LABEL) {
container('golang') {
sh 'go test -v ./...'
sh 'go build -o bin/app ./cmd/'
}
}
}
}
}
}
}
}
八、蓝绿部署流水线
8.1 完整蓝绿部署
pipeline {
agent any
environment {
APP_NAME = 'myapp'
VERSION = "${env.BUILD_NUMBER}"
NAMESPACE = 'production'
DOCKER_IMAGE = "registry.example.com/${APP_NAME}:${VERSION}"
}
stages {
stage('Build & Push') {
steps {
sh "docker build -t ${DOCKER_IMAGE} ."
sh "docker push ${DOCKER_IMAGE}"
}
}
stage('Deploy to Blue') {
steps {
sh """
# 部署到 Blue 环境
envsubst < deploy/blue-green.yaml | kubectl apply -f - -n ${NAMESPACE}
# 等待 Blue 就绪
kubectl rollout status deployment/${APP_NAME}-blue -n ${NAMESPACE} --timeout=300s
"""
}
}
stage('Smoke Test Blue') {
steps {
sh """
# 通过 Blue 服务的 ClusterIP 进行冒烟测试
BLUE_IP=\$(kubectl get svc ${APP_NAME}-blue -n ${NAMESPACE} -o jsonpath='{.spec.clusterIP}')
# 等待服务可用
for i in \$(seq 1 30); do
if curl -sf http://\${BLUE_IP}:8080/health; then
echo "Blue 服务就绪"
break
fi
sleep 2
done
# 冒烟测试
curl -sf http://\${BLUE_IP}:8080/health || exit 1
curl -sf http://\${BLUE_IP}:8080/api/version | grep "${VERSION}" || exit 1
curl -sf http://\${BLUE_IP}:8080/api/users?limit=1 || exit 1
echo "冒烟测试通过"
"""
}
}
stage('Switch Traffic') {
input {
message "Blue 环境冒烟测试通过,是否切换流量?"
ok "切换到 Blue"
submitter "release-managers"
}
steps {
sh """
# 更新 service selector,将流量指向 Blue
kubectl patch svc ${APP_NAME} -n ${NAMESPACE} -p \\
'{"spec":{"selector":{"version":"blue"}}}'
echo "流量已切换到 Blue"
"""
}
}
stage('Verify Production') {
steps {
sh """
# 验证生产流量正常
sleep 10
# 检查错误率
ERROR_RATE=\$(curl -sf 'http://prometheus:9090/api/v1/query' \\
--data-urlencode 'query=rate(http_requests_total{app="${APP_NAME}",status=~"5.."}[1m]) / rate(http_requests_total{app="${APP_NAME}"}[1m])' \\
| jq -r '.data.result[0].value[1] // "0"')
echo "当前错误率: \${ERROR_RATE}"
if (( \$(echo "\${ERROR_RATE} > 0.05" | bc -l) )); then
echo "错误率过高,自动回滚!"
kubectl patch svc ${APP_NAME} -n ${NAMESPACE} -p \\
'{"spec":{"selector":{"version":"green"}}}'
exit 1
fi
echo "生产验证通过"
"""
}
}
stage('Cleanup Green') {
steps {
sh """
# 等待一段时间确认稳定后清理旧环境
kubectl scale deployment ${APP_NAME}-green -n ${NAMESPACE} --replicas=0
echo "Green 环境已缩容"
"""
}
}
}
post {
failure {
sh """
# 失败时自动回滚到 Green
kubectl patch svc ${APP_NAME} -n ${NAMESPACE} -p \\
'{"spec":{"selector":{"version":"green"}}}' || true
echo "已回滚到 Green 环境"
"""
slackSend(
channel: '#alerts',
color: 'danger',
message: "蓝绿部署失败,已回滚: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
success {
slackSend(
channel: '#deployments',
color: 'good',
message: "蓝绿部署成功: ${env.APP_NAME} v${env.VERSION}"
)
}
}
}
8.2 金丝雀部署
// 金丝雀部署:逐步放量
stage('Canary Deploy') {
steps {
sh """
# 部署金丝雀版本(1 个副本)
kubectl set image deployment/${APP_NAME}-canary \
app=${DOCKER_IMAGE} -n ${NAMESPACE} || \
kubectl create deployment ${APP_NAME}-canary \
--image=${DOCKER_IMAGE} -n ${NAMESPACE}
kubectl scale deployment ${APP_NAME}-canary -n ${NAMESPACE} --replicas=1
"""
}
}
stage('Canary 10% Traffic') {
steps {
sh """
# 通过 Istio VirtualService 调整流量比例
kubectl patch virtualservice ${APP_NAME} -n ${NAMESPACE} --type='json' \\
-p='[{"op":"replace","path":"/spec/http/0/route/0/weight","value":90},{"op":"replace","path":"/spec/http/0/route/1/weight","value":10}]'
echo "10% 流量指向金丝雀版本"
sleep 60 # 观察一分钟
"""
}
}
stage('Canary 50% Traffic') {
input {
message "10% 金丝雀测试正常,是否放到 50%?"
ok "放到 50%"
}
steps {
sh """
kubectl patch virtualservice ${APP_NAME} -n ${NAMESPACE} --type='json' \\
-p='[{"op":"replace","path":"/spec/http/0/route/0/weight","value":50},{"op":"replace","path":"/spec/http/0/route/1/weight","value":50}]'
sleep 120 # 观察两分钟
"""
}
}
stage('Canary 100% Traffic') {
input {
message "50% 金丝雀测试正常,是否全量?"
ok "全量发布"
submitter "release-managers"
}
steps {
sh """
kubectl patch virtualservice ${APP_NAME} -n ${NAMESPACE} --type='json' \\
-p='[{"op":"replace","path":"/spec/http/0/route/0/weight","value":0},{"op":"replace","path":"/spec/http/0/route/1/weight","value":100}]'
# 更新主 deployment
kubectl set image deployment/${APP_NAME} \\
app=${DOCKER_IMAGE} -n ${NAMESPACE}
kubectl rollout status deployment/${APP_NAME} -n ${NAMESPACE}
# 清理金丝雀
kubectl delete deployment ${APP_NAME}-canary -n ${NAMESPACE}
"""
}
}
九、流水线好的实践
9.1 流水线性能优化
pipeline {
agent any
options {
// 超时控制
timeout(time: 30, unit: 'MINUTES')
// 不并发构建
disableConcurrentBuilds()
// 保留历史
buildDiscarder(logRotator(numToKeepStr: '30'))
// 时间戳
timestamps()
}
stages {
// 跳过未变更的阶段
stage('Build') {
when {
changeset '**/*.go'
changeset 'go.mod'
}
steps {
sh 'make build'
}
}
// 并行执行无依赖的任务
stage('Test') {
parallel {
stage('Unit Test') {
steps { sh 'make test-unit' }
}
stage('Lint') {
steps { sh 'make lint' }
}
stage('Security Scan') {
steps { sh 'make security-scan' }
}
}
}
// 缓存依赖
stage('Restore Cache') {
steps {
sh '''
if [ -d /go/pkg/mod ]; then
cp -r /go/pkg/mod ./go-mod-cache || true
fi
'''
}
}
}
}
9.2 通知与监控
// vars/pipelineNotify.groovy
def call(Map config = [:]) {
def status = config.status ?: currentBuild.currentResult
def channel = config.channel ?: '#ci-cd'
def color = 'good'
def emoji = ':white_check_mark:'
if (status == 'FAILURE') {
color = 'danger'
emoji = ':x:'
} else if (status == 'UNSTABLE') {
color = 'warning'
emoji = ':warning:'
}
def duration = currentBuild.durationString ?: 'unknown'
def message = """
${emoji} Pipeline ${status}
*Job:* ${env.JOB_NAME}
*Build:* #${env.BUILD_NUMBER}
*Branch:* ${env.BRANCH_NAME ?: 'N/A'}
*Duration:* ${duration}
*URL:* ${env.BUILD_URL}
""".stripIndent().trim()
// Slack 通知
slackSend(channel: channel, color: color, message: message)
// 钉钉通知
if (config.dingtalk) {
dingtalk(
robot: config.dingtalkRobot ?: 'jenkins',
type: 'MARKDOWN',
title: "Pipeline ${status}",
text: message
)
}
// 邮件通知(仅失败时)
if (status == 'FAILURE' && config.emailOnFailure != false) {
emailext(
subject: "[${status}] ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: message,
to: config.emailTo ?: 'team@example.com',
mimeType: 'text/html'
)
}
}
总结
Jenkins Pipeline as Code 的核心价值在于:把 CI/CD 流程从"配置"变成"代码"。这意味着流水线可以版本控制、可以代码审查、可以复用、可以测试。回顾本文要点:
- 声明式优先:声明式 Pipeline 结构清晰、有内置验证机制,适合团队协作。复杂逻辑用
script {}块嵌入,而不是全盘用脚本式 - 多分支流水线是标配:自动为分支和 PR 创建独立 Job,配合
when条件实现"main 部署 staging、release 部署 production、PR 只做检查"的分支策略 - 共享库消除重复:把标准流水线逻辑提取为
vars/standardPipeline.groovy,各项目只需几行配置。版本锁定共享库,避免上游变更影响所有项目 - 并行提速明显:测试、Lint、安全扫描并行执行,构建时间从串行的 20 分钟降到 5 分钟。矩阵构建覆盖多平台多版本
- 制品管理不可省:每次构建的产物要上传到制品库(Artifactory/Nexus/S3),包含版本号、构建号、Git Hash 等元数据。部署时从制品库拉取,不从源码重新构建
- K8s 集成弹性好:用 K8s Pod 作为 Jenkins Agent,按需创建销毁。不同项目用不同容器镜像,环境完全隔离
- 蓝绿/金丝雀要自动化:部署到备用环境→冒烟测试→切流量→验证→清理,全链路自动化。失败自动回滚,人工只在关键决策点
input介入
Pipeline as Code 不是把流水线配置搬到代码文件里那么简单,而是用工程化的方式管理 CI/CD 流程。当每个项目的 Jenkinsfile 都只有十几行(调用共享库),而所有复杂逻辑都在共享库中统一维护、版本化迭代时,CI/CD 才真正实现了"代码化治理"。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- Jenkins Pipeline 官方文档 — Jenkins 项目,参考了Jenkins Pipeline 官方文档相关内容