DevOps实战系列【第十章】:详解Jenkins Pipeline基本概念和语法

时间:2023-01-09 15:02:45

个人亲自录制全套DevOps系列实战教程 :​​手把手教你玩转DevOps全栈技术​

流水线基本概念

官方中文手册:
​​​https://www.jenkins.io/zh/doc/book/pipeline​​​​我们最好在结合英文文档去看,因为翻译过来的中文比较乱。​

Jenkins pipeline是一套插件,它支持实现和集成 continuous delivery pipelines 到Jenkins,即实现CD持续部署的功能。

Jekins的流水线插件会在安装Jenkins时通过“建议安装”的方式自动安装上,如果大家建议安装时失败了,可以单独下载Pipeline plugin去安装一下。

流水线提供了一组可扩展的工具,通过 ​​Pipeline domain-specific language (DSL) syntax​​,翻译过来就是Pipeline有自己的语法DSL,基于groovy语法,对从简单到复杂的交付流水线进行建模。

Jenkins流水线的定义,需要创建一个​​Jenkinsfile文本文件​​,该文件除了可以直接定义在Jenkins的Job中还可以被提交到代码仓库管理(如:gitlab),这样流水线将会作为我们开发项目的一部分,像其他代码一样进行跟踪管理,所以建议使用gitlab管理jenkinsfile的方式去实施Jenkins的流水线。

​说白了:jenkins 流水线 就是通过一个file文件来实现jenkins job的功能。​

流水线的特点:为什么选择流水线?通过jenkins的配置不好吗?

  • 流水线可以通过代码仓库管理,方便我们进行迭代、审核等;
  • 流水线文件独立于jenkins的job,而配置方式比较难实现独立
  • 流水线可以在流程阶段做更丰富的操作,比如暂停,审核等交互式操作;
  • 流水线支持更多的插件扩展
  • DevOps实战系列【第十章】:详解Jenkins Pipeline基本概念和语法

图中是一个通过管道(pipeline流水线)实现的从开发到生产的过程,其中每一个步骤都可以通过Jenkins的流水线的DSL语法去建模。

​概念:SCM=>软件配置管理,他有很多实现工具,比如我们常用的gitlab就是一个SCM工具,而在本文中提到SCM我们默认使用的就是gitlab。​

Jenkins流水线的语法有2种:

  • 脚本式:2014年12月
  • 声明式:2017年2月,为更简便易用而生

实际中我们的jenkinsfile中一般会混用两种语法。


流水线语法概念

  • pipeline:是用户定义的一个CD流水线模型,就是说他是我们定义的jenkinsfile的跟节点,流水线起始于​​pipeline块​​​ 。pipeline块中一般会包含​​stages块​​去完成一个应用程序构建、 测试和部署的阶段。
  • node(节点):它是Jenkins环境的一个机器并且能够执行流水线文件。
  • stage(阶段):​​stage​​ 块中定义了不同的任务,比如 "Build", "Test" 和 "Deploy" 阶段, 这些不同的任务可以通过多种插件去可视化展示或者也可以展示Jenkins执行pipeline的进度和状态。
  • step(步骤):是stage块中的一个任务,他可以告诉Jenkins去做什么,比如在Build节点,在这个任务中可以让Jenkins去执行make命令,注意需要使用sh "make"方式执行,不能直接通过make去执行。

​注意:​​​​stages​​​块和​​steps​​块在声明式和脚本是语法中都可以使用。


声明式语法举例:创建一个Jenkinsfile

// pipeline块作为jenkinsfile的根节点
pipeline {
agent any // 指定可以在任何空闲的代理上执行以下的stages块,代理指的是我们jenkins服务器,如果是集群的话就是其中的某个节点(声明式中必须要有agent)
stages { // 通过stages包括多个stage
stage('Build') { //通过stage定义一个任务,这个任务我们取名叫做”Build“,即代表构建阶段要执行的步骤
steps { // 通过steps包括多个step,而每个step就是我们要执行的任务的具体内容,比如执行一个shell脚本等
echo "hello build"
}
}
stage('Test') { // 定义测试阶段
steps { // 测试阶段需要执行的步骤
echo "hello test"
}
}
stage('Deploy') { // 定义部署阶段
steps { //部署阶段需要执行的步骤
echo "hello deploy"
}
}
}
}

脚本式语法举例:创建一个Jenkinsfile

// 该节点和声明式中的agent是一个意思,表示在任意jenkins节点上执行以下stages,他不需要一个*的pipeline块,而是直接执行节点或任一节点执行以下阶段
node {
// 在脚本式语法中stage块是可选的,即可以直接使用steps来定义步骤,但是如果定义了stage,该stage将可以在jenkins的图像化界面中展示
stage('Build') {
// 其实展示的就是"Build"名字,而对于声明式语法stage是一定要有的,如果不想展示图形化可以专门使用脚本式语法,然后不添加stage即可。
echo "hello build"
}
stage('Test') {
// 这个里边定义的和声明式就一样了,定义当前步骤的steps
echo "hello test"
}
stage('Deploy') {
echo "hello deploy"
}
}


语法详解

官方地址:​​https://www.jenkins.io/zh/doc/book/pipeline/syntax/​


1.注释:Jenkinsfile文件中可以使用两种注释方式

  • // 注释一行内容
  • /* .. 注释框选内容 .. */


2.使用环境变量:

jenkins提供了很多内置的环境变量都可以在Jenkinsfile中使用,可以通过如下地址查看:
​​​http://10.10.1.199:9078/pipeline-syntax/globals​

​使用方法:​​​类似访问Groovy的map属性,关键字是env:​​${env.BUILD_ID}​

pipeline {
agent any
stages {
stage('Example') {
steps {
echo "Running ${env.BUILD_ID} on ${env.JENKINS_URL}"
}
}
}
}

​注意:可以通过printenv或env将已有的环境变量输出​


3.设置变量:声明式和脚本式有所不同

声明式语法:​​​environment​​指令(块)

pipeline {
agent any
// 当前文件全局生效
environment {
CC = 'clang'
}
stages {
stage('Example') {
// 局部生效,所有stage内的steps中可以使用
environment {
DEBUG_FLAGS = '-g'
}
steps {
sh 'printenv'
}
}
}
}

脚本式语法:需要使用一个step为​​withEnv​​的命令来定义变量

node {
/*
withEnv中设置的变量只能在当前块中生效\
就是将环境变量添加到PATH中
*/
withEnv(["PATH+MAVEN=${tool 'M3'}/bin"]) {
sh 'mvn -B verify'
}
}

${tool 'M3'}:其中tool是Jenkinsfile的工具命令,就是要使用一个工具,完整写法如:tool name: 'maven3.8.6', type: 'maven',其中name是我们配置的jenkins全局工具中的名称,而类型就是工具的名字,此处M3来自官网,即他定义了一个全局工具叫M3,此处是给环境变量设置了maven的bin目录,以便sh中的mvn命令得以正常执行,当然也可以直接不设置变量,通过sh "${tool 'M3'}/bin/mvn -B verify",注意是双引号,单引号不能执行插值语法


4.动态设置变量:

所有的脚本执行完后都会有返回状态returnStatus​​​或​​​returnStdout

​​​注意:groovy是可以支持三种引号​

  • 单引号:和我们java中定义字符串一样,不支持解析插值语法,即无法获取${变量名}的值
  • 双引号:可以进行字符串的拼接,且可以执行插值表达式(${变量名})
  • 三引号:支持字符串换行,​​注意:​​​三引号也分三个单引号和三个双引号,意思和单引号、双引号一样,只不过是这里可以换行,如​​['''或"""]​

​注意:​​我们知道shell脚本执行结果是0或非0,而Jenkinsfile中采用了groovy中使用shell脚本获取返回值状态或标准输出的语法:

  • 获取标准输出的shell脚本
  • 获取执行状态的shell脚本
  • 无需返回值的shell脚本

// 获取标准输出
// 第一种
result = sh returnStdout: true ,script: "<shell command>"
// 返回结果末尾会有空格,所以trim()一下
result = result.trim()
// 第二种
result = sh(script: "<shell command>", returnStdout: true).trim()
// 第三种
sh "<shell command> > commandResult"
result = readFile('commandResult').trim()

// 获取执行状态
// 第一种
result = sh returnStatus: true ,script: "<shell command>"
result = result.trim()
// 第二种
result = sh(script: "<shell command>", returnStatus: true).trim()
// 第三种
sh '<shell command> > status'
def r = readFile('status').trim()

//无需返回值,仅执行shell命令
//最简单的方式
sh '<shell command>'

动态定义变量Jenkinsfile脚本如下:

pipeline {
agent any
environment {
// 使用 returnStdout
CC = """${sh(
returnStdout: true,
script: 'echo "clang"'
)}"""
// 使用 returnStatus
EXIT_STATUS = """${sh(
returnStatus: true,
script: 'exit 1'
)}"""
}
stages {
stage('Example') {
environment {
DEBUG_FLAGS = '-g'
}
steps {
// 输出所有变量
sh 'printenv'
}
}
}
}


5.凭证:

jenkins中我们可以创建与第三方进行鉴权的凭证,比如我们之前创建的凭证ID有:gitlab、sonar-key
DevOps实战系列【第十章】:详解Jenkins Pipeline基本概念和语法

​​​用法:在environment块中使用credentials('凭证ID')为变量赋值,然后通过在脚本中使用变量进行鉴权​

pipeline {
agent any
environment {
// 基于用户名和密码方式的凭证赋值,可以使用$USERNAME_PASSWORD_KEY使用凭证,内容格式为[username:passwd]
// 用户名密码的凭证,除了会赋值外,jenkins还会自动创建2个变量,名字为当前定义的变量后加"_USR"和"_PSW",即可以单独获取用户名和密码
USERNAME_PASSWORD_KEY = credentials('gitlab')
// 基于Secret文本方式的凭证赋值,可以通过$SONAR_KEY使用凭证
SONAR_KEY = credentials('sonar-key')
}
stages {
stage('Example stage 1') {
steps {
//
}
}
}
}

​注意:如果要生成其他凭证,比如ssh秘钥或证书,那么需要使用jenkins提供的【片段生成器】,选用withCredentials: Bind credentials to variables来生成脚本​​​ 地址:​​​http://10.10.1.199:9078/job/p1/pipeline-syntax/​​​ DevOps实战系列【第十章】:详解Jenkins Pipeline基本概念和语法

// 注意:keyFileVariable可以用来接收我们配置的ssh的证书信息,比如我指定了私钥内容,可以通过指定keyFileVariable='ABC',然后通过$ABC来获取私钥内容
withCredentials([sshUserPrivateKey(credentialsId: 'gitlab-ssh', keyFileVariable: '')]) {
// some block
}

6.字符串插值:使用groovy语法

// 单引号、双引号、三引号都可以
def singlyQuoted = 'Hello'
def doublyQuoted = "World"

echo 'Hello Mr. ${singlyQuoted}' // 注意:单引号不能解析表达式,将会原样输出
echo "I said, Hello Mr. ${doublyQuoted}"

7.参数:

配置完的参数都会作为​​params​​内置变量的属性被使用。

  • 声明式语法:可以通过在​​parameters指令​​中定义参数
  • 脚本式语法:可以通过​​properties步骤,类似withEnv​

pipeline {
agent any
// 指定参数
parameters {
// 参数名=Greeting, 值=Hello
stringname: 'Greeting', defaultValue: 'Hello', description: 'How should I greet the world?')
}
stages {
stage('Example') {
steps {
// 使用内置params属性引用参数
echo "${params.Greeting} World!"
}
}
}
}

8.故障处理

  • 声明式:使用​​post指令​​支持对多个条件进行处理,**包括:always、unstable、success、failure和changed **
  • 脚本式:使用groovy内置语法try...catch...finally

pipeline {
agent any
stages {
stage('Test') {
steps {
sh 'make check'
}
}
}
// 后处理阶段
post {
// 不管前边阶段什么结果都执行
always {
echo "always is printed!"
}
// 前边阶段失败才会执行
failure {
// mail to: team@example.com, subject: 'The Pipeline failed :('
echo "failure is printed!"
}
// 前边阶段成功才会执行
success {
echo "success!"
}
}
}node {
stage('Test') {
try {
sh 'make check'
}
finally {
// 通过junit插件实现生成xml测试报告到target目录
junit '**/target/*.xml'
}
}
}

9.多代理指定:

​agent指令​​​,前边我们只配置过agent:any,并且都在*pipeline块中配置的,属于全局作用域

一个Jenkinsfile还可以给不同的stage阶段配置不同的agent代理。这样不同的阶段可以被不同的jenkins节点去执行。

​​​注意:​​jenkins集群或单机都可以指定label标签:系统管理->节点管理

pipeline {
// 该属性为必填属性,所以全局先定义成不启用jenkins节点
agent none
stages {
stage('Build') {
// 指定Build阶段使用任何一个jenkins节点都可执行
agent any
steps {
checkout scm
sh 'make'
// stash 是一个暂存命令插件,可以将内容暂存到jenkins master节点,通过unstash取出存储的内容
stash includes: '**/target/*.jar', name: 'app'
}
}
stage('Test on Linux') {
// 指定Test on Linux阶段,只能使用被标签为linux的jenkins节点执行
agent {
label 'linux'
}
steps {
unstash 'app'
sh 'make check'
}
post {
always {
junit '**/target/*.xml'
}
}
}
stage('Test on Windows') {
// 指定Test on Windows阶段,只能使用被标签为windows的jenkins节点执行
agent {
label 'windows'
}
steps {
unstash 'app'
bat 'make check'
}
post {
always {
junit '**/target/*.xml'
}
}
}
}
}

脚本式可以使用node指令:如果要指定标签,则通过node('label'){...}


10.步骤step的参数简化写法:

就是如果一个step语句可以省略参数外部的括号
在groovy中创建map的方式如[key1:value1, key2:value2],而step中多数命令都是以map作为参数:

​​​省略参数外部的所有括号​

git([url: 'git://example.com/amazing-project.git', branch: 'master'])

// ==> 简化方式
git url: 'git://example.com/amazing-project.git', branch: 'master'

​只有一个参数时,除了括号参数名也可省略​

sh([script: 'echo hello'])

// ==> 简化方式
sh 'echo hello'

11.并发执行:

​脚本式语法的高级应用​​​,作为基于groovy的语法,脚本式语法几乎可以直接使用groovy的所有语法而无需修改

优化:第9个案例中,Test on Linux和Test on Windows两个阶段是串行执行,我们此处改为并发执行

​​​指令:parallel​

stage('Test') {
// 并发执行,此处指定的linux是一个parallel的一个分支而已,随意取名,不必和jenkins节点的label相同
parallel linux: {
// 脚本式语法选择linux标签的jenkins节点执行
node('linux') {
checkout scm
try {
unstash 'app'
sh 'make check'
}
finally {
junit '**/target/*.xml'
}
}
},
// parallel的另一个并发执行的分支
windows: {
node('windows') {
/* .. snip .. */
}
}
}

12.options:

可以设置一些jenkins底层对于当前配置此属性块的pipeline的属性限制

比如:当前流水线失败后是否可以重试、是否打印时间戳、是否设置超时时间等

pipeline {
agent any
// 设置1小时超时时间,超过1小时该job还没运行完则终止执行,也可以配置在stage阶段块内部,但设置项较少,参考官网看一下
options {
timeout(time: 1, unit: 'HOURS')
}
stages {
stage('Example') {
steps {
echo 'Hello World'
}
}
}
}

13.input:

可以在执行某个stage阶段时,通过图形化界面与用户交互,即确认后才能继续执行流水线任务,当然也可以取消执行。

pipeline {
agent any
stages {
stage('Example') {
input {
message "Should we continue?"
ok "Yes, we should."
submitter "alice,bob"
parameters {
string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
}
}
steps {
echo "Hello, ${PERSON}, nice to meet you."
}
}
}
}

14.when:

指令允许流水线根据给定的条件决定是否应该执行stage阶段。

参考官网:​​​https://www.jenkins.io/zh/doc/book/pipeline/syntax/#when​


15.script:

声明式中如果想嵌入脚本,即执行定义变量、方法等groovy语法,需要使用script指令来嵌入

pipeline {
agent any
stages {
stage('Example') {
steps {
echo 'Hello World'
// 执行脚本式语法脚本
script {
def browsers = ['chrome', 'firefox']
for (int i = 0; i < browsers.size(); ++i) {
echo "Testing the ${browsers[i]} browser"
}
}
}
}
}
}

16.tools:

可以将我们配置到全局工具中的工具导入到环境变量PATH中,方便我们下文直接使用命令,否则就需要使用绝对路径。

目前只支持:mavenjdkgradle

pipeline {
agent any
tools {
// 注意"maven3.8.6"是我们全局工具中定义的名字
maven "maven3.8.6"
}
stages {
stage('Example') {
steps {
// 此处可以直接使用maven的命令而无需指定绝对路径
sh 'mvn --version'
}
}
}
}