Pipeline 是什么
Jenkins Pipeline 實際上是基於 Groovy 實現的 CI/CD 領域特定語言(DSL),主要分為兩類,一類叫做 Declarative Pipeline
,一類叫做 Scripted Pipeline
。
Declarative Pipeline
體驗上更接近於我們熟知的 travis CI
的 travis.yml
,通過聲明自己要做的事情來規范流程,形如:
pipeline { agent any stages { stage('Build') { steps { // } } stage('Test') { steps { // } } stage('Deploy') { steps { // } } } }
而 Scripted Pipeline
則是舊版本中 Jenkins 支持的 Pipeline 模式,主要是寫一些 groovy 的代碼來制定流程:
node { stage('Build') { // } stage('Test') { // } stage('Deploy') { // } }
一般情況下聲明式的流水線已經可以滿足我們的需要,只有在復雜的情況下才會需要腳本式流水線的參與。
過去大家經常在 Jenkins 的界面上直接寫腳本來實現自動化,但是現在更鼓勵大家通過在項目中增加 Jenkinsfile
的方式把流水線固定下來,實現 Pipeline As Code
,Jenkins 的 Pipeline 插件將會自動發現並執行它。
語法
Declarative Pipeline
最外層有個 pipeline
表明它是一個聲明式流水線,下面會有 4 個主要的部分: agent
,post
,stages
,steps
,我會逐一介紹一下。
Agent
agent
主要用於描述整個 Pipeline 或者指定的 Stage 由什么規則來選擇節點執行。Pipeline 級別的 agent 可以視為 Stage 級別的默認值,如果 stage 中沒有指定,將會使用與 Pipeline 一致的規則。在最新的 Jenkins 版本中,可以支持指定任意節點(any
),不指定(none
),標簽(label
),節點(node
),docker
,dockerfile
和 kubernetes
等,具體的配置細節可以查看文檔,下面是一個使用 docker 的樣例:
agent { docker { image 'myregistry.com/node' label 'my-defined-label' registryUrl 'https://myregistry.com/' registryCredentialsId 'myPredefinedCredentialsInJenkins' args '-v /tmp:/tmp' } }
Tips:
- 如果 Pipeline 選擇了 none,那么 stage 必須要指定一個有效的 agent,否則無法執行
- Jenkins 總是會使用 master 來執行 scan multibranch 之類的操作,即使 master 配置了 0 executors
- agent 指定的是規則而不是具體的節點,如果 stage 各自配置了自己的 agent,需要注意是不是在同一個節點執行的
Stages && Stage
Stages 是 Pipeline 中最主要的組成部分,Jenkins 將會按照 Stages 中描述的順序從上往下的執行。Stages 中可以包括任意多個 Stage,而 Stage 與 Stages 又能互相嵌套,除此以外還有 parallel
指令可以讓內部的 Stage 並行運行。實際上可以把 Stage 當作最小單元,Stages 指定的是順序運行,而 parallel 指定的是並行運行。
接下來的這個 case 很好的說明了這一點:
pipeline { agent none stages { stage('Sequential') { stages { stage('In Sequential 1') { steps { echo "In Sequential 1" } } stage('In Sequential 2') { steps { echo "In Sequential 2" } } stage('Parallel In Sequential') { parallel { stage('In Parallel 1') { steps { echo "In Parallel 1" } } stage('In Parallel 2') { steps { echo "In Parallel 2" } } } } } } } }
除了指定 Stage 之間的順序關系之外,我們還可以通過 when
來指定某個 Stage 指定與否:比如要配置只有在 Master 分支上才執行 push,其他分支上都只運行 build
stages { stage('Build') { when { not { branch 'master' } } steps { sh './scripts/run.py build' } } stage('Run') { when { branch 'master' } steps { sh './scripts/run.py push' } } }
還能在 Stage 的級別設置 environment
,這些就不展開了,文檔里有更詳細的描述。
Steps
steps
是 Pipeline 中最核心的部分,每個 Stage 都需要指定 Steps。Steps 內部可以執行一系列的操作,任意操作執行出錯都會返回錯誤。完整的 Steps 操作列表可以參考 Pipeline Steps Reference,這里只說一些使用時需要注意的點。
- groovy 語法中有不同的字符串類型,其中
'abc'
是 Plain 字符串,不會轉義${WROKSPACE}
這樣的變量,而"abc"
會做這樣的轉換。此外還有''' xxx '''
支持跨行字符串,"""
同理。 - 調用函數的
()
可以省略,使得函數調用形如updateGitlabCommitStatus name: 'build', state: 'success'
,通過,
來分割不同的參數,支持換行。 - 可以在聲明式流水線中通過
script
來插入一段 groovy 腳本
Post
post
部分將會在 pipeline 的最后執行,經常用於一些測試完畢后的清理和通知操作。文檔中給出了一系列的情況,比較常用的是 always
,success
和 failure
。
比如說下面的腳本將會在成功和失敗的時候更新 gitlab 的狀態,在失敗的時候發送通知郵件:
post { failure { updateGitlabCommitStatus name: 'build', state: 'failed' emailext body: '$DEFAULT_CONTENT', recipientProviders: [culprits()], subject: '$DEFAULT_SUBJECT' } success { updateGitlabCommitStatus name: 'build', state: 'success' } }
每個狀態其實都相當於於一個 steps
,都能夠執行一系列的操作,不同狀態的執行順序是事先規定好的,就是文檔中列出的順序。
Shared Libraries
同一個 Team 產出的不同項目往往會有着相似的流程,比如 golang 的大部分項目都會執行同樣的命令。這就導致了人們經常需要在不同的項目間復制同樣的流程,而 Shared Libraries 就解決了這個問題。通過在 Pipeline 中引入共享庫,把常用的流程抽象出來變成一個的指令,簡化了大量重復的操作。
在配置好 lib 之后,Jenkins 會在每個 Pipeline 啟動前去檢查 lib 是否更新並 pull 到本地,根據配置決定是否直接加載。
所有的 Shared Libraries 都要遵循相同的項目結構:
(root)
+- src # Groovy source files
| +- org
| +- foo
| +- Bar.groovy # for org.foo.Bar class
+- vars
| +- foo.groovy # for global 'foo' variable
| +- foo.txt # help for 'foo' variable
+- resources # resource files (external libraries only)
| +- org
| +- foo
| +- bar.json # static helper data for org.foo.Bar
目前我們的使用比較低級,所以只用到了 vars
來存儲全局的變量。
vars 下的每一個 foo.groovy
文件都是一個獨立的 namespace,在 Pipeline 中可以以 foo.XXX
的形式來導入。比如我們有 vars/log.groovy
:
def info(message) { echo "INFO: ${message}" } def warning(message) { echo "WARNING: ${message}" }
那么 Jenkinsfile 中就可以這樣調用:
// Jenkinsfile steps { log.info 'Starting' log.warning 'Nothing to do!' }
大家可能已經注意到了,在 groovy
文件中,我們可以直接像在 steps
中一樣調用已有的方法,比如 echo
和 sh
等。
我們也能在 groovy
文件中去引用 Java 的庫並返回一個變量,比如:
#!/usr/bin/env groovy import java.util.Random; def String name() { def rand = new Random() def t = rand.nextInt(1000) return String.valueOf(t) }
這樣就能夠在 JenkinsFile
中去設置一個環境變量:
// Jenkinsfile environment { NAME = random.name() }
除了定義方法之外,我們還能讓這個文件本身就能被調用,只需要定義一個 call 方法:
#!/usr/bin/env groovy def call() { sh "hello, world" }
還能夠定義一個新的 section,接受一個 Block:
def call(Closure body) { node('windows') { body() } }
這樣可以讓指定的 Body 在 windows 節點上調用:
// Jenkinsfile windows { bat "cmd /?" }
常用技巧
發送郵件通知
主要使用 emailext
,需要在 Jenkins 的配置界面事先配置好,可用的環境變量和參數可以參考文檔 Email-ext plugin
emailext body: '$DEFAULT_CONTENT', recipientProviders: [culprits(),developers()], subject: '$DEFAULT_SUBJECT'
結果同步到 gitlab
同樣需要配置好 gitlab 插件,在 Pipeline 中指定 options
:
// Jenkisfile
options {
gitLabConnection('gitlab')
}
然后就可以在 post 中根據不同的狀態來更新 gitlab 了:
// Jenkisfile failure { updateGitlabCommitStatus name: 'build', state: 'failed' } success { updateGitlabCommitStatus name: 'build', state: 'success' }
文檔參考:Build status configuration
構建過程中可用的環境變量列表
Jenkins 會提供一個完整的列表,只需要訪問 <your-jenkins-url>/env-vars.html/
即可,別忘了需要使用 "${WORKSPACE}"
在 checkout 前執行自定義操作
在 Multibranch Pipeline 的默認流程中會在 checkout 之前和之后執行 git clean -fdx
,如果在測試中以 root 權限創建了文件,那么 jenkins 會因為這個命令執行失敗而報錯。所以我們需要在 checkout 之前執行自定義的任務:
#!/usr/bin/env groovy
// var/pre.groovy
def call(Closure body) {
body()
checkout scm
}
在 Jenkinsfile 中配置以跳過默認的 checkout 行為:
// Jenkisfile
options {
skipDefaultCheckout true
}
在每個 stage 中執行自定義的任務即可:
// Jenkisfile
stage('Compile') {
agent any
steps {
pre {
sh 'pre compile'
}
sh 'real compile'
}
}
總結
Jenkins 作為使用最為廣泛的 CI/CD 平台,網上流傳着無數的腳本和攻略,在學習和開發的時候一定要從基本出發,了解內部原理,多看官方的文檔,不要拿到一段代碼就開始用,這樣才能不會迷失在各式各樣的腳本之中。
更重要的是要結合自己的業務需求,開發和定制屬於自己的流程,不要被 Jenkins 的框架限制住。比如我們是否可以定義一個自己的 YAML 配置文件,然后根據 YAML 來生成 Pipeline,不需要業務自己寫 Pipeline 腳本,規范使用,提前檢查不合法的腳本,核心的模塊共同升級,避免了一個流程小改動需要所有項目組同步更新。這是我現在正在做的事情,有機會再跟大家分享~