Jenkins Pipeline 介紹
要實(shí)現(xiàn)在 Jenkins 中的構(gòu)建工作,可以有多種方式,這里采用比較常用的 Pipeline 這種方式。Pipeline,簡(jiǎn)單來(lái)說(shuō),就是一套運(yùn)行在 Jenkins 上的工作流框架,將原來(lái)獨(dú)立運(yùn)行于單個(gè)或者多個(gè)節(jié)點(diǎn)的任務(wù)連接起來(lái),實(shí)現(xiàn)單個(gè)任務(wù)難以完成的復(fù)雜流程編排和可視化的工作。
Jenkins Pipeline 有幾個(gè)核心概念:
- Node:節(jié)點(diǎn),一個(gè) Node 就是一個(gè) Jenkins 節(jié)點(diǎn),Master 或者 Agent,是執(zhí)行 Step 的具體運(yùn)行環(huán)境,比如我們之前動(dòng)態(tài)運(yùn)行的 Jenkins Slave 就是一個(gè) Node 節(jié)點(diǎn)
- Stage:階段,一個(gè) Pipeline 可以劃分為若干個(gè) Stage,每個(gè) Stage 代表一組操作,比如:Build、Test、Deploy,Stage 是一個(gè)邏輯分組的概念,可以跨多個(gè) Node
- Step:步驟,Step 是最基本的操作單元,可以是打印一句話,也可以是構(gòu)建一個(gè) Docker 鏡像,由各類 Jenkins 插件提供,比如命令:sh 'make',就相當(dāng)于我們平時(shí) shell 終端中執(zhí)行 make 命令一樣。
那么如何創(chuàng)建 Jenkins Pipline 呢?
- Pipeline 腳本是由 Groovy 語(yǔ)言實(shí)現(xiàn)的,但是沒必要單獨(dú)去學(xué)習(xí) Groovy,當(dāng)然你會(huì)的話最好
- Pipeline 支持兩種語(yǔ)法:Declarative(聲明式)和 Scripted Pipeline(腳本式)語(yǔ)法
- Pipeline 也有兩種創(chuàng)建方法:可以直接在 Jenkins 的 Web UI 界面中輸入腳本;也可以通過(guò)創(chuàng)建一個(gè) Jenkinsfile 腳本文件放入項(xiàng)目源碼庫(kù)中
- 一般我們都推薦在 Jenkins 中直接從源代碼控制(SCMD)中直接載入 Jenkinsfile Pipeline 這種方法
創(chuàng)建一個(gè)簡(jiǎn)單的 Pipeline
創(chuàng)建一個(gè)簡(jiǎn)單的 Pipeline,直接在 Jenkins 的 Web UI 界面中輸入腳本運(yùn)行。
新建 Job:在 Web UI 中點(diǎn)擊 New Item -> 輸入名稱:pipeline-demo -> 選擇下面的 Pipeline -> 點(diǎn)擊 OK
-
配置:在最下方的 Pipeline 區(qū)域輸入如下 Script 腳本,然后點(diǎn)擊保存。
node { stage('Clone') { echo "1.Clone Stage" } stage('Test') { echo "2.Test Stage" } stage('Build') { echo "3.Build Stage" } stage('Deploy') { echo "4. Deploy Stage" } } 構(gòu)建:點(diǎn)擊左側(cè)區(qū)域的 Build Now,可以看到 Job 開始構(gòu)建了
隔一會(huì)兒,構(gòu)建完成,可以點(diǎn)擊左側(cè)區(qū)域的 Console Output,可以看到如下輸出信息:

可以看到上面Pipeline 腳本中的4條輸出語(yǔ)句都打印出來(lái)了,證明是符合預(yù)期的。
Pipeline 語(yǔ)法鏈接Pipeline Syntax中進(jìn)行查看,這里有很多關(guān)于 Pipeline 語(yǔ)法的介紹,也可以自動(dòng)幫我們生成一些腳本。
在 Slave 中構(gòu)建任務(wù)
上面創(chuàng)建了一個(gè)簡(jiǎn)單的 Pipeline 任務(wù),但是可以看到這個(gè)任務(wù)并沒有在 Jenkins 的 Slave 中運(yùn)行,那么如何讓任務(wù)跑在 Slave 中呢?之前在添加 Slave Pod 的時(shí)候,需要用到這個(gè) label,我們重新編輯上面創(chuàng)建的 Pipeline 腳本,給 node 添加一個(gè) label 屬性,如下:
node('hwzx-cmp') {
stage('Clone') {
echo "1.Clone Stage"
}
stage('Test') {
echo "2.Test Stage"
}
stage('Build') {
echo "3.Build Stage"
}
stage('Deploy') {
echo "4\. Deploy Stage"
}
}
這里只是給 node 添加了一個(gè) hwzx-cmp 這樣的一個(gè)label,然后保存,構(gòu)建之前查看下 kubernetes 集群中的 Pod:
$ kubectl get pods -n kube-ops
NAME READY STATUS RESTARTS AGE
jenkins-7c85b6f4bd-rfqgv 1/1 Running 4 6d
然后重新觸發(fā)立刻構(gòu)建:
$ kubectl get pods -n kube-ops
NAME READY STATUS RESTARTS AGE
jenkins-7c85b6f4bd-rfqgv 1/1 Running 4 6d
jnlp-0hrrz 1/1 Running 0 23s
會(huì)發(fā)現(xiàn)多了一個(gè)名叫jnlp-0hrrz的 Pod 正在運(yùn)行,隔一會(huì)兒這個(gè) Pod 就不在了:
$ kubectl get pods -n kube-ops
NAME READY STATUS RESTARTS AGE
jenkins-7c85b6f4bd-rfqgv 1/1 Running 4 6d
這也證明Job 構(gòu)建完成了,同樣回到 Jenkins 的 Web UI 界面中查看 Console Output,可以看到如下的信息:

是不是也證明當(dāng)前的任務(wù)在跑在上面動(dòng)態(tài)生成的這個(gè) Pod 中,也符合預(yù)期?;氐?Job 的主界面,也可以看到大家可能比較熟悉的 Stage View 界面:

部署 Kubernetes 應(yīng)用
上面已經(jīng)知道了如何在 Jenkins Slave 中構(gòu)建任務(wù)了,那么如何來(lái)部署一個(gè)原生的 Kubernetes 應(yīng)用呢? 要部署 Kubernetes 應(yīng)用,就得對(duì)之前部署應(yīng)用的流程要非常熟悉才行,之前的流程是怎樣的:
- 編寫代碼
- 測(cè)試
- 編寫 Dockerfile
- 構(gòu)建打包 Docker 鏡像
- 推送 Docker 鏡像到倉(cāng)庫(kù)
- 編寫 Kubernetes YAML 文件
- 更改 YAML 文件中 Docker 鏡像 TAG
- 利用 kubectl 工具部署應(yīng)用
之前在 Kubernetes 環(huán)境中部署一個(gè)原生應(yīng)用的流程應(yīng)該基本上是上面這些流程吧?現(xiàn)在需要把上面這些流程放入 Jenkins 中來(lái)自動(dòng)幫我們完成(當(dāng)然編碼除外),從測(cè)試到更新 YAML 文件屬于 CI 流程,后面部署屬于 CD 的流程。如果按照上面的示例,現(xiàn)在要來(lái)編寫一個(gè) Pipeline 的腳本。
node('hwzx-cmp') {
stage('Clone') {
echo "1.Clone Stage"
}
stage('Test') {
echo "2.Test Stage"
}
stage('Build') {
echo "3.Build Docker Image Stage"
}
stage('Push') {
echo "4.Push Docker Image Stage"
}
stage('YAML') {
echo "5. Change YAML File Stage"
}
stage('Deploy') {
echo "6. Deploy Stage"
}
}
這里將一個(gè)簡(jiǎn)單 golang 程序部署到 kubernetes 環(huán)境中,代碼鏈接:https://github.com/cnych/jenkins-demo。如果按照之前的示例,我們是不是應(yīng)該像這樣來(lái)編寫 Pipeline 腳本:
- 第一步,clone 代碼,這個(gè)沒得說(shuō)吧
- 第二步,進(jìn)行測(cè)試,如果測(cè)試通過(guò)了才繼續(xù)下面的任務(wù)
- 第三步,由于 Dockerfile 基本上都是放入源碼中進(jìn)行管理的,所以我們這里就是直接構(gòu)建 Docker 鏡像了
- 第四步,鏡像打包完成,就應(yīng)該推送到鏡像倉(cāng)庫(kù)中吧
- 第五步,鏡像推送完成,是不是需要更改 YAML 文件中的鏡像 TAG 為這次鏡像的 TAG
- 第六步,萬(wàn)事俱備,只差最后一步,使用 kubectl 命令行工具進(jìn)行部署了
到這里整個(gè) CI/CD 的流程是不是就都完成了。
接下來(lái)就來(lái)對(duì)每一步具體要做的事情進(jìn)行詳細(xì)描述就行了:
第一步,Clone 代碼
stage('Clone') {
echo "1.Clone Stage"
git url: "https://github.com/cnych/jenkins-demo.git"
}
第二步,測(cè)試
由于我們這里比較簡(jiǎn)單,忽略該步驟即可
第三步,構(gòu)建鏡像
stage('Build') {
echo "3.Build Docker Image Stage"
sh "docker build -t qienda/jenkins-demo:${build_tag} ."
}
平時(shí)構(gòu)建的時(shí)候是不是都是直接使用docker build命令進(jìn)行構(gòu)建就行了,那么這個(gè)地方呢?之前提供的 Slave Pod 的鏡像里面是不是采用的 Docker In Docker 的方式,也就是說(shuō)也可以直接在 Slave 中使用 docker build 命令,所以這里直接使用 sh 執(zhí)行 docker build 命令即可。
但是鏡像的 tag 呢?如果使用鏡像 tag,則每次都是 latest 的 tag,這對(duì)于以后的排查或者回滾之類的工作會(huì)帶來(lái)很大麻煩,這里采用和git commit的記錄為鏡像的 tag,這里有一個(gè)好處就是鏡像的 tag 可以和 git 提交記錄對(duì)應(yīng)起來(lái),也方便日后對(duì)應(yīng)查看。但是由于這個(gè) tag 不只是我們這一個(gè) stage 需要使用,下一個(gè)推送鏡像是不是也需要,所以這里把這個(gè) tag 編寫成一個(gè)公共的參數(shù),把它放在 Clone 這個(gè) stage 中,這樣一來(lái)前兩個(gè) stage 就變成了下面這個(gè)樣子:
stage('Clone') {
echo "1.Clone Stage"
git url: "https://github.com/cnych/jenkins-demo.git"
script {
build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
}
stage('Build') {
echo "3.Build Docker Image Stage"
sh "docker build -t qienda/jenkins-demo:${build_tag} ."
}
第四步,推送鏡像
鏡像構(gòu)建完成了,現(xiàn)在就需要將此處構(gòu)建的鏡像推送到鏡像倉(cāng)庫(kù)中去,直接使用 docker hub 即可。
docker hub 是公共的鏡像倉(cāng)庫(kù),任何人都可以獲取上面的鏡像,但是要往上推送鏡像就需要用到一個(gè)帳號(hào)了,所以需要提前注冊(cè)一個(gè) docker hub 的帳號(hào),記住用戶名和密碼,。正常來(lái)說(shuō)我們?cè)诒镜赝扑?docker 鏡像的時(shí)候,需要使用docker login命令,然后輸入用戶名和密碼,認(rèn)證通過(guò)后,就可以使用docker push命令來(lái)推送本地的鏡像到 docker hub 上面去了,如果是這樣的話,這里的 Pipeline 是不是就該這樣寫了:
stage('Push') {
echo "4.Push Docker Image Stage"
sh "docker login -u cnych -p xxxxx"
sh "docker push cnych/jenkins-demo:${build_tag}"
}
如果只是在 Jenkins 的 Web UI 界面中來(lái)完成這個(gè)任務(wù)的話,這里的 Pipeline 是可以這樣寫的,但是是不是推薦使用 Jenkinsfile 的形式放入源碼中進(jìn)行版本管理,這樣的話直接把 docker 倉(cāng)庫(kù)的用戶名和密碼暴露給別人這樣很顯然是非常非常不安全的,更何況這里使用的是 github 的公共代碼倉(cāng)庫(kù),所有人都可以直接看到源碼,所以應(yīng)該用一種方式來(lái)隱藏用戶名和密碼這種私密信息,幸運(yùn)的是 Jenkins 提供了解決方法。
在首頁(yè)點(diǎn)擊 Credentials -> Stores scoped to Jenkins 下面的 Jenkins -> Global credentials (unrestricted) -> 左側(cè)的 Add Credentials:添加一個(gè) Username with password 類型的認(rèn)證信息,如下:

輸入 docker hub 的用戶名和密碼,ID 部分我們輸入dockerHub,注意,這個(gè)值非常重要,在后面 Pipeline 的腳本中我們需要使用到這個(gè) ID 值。
有了上面的 docker hub 的用戶名和密碼的認(rèn)證信息,現(xiàn)在我們可以在 Pipeline 中使用這里的用戶名和密碼了:
stage('Push') {
echo "4.Push Docker Image Stage"
withCredentials([usernamePassword(credentialsId: 'dockerHub', passwordVariable: 'dockerHubPassword', usernameVariable: 'dockerHubUser')]) {
sh "docker login -u ${dockerHubUser} -p ${dockerHubPassword}"
sh "docker push qienda/jenkins-demo:${build_tag}"
}
}
注意這里在 stage 中使用了一個(gè)新的函數(shù)withCredentials,其中有一個(gè) credentialsId 值就是我們剛剛創(chuàng)建的 ID 值,而對(duì)應(yīng)的用戶名變量就是 ID 值加上 User,密碼變量就是 ID 值加上 Password,然后我們就可以在腳本中直接使用這里兩個(gè)變量值來(lái)直接替換掉之前的登錄 docker hub 的用戶名和密碼,現(xiàn)在是不是就很安全了,我只是傳遞進(jìn)去了兩個(gè)變量而已,別人并不知道我的真正用戶名和密碼,只有自己的 Jenkins 平臺(tái)上添加的才知道。
第五步,更改 YAML
上面已經(jīng)完成了鏡像的打包、推送的工作,接下來(lái)更新 Kubernetes 系統(tǒng)中應(yīng)用的鏡像版本了,當(dāng)然為了方便維護(hù),用 YAML 文件的形式來(lái)編寫應(yīng)用部署規(guī)則,比如這里的 YAML 文件:(k8s.yaml)
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: jenkins-demo
spec:
template:
metadata:
labels:
app: jenkins-demo
spec:
containers:
- image: qienda/jenkins-demo:<BUILD_TAG>
imagePullPolicy: IfNotPresent
name: jenkins-demo
env:
- name: branch
value: <BRANCH_NAME>
該 Pod 使用的就是上面推送的鏡像,唯一不同的地方是 Docker 鏡像的 tag 不是平常見的具體的 tag,而是一個(gè)的標(biāo)識(shí),實(shí)際上如果將這個(gè)標(biāo)識(shí)替換成上面的 Docker 鏡像的 tag,是不是就是最終本次構(gòu)建需要使用到的鏡像?怎么替換呢?其實(shí)也很簡(jiǎn)單,使用一個(gè)sed命令就可以實(shí)現(xiàn)了:
stage('YAML') {
echo "5. Change YAML File Stage"
sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml"
sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s.yaml"
}
上面的 sed 命令就是將 k8s.yaml 文件中的 標(biāo)識(shí)給替換成變量 build_tag 的值。
第六步,部署
Kubernetes 應(yīng)用的 YAML 文件已經(jīng)更改完成了,之前手動(dòng)部署的環(huán)境下,接使用 kubectl apply 命令就可以直接更新應(yīng)用了,當(dāng)然這里只是寫入到了 Pipeline 里面,思路都是一樣的:
stage('Deploy') {
echo "6. Deploy Stage"
sh "kubectl apply -f k8s.yaml"
}
這樣到這里整個(gè)流程就算完成了。
人工確認(rèn)
理論上來(lái)說(shuō)上面的6個(gè)步驟其實(shí)已經(jīng)完成了,但是一般在我們的實(shí)際項(xiàng)目實(shí)踐過(guò)程中,可能還需要一些人工干預(yù)的步驟,比如開發(fā)提交了一次代碼,測(cè)試也通過(guò)了,鏡像也打包上傳了,但是這個(gè)版本并不一定就是要立刻上線到生產(chǎn)環(huán)境的,可能需要將該版本先發(fā)布到測(cè)試環(huán)境、QA 環(huán)境、或者預(yù)覽環(huán)境之類的,總之直接就發(fā)布到線上環(huán)境去還是挺少見的,所以需要增加人工確認(rèn)的環(huán)節(jié),一般都是在 CD 的環(huán)節(jié)才需要人工干預(yù),比如這里的最后兩步,就可以在前面加上確認(rèn):
stage('YAML') {
echo "5. Change YAML File Stage"
def userInput = input(
id: 'userInput',
message: 'Choose a deploy environment',
parameters: [
[
$class: 'ChoiceParameterDefinition',
choices: "Dev\nQA\nProd",
name: 'Env'
]
]
)
echo "This is a deploy step to ${userInput.Env}"
sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml"
sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s.yaml"
}
這里使用了 input 關(guān)鍵字,里面使用一個(gè) Choice 的列表來(lái)讓用戶進(jìn)行選擇,然后在選擇了部署環(huán)境后,當(dāng)然也可以針對(duì)不同的環(huán)境再做一些操作,比如可以給不同環(huán)境的 YAML 文件部署到不同的 namespace 下面去,增加不同的標(biāo)簽等操作:
stage('Deploy') {
echo "6. Deploy Stage"
if (userInput.Env == "Dev") {
// deploy dev stuff
} else if (userInput.Env == "QA"){
// deploy qa stuff
} else {
// deploy prod stuff
}
sh "kubectl apply -f k8s.yaml"
}
由于這一步也屬于部署的范疇,所以可以將最后兩步都合并成一步,最終的 Pipeline 腳本如下:
node('hwzx-cmp') {
stage('Clone') {
echo "1.Clone Stage"
git url: "https://github.com/cnych/jenkins-demo.git"
script {
build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
}
stage('Test') {
echo "2.Test Stage"
}
stage('Build') {
echo "3.Build Docker Image Stage"
sh "docker build -t qienda/jenkins-demo:${build_tag} ."
}
stage('Push') {
echo "4.Push Docker Image Stage"
withCredentials([usernamePassword(credentialsId: 'dockerHub', passwordVariable: 'dockerHubPassword', usernameVariable: 'dockerHubUser')]) {
sh "docker login -u ${dockerHubUser} -p ${dockerHubPassword}"
sh "docker push qienda/jenkins-demo:${build_tag}"
}
}
stage('Deploy') {
echo "5. Deploy Stage"
def userInput = input(
id: 'userInput',
message: 'Choose a deploy environment',
parameters: [
[
$class: 'ChoiceParameterDefinition',
choices: "Dev\nQA\nProd",
name: 'Env'
]
]
)
echo "This is a deploy step to ${userInput}"
sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s.yaml"
sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s.yaml"
if (userInput == "Dev") {
// deploy dev stuff
} else if (userInput == "QA"){
// deploy qa stuff
} else {
// deploy prod stuff
}
sh "kubectl apply -f k8s.yaml"
}
}
現(xiàn)在 Jenkins Web UI 中重新配置 jenkins-demo 這個(gè)任務(wù),將上面的腳本粘貼到 Script 區(qū)域,重新保存,然后點(diǎn)擊左側(cè)的 Build Now,觸發(fā)構(gòu)建,然后過(guò)一會(huì)兒就可以看到 Stage View 界面出現(xiàn)了暫停的情況:

這就是上面 Deploy 階段加入了人工確認(rèn)的步驟,所以這個(gè)時(shí)候構(gòu)建暫停了,需要人為的確認(rèn)下,比如這里選擇 QA,然后點(diǎn)擊 Proceed,就可以繼續(xù)往下走了,然后構(gòu)建就成功了,在 Stage View 的 Deploy 這個(gè)階段可以看到如下的一些日志信息:

打印出來(lái)了 QA,和剛剛的選擇是一致的,現(xiàn)在去 Kubernetes 集群中觀察下部署的應(yīng)用:
$ kubectl get deployment -n kube-ops
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
jenkins 1 1 1 1 7d
jenkins-demo 1 1 1 0 1m
$ kubectl get pods -n kube-ops
NAME READY STATUS RESTARTS AGE
jenkins-7c85b6f4bd-rfqgv 1/1 Running 4 7d
jenkins-demo-f6f4f646b-2zdrq 0/1 Completed 4 1m
$ kubectl logs jenkins-demo-f6f4f646b-2zdrq -n kube-ops
Hello, Kubernetes!I'm from Jenkins CI!
可以看到應(yīng)用已經(jīng)正確的部署到了 Kubernetes 的集群環(huán)境中了。