使用 Jenkins 構建 CI-CD 之多分支流水線指北(實戰)

手把手帶你一起從 0 到 1 搭建一個企業級的自動化構建流程 網上完整的關於多分支流水線的配置很少,希望這篇不短的文章能給你帶來幫助

簡介

緣起

由於公司的 Jenkins 配置沒有部署成功的通知,在我學了幾天的 Jenkins 後終於是對公司的 Jenkins 配置下手了,結果我剛裝完dingtalk插件自動重啓後,發現之前主管配置的構建項目數據都丟失了,害,正好給了我練手的機會,於是就有了以下從 0 到 1 的辛酸歷程。

在 Docker 中安裝並運行 Jenkins

這裏假設你的服務器已經裝好了 docker

使用的鏡像是jenkinsci/blueocean,這是一個 jenkins 的穩定及持續維護的鏡像源,本身就集成了 Blue Ocean 等使用插件,非常方便。

拉取鏡像

docker pull jenkinsci/blueocean
複製代碼

運行 Jenkins

docker run -idt --name kmywjenkins -p 9090:8080 -p 60000:50000 -v jenkins-data:/var/jenkins_home -v /data/web-data/docker.sock:/var/run/docker.sock jenkinsci/blueocean
複製代碼

參數解釋:

-idt 以交互的方式、新建一個模擬終端運行容器

--name 容器的別名

-p 指定容器映射宿主機的端口 -> 宿主機端口: 容器端口

-v jenkins-data:/var/jenkins_home Jenkins 容器在工作的時候,如果要執行 Docker 的命令(例如 docker ps、docker run 等),需要有個途徑能連接到宿主機的 docker 服務,此參數就是用來建立容器和宿主機 docker 服務的連接的

-v /data/web-data/docker.sock:/var/run/docker.sock 將該容器的數據保留在宿主機的目錄,這樣即使容器崩潰了,裏面的配置和任務都不會丟失

需要注意的是,docker 中默認是以jenkins用戶運行的 Jenkins,如需以 root 用戶可以加參數-u root,本示例未指定 root。

訪問 Jenkins Docker 容器

有時候需要進入 Jenkins 容器執行一些命令,可以通過docker exec命令訪問,例如:docker exec -it [containerid] bash

若要手動重啓 Jenkins,可以執行以下命令:docker restart [containerid]

Jenkins 基本配置

通過以上步驟,如果正常走到這裏,可以通過以下地址訪問http://121.41.16.183:9090/,ip 地址爲服務器的地址。

解鎖 jenkins

輸入一下命令獲取解鎖的 token,docker exec kmywjenkins cat /var/jenkins_home/secrets/initialAdminPassword

在瀏覽器中輸入對應的 token 以解鎖:

創建憑據

連接 git 倉庫,ssh 連接服務器均需要相應的憑據,可以在憑據管理中先創建好,然後需要使用的地方直接選擇憑據即可。這裏以連接 git、ssh 需要的憑據爲例:

  1. 我司用得版本管理工具是 gitte,以 gitte 爲例,其它版本管理工具配置也一樣

​ 類型選擇Username with password

​ 用戶名密碼爲登錄 gitte 的賬號密碼

​ ID 是憑據的唯一標識,可自定義,後面在 JenkinsFile 中通過 ID 去引用憑據

配置後的結果

  1. ssh 連接服務器時需要密鑰,我們先在服務器生成一對公私鑰,然後複製私鑰,填入即可

    類型選擇SSH Username with private key

    Username是連接服務器的用戶名,如jenkins

    Private Key項選中Enter directly,點擊 Add,粘貼剛複製的私鑰

配置後的結果

創建一個多分支流水線

之前的 Jenkins 任務是 FreeStyle 的方式創建的,這種方式不夠靈活,界面也不夠清爽,這裏選擇使用聲明式流水線方式(Declarative Pipeline)創建,可以多分支獨立構建,便於以後的擴展。

我們這裏使用 BlueOcean 這種方式來完成此處 CI/CD 的工作,BlueOcean 是 Jenkins 團隊從用戶體驗角度出發,專爲 Jenkins Pipeline 重新設計的一套 UI 界面,仍然兼容以前的 fressstyle 類型的 job,BlueOcean 具有以下的一些特性:

如果安裝的是jenkinsci/blueocean鏡像,默認是已經集成了 BlueOcean,沒有的可前往插件管理安裝對應的插件。

點擊打開 Blue Ocean,可以看到已經創建好的兩個流水線,分別是前端和後臺,需要用到不同的工具,在後面會提到,如何創建流水線

點擊創建流水線

我司用的是 gitte,所以選擇 Git,然後填入要連接的倉庫地址,需要連接到 Git 倉庫的憑據,我們之前已經創建好了,直接選中即可,如果未創建,在下面的表單直接編輯即可,最後點擊創建流水線

到這裏我們就創建了一個多分支流水線,Jenkins 會掃描倉庫,帶有 JenkinsFile 的分支會被檢測出來,JenkinFile 是多分支流水線的配置文件,使用的是 Groovy 語法,可以直接點擊創建流水線,Jenkins 會自動爲你的項目創建一個 JenkinsFile

現在可以可視化地編輯想要執行的階段及步驟,這裏加了一個打包的階段,裏面有個步驟是提示開始打包,點擊保存

填入提交信息,點擊Save & Run,會講 JenkinsFile 上傳到 git,並根據 JenkinsFile 執行一個構建任務,目前的構建步驟只有一個,是提示開始打包

我這裏不知道爲什麼會卡在這個地方不動,所以我在 vscode 直接創建並編輯 JenkinsFile,這種方式更靈活,我更推薦這種方式,下面我會先簡單介紹下 JeninsFile 的基礎語法,僅包含本項目用到的,對於中小企業的構建需求,基本夠用了。

JenkinsFile 基礎語法

只需先了解大致的語法,具體的用法會在後面說明

pipeline {
  agent any
  environment {
    HOST_TEST = 'root@121.41.16.183'
    HOST_ONLINE = 'jenkins@39.101.219.110'
    SOURCE_DIR = 'dist/*'
    TARGET_DIR = '/data/www/kuaimen-yunying-front'
  }
  parameters {
    choice(
      description: '你需要選擇哪個環境進行部署 ?',
      name: 'env',
      choices: ['測試環境', '線上環境']
    )    
    string(name: 'update', defaultValue: '', description: '本次更新內容?')      
  }
  triggers {
    GenericTrigger(
     genericVariables: [
      [key: 'ref', value: '$.ref']
     ],
     causeString: 'Triggered on $ref',
     token: 'runcenter-front-q1w2e3r4t5',
     tokenCredentialId: '',
     printContributedVariables: true,
     printPostContent: true,
     silentResponse: false,
     regexpFilterText: '$ref',
     regexpFilterExpression: 'refs/heads/' + BRANCH_NAME
    )
  } 
  stages {
    stage('獲取git commit message') {
     steps {
       script {
         env.GIT_COMMIT_MSG = sh (script: 'git log -1 --pretty=%B ${GIT_COMMIT}', returnStdout: true).trim()
       }
     }
  }
    
    stage('打包') {
      steps {
        nodejs('nodejs-12.16') {
          echo '開始安裝依賴'
          sh 'yarn'
          echo '開始打包'
          sh 'yarn run build'
        }
      }
    }

    stage('部署') {
      when {
        expression {
          params.env == '測試環境'
        }
      }
      steps {
        sshagent(credentials: ['km-test2']) {
          sh "ssh -o StrictHostKeyChecking=no ${HOST_TEST} uname -a"
          sh "scp -r ${SOURCE_DIR} ${HOST_TEST}:${TARGET_DIR}"
          sh 'echo "部署成功~"'
        }
      }
    }

    stage('發佈') {
      when {
        expression {
          params.env == '線上環境'
        }
      }
      steps {
        sshagent(credentials: ['km-online']) {
          sh "ssh -o StrictHostKeyChecking=no ${HOST_ONLINE} uname -a"
          sh "scp -r ${SOURCE_DIR} ${HOST_ONLINE}:${TARGET_DIR}"
          sh 'echo "發佈成功~"'
        }
      }
    }
  }

  post {
    success {
      dingtalk (
        robot: '77d4c82d-3794-4583-bc7f-556902fee6b0',
        type: 'MARKDOWN',
        atAll: true,
        title: '你有新的消息,請注意查收',
        text:[
          '# 運營管理系統發佈通知',
          '---',
          '#### **所屬:前端**',
          "#### **構建任務:${env.BUILD_DISPLAY_NAME}**",
          "#### **Git commit:${env.GIT_COMMIT_MSG}**",
          "#### **本次更新內容:${params.update}**",
          "#### **部署環境:${params.env}**",
          '#### **構建結果:成功**'
        ]
      )
    }
  }
}
複製代碼

pipeline 必須在最外層

agent 定義了在哪個環境裏執行,默認 any

stages 階段,標識構建流程的標籤塊,子節點是stage

steps 執行步驟

post 所有階段執行完成後執行一些邏輯

when 可以控制該階段是否執行

environment 環境變量,在這裏定義的變量,JenkinsFile 的任何地方都可以訪問

tools 項目使用到的構建工具,聲明系統配置中已經定義好的工具,如maven

parameters 定義參數,可以提供用戶輸入或者選擇

post 構建結束後會執行這裏,有successfailuresuccess,本示例將在success(構建成功時)發起釘釘通知

CI/CD 流程

由於我司的技術團隊較小,CI/CD 流程就沒那麼複雜,不會包含代碼檢查、自動化測試、Code Review 等流程,我將簡要說明我所搭建的前端與後端 CI/CD 流程以及爲什麼這麼搭建。

前端

提供兩種構建方式,一種是代碼上傳自動構建,一種是參數化構建,可選擇部署到測試環境還是線上環境。

自動構建默認部署到測試環境,由於線上環境很重要,自動化構建會有一定風險,所以需要人工干預選擇參數進行構建。

  1. 提交代碼到 master,自動觸發構建 如果是參數化構建,這一步是手動選擇要構建的環境,然後開始構建
  2. 安裝依賴
  3. 打包
  4. 上傳到服務器
  5. 如果成功發起釘釘通知

後端

後端的所有項目都是放在一個 git 倉庫中,所以就沒有做自動構建

  1. 參數化構建 可選擇要構建的環境、打包的項目、是否需要全量打包
  2. 清除舊數據
  3. 打包
  4. 上傳到服務器
  5. 殺掉相應的進程
  6. 啓動相應的進程
  7. 如果成功發起釘釘通知

接下來就每一步作詳細說明,以及可能遇到的坑

自動觸發構建

什麼是自動觸發構建

當我們提交新的代碼到 git 倉庫,Jenkins 就會自動開始構建已經配置好的該項目的任務

原理

在 git 倉庫配置一個 Jenkins 服務器的 webhook 地址,當 git 倉庫有變動時會請求這個地址,Jenkins 就能收到通知然後開始構建任務

配置

  1. 我們需要先安裝一個插件 Multibranch Scan Webhook Trigger,可進入插件管理搜索進行安裝

  2. 進入項目的配置界面,勾選 Scan by webhook,填入自定義 token,需要確保 token 的唯一性,不會與其它項目的衝突

  3. 過濾分支

這是一個多分支流水線,Jenkins 默認會檢出所有包含 Jenkinsfile 的分支,如果配置了 webhook,就會自動觸發相應分支的構建任務;有時候我們只想 master 發生變化後纔去構建任務,這時就用到了過濾分支的配置,進入項目配置,在分支源git項找到add按鈕並點擊

