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 作為別名,他們都等同於使用哈希自動的進行均衡,例如@hourlyH * * * *相同,表示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必須只有一個 stepsparallel ,嵌套的stage本身不能再進入 parallel 階段,任何包含parallelstage不能包含agenttools,因為它們沒有相關的step

    另外,可以添加 failFast true 到包含 parallelstage 中,表示當其中一個進程失敗時,所有的 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: