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)的诉儒,但是沒(méi)必要單獨(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ù)并沒(méi)有在 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è)沒(méi)得說(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)境中了。