選擇根據名稱過濾(支持通配符),或者你可以選擇根據名稱過濾(支持正則表達式),效果一樣,只是過濾格式不太一樣,我這裏在相應的地方填入master,即只檢索master分支,這樣就達到我們想要的效果了。

  1. 進入遠端倉庫(我這裏是 Gitte),點擊 Webhooks,接着點擊添加 WebHook

  2. 填入 URL,IP 地址爲 Jenkins 部署的服務器,token 爲我們剛設置的,/multibranch-webhook-trigger/invoke 是固定地址,點擊添加

  3. 會自動發起一個請求,即我們剛填寫的,如相應如下則表示配置成功,相應的構建任務也會自動執行

自動化打包

前端

使用了yarn進行安裝依賴及打包,需要先配置nodejs環境

  1. 進入插件管理搜索nodejs進行安裝
  2. 進入全局工具配置,新增如下配置,別名可以自定義,建議格式爲nodejs-版本號,該項目用的是yarn,所以在Global npm package to install,加入了配置項,構建的時候會自動安裝yarn,如果是 npm 可以忽略該配置
  3. Jenkinsfile 配置 前端的比較簡單
pipeline {
   stage('打包') {
      steps {
        
        nodejs('nodejs-12.16') {
          echo '開始安裝依賴'
          sh 'yarn'
          echo '開始打包'
          sh 'yarn run build'
        }
      }
    }
}
複製代碼

後端(Java)

pipeline {
  tools {
        maven 'Maven3.6.3'
    }
  parameters {
        
        choice(
            description: '你需要選擇哪個環境進行部署 ?',
            name: 'env',
            choices: ['測試環境', '線上環境']
        ) 
        
        choice(
            description: '你需要選擇哪個模塊進行構建 ?',
            name: 'moduleName',
            choices: ['kuaimen-contract', 'kuaimen-core', 'kuaimen-eureka-server', 'kuaimen-manage', 'kuaimen-member', 'kuaimen-order', 'kuaimen-shop', 'tiemuzhen-manage']
        )   
        booleanParam(name: 'isAll', defaultValue: false, description: '是否需要全量包含clean && build)')     
        string(name: 'update', defaultValue: '', description: '本次更新內容?')    
    }
  
  stages {
    stage('全量清除舊數據...') {
            when {
                expression {
                    params.isAll == true
                }
            }
            steps {
                echo "開始全量清除"      
                sh "mvn package clean -Dmaven.test.skip=true"
            }
        }
        stage('全量打包應用') {
            when {
                expression {
                    params.isAll == true
                }
            }
            steps {
                echo "開始全量打包"   
                sh "mvn package -Dmaven.test.skip=true"
                echo '打包成功'
            }
        }
        stage('清除舊數據...') {
            when {
                expression {
                    params.isAll == false
                }
            }
            steps {
                echo "開始清除${params.moduleName}模塊"      
                sh "cd ${params.moduleName} && mvn package clean -Dmaven.test.skip=true"
            }
        }
        stage('打包應用') {
            when {
                expression {
                    params.isAll == false
                }
            }
            steps {
                echo "開始打包${params.moduleName}模塊"   
                sh "cd ${params.moduleName} && mvn package -Dmaven.test.skip=true"
                echo '打包成功'
            }
        }
  }
}
複製代碼

parameters

parameters中主要是提供參數化構建的選項,在其它地方可以通過"${params.isAll}"這種形式拿到用戶的交互信息,配置後效果如下:

when>expression表達式中的參數如果未true,則執行,反之跳過該stage

mvn在系統配置中默認就已經提供了該環境,進入系統全局工具配置,添加如下配置(類似 nodejs)

這種方式引用

tools {
  maven 'Maven3.6.3'
}
複製代碼

自動化部署

前端

