声明式 vs. 脚本式:选择你的 Jenkins Pipeline 语法
比较声明式和脚本式 Jenkins Pipeline 语法,包含示例以及何时使用每种语法的实用指导。
声明式 vs. 脚本式:选择你的 Jenkins Pipeline 语法
Jenkins Pipeline 提供了两种编写 Jenkinsfile 的方式:声明式和脚本式。两者都可以构建、测试和部署你的应用,但在可读性、验证和 Groovy 灵活性方面,它们会引导你做出不同的权衡。
如果你的团队正在为新流水线选择语法,请从六个月后你需要维护的工作流开始考虑。声明式通常更易于阅读和审查。当流水线确实需要动态行为时,脚本式能提供更直接的 Groovy 控制。
理解 Jenkins Pipeline
在深入探讨语法之前,让我们简要重申一下什么是 Jenkins Pipeline。Pipeline 是一套插件,支持在 Jenkins 中实现和集成持续交付流水线。它本质上是一系列自动化步骤,定义了从代码提交到部署的整个软件交付过程。这些步骤在 Jenkinsfile 中定义,通常用 Groovy 编写,并提供了一种强大的方式来管理复杂的构建、测试和部署场景。
Jenkins Pipeline as Code 提供了几个关键优势:
- 版本控制:
Jenkinsfile像应用代码一样存储在源代码控制中,支持版本管理、审计和协作。 - 可重复性:确保交付过程在不同环境和运行中一致执行。
- 可见性:提供对整个交付过程的清晰且易于理解的视图。
- 持久性:Pipeline 可以在 Jenkins 主节点重启后继续存在。
- 可扩展性:通过共享库,复杂的逻辑可以被抽象和重用。
声明式 Pipeline
声明式 Pipeline 是较新的、有主见的语法,旨在使编写和理解流水线更容易。它提供了一种结构化的方法,包含预定义的块,这有助于团队保持 Jenkinsfiles 的一致性。
特性和语法
声明式 Pipeline 强制执行由顶级块(如 pipeline、agent、stages、steps、post、environment、parameters、options、triggers、tools、input 和 when)定义的特定结构。这种结构通过为工作流的不同部分提供清晰的边界,简化了流水线定义。
以下是声明式 Pipeline 的基本结构:
pipeline {
agent any // 或者 'label'、'docker' 等
stages {
stage('Build') {
steps {
echo '正在构建应用...'
sh 'mvn clean install'
}
}
stage('Test') {
steps {
echo '正在运行测试...'
sh 'mvn test'
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
echo '正在部署到生产环境...'
script {
// 如果绝对需要,可以在这里放入类似脚本式的逻辑
// 例如,调用共享库函数
// mySharedLibrary.deployApplication()
}
}
}
}
post {
always {
echo '流水线已完成。'
}
success {
echo '流水线成功!'
}
failure {
echo '流水线失败。'
}
}
}
声明式 Pipeline 的优势
- 简单性和可读性:预定义的结构使流水线易于阅读和理解,即使是非专家也能看懂。它感觉更像一个配置文件。
- 结构化方法:强制实施最佳实践和跨流水线的一致性,减少了学习曲线和出错的可能性。
- 内置功能:为常见的 CI/CD 模式提供了丰富的内置功能,例如条件执行(
when)、构建后操作(post)、并行阶段执行以及各种管理流水线流程的选项。 - 易于学习:由于语法有主见,没有广泛 Groovy 知识的开发人员也可以快速上手。
- 验证:因为声明式有更严格的规则,Jenkins 可以在执行前验证更多的结构。
声明式 Pipeline 的局限性
- 灵活性较低:对于需要预定义块之外的自定义 Groovy 逻辑的高度复杂或动态工作流,其刚性结构可能具有限制性。
- 直接的 Groovy 访问受限:虽然可以使用
script块注入脚本式 Pipeline 语法,但过度使用可能会削弱声明式语法的优势,并使流水线更难阅读。
何时使用声明式 Pipeline
声明式 Pipeline 是大多数常见 CI/CD 场景的推荐选择。它们非常适合:
- 刚接触 Jenkins 或 Pipeline as Code 的团队。
- 具有直接或中等复杂度的构建、测试和部署流程的项目。
- 确保跨多个流水线的一致性和可维护性。
- 利用 Jenkins 的内置功能实现常见模式,如并行执行、条件阶段和通知。
脚本式 Pipeline
脚本式 Pipeline 直接构建在 Groovy 编程语言之上,是 Jenkins Pipeline as Code 的原始语法。它提供了最大的灵活性和能力,允许开发人员实现高度定制和动态的自动化流程。
特性和语法
脚本式 Pipeline 从上到下顺序执行,就像传统的 Groovy 脚本一样。它使用 Groovy 的完整语法,并通过 node、stage、checkout、sh、git 等方法利用 Jenkins Pipeline DSL(领域特定语言)。这提供了对 Jenkins API 和 Groovy 语言全部功能的直接访问。
以下是脚本式 Pipeline 的基本结构:
node('my-agent-label') {
stage('Prepare') {
echo '正在准备工作区...'
checkout scm
}
stage('Build') {
echo '正在构建应用...'
try {
sh 'mvn clean install'
} catch (err) {
echo "构建失败: ${err}"
// 自定义错误处理
currentBuild.result = 'FAILURE'
throw err
}
}
stage('Test') {
echo '正在运行测试...'
// 动态确定测试套件
def testSuites = sh(script: 'find tests -name "*.test"', returnStdout: true).trim().split('\n')
if (testSuites.isEmpty()) {
echo '未找到测试。'
} else {
for (suite in testSuites) {
echo "正在运行测试套件: ${suite}"
sh "./run-test.sh ${suite}"
}
}
}
stage('Deploy') {
// 复杂的条件逻辑
if (env.BRANCH_NAME == 'main' && currentBuild.currentResult == 'SUCCESS') {
echo '正在部署到生产环境...'
sh './deploy-prod.sh'
} else if (env.BRANCH_NAME == 'develop') {
echo '正在部署到预发布环境...'
sh './deploy-staging.sh'
} else {
echo '此分支无需部署。'
}
}
// 构建后操作可以使用 try-finally 块或自定义逻辑实现
// 例如,发送通知
if (currentBuild.result == 'SUCCESS') {
echo '流水线成功完成!'
// notifySuccess()
} else {
echo '流水线失败。'
// notifyFailure()
}
}
脚本式 Pipeline 的优势
- 最大灵活性:提供 Groovy 的全部功能,允许高度复杂和动态的逻辑、自定义循环、错误处理和数据处理。
- 直接 Jenkins API 访问:为 Jenkins 和 Groovy API 的使用提供了更多空间,尽管某些操作仍然依赖于插件、权限和脚本安全沙箱。
- 动态行为:非常适合需要动态代理分配、基于运行时条件的并行执行或高级资源管理的工作流。
- 可扩展性:非常适合创建复杂的共享库,这些库封装了可重用的复杂逻辑,供声明式 Pipeline 使用。
脚本式 Pipeline 的局限性
- 学习曲线陡峭:需要扎实的 Groovy 知识,这对于不熟悉该语言的团队来说可能是一个障碍。
- 缺乏主见:没有严格的结构,流水线可能变得不一致,并且在不同项目或开发人员之间更难阅读或维护。
- 容易出错:Groovy 的灵活性意味着更多编码错误的机会,并且与声明式相比,内置验证较少。
- 可读性挑战:复杂的脚本式 Pipeline 可能很快变得难以解析和理解,阻碍协作和故障排除。
- 流水线特定语法较少:许多常见的 CI/CD 模式(如
post操作或when条件)需要使用 Groovy 结构(例如try-catch-finally、if语句)手动实现。
声明式 vs. 脚本式:并排比较
为了帮助总结差异,这里有一个比较表:
| 特性 | 声明式 Pipeline | 脚本式 Pipeline |
|---|---|---|
| 语法结构 | 有主见,预定义的顶级块。 | 灵活,基于 Groovy,顺序执行。 |
| 学习曲线 | 对初学者更容易,需要的 Groovy 知识较少。 | 更陡峭,需要 Groovy 专业知识。 |
| 可读性 | 由于结构化块和清晰的语法而很高。 | 对于复杂脚本可能较低,取决于开发人员风格。 |
| 灵活性 | 限于预定义结构;script 块用于 Groovy。 |
无限,Groovy 的全部功能。 |
| 内置功能 | 丰富的常见 CI/CD 模式(post、when、parallel)。 |
需要使用 Groovy 结构手动实现。 |
| 错误处理 | post 块用于全局或阶段特定操作。 |
手动 try-catch-finally 块。 |
| 可扩展性 | 利用共享库实现复杂的 Groovy 逻辑。 | 直接编写复杂的 Groovy 逻辑。通常创建共享库。 |
| 代理控制 | 全局 agent 或阶段级 agent。 |
node 块,可以在任何地方定义代理。 |
| 使用场景 | 标准 CI/CD 工作流,简单到中等复杂度。 | 高度动态、复杂、自定义的工作流;共享库开发。 |
| JSON/YAML 感觉 | 更像配置语言。 | 纯编程语言。 |
选择正确的语法
在决定使用声明式还是脚本式 Pipeline 时,请考虑以下因素:
- 团队的 Groovy 专业知识:如果你的团队缺乏扎实的 Groovy 技能,声明式的学习曲线会浅得多,并能促进更快的采用。
- 工作流复杂性:对于大多数标准 CI/CD 工作流(构建、测试、部署),声明式完全足够,并且由于其可读性和内置功能通常更胜一筹。对于高度动态、条件性或自定义资源密集型任务,脚本式可能是必要的。
- 可维护性和可读性:声明式流水线通常更易于阅读和维护,尤其是在拥有许多流水线和开发人员的大型组织中。这种一致性减少了认知负荷。
- 现有流水线生态系统:如果你有现有的脚本式流水线或使用脚本式语法构建的强大共享库集,你可能为了保持一致性而坚持使用它,或者在适当的地方逐步迁移到声明式。
- 未来增长:声明式流水线通常足够,并且可以通过共享库扩展自定义逻辑,而共享库本身通常用脚本式 Groovy 编写。这通常是更好的混合方法。
决策的最佳实践
- 从声明式开始:对于新流水线,默认使用声明式。它涵盖了绝大多数 CI/CD 用例,并促进了一致性和可读性。
- 利用共享库:当你在声明式流水线中遇到重复或复杂的逻辑时,将该逻辑抽象到共享库中。共享库主要用脚本式 Groovy 编写,允许你结合两者的优点:声明式的结构和脚本式的灵活性。
- 避免过度脚本化声明式:虽然声明式允许
script块,但尽量保持它们最小化。如果script块变得太大或太复杂,这强烈表明该逻辑应该移到共享库函数中。 - 考虑迁移:如果你有遗留的脚本式流水线变得难以维护,考虑将它们重构为声明式语法,将复杂部分移到共享库中。
要点
对于新的 Jenkinsfiles,选择声明式,除非你有具体的理由不这样做。将重复或复杂的 Groovy 移到共享库中,并将完全脚本式的流水线保留给那些无法干净地适应声明式阶段、条件和步骤的工作流。