「持續整合實踐系列 」Jenkins 2.x 構建CI自動化流水線常見技巧

狂師發表於2020-06-05

在上一篇文章中,我們介紹了Jenkins 2.x實現流水線的兩種語法,以及在實際工作中該如何選擇指令碼式語法或宣告式語法。原文可查閱:「持續整合實踐系列」Jenkins 2.x 搭建CI需要掌握的硬核要點(一)

在使用傳統的Jenkins Web介面和專案時,比如自由風格型別的任務,我們對處理流程的控制能力是有限的。所採用的典型形式是任務鏈:任務完成後觸發其他的任務。或者我們可能會包括構建後處理,不管任務成功完成與否,總是去做一些類似傳送通知的事情。

除了這些基本的功能外,還可以新增條件性構建步驟外掛,通過基於單個或者多個條件的構建步驟來定義更加複雜的流程。但即便如此,相比於我們編寫程式時可以直接控制執行流程的方法,條件性構建步驟外掛對流程的控制能力依然有限。

在本篇中,我們將聊一下,關於Jenkins流水線DSL語言所提供的用於控制流水線執行流程基本結構和一些常見技巧。

1. Pipeline流水線指令常見結構

正如在系列第一篇文章中介紹到的,Jenkins DSL採用的是Groovy指令碼語言。這也意味著如果當你掌握了Groovy語言,可以按照需求在流水線中使用Groovy語言的結構和習慣用法,針對這一類使用者,通常會更傾向於用指令碼式語法來實現流水線。但不管採用的是哪種語法,從流水線組成的角度來講,都是由一些不同指令+步驟構建結構化程式碼塊。

對於指令碼式流水線,基本結構如下:

node('worker'){
    stage('階段'){
        // DSL
    }
}

構建指令碼式流水線常用的結構或者說程式碼塊節點主要由nodestage兩個組成。

而,宣告式流水線基本結構構成環節相對要多一些,整理了一張圖如下:

需要劃一個重點:可以簡單理解node是用於指令碼式流水線,而agent則是用於宣告式流水線。

Jenkins Pipeline支援的指令(常見):

指令名 說明 作用域
agent 指定流水線或特定階段在哪裡執行。 stage 或pipeline
environment 設定環境變數 stage或pipeline
tools 自動下載並安裝指定的工具,並將其加入到PATH變數中 stage或pipeline
input 暫停pipeline,提示輸入內容 stage
options 用來指定一些預定義選項 stage 或 pipeline
parallel 並行執行多個step stage
parameters 允許執行pipeline前傳入一些引數 pipeline
triggers 定義執行pipeline的觸發器 pipeline
when 定義階段執行的條件 stage
build 觸發其他的job steps

options Jenkins Pipeline常見配置引數:

引數名 說明 例子
buildDiscarder 保留最近歷史構建記錄的數量 buildDiscarder(logRotator(numToKeepStr: '10')
timestamps 新增時間戳到控制檯輸出 timestamps()
disableConcurrentBuilds 阻止Jenkins併發執行同一個流水線 disableConcurrentBuilds()
retry pipeline發生失敗後重試次數 retry(4)
timeout pipeline執行超時時間 timeout(time:1, unit: 'HOURS')

示例:

pipeline{
    agent any
    options{
        buildDiscarder(logRotator(numToKeepStr: '10')
        timestamps()
        retry(3)
        timeout(time:1, unit: 'HOURS')
    }
    stages{
        stage('demo'){
            steps{
                sh 'echo hello'
            }
        }
    }
}

更多pipeline指令,可參見官方介紹:

https://www.jenkins.io/doc/book/pipeline/syntax/#

下述僅挑幾個常用的,用於流水線流程控制選項的指令項,介紹一些常用技巧。

2. 超時(Timeout)

這個timeout步驟允許限制等待某個行為發生時指令碼所花費的時間。其語法相當簡單。示例如下:

timeout(time:60,unit:'SECONDS'){
    //該程式碼塊中的過程被設定為超時
}

預設的時間單位是min。如果發生超時,該步驟就會丟擲一個異常。如果異常沒有被處理,將導致整個流水線過程被中止。

通常推薦的做法是,在使用timeout對任何造成流水線暫停的步驟(如一個input步驟)進行封裝,這樣做的結果是,即使出現差錯導致在限定的時間內沒有得到期望的輸入,流水線也會繼續執行。

示例如下:

node{
    def response
    stage('input'){
        timeout(time:10,unit:'SECONDS'){
            response = input message :'Please Input User'
            parameters:[string(defaultValue:'mikezhou',description:'Enter UserId:',name:'userid')]
        }
        echo "Username = " + response
    }
}

在這種情況下,Jenkins將會給使用者10s做出反應,如果時間到了,Jenkins會丟擲一個異常來中止流水線。

如果實際在設計流水線時,當超時發生時,並不想中止流水線向下執行,可以引入try...catch程式碼塊來封裝timeout。

如下程式碼塊所示:

node{
    def response
    stage('input')
{
      try {
        timeout(time:10,unit:'SECONDS'){
            response = input message :'Please Input User'
            parameters:[string(defaultValue:'mikezhou',description:'Enter UserId:',name:'userid')]
         }
       }
       catch(err){
            response = 'user1'
      }
    }
}

需要注意的是,在處理異常的時候,可以在捕獲異常處設定為期望的預設值。

3. 重試(retry)

這個retry閉包將程式碼封底裝為一個步驟,當程式碼中有異常發生時,該步驟可以重試n次。其語法如下:

retry(n){
  //程式碼過程
}

如果達到重試的限制並且發生了一個異常,那麼整個過程將會被中止(除非異常被處理,如使用try...catch程式碼塊)

retry(2){
    try {
       def result=build job: "test_job"
       echo result
      }
    catch(err){
        if(!err.getMessage().contains("UNSTABLE"))
        throw err
    }
}

4. 等待直到(waitUntil)

引入waitUntil步驟,會導致整個過程一直等待某件事發生,通常這裡的“某件事”指的是可以返回true的閉包。

如果程式碼過程永不返回true的話,這個步驟將會無期限地等待下去而不會結束。所以一般常見的做法,會結合timeout步驟來封裝waitUntil步驟。

例如,使用waitUntil程式碼塊來等待一個標記檔案出現:

timeout(time:15,unit:'SECONDS'){
    waitUntil{
        def ret = sh returnStatus:true,script:'test -e /home/jenkins2/marker.txt'
        return (ret==0)
    }
}

再舉一個例子,假如我們要等待一個Docker容器執行起來,以便我們可以在流水線中通過REST API呼叫獲取一些資料。在這種情況下,如果這個URL還不可用,就會得到一個異常。為了保證異常被丟擲的時候程式不會立即退出,我們可以使用try...catch程式碼塊來捕獲異常並且返回false。

timeout(time:150,unit:'SECONDS'){
    waitUntil{
        try{
            sh "docker exec ${containerid} curl --silent http://127.0.0.1:8080/api/v1/registry >/test/output/url.txt"
            return true
        }
        catch(err)
            return false
    }
}

5.Stash暫存:實現跨節點檔案共享

在Jenkins的DSL中,stashunstash函式允許在流水線的節點間和階段間儲存或獲取檔案。

基本用法格式:

stash name:"<name>" [includes:"<pattern>" excludes:"<pattern>"]
unstash "<name>"

我們通過名稱或模式來指定一個被包括或被排除的檔案的集合。給這些檔案的暫存處命名,以便後面通過這個名稱使用這些檔案。

提到stash,很多讀者可能會把Jenkins stashGit stash功能弄混,需要說明一下,Jenkins stashGit stash功能是不同的。Git stash函式是為了暫存一個工作目錄的內容,快取那些還沒有提交到原生程式碼倉庫的程式碼。而Jenkins stash函式是為了暫存檔案,以便在節點間共享。

例如,master節點和node節點,實現跨主機共享檔案:

pipeline{
    agent none
    stages{
        stage('stash'){
            agent { label "master" }
            steps{
                writeFile file: "test.txt", text: "$BUILD_NUMBER"
                stash name: "test", includes: "test.txt"
            }
        }
        stage('unstash'){
            agent { label "node" }
            steps{
                script{
                    unstash("test")
                    def content = readFile("test.txt")
                    echo "${content}"
                }
            }
        }
    }
}

如果你覺得文章還不錯,請大家點贊分享下。你的肯定是我最大的鼓勵和支援。

相關文章