pipeline {
  agent any
  environment {
    HOST_TEST = 'root@121.41.16.183'
    HOST_ONLINE = 'jenkins@39.101.219.110'
    SOURCE_DIR = 'dist/*'
    TARGET_DIR = '/data/www/kuaimen-yunying-front'
  }
   stage('部署') {
      when {
        expression {
          params.env == '測試環境'
        }
      }
      steps {
        sshagent(credentials: ['km-test2']) {
          sh "ssh -o StrictHostKeyChecking=no ${HOST_TEST} uname -a"
          
          sh "scp -r ${SOURCE_DIR} ${HOST_TEST}:${TARGET_DIR}"
          sh 'echo "部署成功~"'
        }
      }
    }

    stage('發佈') {
      when {
        expression {
          params.env == '線上環境'
        }
      }
      steps {
        sshagent(credentials: ['km-online']) {
          sh "ssh -o StrictHostKeyChecking=no ${HOST_ONLINE} uname -a"
          sh "scp -r ${SOURCE_DIR} ${HOST_ONLINE}:${TARGET_DIR}"
          sh 'echo "發佈成功~"'
        }
      }
    }
  }
}
複製代碼

environment定了全局變量,在其它地方可直接引用

sshagent用於連接服務器,需要先安裝插件 ssh-agentcredentials是連接服務器的憑據 ID,我們在一開始已經教大家創建好了

後端

pipeline {
  agent any
  environment {
        HOST_TEST = 'root@121.41.16.183'
        TARGET_DIR = '/data/www/kuaimen-auto'
        HOST_ONLINE = 'jenkins@39.101.219.110'
  }
  
  tools {
        maven 'Maven3.6.3'
  }
  
  stage('部署應用') {
            when {
                expression {
                    params.env == '測試環境'
                }
            }
            steps {
                echo "開始部署${params.moduleName}模塊"   
                sshagent(credentials: ['km-test2']) {
                    sh "ssh -v -o StrictHostKeyChecking=no ${HOST_TEST} uname -a"
                    
                    sh "cd ${params.moduleName}/target && scp *.jar ${HOST_TEST}:${TARGET_DIR}/${params.moduleName}"
                    
                    sh "ssh -o StrictHostKeyChecking=no ${HOST_TEST} \"uname;ps -ef | egrep ${params.moduleName}.*.jar | egrep -v grep | awk '{print \\\$2}' | xargs -r sudo kill -9\""
                    
                    sh "ssh -o StrictHostKeyChecking=no ${HOST_TEST} \"nohup /data/apps/jdk1.8/bin/java -jar ${TARGET_DIR}/${params.moduleName}/${params.moduleName}-0.0.1-SNAPSHOT.jar --spring.profiles.active=test >/dev/null 2>&1 &\""
                    sh 'echo "部署成功~"'
                }
                echo '部署成功'
            }
        }
        stage('發佈應用') {
            when {
                expression {
                    params.env == '線上環境'
                }
            }
            steps {
                echo "開始發佈${params.moduleName}模塊"   
                sshagent(credentials: ['km-online']) {
                    sh "ssh -v -o StrictHostKeyChecking=no ${HOST_ONLINE} uname -a"
                    sh "cd ${params.moduleName}/target && scp *.jar ${HOST_ONLINE}:${TARGET_DIR}/${params.moduleName}"
                    sh "ssh -o StrictHostKeyChecking=no ${HOST_ONLINE} \"uname;ps -ef | egrep ${params.moduleName}.*.jar | egrep -v grep | awk '{print \\\$2}' | xargs -r sudo kill -9\""
                    sh "ssh -o StrictHostKeyChecking=no ${HOST_ONLINE} \"nohup /data/apps/jdk1.8/bin/java -jar ${TARGET_DIR}/${params.moduleName}/${params.moduleName}-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev >/dev/null 2>&1 &\""
                    sh 'echo "發佈成功~"'
                }
                echo '發佈成功'
            }
        }
}
複製代碼

