用代碼實現(xiàn)流水線部署,像詩一般優(yōu)雅
你好,我是悟空。
本文目錄如下:
圖片
這次我們要接著上面的話題聊下如何通過通過編寫代碼的方式實現(xiàn)自動化部署 Java 項目。
而用代碼方式其實就是使用 Jenkins 強大的 pipeline 功能來實現(xiàn)的。
通過本篇你可以學習到如下內(nèi)容:
- Pipeline 的概念、優(yōu)點、缺點。
- 實戰(zhàn):通過編寫 pipeline 來部署一個完整的后端項目。
- pipeline 傳參的合理利用。
- 多選插件、遠程傳輸文件、遠程執(zhí)行命令的使用。
一、Pipeline
1.1 流水線
要了解什么是Pipeline,就必須知道什么是流水線。類似于食品工廠包裝食品,食品被放到傳送帶上,經(jīng)過一系列操作后,包裝完成,這種工程就是流水線工程。
1.2 Pipeline 是什么
在自動化部署中,開發(fā)完成的代碼經(jīng)過一系列順序操作后被部署完成,這個就是部署過程中的流水線,我們通常稱作 pipeline。
之前我們的部署步驟都是通過在 Jenkins 的 UI 界面上配置出來的,但其實 Jenkisn 2.x 版本已經(jīng)可以支持編寫代碼的方式來啟動自動化部署了,通過“代碼”來描述部署流水線。
Jenkins pipeline其實就是基于一種聲明式語言,用于描述整條流水線是如何進行的。流水線的內(nèi)容包括執(zhí)行編譯、打包、測試、輸出測試報告等步驟。
1.3 為什么要用 Pipeline
Pipeline 通過代碼來實現(xiàn),其實就具有很多代碼的優(yōu)勢了,比如:
- 支持傳參:可以在 Pipeline 代碼里面配置用戶要輸入或選擇的參數(shù),這個功能真的太棒了。比如可以傳 Gitlab 分支名、部署哪個服務等。
- 更好地版本化:將 pipeline 代碼提交到軟件版本庫中進行版本控制。
- 更好地協(xié)作:pipeline 的每次修改對所有人都是可見的。除此之外,還可以對pipeline進行代碼審查
- 更好的重用性:手動操作沒法重用,但是代碼可以重用。
當然 pipeline 的缺點也是有的:
- 學習成本高:需要熟悉 pipeline 的語法規(guī)則。
- 復雜:代碼不夠直觀,編寫的邏輯可能很復雜,容易出錯。
1.4 如何使用 Pipeline
在之前的文章中,我是通過創(chuàng)建一個自由風格的項目來實現(xiàn)自動化部署,其實還可以通過創(chuàng)建一個Pipeline 來實現(xiàn),如下圖所示:
創(chuàng)建 Pipeline 任務
然后就可以在配置流水線的地方編寫代碼了。如下圖所示:
編寫流水線代碼
1.5 Pipeline 基礎結構
Pipeline 基礎結構如下所示:
pipeline{
//指定運行此流水線的節(jié)點
agent any
//流水線的階段
stages{
//階段1 獲取代碼
stage("CheckOut") {
steps {
script {
echo "獲取代碼"
}
}
}
}
- pipeline 部分:代表整條流水線,包含整條流水線的邏輯。
- stage 部分:代表流水線的某個階段。每個階段都必須有名稱,本例中,"CheckOut" 就是此階段的名稱。
- stages 部分:流水線中多個stage的容器。stages 部分至少包含一個 stage。steps 部分:代表階段中的一個或多個具體步驟(step)的容器。steps 部分至少包含一個步驟,本例中,echo就是一個步驟。在一個 stage 中有且只有一個steps。
- agent 部分:指定流水線的執(zhí)行位置(Jenkins agent)。流水線中的每個階段都必須在某個地方(物理機、虛擬機或Docker容器)執(zhí)行,agent 部分即指定具體在哪里執(zhí)行。
以上每一個部分都不能少,否則 Jenkins 會報錯。
二、部署思路
2.1 Jenkins 承擔的角色
Jenkins 承擔的角色如下圖所示:
圖片
Jenkins 打包部署原理圖
- (1)Jenkins 部署在一臺服務器上,然后安裝了很多必備的 Jenkins 插件。比如拉取 Gitlab 倉庫代碼的插件、遠程執(zhí)行命令和拷貝文件的插件。
- (2)Jenkins 開始運行一個任務時,通過 Git 插件從 Gitlab 倉庫拉取代碼到本地目錄。
- (3)Jenkins 通過 JDK 和 Maven 工具對 Java 代碼進行打包部署。
- (4)Jenkins 將 JAR 包拷貝到遠程服務器的固定目錄下。
- (5)Jenkins 通過 SSH 插件執(zhí)行遠程命令,將包進行備份操作。
- (6)Jenkins 通過執(zhí)行遠程命令,更新 JAR 包。
- (7)Jenkins 通過執(zhí)行遠程命令,重啟容器。
2.2 通過流水線來部署項目
我們項目是 Java 項目,所以通過流水線來部署項目的步驟如下圖所示:
流水線部署步驟
三、獲取 Gitlab 分支代碼
Pipeline 的強大之處是可以支持傳參以及獲取參數(shù),為了讓用戶可以選擇獲取不同的分支代碼,我在 pipeline 代碼中配置了一個參數(shù):獲取指定的 Gitlab 分支代碼。
3.1 Gitlab 分支配置
在 流水線代碼中添加 parameters 節(jié)點,指定類型為 string,配置相關的屬性。
pipeline {
parameters {
string (
name: 'GIT_BRANCH', // 參數(shù)名,后面 steps 中會用到
defaultValue: 'dev-01.30', // 默認值,會顯示在界面上,用戶可以改。
description: '請選擇部署的分支' // 說明
)
}
// 其他代碼
...
}
通過參數(shù)部分,定義了一個名為GIT_BRANCH的參數(shù),它允許用戶在構建過程中選擇要構建的分支。默認情況下,分支被設置為dev-01.30,用戶可以選擇不同的分支。
在腳本中,這個參數(shù)可以通過params.GIT_BRANCH 獲取到。
保存配置后,需要先運行一次這個項目才能看到參數(shù)配置。如下圖所示:右邊就是參數(shù)配置。
3.2 配置環(huán)境參數(shù)
接著我們還需要定義一些常用的環(huán)境變量信息,比如 Gitlab 的倉庫地址,代碼如下所示:
pipeline {
parameters {
// 配置信息
...
}
// 環(huán)境變量:定義 GitLab 倉庫的 URL 和分支
environment {
GIT_URL = 'https://xxx/xxx.git'
}
// 其他代碼
...
}
environment 節(jié)點為環(huán)境變量信息,GIT_URL 變量代表 Gitlab 的倉庫地址。在腳本中,這個變量可以通過${GIT_URL}使用。
3.3 獲取 Gitlab 分支代碼
接下來我們來看下如何在 pipeline 中添加一個獲取 gitlab 倉庫代碼的步驟。
pipeline {
agent any
parameters {
string(
name: 'GIT_BRANCH',
defaultValue: 'dev-01.30',
description: '請選擇部署的分支')
}
// 定義 GitLab 倉庫的 URL 和分支
environment {
GIT_URL = 'https://XXX/xxx.git'
}
stages {
stage('獲取最新代碼') {
steps {
script {
// 使用 params 對象獲取參數(shù)值
def branchName = params.GIT_BRANCH
echo "Building branch: ${branchName}"
// 使用 git 插件檢出倉庫的特定分支
checkout([
$class: 'GitSCM',
branches: [[name: "${branchName}"]],
doGenerateSubmoduleConfigurations: false,
extensions: [],
submoduleCfg: [],
userRemoteConfigs: [[
credentialsId: '211583e9-8ee1-4fa2-9edd-d43a963de8f2', // 在 Jenkins 憑據(jù)中定義的 GitLab 憑據(jù) ID
url: "${GIT_URL}"
]]
])
}
}
}
}
}
- 參數(shù)定義:通過參數(shù)部分,定義了一個名為GIT_BRANCH的參數(shù),它允許用戶在構建過程中選擇要構建的分支。默認情況下,分支被設置為dev-01.30,用戶可以選擇不同的分支。
- 環(huán)境變量定義:在環(huán)境部分,設置了GIT_URL變量,它是GitLab倉庫的URL。在腳本中,這個變量可以通過${GIT_URL}使用。
- 階段定義:在stages部分,定義了一個名為"獲取最新代碼"的階段。
- 步驟定義:在階段內(nèi),使用了script塊來執(zhí)行Groovy腳本。這個腳本首先獲取了GIT_BRANCH參數(shù)的值,然后使用Jenkins的Git插件檢出指定的分支。
- 檢出代碼:checkout步驟是用來從GitLab倉庫檢出代碼的關鍵部分。它使用了GitSCM類,并傳遞了相應的參數(shù),包括分支名、GitLab憑據(jù)等。
注意:獲取分支的憑證是一個 ID,這個憑證信息是在 Jenkins 系統(tǒng)配置中加的??梢园凑杖缦马撁媛窂教砑討{證:Dashboard->Manage Jenkins->Credentials->System->Add domain。也可以通過如下 URL 訪問
http://<你的服務器 IP>:8080/manage/credentials/store/system/
圖片
3.4 測試 pipeline 執(zhí)行
我們可以運行一下這個項目來測試 pipeline 代碼。運行結果如下圖所示,可以看到右側的階段視圖,整體耗時和每個步驟的耗時,以及每個步驟的成功與否都顯示出來了,非常直觀。
圖片
四、編譯代碼
本篇主要講解的是部署 Java 項目,所以編譯項目也是采用 Maven 打包的方式。在 pipeline 腳本中執(zhí)行 mvn 打包命令即可。
stage('編譯代碼') {
steps {
script {
echo "--------------- 步驟:開始編譯 --------------- "
bat 'mvn clean package'
echo "--------------- 步驟:編譯完成 --------------- "
}
}
}
核心代碼:bat 'mvn clean package'
因為我的 Jenkins 是部署在 Windows 機器上,所以執(zhí)行命令用的 windows 自帶的 bat 工具來執(zhí)行的。
關于 maven 工具的配置可以看之前寫的第二篇內(nèi)容:
喝杯咖啡,一鍵部署完成?。ńㄗh收藏)
五、上傳 JAR 包
編譯完成后,就可以將 Jenkins 工作空間的 JAR 包上傳到服務器的 temp 目錄下。
如果你想部署指定的某些微服務,可以通過傳參的方式來上傳和更新指定的微服務。原理圖如下所示:
圖片
5.1 支持勾選多個服務
為了實現(xiàn)可以選擇部署哪些微服務,需要安裝一個多選插件:Extended Choice Parameter。
圖片
Extended Choice Parameter 插件
接下來是編寫的參數(shù)配置代碼:
parameters {
extendedChoice (
defaultValue: 'All',
description: '需要部署的微服務',
multiSelectDelimiter: ',',
name: 'SERVICE_NAME',
quoteValue: false,
saveJSONParameterToFile: false,
type: 'PT_CHECKBOX',
value:'All, passjava-account, passjava-file',
visibleItemCount: 10
)
}
- defaultValue: 參數(shù)的默認值。在這里,默認值為 'All'。
- description: 參數(shù)的描述或提示。這里描述為 '需要部署的微服務',表示選擇需要部署的微服務。
- multiSelectDelimiter: 多選時的分隔符。這里設置為 ',',表示使用逗號作為分隔符。
- name: 參數(shù)的名稱。這里是 'SERVICE_NAME'。
- quoteValue: 確定是否對值加上引號。這里設置為 false,表示不加引號。
- saveJSONParameterToFile: 是否將 JSON 參數(shù)保存到文件。
- type: 參數(shù)的類型。這里是 'PT_CHECKBOX',表示復選框類型。
- value: 可選的值列表。在這里,可選的值有 'All'、'passjava-account'、'passjava-file'。
- visibleItemCount: 可見的選項數(shù)量。這里設置為 10。
配置保存后并運行一次后,就可以在 pipeline 中看到配置選項:
圖片
實現(xiàn)的效果如下圖右下角所示,可以支持多選。
圖片
5.2 上傳 JAR 包
上傳需要使用 sshPublisher 插件,在第二篇文章中已介紹了。
下面上傳代碼的作用是遍歷 filesToCopy 列表中的文件,然后通過 SSH 將這些文件上傳到遠程服務器的指定目錄中。
filesToCopy.eachWithIndex { file, index ->
echo "開始上傳 JAR 包 ${file} ..."
sshPublisher(
failOnError: true,
publishers: [
sshPublisherDesc(
configName: "${SSH_URL}",
verbose: true,
transfers: [
sshTransfer(
execCommand: '',
execTimeout: 120000,
flatten: false,
makeEmptyDirs: false,
noDefaultExcludes: false,
patternSeparator: '[, ]+',
remoteDirectory: 'apps/temp/',
remoteDirectorySDF: false,
removePrefix: removePrefixs[index],
sourceFiles: file
)
]
)
]
)
echo "完成上傳 JAR 包 ${file}"
}
- filesToCopy.eachWithIndex { file, index -> ... }: 這是一個 Groovy 中的迭代循環(huán),對列表 filesToCopy 中的每個文件執(zhí)行相應的操作。file 是當前迭代的文件,index 是該文件在列表中的索引。
- echo "開始上傳 JAR 包 ${file} ...": 這是一個打印語句,用于輸出日志,顯示當前正在上傳的 JAR 包的文件名。
- sshPublisher { ... }: 這是一個 SSH 發(fā)布器,用于通過 SSH 連接到遠程服務器并執(zhí)行相應的操作。
- failOnError: true: 如果 SSH 連接或傳輸過程中出現(xiàn)錯誤,將會終止流水線執(zhí)行。
- configName: "${SSH_URL}": 這是 SSH 配置的名稱,${SSH_URL} 是一個變量,指定 SSH 連接的配置信息。
- transfers: [sshTransfer { ... }]: 這是 SSH 傳輸操作的列表,包含了將要執(zhí)行的文件傳輸任務。
- remoteDirectory: 'apps/temp/': 遠程服務器上的目標目錄,這里設置為 apps/temp/,表示將文件上傳到遠程服務器的 apps/temp/ 目錄下。
- removePrefix: removePrefixs[index]: 這是一個用于移除文件路徑前綴的設置,根據(jù)當前文件在列表中的索引,從相應的 removePrefixs 數(shù)組中獲取相應的前綴進行移除。
- sourceFiles: file: 要傳輸?shù)脑次募?,即當前迭代的文件?/li>
- echo "完成上傳 JAR 包 ${file}": 這是另一個打印語句,用于輸出日志,表示當前文件的上傳已經(jīng)完成。
filesToCopy 就是通過用戶勾選的服務名轉成了對應的本地 JAR 包路徑。
switch(service) {
case 'passjava-account':
filesToCopy.add("passjava-modules/passjava-module-account/passjava-module-account-core/target/passjava-account.jar")
removePrefixs.add("passjava-modules/passjava-module-account/passjava-module-account-core/target")
break
case ...
}
六、備份服務器 JAR 包
備份的思路:逐個處理 serviceNameList 中的服務名稱,然后通過 SSH 連接到遠程服務器執(zhí)行備份操作。serviceNameList 就是用戶勾選的服務名的集合。原理圖如下所示:
備份服務器 JAR 包
核心代碼如下:
serviceNameList.each { service ->
echo "開始備份微服務 ${service} 包"
sshPublisher(
failOnError: true,
publishers: [
sshPublisherDesc(
configName: "${SSH_URL}",
verbose: true,
transfers: [
sshTransfer(
execCommand: "mkdir -p /nfs-data/wukong/apps/bak/${timestamp} && cd /nfs-data/wukong/apps && mv ${service}.jar ./bak/${timestamp}/${service}-${timestamp}.jar"
)
]
)
]
)
- serviceNameList.each { service -> ... }: 這是一個 Groovy 中的迭代循環(huán),對列表 serviceNameList 中的每個服務名稱執(zhí)行備份操作。service 是當前迭代的服務名稱。
- echo "開始備份微服務 ${service} 包": 這是一個打印語句,用于輸出日志,顯示當前正在備份的微服務的名稱。
- sshPublisher { ... }: 這是一個 SSH 發(fā)布器,用于通過 SSH 連接到遠程服務器并執(zhí)行相應的操作。
- failOnError: true: 如果 SSH 連接或執(zhí)行過程中出現(xiàn)錯誤,將會終止流水線執(zhí)行。
- configName: "${SSH_URL}": 這是 SSH 配置的名稱,${SSH_URL} 是一個變量,用于指定 SSH 連接的配置信息。
- transfers: [sshTransfer { ... }]: 這是 SSH 傳輸操作的列表,包含了將要執(zhí)行的文件傳輸任務。
- execCommand: "...": 這是要在遠程服務器上執(zhí)行的命令。在這里,使用了 mkdir 命令創(chuàng)建備份目錄,然后將當前服務的 JAR 包移動到備份目錄下,并加上時間戳作為文件名,以實現(xiàn)備份。
這段代碼的作用是遍歷 serviceNameList 列表中的服務名稱,然后通過 SSH 連接到遠程服務器執(zhí)行備份操作,將每個服務的 JAR 包移動到指定的備份目錄,并根據(jù)時間戳進行命名。
七、更新 JAR 包
更新最新的 JAR 包就是將最新的 JAR 包放到對應的容器映射的目錄,后面重啟容器的時候,就能用最新的 JAR 包啟動了。原理圖如下所示:
更新 JAR 包
serviceNameList.eachWithIndex { service, index ->
echo "開始更新第 ${index + 1} 個 JAR 包,/nfs-data/wukong/apps/temp/${service}.jar ..."
sshPublisher(
failOnError: true,
publishers: [
sshPublisherDesc(
configName: "${SSH_URL}",
verbose: true,
transfers: [
sshTransfer(
execCommand: "cd /nfs-data/wukong/apps && mv -f ./temp/${service}.jar ${service}.jar",
execTimeout: 120000
)
]
)
]
)
echo "----- 完成更新 JAR 包 -----"
}
- serviceNameList.eachWithIndex { service, index -> ... }: 這是一個 Groovy 中的迭代循環(huán),對列表 serviceNameList 中的每個服務名稱執(zhí)行更新操作。service 是當前迭代的服務名稱,index 是該服務在列表中的索引。
- echo "開始更新第 ${index + 1} 個 JAR 包,/nfs-data/wukong/apps/temp/${service}.jar ...": 這是一個打印語句,用于輸出日志,顯示當前正在更新的 JAR 包的名稱及路徑。
- sshPublisher { ... }: 這是一個 SSH 發(fā)布器,用于通過 SSH 連接到遠程服務器并執(zhí)行相應的操作。
- failOnError: true: 如果 SSH 連接或執(zhí)行過程中出現(xiàn)錯誤,將會終止流水線執(zhí)行。
- configName: "${SSH_URL}": 這是 SSH 配置的名稱,${SSH_URL} 是一個變量,用于指定 SSH 連接的配置信息。
- transfers: [sshTransfer { ... }]: 這是 SSH 傳輸操作的列表,包含了將要執(zhí)行的文件傳輸任務。
- execCommand: "cd /nfs-data/wukong/apps && mv -f ./temp/${service}.jar ${service}.jar": 這是要在遠程服務器上執(zhí)行的命令。在這里,使用 mv 命令將位于 /nfs-data/wukong/apps/temp/ 目錄下的 ${service}.jar 移動到 /nfs-data/wukong/apps/ 目錄,并覆蓋同名的文件。
- execTimeout: 120000: 這是執(zhí)行命令的超時時間,單位是毫秒。在這里,設置為 120000,即 120 秒。
- echo "----- 完成更新 JAR 包 -----": 這是另一個打印語句,用于輸出日志,表示當前 JAR 包的更新操作已經(jīng)完成。
這段代碼的作用是遍歷 serviceNameList 列表中的服務名稱,然后通過 SSH 連接到遠程服務器執(zhí)行更新操作,將每個服務在 /nfs-data/wukong/apps/temp/ 目錄下的 JAR 包移動到對應的位置,完成更新。
八、啟動多個服務
啟動服務就是將 docker swarm 管理的服務重啟下,原理圖如下所示:
圖片
后端項目使用 Docker Swarm 部署的,重啟服務的命令如下:
sudo docker service update --force <服務名>
我們可以編寫遠程執(zhí)行這行命令的代碼,pipeline 核心代碼如下:
serviceNameList.eachWithIndex { service, index ->
echo "開始重啟第 ${index + 1} 個微服務,${service} ..."
sshPublisher(
failOnError: true,
publishers: [
sshPublisherDesc(
configName: "${SSH_URL}",
verbose: true,
transfers: [
sshTransfer(
execCommand: "sudo docker service update --force ${commands[service]}",
execTimeout: 120000
)
]
)
]
)
echo "----- 完成重啟微服務 -----"
}
這段代碼的作用是遍歷 serviceNameList 列表中的服務名稱,然后通過 SSH 連接到遠程服務器執(zhí)行重啟操作,完成微服務的重啟。
最后整個執(zhí)行結果如下圖所示:總共耗時 3min41s。
圖片
九、總結
通過本篇的實戰(zhàn)內(nèi)容,我們學習到了通過編寫 pipeline 代碼來實現(xiàn)部署后端項目。推薦大家都用 pipeline 來部署項目,好處是更加靈活。
另外本篇還沒有對 Jenkins pipeline 的版本管理,我們其實可以將 pipeline 代碼作為一個文件上傳到 Gitlab,然后通過 Jenkins 拉取最新的 jenkins pipeline 文件來執(zhí)行部署,這樣更便于管理 pipeline 文件。