Jenkins 流水线用于实现自动化且可复现的 CI/CD 流水线,支持声明式与脚本式两种语法,。 一种公认的最佳实践是将流水线脚本编写为 Jenkinsfile,并托管在远程仓库(比如 GitHub)中。

参考文档:

https://www.jenkins.io/doc/pipeline/tour/getting-started

https://www.jenkins.io/doc/book/pipeline/syntax

声明式流水线

声明式流水线可读性比较好,上手比较简单,但是没有那么灵活。

使用远程仓库托管流水线脚本的情况下,仓库会被下载到流水线的工作路径中。 可以用 pwd 命令或者环境变量 WORKSPACE 确认实际的工作路径,一般是 <agent-remote-directory>/workspace/<pipeline-name>

Jenkins 流水线配置中默认的轻量级检出只会获取流水线脚本文件本身(比如常用的 Jenkinsfile),仓库中的其他文件将不会被检出。 但是 pipeline 实际上包含了一个隐式的 checkout scm,它会执行额外的命令,来将仓库的指定分支完整地检出到节点上。

声明式流水线简单示例

先提供一个部署服务端的简单示例:

pipeline {
    agent any
    parameters {
        string(name: 'PORT', defaultValue: '8080', description: 'The port which the application will listen on.')
    }
    stages {
        stage('Prepare') {
            steps {
                sh '''
                    cd ./script
                    chmod +x ./*.sh
                '''
            }
        }
        stage('Deploy') {
            steps {
                echo "Deploying to agent on port ${params.PORT}."
                sh "./script/deploy.sh ${params.PORT}"
            }
        }
    }
    post {
        success {
            echo 'Deployment succeeded!'
            sh './script/cleanup_succeeded.sh'
        }
        failure {
            echo 'Deployment failed!'
            sh './script/cleanup_failed.sh'
        }
        cleanup {
            echo 'This will always run last.'
            sh './script/cleanup_common.sh'
        }
    }
}

上面的脚本将具体的命令封装在脚本里,因此需要将 Jenkinsfile 跟脚本一起托管在仓库中,并以 Pipeline script from SCM 的方式进行配置。

项目目录结构:

.
├── Jenkinsfile
└── script
    ├── cleanup_common.sh
    ├── cleanup_failed.sh
    ├── cleanup_succeeded.sh
    └── deploy.sh

它的大致效果:

  • Jenkins 随机选择一个可用的节点,在该节点上执行脚本中的各个 stage
  • 在 Jenkins 界面上提供一个 PORT 参数,用于指定部署的端口号。
  • ./script 路径内的 .sh 脚本添加可执行权限,随后执行部署脚本。
  • 视部署成功与否执行对应的清理脚本,最终再执行公共清理脚本

接下来简单介绍一下使用到的语法:

  • pipeline

    声明式流水线必须写在 pipeline 的内部。

  • agent

    可以写在 pipelinematrixstage 下。

    用于指定作用域中任务的执行节点。 可通过 label 限制节点的标签,可以使用标签条件,比如 agent { label 'ubuntu-22.04 && aarch64' }agent { label 'ubuntu-22.04 || ubuntu-24.04' }

    作用域的执行节点由最接近它的 agent 决定,比如可以在 pipeline 指定全局的 agent,再对各个 stage 单独指定其 agent。 每个 agent 的限制条件可能筛选出不止一个节点,但只会执行在单个节点上。

  • parameters

    用于指定流水线的参数,执行一次后会自动同步到 Jenkins 界面上。

  • stage

    流水线需要执行的任务。 为了便于维护,不建议直接在 Jenkinsfile 中编写复杂的逻辑,最好把需要执行的命令封装在单独的脚本中。

  • post

    可以写在 pipelinematrixstage 下。

    用于指定流水线执行完成后的一些附加命令,通常用于清理环境。

matrix 示例

再提供一个在不同 CPU 架构与不同操作系统的节点上进行构建与测试的示例,使用 matrix 实现:

pipeline {
    agent none
    parameters {
        choice(
            name: 'ARCHITECTURE_FILTER',
            choices: ['all', 'aarch64', 'x86_64'],
            description: 'The CPU Architecture of agent for build and test'
        )
    }
    stages {
        stage('Build And Test') {
            matrix {
                agent {
                    label "${ARCHITECTURE} && ${PLATFORM}"
                }
                when { anyOf {
                    expression { params.ARCHITECTURE_FILTER == 'all' }
                    expression { params.ARCHITECTURE_FILTER == "${ARCHITECTURE}" }
                } }
                axes {
                    axis {
                        name 'ARCHITECTURE'
                        values 'aarch64', 'x86_64'
                    }
                    axis {
                        name 'PLATFORM'
                        values 'ubuntu-22.04', 'windows-2022'
                    }
                }
                excludes {
                    exclude {
                        axis {
                            name 'ARCHITECTURE'
                            values 'aarch64'
                        }
                        axis {
                            name 'PLATFORM'
                            values 'windows-2022'
                        }
                    }
                }
                stages {
                    stage('Build') {
                        steps {
                            echo "Building on ${PLATFORM} (${ARCHITECTURE})"
                        }
                    }
                    stage('Test') {
                        steps {
                            echo "Testing on ${PLATFORM} (${ARCHITECTURE})"
                        }
                    }
                }
                post {
                    cleanup {
                        echo "Cleaning up on ${PLATFORM} (${ARCHITECTURE})"
                    }
                }
            }
        }
    }
}

这个脚本用于在不同的 CPU 架构与操作系统上执行构建与测试。 它提供两个参数维度(CPU 架构与操作系统),并为这两个参数分别指定一个静态的取值范围。 通过 excludes 静态地排除掉 CPU 架构为 aarch64 且操作系统为 windows-2022 的参数组合。 通过 parameters 在 Jenkins 界面中提供一个 ARCHITECTURE_FILTER 参数,使用 when 动态过滤出需要执行的 CPU 架构范围。 对于过滤后的所有参数组合,并行地选择标签匹配的节点执行构建与测试。 最后对每个组合分别进行清理。

介绍一下脚本中涉及的 matrix 相关语法:

  • matrix

    需要写在 stage 中。

    matrix 用于实现多维度参数组合的并行执行。 但这些维度的取值范围只能由 axes 指定,不支持动态扩展。

    matrix 内可以包含:

    • agent
    • environment
    • post
  • when

    可配合 parameters 使用,限制参数组合。

  • axes

    指定若干静态的参数维度,每个维度是一个 axis,Jenkins 会遍历所有的参数组合去执行 matrix 内的 stages

  • excludes

    以静态的方式过滤部分参数组合。

脚本式流水线

脚本式流水线可读性稍弱一些,但是会更灵活,适合复杂的场景。

脚本式流水线简单示例

这个示例与上文声明式流水线简单示例实现的功能相同,都是部署一个服务端应用。

properties([parameters([
    string(name: 'PORT', defaultValue: '8080', description: 'The port which the application will listen on.')
])])

node {
    try {
        stage('Prepare') {
            checkout scm
            sh '''
                cd ./script
                chmod +x ./*.sh
            '''
        }
        stage('Deploy') {
            echo "Deploying to agent on port ${params.PORT}."
            sh "./script/deploy.sh ${params.PORT}"
        }
    } catch (e) {
        echo 'Deployment failed!'
        sh './script/cleanup_failed.sh'

        throw e
    } finally {
        def currentResult = currentBuild.result ?: 'SUCCESS'

        if (currentResult == 'SUCCESS') {
            echo 'Deployment succeeded!'
            sh './script/cleanup_succeeded.sh'
        }

        echo 'This will always run last.'
        sh './script/cleanup_common.sh'
    }
}

代码很清晰,就不做过多解释了,简单提几点:

  • node 的作用与 agent 类似。
    • 都可以筛选节点标签,比如 node('ubuntu-22.04 && aarch64')
    • node 发生嵌套时,实际执行节点由最接近的 node 决定。
    • 都能保证闭包内脚本执行在单个节点上。
  • 与声明式不同,脚本式流水线需要显式调用 checkout scm
  • stage 中不需要写 steps
  • post 的功能由异常处理实现,代码可读性会变弱。

脚本式流水线矩阵示例

properties([parameters([
    string(
        name: 'BM_TYPE_RANGE',
        defaultValue: 'oltp_read_only, oltp_write_only, oltp_read_write',
        description: 'The types of benchmark will be executed'
    ),
    string(
        name: 'BM_THREADS_RANGE',
        defaultValue: '1, 64, 1024',
        description: 'Benchmark will be executed with each number of threads in the range'
    ),
])])

def BM_TYPE_RANGE = params.BM_TYPE_RANGE.split(',').collect { it.trim() }
def BM_THREADS_RANGE = params.BM_THREADS_RANGE.split(',').collect { it.trim() }

def jobs = [:]

BM_TYPE_RANGE.each { BM_TYPE ->
    BM_THREADS_RANGE.each { BM_THREADS ->
        def name = "${BM_TYPE}-${BM_THREADS}"
        jobs[name] = {
            node('server') {
                def SERVER_IP = env.SERVER_IP

                try {
                    stage('Install Database') {
                        echo "Installing database on ${SERVER_IP}"
                    }

                    node('client') {
                        withEnv([
                            "SERVER_IP=${SERVER_IP}",
                            "BM_TYPE=${BM_TYPE}",
                            "BM_THREADS=${BM_THREADS}",
                        ]) {
                            stage('Run Benchmark') {
                                echo "Running benchmark connection to ${SERVER_IP}"
                            }
                        }
                    }
                } catch (e) {
                    throw e
                } finally {
                    stage('Cleanup') {
                        echo "Cleaning up on server ${SERVER_IP}"
                    }
                }
            }
        }
    }
}

stage('Matrix Benchmarks') {
    parallel(jobs)
}

这个脚本用于以不同的线程数对数据库执行不同类型的基准测试。 线程数与基准测试类型均通过参数控制,取值之间以逗号分隔。 数据库服务端与基准测试客户端是分离部署的。 但数据库服务端的实际执行节点不固定,因此需要在执行过程中动态获取其 IP。 考虑到子网可能比较复杂,建议在配置节点时就把 IP 手动写在环境变量中,比如这个脚本里使用的 SERVER_IP

代码也比较清晰,注意几点就行:

  • 脚本式流水线在 stage 之间传递变量是比较灵活的,比方说这里获取了服务端的 SERVER_IP 之后再通过变量传递给客户端。
  • 并行执行是通过 parallel 实现的。