devops-2:Jenkins的使用及Pipeline語法講解

DevOps-Jenkins

Jenkins簡介

Jenkins是一個開源軟體項目,是基於Java開發的一種持續集成工具,用於監控持續重複的工作,旨在提供一個開放易用的軟體平台,使軟體項目可以進行持續集成。

什麼是持續集成(CI)?

CI(Continuous integration,中文意思是持續集成)是一種軟體開發實踐。持續集成強調開發人員提交了新程式碼之後,立刻進行構建、編譯、(單元)測試等這個過程,每次提交新程式碼都要進行此類重複操作,為了提高工作效率,避免重複工作及重複工作導致差別化問題。

什麼是持續部署(CD)?

CD(Continuous Delivery, 中文意思持續交付)是在持續集成的基礎上,將集成後的程式碼部署到更貼近真實運行環境中,也就是說它是CI後緊跟的一個環節,當程式碼審核完畢打包成成品後,需要部署到真實的環境中去,這個過程也會根據程式碼的跟新持續的進行。

Jenkins安裝

採用docker形式安裝,首先安裝好docker服務,然後在安裝機器中對要存儲Jenkins數據的目錄進行賦權:

# groupadd jenkins -g 1000 && useradd jenkins -u 1000 -g jenkins
# chown -R 1000:1000 /jenkins

uid 和 gid必須是1000,和Jenkins鏡像中的對應

開始安裝:

# docker network create jenkins

# docker run \
  --name jenkins-docker \
  --detach \
  --privileged \
  --network jenkins \
  --network-alias docker \
  --env DOCKER_TLS_CERTDIR=/certs \
  --volume /jenkins/certs:/certs/client \
  --volume /jenkins/data:/var/jenkins_home \
  --publish 8080:8080 \
  --publish 50000:50000 \
  jenkins/jenkins:latest

## 執行此命令獲取初始密碼
# docker exec -it jenkins-docker cat /var/jenkins_home/secrets/initialAdminPassword

這個時候就可以通過 serverip:8080 來訪問到Jenkins。

流水線(pipeline)概括

想要更好的使用Jenkins,必須掌握其流水線(pipeline)的使用。

流水線概述

  • 默認文件名為 Jenkinsfile
  • 採用 Groovy 語法
  • 其可以實現對程式碼的整合、編譯、品質檢測或部署等一系列操作,相當於我們說的「腳本」

流水線腳本的分類包括:腳本式、聲明式

  • 腳本式語法結構

    node {
        stage('Example') {
            try {
                sh 'exit 1'
            }
            catch (exc) {
                echo 'Something failed, I should sound the klaxons!'
                throw
            }
        }
    }
    
  • 聲明式語法結構

    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"
                        }
                    }
                }
            }
        }
    }
    

我們應該用哪種方式來編寫 Jenkinsfile?

推薦使用聲明式語法結構來編寫 Jenkinsfile,聲明式語法結構的靈活性及可讀性都要比腳本式語法結構要高,並且聲明式結構中可以包含腳本式結構,可以說更強大一些,並且便於學習。

如何快速上手Jenkinsfile?

  1. 多看Jenkinsfile示例文件,及官方的語法介紹:Pipeline Syntax (jenkins.io),對語法有一個基礎掌握。
  2. 學會使用腳本生成器

