Jenkins Pipeline
Dec 16, 2019 21:30 · 4982 words · 10 minute read
1 CI/CD 介绍
CI(Continuous Integration,持续集成)/CD(Continuous Delivery,持续交付)是一种通过在应用开发阶段引入自动化来频繁向客户交付应用的方法。
CI/CD 的核心概念是:
- 持续集成
- 持续交付
- 持续部署
作为一个面向开发和运营团队的解决方案,CI/CD 主要针对在集成新代码时所引发的问题(亦称“集成地狱”)。
具体而言,CI/CD 在整个应用生命周期内(从集成和测试阶段到交付和部署)引入了持续自动化和持续监控,这些关联的事务通常被称为“CI/CD 管道”,由开发和运维团队以敏捷方式协同支持。
1.1 CI 和 CD 的区别
CI/CD 中的 CI 指持续集成,它属于开发人员的自动化流程。成功的 CI 意味着应用代码的最新更改会定期构建、测试并合并到共享存储中。该解决方案可以解决在一次开发中有太多应用分支,从而导致相互冲突的问题。
持续交付通常是指开发人员对应用的更改会自动进行错误测试并上传到存储库(如 GitLab 或容器注册表),然后由运维团队将其部署到实时生产环境中,旨在解决开发和运维团队之间可见性及沟通较差的问题,因此持续交付的目的就是确保尽可能减少部署新代码时所需的工作量。
CI/CD 中的 CD 指的是持续交付或持续部署,这些相关概念有时会交叉使用。两者都事关管道后续阶段的自动化,但它们有时也会单独使用,用于说明自动化程度。
持续部署指的是自动将开发人员的更改从存储库发布到生产环境中以供客户使用,它主要为解决因手动流程降低应用交付速度,从而使运维团队超负荷的问题。持续部署以持续交付的优势为根基,实现了管道后续阶段的自动化。
CI/CD 既可能仅指持续集成和持续交付构成的关联环节,也可以指持续集成、持续交付和持续部署这三个方面构成的关联环节。更为复杂的是有时持续交付也包含了持续部署流程。
纠缠于这些语义其实并无必要,只需记得 CI/CD 实际上就是一个流程(通常表述为管道),用于在更大程度上实现应用开发的持续自动化和持续监控。
1.2 持续集成(CI)
现代应用开发的目标是让多位开发人员同时开发同一个应用的不同功能。但是,如果企业安排在一天内将所有分支源代码合并在一起,最终可能导致工作繁琐、耗时,而且需要手动完成。这是因为当一位独立工作的开发人员对应用进行更改时,有可能会有其他开发人员同时进行更改,从而引发冲突。
持续集成可以帮助开发人员更加频繁地将代码更改合并到共享分支或主干中。一旦开发人员对应用所做的更改被合并,系统就会通过自动构建应用并运行不同级别的自动化测试(通常是单元测试和集成测试)来验证这些更改,确保更改没有对应用造成破坏。这意味着测试内容涵盖了从类和函数到构成整个应用的不同模块,如果自动化测试发现新代码和现有代码之间有冲突,持续集成(CI)可以更加轻松地快速修复这些错误。
1.3 持续交付(CD)
完成持续集成中构建单元测试和集成测试的自动化流程后,通过持续交付可自动将已验证的代码发布到存储库。为了实现高效的持续交付流程,务必要确保持续交付已内置于开发管道。持续交付的目标是拥有一个可随时部署到生产环境的代码库。
在持续交付中,每个阶段(从代码更改的合并到生产就绪型构建版本的交付)都涉及测试自动化和代码发布自动化。在流程结束时,运维团队可以快速、轻松地将应用部署到生产环境中。
1.4 持续部署
对于一个成熟的 CI/CD 管道来说,最后的阶段是持续部署。作为持续交付(自动将生产就绪型构建版本发布到代码存储库)的延伸,持续部署可以自动将应用发布到生产环境中。由于生产之前的管道阶段没有手动门控,因此持续部署在很大程度上都得依赖于精心设计的测试自动化。
实际上,持续部署意味着开发人员对应用的更改在编写后的几分钟内就能生效,这更加便于持续接收和整合用户反馈。总而言之,所有这些 CI/CD 的关联步骤都有助于降低应用的部署风险,因此更便于以小件的方式(非一次性)发布对应用的更改。不过,由于还需要编写自动化测试以适应 CI/CD 管道中的各种测试和发布阶段,因此前期投资会很大。
2 Jenkins 流水线
2.1 什么是流水线
Jenkins 流水线(Pipeline)是一套插件,它支持实现并把持续提交流水线(Continuous Delivery Pipeline)集成到 Jenkins。
持续提交流水线(Continuous Delivery Pipeline)会经历一个复杂的过程:从版本控制、向用户和客户提交软件,软件的每次变更(提交代码到仓库)到软件发布(Release)。这个过程包括以一种可靠并可重复的方式构建软件,以及通过多个测试和部署阶段来开发构建好的软件(称为 Build)。
流水线提供了一组可扩展的工具,通过流水线语法对从简单到复杂的交付流水线作为代码进行建模,Jenkins 流水线的定义被写在一个文本文件中,一般为 Jenkinsfile,该文件“编制”了整个构建软件的过程,该文件一般也可以被提交到项目的代码仓库中,在 Jenkins 中可以直接引用。这是流水线即代码的基础,将持续提交流水线作为应用程序的一部分,像其他代码一样进行版本化和审查。创建 Jenkinsfile 并提交到代码仓库中的好处如下:
- 自动地为所有分支创建流水线构建过程
- 在流水线上进行代码复查/迭代
- 对流水线进行审计跟踪
- 流水线的代码可以被项目的多个成员查看和编辑
流水线主要分为以下几种区块:Pipeline、Node、Stage、Step 等:
- Pipeline(流水线):Pipeline 是用户定义的一个持续提交(CD)流水线模型。流水线的代码定义了整个的构建过程,包括构建、测试和交付应用程序的阶段。另外,Pipeline 块是声明式流水线语法的关键部分。
- Node(节点):Node(节点)是一个机器,它是 Jenkins 环境的一部分,另外,Node 块是脚本化流水线语法的关键部分。
- Stage(阶段):Stage 块定义了在整个流水线的执行任务中概念不同的子集(比如 Build、Test、Deploy 阶段),它被许多插件用于可视化 Jenkins 流水线当前的状态/进展。
- Step(步骤):本质上是指通过一个单一的任务告诉 Jenkins 在特定的时间点需要做什么,比如要执行 shell 命令,可以使用 sh SHELL_COMMAND。
2.2 声明式流水线
在声明式流水线语法中,Pipeline 块定义了整个流水线中完成的所有工作,比如:
Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
stages {
stage('Build') {
steps {
// do build
}
}
stage('Test') {
steps {
// do test
}
}
stage('Deploy') {
steps {
// do deploy
}
}
}
}
2.3 脚本式流水线
Jenkinsfile (Scripted Pipeline)
node {
stage('Build') {
// do build
}
stage('Test') {
// do test
}
stage('Deploy') {
// do deploy
}
}
2.4 流水线示例
Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'make'
}
}
stage('Test') {
steps {
sh 'make check'
junit 'reports/**/*.xml'
}
}
stage('Deploy') {
steps {
sh 'make publish'
}
}
}
}
等同于以下脚本式流水线:
Jenkinsfile (Scripted Pipeline)
node {
stage('Build') {
sh 'make'
}
stage('Test') {
sh 'make check'
junit 'reports/**/*.xml'
}
stage('Deploy') {
sh 'make publish'
}
}
3 Pipeline 语法
3.1 声明式流水线
所有有效的声明式流水线必须包含在一个 Pipeline 块中,以下是一个 Pipeline 块的格式:
pipeline {
/* insert Declarative Pipeline here */
}
在声明式流水线中有效的基本语句和表达式遵循与 Groovy 的语法同样的规则,但有以下例外:
- 流水线顶层必须是一个 block,pipeline{}。
- 没有分号作为语句分隔符,每条语句都在自己的行上。
- 块只能由 Sections、Directives、Steps 或 assignment statements 组成。
3.1.1 Sections
声明式流水线中的 Sections 通常包含一个或多个 agent、Stages、post、Directives 和 Steps。
agent
agent 部分指定了整个流水线或特定的部分,在 Jenkins 环境中执行的位置取决于 agent 区域的位置,该部分必须在 Pipeline 块的顶层被定义,但是 stage 级别的使用是可选的。
- any:在任何可用的代理上执行流水线或 stage。
- none:当在 pipeline 块的顶部没有全局 agent,该参数将会被分配到整个流水线的运行中,并且每个 stage 部分都需要包含它自己的 agent,比如:agent none。在提供了标签的 Jenkins 环境中可用代理上执行流水线或 stage。例如:agent { label ‘my-defined-label’}。
- node:agent { node { label ’labelName’} } 和 agent { label ’labelName’ }一样,但是 node 允许额外的选项(比如 customWorkspace)。
- dockerfile: 执行流水线或 stage,使用从源码包含的 Dockerfile 所构建的容器。
agent {
dockerfile {
filename 'Dockerfile.build'
dir 'build'
label 'my-label'
additionalBuildArgs '--build-arg version=1.0.0'
}
}
stages
stages 包含一个或多个 stage 指令,stages 部分是流水线描述的大部分工作(work)的位置。建议 stages 至少包含一个 stage 指令,用于持续交付过程的某个离散的部分,比如构建、测试或部署。
Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
stages {
stage('Example') {
steps {
echo 'Hello World'
}
}
}
}
post
post 部分定义一个或多个 steps,这些阶段根据流水线或 stage 的完成情况而运行(取决于流水线中 post 部分的位置)。post 支持以下 post-condition 块之一:
- always:无论流水线或 stage 的完成状态如何,都允许在 post 部分运行该步骤。
- changed:只有当前流水线或 stage 的完成状态与它之前的运行不同时,才允许在 post 部分运行该步骤。
- failure:只有当前流水线或 stage 的完成状态为失败(failure),才允许在 post 部分运行该步骤,通常这时在 Web 界面中显示为红色。
- success:当前状态为成功(success),执行 post 步骤,通常在 Web 界面中显示为蓝色或绿色。
- unstable:当前状态为不稳定(unstable),执行 post 步骤,通常由于测试失败或代码违规等造成,在 Web 界面中显示为黄色。
- aborted:当前状态为放弃(aborted),执行 post 步骤,通常由于流水线被手动放弃触发,这时在 Web 界面中显示为灰色。
steps
steps 部分在给定的 stage 指令中执行的一个或多个步骤。
在 steps 定义执行一条 shell 命令:
Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
stages {
stage('Example') {
steps {
echo 'Hello World'
}
}
}
}
Directives
Directives 用于一些执行 stage 时的条件判断,主要分为 environment、options、parameters、triggers、stage、tools、input、when 等。
Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
environment {
CC = 'clang'
}
stages {
stage('Example') {
environment {
AN_ACCESS_KEY = credentials('my-prefined-secret-text')
}
steps {
sh 'printenv'
}
}
}
}
3.2 脚本式流水线
脚本化流水线与声明式流水线一样都是建立在底层流水线的子系统上,与声明式流水线不同的是,脚本化流水线实际上是由 Groovy 构建。Groovy 语言提供的大部分功能都可以用于脚本化流水线的用户,这意味着它是一个非常有表现力和灵活的工具,可以通过它编写持续交付流水线。
脚本化流水线和其他传统脚本一致都是从 Jenkinsfile 的顶部开始向下串行执行,因此其提供的流控制也取决于 Groovy 表达式,比如:if/else 条件:
Jenkinsfile (Scripted Pipeline)
node {
stage('Example') {
if (env.BRANCH_NAME == 'master') {
echo 'I only execute on the master branch'
} else {
echo 'I execute elsewhere'
}
}
}
另一种方法是使用 Groovy 的异常处理支持来管理脚本化流水线的流控制,无论遇到什么原因的失败,它们都会抛出一个异常,处理错误的行为必须使用 Groovy 中的 try/catch/finally 块:
Jenkinsfile (Scripted Pipeline)
node {
stage('Example') {
try {
sh 'exit 1'
} else {
echo 'Something failed, I should sound the klaxons!'
throw
}
}
}
4 Jenkinsfile 的使用
Jenkinsfile 是一个文本文件,包含了 Jenkins 流水线的定义并被用于源代码控制。
通常将 Jenkinsfile 放置于代码仓库中:
- 方便对流水线上的代码进行复查/迭代
- 对管道进行审计跟踪
- 流水线真正的源代码能够被项目的多个成员查看和编辑
4.1 构建
对于许多项目来说,流水线中工作(work)的开始就是构建(build),这个阶段的主要工作是进行源代码的组装、编译或打包。Jenkinsfile 文件不是替代现有的构建工具,如 GNU/Make、Maven、Gradle 等,可以视其为一个将项目开发周期的多个阶段(构建、测试、部署等)绑定在一起的粘合层。
Jenkins 有许多插件用于构建工具,假设系统为 Unix/Linux,只需要从 shell 步骤(sh)调用 make 即可进行构建,Windows 系统可以使用 bat:
Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'make'
archiveArtifacts artifacts: '**/target/*.jar', fingerprint: true
}
}
}
}
- steps 的 shmake 表示如果命令的状态码为 0,则继续,为非零则失败。
- archiveArtifacts 用于捕获构建后生成的文件。
对应的脚本式流水线如下:
Jenkinsfile (Scripted Pipeline)
node {
stage('Build') {
sh 'make'
archiveArtifacts artifacts: '**/target/*.jar', fingerprint: true
}
}
4.2 测试
运行自动化测试是任何成功的持续交付过程中的重要组成部分,因此 Jenkins 有许多测试记录、报告和可视化工具,这些工具都是由插件提供。下面的例子将使用 JUnit 插件提供的 junit 工具进行测试。
如果测试失败,流水线就会被标记为不稳定,这时 Web 界面中的球就显示为黄色。基于记录的测试报告,Jenkins 也可以提供历史趋势分析和可视化:
Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
stages {
stage('Test') {
steps {
sh 'make check || true'
junit '**/target/*.xml'
}
}
}
}
- 当 sh 步骤状态码为 0 时,调用 junit 进行测试。
- junit 捕获并关联匹配 */target/.xml 的 Junit XML 文件。
对应的脚本式流水线如下:
Jenkinsfile (Scripted Pipeline)
node {
stage('Test') {
sh 'make check || true'
junit '**/target/*.xml'
}
}
4.3 部署
当编译构建和测试都通过后,就会将编译生成的包推送到生产环境中,从本质上讲,Deploy 阶段只可能发生在之前的阶段都成功完成后才会进行,否则流水线会提前退出:
Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
stages {
stage('Deploy') {
when {
expression {
currentBuild.result == null || currentBuild.result == 'SUCCESS'
}
}
steps {
sh 'make publish'
}
}
}
}
- 当前 build 结果为 SUCCESS 时,执行 publish。
对应的脚本式流水线如下:
Jenkinsfile (Scripted Pipeline)
node {
stage('Deploy') {
if (currentBuild.result == null || currentBuild.result == 'SUCCESS') {
sh 'make publish'
}
}
}