概述

Pipeline as Code 是 Jenkins 从"拖拽式配置"走向"代码化"的分水岭。把流水线定义写在 Jenkinsfile 中,纳入 Git 版本控制,意味着每次流水线变更都有 diff 可审查、有历史可追溯、有分支可回滚。从 Jenkinsfile 语法到生产级流水线设计,详细梳理 Pipeline as Code 的核心实践。

参考来源:Jenkins Pipeline 官方文档

一、Jenkinsfile 语法

1.1 声明式 vs 脚本式

Jenkins Pipeline 有两种语法风格:

维度声明式(Declarative)脚本式(Scripted)
语法结构化 DSLGroovy 代码
可读性高,接近配置文件低,需要 Groovy 知识
灵活性受限于 DSL 约束完全自由
输入验证内置 postwhen 等结构需手动实现
推荐场景标准流水线、团队协作复杂逻辑、条件分支多
// === 声明式 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 流程从"配置"变成"代码"。这意味着流水线可以版本控制、可以代码审查、可以复用、可以测试。回顾本文要点:

  1. 声明式优先:声明式 Pipeline 结构清晰、有内置验证机制,适合团队协作。复杂逻辑用 script {} 块嵌入,而不是全盘用脚本式
  2. 多分支流水线是标配:自动为分支和 PR 创建独立 Job,配合 when 条件实现"main 部署 staging、release 部署 production、PR 只做检查"的分支策略
  3. 共享库消除重复:把标准流水线逻辑提取为 vars/standardPipeline.groovy,各项目只需几行配置。版本锁定共享库,避免上游变更影响所有项目
  4. 并行提速明显:测试、Lint、安全扫描并行执行,构建时间从串行的 20 分钟降到 5 分钟。矩阵构建覆盖多平台多版本
  5. 制品管理不可省:每次构建的产物要上传到制品库(Artifactory/Nexus/S3),包含版本号、构建号、Git Hash 等元数据。部署时从制品库拉取,不从源码重新构建
  6. K8s 集成弹性好:用 K8s Pod 作为 Jenkins Agent,按需创建销毁。不同项目用不同容器镜像,环境完全隔离
  7. 蓝绿/金丝雀要自动化:部署到备用环境→冒烟测试→切流量→验证→清理,全链路自动化。失败自动回滚,人工只在关键决策点 input 介入

Pipeline as Code 不是把流水线配置搬到代码文件里那么简单,而是用工程化的方式管理 CI/CD 流程。当每个项目的 Jenkinsfile 都只有十几行(调用共享库),而所有复杂逻辑都在共享库中统一维护、版本化迭代时,CI/CD 才真正实现了"代码化治理"。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. Jenkins Pipeline 官方文档 — Jenkins 项目,参考了Jenkins Pipeline 官方文档相关内容