需要注意的是,在匹配進程的那段 shell 中的awk '{print \\\$2}'$符號需要用三個反斜線進行轉義,不然會無法執行成功,這裏曾卡了好久,希望你們別踩坑了

部署完成後發起通知

我們這裏使用釘釘發起通知,主要原理是在釘釘羣創建一個 webhook 機器人,然後把 webhook 的地址填入 DingTalk 插件的配置項,最後在 JenkinsFile 中進行如下配置即可:

pipeline {
  stage('獲取git commit message') {
     steps {
       script {
         
         env.GIT_COMMIT_MSG = sh (script: 'git log -1 --pretty=%B ${GIT_COMMIT}', returnStdout: true).trim()
       }
     }
  }
  post {
     success {
            dingtalk (
                robot: '77d4c82d-3794-4583-bc7f-556902fee6b0',
                type: 'MARKDOWN',
                atAll: true,
                title: '你有新的消息,請注意查收',
                text:[
                '# 運營管理系統發佈通知',
                '---',
                '#### **所屬:後端**',
                "#### **構建任務:${env.BUILD_DISPLAY_NAME}**",
                "#### **本次更新內容:${params.update}**",
                "#### **部署環境:${params.env}**",
                '#### **構建結果:成功**'
                ]
            )
        }
  }
}
複製代碼

GIT_COMMIT這個是 Jenkins 系統全局變量,獲得的是 git commit ID,然後通過它拿到具體的提交信息,並賦值給env.GIT_COMMIT_MSG,全局變量可以通過這種方式訪問env.BUILD_DISPLAY_NAME

robot 爲機器人 ID,在系統配置中添加如下配置項

webhook 在創建完機器人的時候能夠拿到

如何創建釘釘機器人

點擊羣設置 -> 智能羣助手

選擇自定義機器人,配置完成後就可以看到 webhook 的地址了

開始構建

經過上面的配置,我們已經完成了前端、後臺的自動化構建配置,接下來再說明一下分別是如何觸發構建的

前端

  1. 提交代碼到 master,會自動執行構建任務,並部署到測試環境,部署成功後會在釘釘羣發起提醒
  2. 參數化構建 點擊Build with Parameters,選擇相應的參數進行構建,線上環境必須通過這種方式,保證一定的安全性

後端

後端只配置了參數化構建,原因前面已經說了,選擇要構建的環境、模塊進行構建

使用 Blue Ocean 構建(推薦)

點擊打開 Blue Ocean

選擇要構建的分支

彈出參數選擇,這和Build with Parameters差不多,但是界面更好看,更清爽了,選擇後點擊Run即可開始構建

構建結果,很直觀,根據顏色可以判斷構建成功了,如果失敗了是紅色

回滾

在 Blue Ocean 的活動欄可以看到歷史構建,點擊如下位置的按鈕可以重新構建該歷史項,即回滾

寫在最後

到這裏終於告一段落了,雖然折騰了不少時間,但是將公司的工程化流程完善了還是有點小小的成就感的,以後可以愉快得寫代碼了,自動化的事情就交給 Jenkins 了。

將這個記錄下來一個是方便以後隨時查閱,還有一個是希望能讓朋友們少踩些坑,完~

附錄

Jenkins 官方文檔

BlueOcean 實戰多分支 pipeline 構建 (Jenkins)

Complete Jenkins Pipeline Tutorial for Beginners [FREE]

實戰筆記:Jenkins 打造強大的前端自動化工作流

Jenkins:添加 SSH 全局憑證

釘釘通知系列 Jenkins 發佈後自動通知

使用 Generic Webhook Trigger 觸發 Jenkins 多分支流水線自動化構建

Jenkins pipeline 單引號、雙引號和轉義字符

Jenkins Blue Ocean 的使用

How to Execute Linux Commands on Remote System over SSH

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://juejin.cn/post/6883769774564884488