Jenkinsfile 使用笔记
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
可以写在
pipeline
、matrix
、stage
下。用于指定作用域中任务的执行节点。 可通过
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
可以写在
pipeline
、matrix
、stage
下。用于指定流水线执行完成后的一些附加命令,通常用于清理环境。
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
实现的。