聲明式常用語法介紹

  1. 最外層的pipeline,整條流水線的開始,裡邊定義了流水線所有內容

    pipeline{
    
    }
    
  2. agent,指定了流水線的執行節點,可以在pipeline的下行指定,也可以在stages的下行指定,agent有四個可選參數:

    • any:在任意節點執行pipeline
    • none:未定義,當頂層的agent none時,必須在每個stage下再單獨定義agent
    • label:指定運行節點的label
    • node:自定義運行節點配置,例如指定label或customWorkspace,agent { node { label 'labelName' } } 和 agent { label 'labelName' }相同, 但 node支援其他選項 (比如customWorkspace).
    pipeline{
        agent{
            node{
                label "container"
                customWorkspace '/some/other/path'  //指定自定義的工作空間,其實就是指定自定義路徑
            }
        }
    }
    
  3. stages(階段):包含一個或多個stage的序列,大部分執行操作都在這裡,將每個離散部分串接起來,比如構建、測試和部署。

  4. stage:包含在stages中,pipeline完成的所有實際工作都需要包含到stage中,每一個階段都要有一個名字。

  5. steps(步驟):1個stage中,一般都要運行1個steps,裡邊可以直接運行linux命令。相當於3-5點是一個連續的、互相關聯的,例如:

    pipeline {
        agent any
        stages { 
            stage('Example') {
                agent {  //選擇執行節點,可以在此層級指定
                  label 'nginx'
                }
                steps {  //執行的步驟
                    echo 'Hello World'
                }
            }
        }
    }
    
  6. post:定義Pipeline或stage運行結束時的操作有以下多個參數可選:

    • always:無論Pipeline運行的完成狀態如何都會運行

    • changed:只有當前Pipeline或steps運行的狀態與先前完成的Pipeline的狀態不同時,才能運行

    • failure:僅噹噹前Pipeline或steps處於「失敗」狀態時才運行

    • success:僅噹噹前Pipeline或steps具有「成功」狀態時才運行

    • unstable:只有當前Pipeline或steps具有「不穩定」狀態才能運行

    • aborted:只有當前Pipeline或steps處於「中止」狀態時才能運行

    • unsuccessful:只有當前Pipeline或steps運行為「未成功」狀態才能運行

    • cleanup:無論Pipeline或steps運行後為什麼狀態,只要前邊定義的狀態都沒匹配到,則運行

    示例:

    pipeline {
        agent any
        stages {
            stage('Example') {
                steps {
                    echo 'Hello World'
                }
            }
        }
        post { 
            always { 
                echo 'I will always say Hello again!'
            }
        }
    }
    
  7. environment:環境變數,可以定義全局變數和特定的階段變數,取決於其在流水線的位置,該指定支援一個特殊的方法credentials(),此方法可用於在Jenkins環境中通過標識符訪問預定義的憑證,其有很多種類型:

    • Secret Text:指定的環境變數將設置為密文內容
    • Secret File:指定的環境變數將設置為臨時創建的文件的位置
    • Username and password:指定的環境變數將設置為username:password,並將自動定義為兩個環境變數:MYVARNAME_USR和MYVARNAME_PSW
    • SSH with Private Key:指定的環境變數將設置為臨時創建的SSH密鑰文件的位置,並且可以自動定義為兩個環境變數:MYVARNAME_USR和MYVARNAME_PSW

    示例1:

    pipeline {
        agent any
        environment { 
            CC = 'clang'
        }
        stages {
            stage('Example Secret Text') {
                environment { 
                    // 需要提前將憑據創建好Secret Text類型的憑據
                    AN_ACCESS_KEY = credentials('my-predefined-secret-text') 
                }
                steps {
                    sh 'printenv'
                }
            }
        }
    }
    

    示例2:

    pipeline {
        agent any
        stages {
            stage('Example Username/Password') {
                environment {
                    // 提前創建好Username and Password類型的憑據
                    SERVICE_CREDS = credentials('my-predefined-username-password')
                }
                steps {
                    sh 'echo "Service user is $SERVICE_CREDS_USR"'
                    sh 'echo "Service password is $SERVICE_CREDS_PSW"'
                }
            }
            stage('Example SSH Username with private key') {
                environment {
                    // 提前創建好SSH Username with private key類型的憑據
                    SSH_CREDS = credentials('my-predefined-ssh-creds')
                }
                steps {
                    sh 'echo "SSH private key is located at $SSH_CREDS"'
                    sh 'echo "SSH user is $SSH_CREDS_USR"'
                    sh 'echo "SSH passphrase is $SSH_CREDS_PSW"'
                }
            }
        }
    }
    
  8. options:此指令允許從Pipeline本身中配置一些特定的選項。Pipeline提供了許多這樣的選項,例如buildDiscarder,也可以由插件提供,例如timestamps。分享一些參數:

    • buildDiscarder:保持構建的最大個數,示例:options { buildDiscarder(logRotator(numToKeepStr: '1')) }

    • checkoutToSubdirectory:在workspace的子目錄中執行源程式碼自動管理切換,示例:options { checkoutToSubdirectory('foo') }

    • disableConcurrentBuilds:不允許同時執行管道。可用於防止同時訪問共享資源等,示例:options { disableConcurrentBuilds() }

    • disableResume:如果控制器重新啟動,則不允許恢復pipeline,示例: options { disableResume() }

    • newContainerPerStage:使用docker或dockerfile agent時。每個stage將在同一節點上的新容器實例中運行,而不是在同一容器實例中運行的所有stages。

    • overrideIndexTriggers:

    • retry:失敗時,按指定次數重試整個管道,可以針對Pipeline或者Stage,示例: options { retry(3) }

    • skipStagesAfterUnstable:一旦構建狀態變得不穩定,就跳過各個stages。示例: options { skipStagesAfterUnstable() }

    • timeout:設置管道運行的超時時間,此參數可以針對Pipeline或者Stage,例如:options { timeout(time: 1, unit: 'HOURS') }

    • timestamps: 所有控制台輸出前加上線路發出的時間,此參數可以針對Pipeline或者Stage,示例:options { timestamps() }

      pipeline {
          agent any
          options {
              timeout(time: 1, unit: 'HOURS') 
              timestamps()
              buildDiscarder(logRotator(numToKeepStr: '10'))
          }
          stages {
              stage('Example') {
                  options {
                      timeout(time: 1200, unit: 'SECONDS') 
                  }
                  steps {
                      echo 'Hello World'
                  }
              }
          }
      }
      
  9. parameters:為Pipeline運行前提供參數,有幾種參數類型供選擇:

    • string:字元串類型,示例:parameters { string(name: 'DEPLOY_ENV', defaultValue: 'staging', description: '') }
    • text:文本類型,可以包含多行,示例: parameters { text(name: 'DEPLOY_TEXT', defaultValue: 'One\nTwo\nThree\n', description: '') }
    • booleanParam:布爾類型,示例: parameters { booleanParam(name: 'DEBUG_BUILD', defaultValue: true, description: '') }
    • choice:選擇類型,多個選項任選其一,示例: parameters { choice(name: 'CHOICES', choices: ['one', 'two', 'three'], description: '') }
    • password:密碼類型,可以提前設置密碼,示例: parameters { password(name: 'PASSWORD', defaultValue: 'SECRET', description: 'A secret password') }
    pipeline {
        agent any
        parameters {
            string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
            text(name: 'BIOGRAPHY', defaultValue: '', description: 'Enter some information about the person')
            booleanParam(name: 'TOGGLE', defaultValue: true, description: 'Toggle this value')
            choice(name: 'CHOICE', choices: ['One', 'Two', 'Three'], description: 'Pick something')
            password(name: 'PASSWORD', defaultValue: 'SECRET', description: 'Enter a password')
        }
        stages {
            stage('Example') {
                steps {
                    echo "Hello ${params.PERSON}"
                    echo "Biography: ${params.BIOGRAPHY}"
                    echo "Toggle: ${params.TOGGLE}"
                    echo "Choice: ${params.CHOICE}"
                    echo "Password: ${params.PASSWORD}"
                }
            }
        }
    }
    

    使用parameters參數有一個bug,首次構建時不會讓你選擇參數,第二次才可以選擇。

  10. triggers:觸發器,定義了Pipeline自動化觸發的方式,可觸發的方式有:

    • cron:計劃任務定期觸發,示例: triggers { cron('H */4 * * 1-5') }

    • pollSCM:與cron方式類似,但是必須發現有源碼的變化,才會觸發,示例: triggers { pollSCM('H */4 * * 1-5') }

      pollSCM觸發器僅在Jenkins 2.22或更高版本中可用。

    • upstream:接受以逗號分隔的作業字元串和閾值。當字元串中的任何作業以最小閾值結束時,將會觸發,示例:triggers { upstream(upstreamProjects: 'job1,job2', threshold: hudson.model.Result.SUCCESS) }

    pipeline {
        agent any
        triggers {
            cron('H */4 * * 1-5')
        }
        stages {
            stage('Example') {
                steps {
                    echo 'Hello World'
                }
            }
        }
    }
    

    Jenkins cron syntax

    Jenkins cron語法遵循cron公共的語法(略有不同),每行由5個欄位組成,由製表符或空格分隔。

    分鐘 小時 日 月 周
    Minutes within the hour (0–59) The hour of the day (0–23) The day of the month (1–31) The month (1–12) The day of the week (0–7) where 0 and 7 are Sunday.

    要為一個欄位指定多個值,可以使用以下運算符。按優先順序展示:

    • * 指定所有的值
    • M-N 指定值得範圍
    • M-N/X or */X 在指定的範圍或者整個範圍內,按照 X的值為間隔步長
    • A,B,…,Z指定多個值

    為了允許定期調度的任務在系統上產生均勻負載,應儘可能使用符號H(表示「哈希」),使其執行pipeline的時間分散,更好的利用資源。

    此外,支援使用 @yearly, @annually, @monthly, @weekly, @daily, @midnight, and @hourly 作為別名,他們都等同於使用哈希自動的進行均衡,例如@hourly 與H * * * *相同,表示1小時內任意時間執行即可。

    舉一些cron例子:

    1. 每15分鐘執行一次,可以是 at :07, :22, :37, :52:triggers{ cron('H/15 * * * *') }
    2. 在每小時的前30分鐘,每10分鐘執行一次:triggers{ cron('H(0-29)/10 * * * *') }
    3. 每周的周一到周五的,9點-16點之間的第45分鐘每兩個小時執行一次:triggers{ cron('45 9-16/2 * * 1-5') }
    4. 1-11月的每個1號和15號都要隨機執行一次:triggers{ cron('H H 1,15 1-11 *') }
  11. tools:工具,目前僅支援三種工具:maven、jdk和gradle。工具的名稱必須在系統設置—>全局工具配置中定義。定義好後即可開始調用:

    pipeline {
        agent any
        tools {
            maven 'apache-maven-3.0.1' 
        }
        stages {
            stage('Example') {
                steps {
                    sh 'mvn --version'
                }
            }
        }
    }
    

    第一次運行此工具時,如果沒有會去下載,或者會根據你指定的獲取方式去獲取,比如直接從其他機器打包過來解壓等方式。

  12. input:stage 的 input 指令允許你使用 input step提示輸入。 在應用了 options 後,進入 stage 的 agent 或評估 when 條件前, stage 將暫停。 如果 input 被批准, stage 將會繼續。 作為 input 提交的任何參數都將在環境中用於其他 stage 使用。可選配置:

    • message:必需的。 呈現給用戶的資訊。
    • id:可選的, 默認為 stage 名稱。
    • ok:可選的,表單中 ok 按鈕的描述文本。
    • submitter:可選的,以逗號分割的用戶列表,只有列表用戶才可以提交input內容。
    • parameters:可選參數列表,與前文介紹的相同。

    示例:

    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."
                }
            }
        }
    }
    
  13. when:邏輯判斷,可以給定條件決定要不要執行此stage,when指令必須包含至少一個條件,如果when包含多個條件,則必須所有的條件都返回true,stage才會運行。內置條件有:

    • brance:分支匹配,當正在構建的分支與預設給定的分支匹配時,執行此stage,例如: when { branch 'master' },此參數僅適用於多分支流水線。
    • environment:當指定的環境變數是給定的值時,執行此stage,例如:when { environment name: 'DEPLOY_TO', value: 'production' }
    • expression:當指定的Groovy表達式評估為true時,執行此stage,例如: when { expression { return params.DEBUG_BUILD } }
    • not:當嵌套條件是錯誤時,執行這個stage,必須包含一個條件,例如: when { not { branch 'master' } }
    • allOf:當所有的嵌套條件都正確時(當when中有多個條件時,默認就是此策略),執行這個此stage,必須包含至少一個條件,例如: when { allOf { branch 'master'; environment name: 'DEPLOY_TO', value: 'production' } }
    • anyOf:當至少有一個嵌套條件為真時,執行這個階段,必須包含至少一個條件,例如: when { anyOf { branch 'master'; branch 'staging' } }
    • beforeAgent:默認為false,默認情況下,如果在stage中包含agent,則會在執行when之前會先執行agent,如果將beforeAgent設置為true,則表示先執行when條件,when條件判斷為執行stage後,才執行agent。

    示例:

    pipeline {
        agent any
        parameters {
            choice(name: 'BRANCH_NAME', choices: ['production', 'staging'], description: 'Pick something')
            string defaultValue: 'production', name: 'DEPLOY_TO', trim: true
        }
        stages {
            stage('Example Build') {
                when {
                    expression { BRANCH_NAME ==~ /(production|staging)/ }
                }
                steps {
                    echo "Start building ${BRANCH_NAME}"
                }
            }
            stage('Example Deploy') {
                when {
                    anyOf {
                        environment name: 'DEPLOY_TO', value: 'production'
                        environment name: 'DEPLOY_TO', value: 'staging'
                    }
                }
                steps {
                    echo 'Deploying'
                }
            }
        }
    }
    
  14. Parallel:並行,聲明式流水線的stage可以在他們內部聲明多個嵌套stage, 它們將並行執行。注意,一個stage必須只有一個 steps 或 parallel ,嵌套的stage本身不能再進入 parallel 階段,任何包含parallel的stage不能包含agent或tools,因為它們沒有相關的step。

    另外,可以添加 failFast true 到包含 parallel的 stage 中,表示當其中一個進程失敗時,所有的 parallel 階段都將被終止。

    示例:

    pipeline {
        agent any
        options {
            parallelsAlwaysFailFast() //表示後續所有的parallel階段中都設置為failFast true
            timestamps()
        }
        stages {
            stage('Build Stage') {
                steps {
                    echo 'This stage is Build.'
                }
            }
            stage('Parallel Stage --> Deploy') {
                when {
                    not{
                        branch 'prod'
                    }
                }
                parallel {
                    stage('Branch dev') {
                        agent {
                            label "dev"
                        }
                        steps {
                            echo "On Branch dev deploy"
                        }
                    }
                    stage('Branch int') {
                        agent {
                            label "int"
                        }
                        steps {
                            echo "On Branch int deploy"
                        }
                    }
                    stage('Branch test') {
                        agent {
                            label "test"
                        }
                        stages {
                            stage('Nested 1') {
                                steps {
                                    echo "In stage Nested 1 within Branch test"
                                }
                            }
                            stage('Nested 2') {
                                steps {
                                    echo "In stage Nested 2 within Branch test"
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    

    此示例中:只要當branch不是prod時,則將同時在dev/int/test三個環境中deploy,test環境的deploy又分為兩個stage,此兩個stage並不是並行執行,而是串列執行。

  15. script:腳本式語法,可以在聲明式流水線中使用,包含在step里,調用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"
                        }
                    }
                }
            }
        }
    }
    

腳本式語法

腳本式pipeline也是建立在底層流水線上的,與聲明式不同的是, 腳本化流水線實際上是由 Groovy構建的通用 DSL, Groovy 語言提供的大部分功能都可以用於腳本化流水線。

  1. 腳本化流水線從 Jenkinsfile 的頂部開始向下串列執行, 就像 Groovy 或其他語言中的大多數傳統腳本一樣。 因此,提供流控制取決於 Groovy 表達式, 比如 if/else 條件, 例如:

    node {
        stage('Example') {
            if (env.BRANCH_NAME == 'master') {
                echo 'I only execute on the master branch'
            } else {
                echo 'I execute elsewhere'
            }
        }
    }
    
  2. 另一種方法是使用Groovy的異常處理支援來管理腳本化流水線流控制。當 步驟 失敗 ,無論什麼原因,它們都會拋出一個異常。處理錯誤的行為必須使用Groovy中的 try/catch/finally 塊 , 例如:

    node {
        stage('Example') {
            try {
                sh 'exit 1'
            }
            catch (exc) {
                echo 'Something failed, I should sound the klaxons!'
                throw
            }
        }
    }
    
Tags: