个人亲自录制全套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,而配置方式比较难实现独立
- 流水线可以在流程阶段做更丰富的操作,比如暂停,审核等交互式操作;
- 流水线支持更多的插件扩展
图中是一个通过管道(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
用法:在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/
// 注意: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中,方便我们下文直接使用命令,否则就需要使用绝对路径。
目前只支持:maven、jdk、gradlepipeline { agent any tools { // 注意"maven3.8.6"是我们全局工具中定义的名字 maven "maven3.8.6" } stages { stage('Example') { steps { // 此处可以直接使用maven的命令而无需指定绝对路径 sh 'mvn --version' } } } }