本學習筆記參考《Jenkins 2.x實踐指南》。
1. Jenkins 簡介
Jenkins 是一款自動化的任務執行工具。通常用於持續整合/持續交付領域。
可以通過介面或Jenkinsfile告訴Jenkins執行什麼任務,何時執行。理論上,我們可以讓它執行任何任務,但是通常只應用於持續整合和持續交付。
持續整合將軟體生產過程從手工模式帶入流水線模式,軟體生產的某個環節都對應流水線上的每個環節。
流水線模式還能幫助我們將知識固化到自動化流水線中,在一定程度上解決了知識被人帶走的問題。(手工模式對於整個軟體生產流程很少有人全部知道,使用流水線模式整個軟體生產的過程都以pipeline as code的形式保留下來,每個人都能看)
2. pipeline介紹
什麼是pipeline
- 學術的講:軟體從程式碼版本庫到使用者手中這一過程的自動化表現;
- 通俗的講:主要是指程式碼的自動構建、測試、打包和部署等過程;
Jenkins 1.x只能通過介面手動操作來“描述”部署流水線。Jenkins 2.x終於支援pipeline as code了,可以通過“程式碼”來描述部署流水線。
- 更好地版本化:將pipeline提交到軟體版本庫中進行版本控制。
- 更好地協作:pipeline的每次修改對所有人都是可見的。除此之外,還可以對pipeline進行程式碼審查。
- 更好的重用性:手動操作沒法重用,但是程式碼可以重用。
什麼是JenkinsFile
Jenkinsfile就是一個文字檔案,也就是部署流水線概念在Jenkins中的表現形式。像Dockerfile之於Docker。所有部署流水線的邏輯都寫在Jenkinsfile中。
JK File 定義了流水線的部署邏輯
Jenkins預設不支援pipeline,需要安裝pipeline外掛。
pipeline語法的支援
pipeline支援以下兩種語法:
- Groovy語法(node為根節點的是指令碼式語法);
- 宣告式語法(2.5版本開始支援,pipeline為根節點的是宣告式語法,推薦使用)
3. pipeline 語法
Groovy語法
這裡Groovy語法不是重點,但是還是需要了解必要的知識。如果想要詳細瞭解,可以參考Groovy教程
// 一般用def定義變數
// 語句末尾不新增分號
def x = "abs"
// Groovy中的方法呼叫可以省略括號
println x
println "chensongxia"
// 支援命名引數
def m1(String givenName, String familyName) {
return givenName + " " + familyName
}
// 呼叫時可以這樣
println m1("chen", "songxia")
s1 = m1 familyName = "ru", givenName = "zhao"
println s1
// 支援預設引數
def m2(String name = "hi") {
return name
}
// 括號不能省略
println m2()
println m2("chen")
// 支援單引號、雙引號。雙引號支援插值,單引號不支援
println '------支援單引號、雙引號。雙引號支援插值,單引號不支援 ---------'
println ''
def name = "world";
println "hello ${name}"
println 'hello ${name}'
// 支援三引號。三引號分為三單引號和三雙引號。它們都支援換行,區別在於只有三雙引號支援插值。
println ''
println '-----支援三引號。三引號分為三單引號和三雙引號。它們都支援換行,區別在於只有三雙引號支援插值。-----'
println """line one
line two
line three
${name}
"""
println '''line one
line two
line three
${name}
'''
// 支援閉包
println '-------支援閉包---------'
def codeBlock = { println "hello closure" }
// 閉包可以被當成函式直接呼叫
codeBlock();
// 閉包可以被當成一個引數傳遞給另外一個方法
def pipeline(closure){
closure()
}
pipeline(codeBlock)
def stage(String name,closure){
println name
closure()
}
stage("jenkins",codeBlock)
pipeline組成
Jenkins pipeline其實就是基於Groovy語言實現的一種DSL(領域特定語言),用於描述整條流水線是如何進行的。流水線的內容包括執行編譯、打包、測試、輸出測試報告等步驟。
從軟體版本控制庫到使用者手中這一過程可以分成很多階段,每個階段只專注處理一件事情,而這件事情又是通過多個步驟來完成的,這就是軟體工程的pipeline。Jenkins對這個過程進行抽象,設計出一個基本的pipeline結構。
pipeline {
agent any
stages {
stage('Hello') {
steps {
echo 'Hello World'
}
}
}
}
- pipeline:代表整條流水線,包含整條流水線的邏輯。
- stage部分:階段,代表流水線的階段。每個階段都必須有名稱。本例中,build就是此階段的名稱。
- stages部分:流水線中多個stage的容器。stages部分至少包含一個stage。
- steps部分:代表階段中的一個或多個具體步驟(step)的容器。steps部分至少包含一個步驟,本例中,echo就是一個步驟。在一個stage中有且只有一個steps。
- agent部分:指定流水線的執行位置(Jenkins agent)。流水線中的每個階段都必須在某個地方(物理機、虛擬機器或Docker容器)執行,agent部分即指定具體在哪裡執行。
以上每一個部分(section)都是必需的,少一個,Jenkins都會報錯。
Jenkins的很多外掛可以被直接拿來當作一個步驟使用。只要安裝了這些適配了Jenkins pipeline的外掛,就可以使用其提供的pipeline步驟。
Jenkins官方還提供了pipeline步驟參考文件(https://jenkins.io/doc/pipeline/steps/)。
post部分
根據pipeline或階段的完成狀態,post部分分成多種條件塊,包括:
• always:不論當前完成狀態是什麼,都執行。
• changed:只要當前完成狀態與上一次完成狀態不同就執行。
• fixed:上一次完成狀態為失敗或不穩定(unstable),當前完成狀態為成功時執行。
• regression:上一次完成狀態為成功,當前完成狀態為失敗、不穩定或中止(aborted)時執行。
• aborted:當前執行結果是中止狀態時(一般為人為中止)執行。
• failure:當前完成狀態為失敗時執行。
• success:當前完成狀態為成功時執行。
• unstable:當前完成狀態為不穩定時執行。
• cleanup:清理條件塊。不論當前完成狀態是什麼,在其他所有條件塊執行完成後都執行。post部分可以同時包含多種條件塊。
下面給出一個列子:
pipeline支援的指令
基本結構滿足不了現實多變的需求。所以,Jenkins pipeline通過各種指令(directive)來豐富自己。指令可以被理解為對Jenkins pipeline基本結構的補充。
Jenkins pipeline支援的指令有:
• environment:用於設定環境變數,可定義在stage或pipeline部分。
• tools:可定義在pipeline或stage部分。它會自動下載並安裝我們指定的工具,並將其加入PATH變數中。
• input:定義在stage部分,會暫停pipeline,提示你輸入內容。
• options:用於配置Jenkins pipeline本身的選項,比如options {retry(3)}指當pipeline失敗時再重試2次。options指令可定義在stage或pipeline部分。
• parallel:並行執行多個step。在pipeline外掛1.2版本後,parallel開始支援對多個階段進行並行執行。
• parameters:與input不同,parameters是執行pipeline前傳入的一些引數。
• triggers:用於定義執行pipeline的觸發器。
• when:當滿足when定義的條件時,階段才執行。
在使用指令時,需要注意的是每個指令都有自己的“作用域”。如果指令使用的位置不正確,Jenkins將會報錯。
配置pipeline本身(option指令)(用到的時候可以來參考這塊)
options指令用於配置整個Jenkins pipeline本身的選項。根據具體的選項不同,可以將其放在pipeline塊或stage塊中。以下例子若沒有特別說明,options被放在pipeline塊中。
• buildDiscarder:儲存最近歷史構建記錄的數量。當pipeline執行完成後,會在硬碟上儲存製品和構建執行日誌,如果長時間不清理會佔用大量空間,設定此選項後會自動清理。
參考這個文件:https://weread.qq.com/web/reader/12f320007184556612f32b6k1ff325f02181ff1de7742fc
這節比較有用,可以參考下
在宣告式pipeline中使用指令碼
Jenkins pipeline專門提供了一個script步驟,你能在script步驟中像寫程式碼一樣寫pipeline邏輯。
pipeline {
agent any
stages {
stage('Hello') {
steps {
script{
def s = "Hello World"
println s
}
echo 'Hello World'
}
}
}
}
可以看出,在script塊中的其實就是Groovy程式碼。大多數時候,我們是不需要使用script步驟的。如果在script步驟中寫了大量的邏輯,則說明你應該把這些邏輯拆分到不同的階段,或者放到共享庫中。共享庫是一種擴充套件Jenkins pipeline的技術
pipeline內建基礎步驟(重要)
Jenkins 的pipeline內建了很多基礎步驟,我們可以直接使用。
參考章節:https://weread.qq.com/web/reader/12f320007184556612f32b6k4e73277021a4e732ced3b55
4. 環境變數與構建工具
環境變數
環境變數可以被看作是pipeline與Jenkins互動的媒介。比如,可以在pipeline中通過BUILD_NUMBER變數知道構建任務的當前構建次數。環境變數可以分為Jenkins內建變數和自定義變數。
1. 內建環境變數
使用方式:
小技巧:在除錯pipeline時,可以在pipeline的開始階段加一句:sh 'printenv',將env變數的屬性值列印出來。這樣可以幫助我們避免不少問題。
不推薦方法三,因為出現變數衝突時,非常難查問題。
Jenkins的控制檯也對內建的環境變數做了說明,可以自己開啟看下。
2. 自定義環境變數
environment指令可以在pipeline中定義,代表變數作用域為整個pipeline;也可以在stage中定義,代表變數只在該階段有效。
pipeline {
agent any
environment {
DEVOPS_IP = '10.50.1.50'
}
stages {
stage('Hello') {
environment {
NAME = 'Hello World...'
}
steps {
echo "${DEVOPS_IP}"
echo "${NAME}"
sh 'printenv'
}
}
}
}
但是這些變數都不是跨pipeline的,比如pipeline a訪問不到pipeline b的變數。在pipeline之間共享變數可以通過引數化pipeline來實現。
在實際工作中,還會遇到一個環境變數引用另一個環境變數的情況。在environment中可以這樣定義:
在定義變數的時候,為了防止變數衝突,建議將變數加上字首(自定義的環境變數會覆蓋預設的環境變數)。
3. 自定義全域性環境變數
env中的變數都是Jenkins內建的,或者是與具體pipeline相關的。有時候,我們需要定義一些全域性的跨pipeline的自定義變數。
進入Manage Jenkins→Configure System→Global properties頁,勾選“Environment variables”核取方塊,單擊“Add”按鈕,在輸入框中輸入變數名和變數值即可。
自定義全域性環境變數會被加入 env 屬性列表中,所以,使用自定義全域性環境變數與使用Jenkins內建變數的方法無異:${env.g name}。
構建工具
構建每一步都是可重複的,儘量與機器無關。所以,構建工具的安裝、設定也應該是自動化的、可重複的。
雖然Jenkins只負責執行構建工具提供的命令,本身沒有實現任何構建功能,但是它提供了構建工具的自動安裝功能。
1. tool指令
tools指令能幫助我們自動下載並安裝所指定的構建工具,並將其加入PATH變數中。這樣,我們就可以在sh步驟裡直接使用了。但在agent none的情況下不會生效。通過命令sh "printenv",可以看到環境變數的所有值。
tools指令預設支援3種工具:JDK、Maven、Gradle。通過安裝外掛,tools指令還可以支援更多的工具。
配置maven
進入Manage Jenkins→Global Tool Configuration→Maven頁,設定如下圖所示。
pipeline {
agent any
environment {
DEVOPS_IP = '10.50.1.50'
}
tools {
maven 'maven.3.6.3'
}
stages {
stage('Hello') {
environment {
NAME = 'Hello World...'
}
steps {
echo "${DEVOPS_IP}"
echo "${NAME}"
sh 'printenv'
sh 'java -version'
sh 'mvn -v'
}
}
}
}
這樣,當執行到tools指令時,Jenkins會自動下載並安裝Maven。將mvn命令加入環境變數中,可以使我們在pipeline中直接執行mvn命令。
Config File Provider外掛可以設定全域性的setting設定。
利用環境變數支援更多的構建工具
如果想讓Jenkins支援更多的構建工具,也是同樣的做法:在Jenkins agent上安裝構建工具,並記錄下它的可執行命令的目錄,然後在需要使用此命令的Jenkinspipeline的PATH環境變數中加入該可執行命令的目錄。
利用tools作用域實現多版本編譯
在實際工作中,有時需要對同一份原始碼使用多個版本的編譯器進行編譯。tools指令除了支援pipeline作用域,還支援stage作用域。
在列印出來的日誌中,會發現每個stage下的JAVA_HOME變數的值都不一樣。
5. 程式碼質量
靜態程式碼分析
使用maven外掛在某個stage加入靜態程式碼分析。(靜態程式碼掃描通常被安排在編譯之後)
單元測試
效能測試
Jenkins整合SonarQube(有時間將這個環境搭建起來,將分析報告推送到GitLab)
Allure測試報告(美化測試報告,不是重點)
6. 觸發 pipeline 執行
自動化是指pipeline按照一定的規則自動執行。而這些規則被稱為pipeline觸發條件。
對於pipeline觸發條件,可以從兩個維度來區分:時間觸發和事件觸發。接下來,我們從這兩個維度分別介紹。
時間觸發
時間觸發是指定義一個時間,時間到了就觸發pipeline執行。在Jenkins pipeline中使用trigger指令來定義時間觸發。
tigger指令只能被定義在pipeline塊下,Jenkins內建支援cron、pollSCM,upstream三種方式。其他方式可以通過外掛來實現。
使用場景:一些比較重的任務可以放到凌晨再跑。
事件觸發
事件觸發就是發生了某個事件就觸發pipeline執行。這個事件可以是你能想到的任何事件。比如手動在介面上觸發、其他job主動觸發、HTTP API Webhook觸發等。
常用的有下面幾種:
- 由上游任務觸發:upstream(其他job來觸發)
- GitLab通知觸發
- 在pipeline中實現GitLab trigger
將構建狀態資訊推送到GitLab
Generic Webhook Trigger
安裝 Generic Webhook Trigger 外掛(下文使用 GWT 簡稱)後,Jenkins 會暴露一個 API:<JENKINS URL>/generic-webhook-trigger/invoke,即由GWT外掛來處理此API的請求。
如何處理呢?GWT外掛接收到JSON或XML的HTTP POST請求後,根據我們配置的規則決定觸發哪個Jenkins專案。基本原理就這麼簡單。
7. 多分支構建
在實際專案中,往往需要多分支同時進行開發。如果為每個分支都分別建立一個Jenkins專案,實在有些多餘。
不同分支釋出不同環境
when指令的用法
when指令允許pipeline根據給定的條件,決定是否執行階段內的步驟。when指令必須至少包含一個條件。when指令除了支援branch判斷條件,還支援多種判斷條件。
這塊內容非常有用,可以經常參考。
https://weread.qq.com/web/reader/12f320007184556612f32b6k9a132c802349a1158154a83
GitLab trigger對多分支pipeline的支援
Generic Webhook Trigger外掛在多分支pipeline場景下的應用
在多分支pipeline場景下,我們希望觸發某個分支的構建執行。
8. 引數化pipeline
什麼是引數化pipeline
引數化pipeline是指可以通過傳參來決定pipeline的行為。
使用parameters指令
parameters {
string defaultValue: 'none', description: '字串', name: 'D_ENV', trim: true
text defaultValue: 'a\nb\nc\n', description: '文字', name: 'D_TEXT'
choice choices: 'a\nb\nc\n', description: '選一個', name: 'D_CHOICE'
booleanParam defaultValue: false, description: '布林值引數', name: 'FLAG'
password name: 'PASSWORD',defaultValue:'SECRET',description: 'password'
}
參考這個文章:https://zhuanlan.zhihu.com/p/80622378。
由另外一個pipeline傳引數
可以在一個pipeline中“呼叫”另一個pipeline。在Jenkins pipeline中可以使用build步驟實現此功能。build步驟是pipeline外掛的一個元件,所以不需要另外安裝外掛,可以直接使用。
build步驟其實也是一種觸發pipeline執行的方式,它與triggers指令中的upstream方式有兩個區別:
(1) build步驟是由上游pipeline使用的,而upstream方式是由下游pipeline使用的。
(2) build步驟是可以帶引數的,而upstream方式只是被動觸發,並且沒有帶引數。
使用Conditional BuildStep外掛處理複雜的判斷邏輯
Conditional BuildStep外掛(https://plugins.jenkins.io/conditional-buildstep)可以讓我們像使用when指令一樣進行條件判斷。以下程式碼就是安裝Conditional BuildStep外掛後的寫法。
使用input步驟
執行input步驟會暫停pipeline,直到使用者輸入引數。這是一種特殊的引數化pipeline的方法。我們可以利用input步驟實現以下兩種場景:
(1)實現簡易的審批流程。例如,pipeline暫停在部署前的階段,由負責人點選確認後,才能部署。
(2)實現手動測試階段。在pipeline中增加一個手動測試階段,該階段中只有一個input步驟,當手動測試通過後,測試人員才可以通過這個input步驟。
input步驟可以與timeout步驟實現超時自動中止pipeline,防止無限等待。以下pipeline一小時後不處理就自動中止。
9. 憑證管理
為什麼要憑證管理
在Jenkinsfile或部署指令碼中使用明文密碼會造成安全隱患。但是為什麼還頻繁出現明文密碼被上傳到GitHub上的情況呢
(1)程式設計師或運維人員不知道如何保護密碼。
(2)管理者沒有足夠重視,否則會給更多的時間讓程式設計師或運維人員想辦法隱藏明文密碼。
憑證是什麼
比如使用SSH登入遠端機器時,使用者名稱和密碼或SSH key就是憑證。而這些憑證不可能以明文寫在Jenkinsfile中。Jenkins憑證管理指的就是對這些憑證進行管理。
為了最大限度地提高安全性,在Jenkins master節點上對憑證進行加密儲存(通過Jenkins例項ID加密),只有通過它們的憑證ID才能在pipeline中使用,並且限制了將證照從一個Jenkins例項複製到另一個Jenkins例項的能力。
也因為所有的憑證都被儲存在Jenkins master上,所以在Jenkins master上最好不要執行任務,以免被pipeline非法讀取出來。
建立憑證
• Kind:選擇憑證型別。
• Scope:憑證的作用域。有兩種作用域:
◦ Global,全域性作用域。如果憑證用於pipeline,則使用此種作用域。
◦ System,如果憑證用於Jenkins本身的系統管理,例如電子郵件身份驗證、代理連線等,則使用此種作用域。
• ID:在pipeline使用憑證的唯一標識。
新增憑證後,安裝Credentials Binding Plugin外掛(https://plugins.jenkins.io/credentials-binding),通過其提供的withCredentials步驟就可以在pipeline中使用憑證了。
常用憑證型別
- Secret text
- Username with password
- Secret file
- SSH Username with private key
優雅的使用憑證
宣告式pipeline提供了credentials helper方法(只能在environment指令中使用)來簡化憑證的使用。以下是使用方法。
https://weread.qq.com/web/reader/12f320007184556612f32b6k14b3246024514bfa6bb1534
使用HashiCorp Vault(加強版的憑證管理工具)
在Jenkins日誌中隱藏敏感資訊
10. 製品管理
常見的製品管理倉庫
- Nexus
- Artifactory
版本號管理
11.視覺化構建及檢視
Green Balls外掛
這個外掛的功能是讓構建成功的pipeline程式設計綠色。
Build Monitor View外掛
Build Monitor View外掛(https://plugins.jenkins.io/build-monitor-plugin)可以將Jenkins專案以一塊“看板”的形式呈現。
檢視
使用檢視可以對pipeline進行分組管理。
12.自動化部署
部署和釋出的區別
部署是指將應用程式放到對應的伺服器上。
釋出是指使用者能訪問到新的功能點。
Jenkins整合Ansible實現自動化部署
Ansible採用了與Puppet、Chef不一樣的解決方案,不需要在受控機器上安裝額外的客戶端軟體。原因是Ansible使用的是SSH協議與受控機器進行通訊的,一般伺服器預設有SSH服務。Ansible也因此被稱為agentless(去客戶端的)。
Puppet和Chef都自己做了一套DSL,而Ansible使用YAML格式作為自己的DSL格式。
13. 通知
郵件通知
釘釘通知
Http請求通知
14. 分散式構建與並行構建
Jenkins的架構
Jenkins採用的是“master+agent”架構(有時也稱為“master+slave”架構)。Jenkins master負責提供介面、處理HTTP請求及管理構建環境;構建的執行則由Jenkins agent負責(早期,agent也被稱為slave。目前還有一些外掛沿用slave的概念)。
基於這樣的架構,只需要增加agent就可以輕鬆支援更多的專案同時執行。這種方式稱為Jenkins agent的橫向擴容。
對於Jenkins master,存在單節點問題是顯而易見的,但是目前還沒有很好的解決方案。
在學習Jenkins的過程中,發現各種文件中摻雜著node、executor、agent、slave4個術語,新手很容易被它們弄得一頭霧水。它們分別是什麼意思呢?
• node:節點,指包含Jenkins環境及有能力執行專案的機器。master和agent都被認為是節點。
• agent:代理,在概念上指的是相對於Jenkins master的一種角色,實際上是指執行在機器或容器中的一個程式,它會連線上Jenkins master,並執行Jenkinsmaster分配給它的任務。
• slave:“傀儡”,與agent表達的是一個東西,只是叫法不同。
• executor:執行器,是真正執行專案的單元。一個執行器可以被理解為一個單獨的程式(事實上是執行緒)。在一個節點上可以執行多個執行器。
增加agent
https://weread.qq.com/web/reader/12f320007184556612f32b6k697324802676974ce5aceab
如何使用agent
agent部分描述的是整個pipeline或在特定階段執行任務時所在的agent。換句話說,Jenkins master根據此agent部分決定將任務分配到哪個agent上執行。agent部分必須在pipeline塊內的頂層定義,而stage塊內的定義是可選的。
agent any告訴Jenkins master任何可用的agent都可以執行。
agent部分的定義可以放在階段中,用於指定該stage執行時的agent。
通過標籤指定agent
customWorkspace,自定義工作目錄。
不分配具體的agent
如果希望每個stage都執行在指定的agent中,那麼pipeline就不需要指定agent了。
在預設情況下,階段內所有的程式碼都將在指定的Jenkins agent上執行。when指令提供了一個beforeAgent選項,當它的值為true時,只有符合when條件時才會進入該Jenkins agent。這樣就可以避免沒有必要的工作空間的分配,也就不需要等待可用的Jenkins agent了。
將任務構建交給Docker
Docker拉取映象時,預設是從Docker官方中心倉庫拉取的。那麼如何實現從私有倉庫拉取呢?比如在“製品管理”章節中在Nexus上建立的私有倉庫。
Docker外掛為我們提供了介面操作,具體步驟如下:
進入Manage Jenkins→Configure System頁面,找到“Pipeline ModelDefinition”部分
Docker Label:當 pipeline 中的 agent 部分沒有指定 label 選項時,就會使用此配置。如docker {image'maven:3-alpine'}。
• Docker registry URL:Docker私有倉庫地址。
• Registry credentials:登入Docker私有倉庫的憑證。
並行構建
可以用於優化構建速度。
一些名詞
- pipeline as code