JenkinsPipeline部署一個Kubernetes 應用
https://www.qikqiak.com/k8s-book/docs/37.Jenkins%20Pipeline.html
1 Jenkins Pipeline 介紹
https://www.cnblogs.com/wangxu01/articles/11156603.html
Pipeline,簡單來說,就是一套運行在 Jenkins 上的工作流框架,將原來獨立運行於單個或者多個節點的任務連接起來,實現單個任務難以完成的復雜流程編排和可視化的工作。
Jenkins Pipeline 有幾個核心概念:
- Node:節點,一個 Node 就是一個 Jenkins 節點,Master 或者 Agent,是執行 Step 的具體運行環境,比如我們之前動態運行的 Jenkins Slave 就是一個 Node 節點
- Stage:階段,一個 Pipeline 可以划分為若干個 Stage,每個 Stage 代表一組操作,比如:Build、Test、Deploy,Stage 是一個邏輯分組的概念,可以跨多個 Node
- Step:步驟,Step 是最基本的操作單元,可以是打印一句話,也可以是構建一個 Docker 鏡像,由各類 Jenkins 插件提供,比如命令:sh 'make',就相當於我們平時 shell 終端中執行 make 命令一樣。s
那么我們如何創建 Jenkins Pipline 呢?
- Pipeline 腳本是由 Groovy 語言實現的,但是我們沒必要單獨去學習 Groovy,當然你會的話最好
- Pipeline 支持兩種語法:Declarative(聲明式)和 Scripted Pipeline(腳本式)語法
- Pipeline 也有兩種創建方法:可以直接在 Jenkins 的 Web UI 界面中輸入腳本;也可以通過創建一個 Jenkinsfile 腳本文件放入項目源碼庫中
- 一般我們都推薦在 Jenkins 中直接從源代碼控制(SCMD)中直接載入 Jenkinsfile Pipeline 這種方法
2 創建一個簡單的 Pipeline
直接在 Jenkins 的 Web UI 界面中輸入腳本運行
- 新建 Job:在 Web UI 中點擊 New Item -> 輸入名稱:pipeline-demo -> 選擇下面的 Pipeline -> 點擊 OK
- 配置:在最下方的 Pipeline 區域輸入如下 Script 腳本,然后點擊保存。

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:發布" } }
- 構建:點擊左側區域的 Build Now,可以看到 Job 開始構建了
隔一會兒,構建完成,可以點擊左側區域的 Console Output,我們就可以看到如下輸出信息:


3 在Slave中構建任務
編輯上面創建的 Pipeline 腳本,給 node 添加一個 label 屬性,如下:
node('wx-jnlp') { 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 添加了一個 wx-jnlp 這樣的一個label,然后我們保存,構建之前查看下 kubernetes 集群中的 Pod:
[root@k8s-master ~]# kubectl -n kube-ops get pod NAME READY STATUS RESTARTS AGE jenkins2-8b7f7bdb7-h9kzt 1/1 Running 0 4h42m [root@k8s-master ~]# #然后重新觸發立刻構建: [root@k8s-master ~]# kubectl -n kube-ops get pod NAME READY STATUS RESTARTS AGE jenkins2-8b7f7bdb7-h9kzt 1/1 Running 0 4h43m jnlp-3tcdd 1/1 Running 0 4s [root@k8s-master ~]# [root@k8s-master ~]# kubectl -n kube-ops get pod NAME READY STATUS RESTARTS AGE jenkins2-8b7f7bdb7-h9kzt 1/1 Running 0 4h43m [root@k8s-master ~]#
#多了一個Pod 正在運行,隔一會兒這個 Pod 就不再了:
Job 構建完成了,同樣回到 Jenkins 的 Web UI 界面中查看 Console Output,可以看到如下的信息:


4 部署Kubernetes 應用
部署應用的流程
- 編寫代碼
- 測試
- 編寫 Dockerfile
- 構建打包 Docker 鏡像
- 推送 Docker 鏡像到倉庫
- 編寫 Kubernetes YAML 文件
- 更改 YAML 文件中 Docker 鏡像 TAG
- 利用 kubectl 工具部署應用
這里我們來將一個簡單 golang 程序,部署到 kubernetes 環境中,代碼鏈接:https://github.com/cnych/jenkins-demo。如果按照之前的示例,我們是不是應該像這樣來編寫 Pipeline 腳本:
- 第一步,clone 代碼,這個沒得說吧
- 第二步,進行測試,如果測試通過了才繼續下面的任務
- 第三步,由於 Dockerfile 基本上都是放入源碼中進行管理的,所以我們這里就是直接構建 Docker 鏡像了
- 第四步,鏡像打包完成,就應該推送到鏡像倉庫中吧
- 第五步,鏡像推送完成,是不是需要更改 YAML 文件中的鏡像 TAG 為這次鏡像的 TAG
- 第六步,萬事俱備,只差最后一步,使用 kubectl 命令行工具進行部署了
項目地址https://github.com/cnych/jenkins-demo.git 我fork了一下
第一步,Clone 代碼
stage('Clone') { echo "1.Clone Stage" git url: "https://github.com/wangxu01/jenkins-demo.git" }
第二步,測試
由於我們這里比較簡單,忽略該步驟即可
第三步,構建鏡像
stage('Build') { echo "3.Build Docker Image Stage" sh "docker build -t wangxu01/jenkins-demo:${build_tag} ." }
如果我們使用鏡像 tag,則每次都是 latest 的 tag,這對於以后的排查或者回滾之類的工作會帶來很大麻煩,我們這里采用和git commit的記錄為鏡像的 tag,這里有一個好處就是鏡像的 tag 可以和 git 提交記錄對應起來,也方便日后對應查看。但是由於這個 tag 不只是我們這一個 stage 需要使用,下一個推送鏡像是不是也需要,所以這里我們把這個 tag 編寫成一個公共的參數,把它放在 Clone 這個 stage 中,這樣一來我們前兩個 stage 就變成了下面這個樣子:
stage('Clone') { echo "1.Clone Stage" git url: "https://github.com/wangxu01/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 wangxu01/jenkins-demo:${build_tag} ." }
第四步,推送鏡像
鏡像構建完成了,現在我們就需要將此處構建的鏡像推送到鏡像倉庫中去,當然如果你有私有鏡像倉庫也可以,我們這里還沒有自己搭建私有的倉庫,所以直接使用 docker hub 即可,以后更改成 Harbor 倉庫。

docker hub 是公共的鏡像倉庫,任何人都可以獲取上面的鏡像,但是要往上推送鏡像我們就需要用到一個帳號了,所以我們需要提前注冊一個 docker hub 的帳號,記住用戶名和密碼,我們這里需要使用。正常來說我們在本地推送 docker 鏡像的時候,是不是需要使用docker login命令,然后輸入用戶名和密碼,認證通過后,就可以使用docker push命令來推送本地的鏡像到 docker hub 上面去了,如果是這樣的話,我們這里的 Pipeline 是不是就該這樣寫了:
stage('Push') { echo "4.Push Docker Image Stage" sh "docker login -u wangxu01 -p xxxxx" sh "docker push wangxu01/jenkins-demo:${build_tag}" }
如果我們只是在 Jenkins 的 Web UI 界面中來完成這個任務的話,我們這里的 Pipeline 是可以這樣寫的,但是我們是不是推薦使用 Jenkinsfile 的形式放入源碼中進行版本管理,這樣的話我們直接把 docker 倉庫的用戶名和密碼暴露給別人這樣很顯然是非常非常不安全的,更何況我們這里使用的是 github 的公共代碼倉庫,所有人都可以直接看到我們的源碼,所以我們應該用一種方式來隱藏用戶名和密碼這種私密信息,幸運的是 Jenkins 為我們提供了解決方法。
在首頁點擊 Credentials -> Stores scoped to Jenkins 下面的 Jenkins -> Global credentials (unrestricted) -> 左側的 Add Credentials:添加一個 Username with password 類型的認證信息,如下:





輸入 docker hub 的用戶名和密碼,ID 部分我們輸入dockerHub,注意,這個值非常重要,在后面 Pipeline 的腳本中我們需要使用到這個 ID 值。
有了上面的 docker hub 的用戶名和密碼的認證信息,現在我們可以在 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 wangxu01/jenkins-demo:${build_tag}" } }
注意我們這里在 stage 中使用了一個新的函數withCredentials,其中有一個 credentialsId 值就是我們剛剛創建的 ID 值,而對應的用戶名變量就是 ID 值加上 User,密碼變量就是 ID 值加上 Password,然后我們就可以在腳本中直接使用這里兩個變量值來直接替換掉之前的登錄 docker hub 的用戶名和密碼,現在是不是就很安全了,我只是傳遞進去了兩個變量而已,別人並不知道我的真正用戶名和密碼,只有我們自己的 Jenkins 平台上添加的才知道。
第五步,更改 YAML
上面我們已經完成了鏡像的打包、推送的工作,接下來我們是不是應該更新 Kubernetes 系統中應用的鏡像版本了,當然為了方便維護,我們都是用 YAML 文件的形式來編寫應用部署規則,比如我們這里的 YAML 文件:(k8s.yaml)
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: jenkins-demo spec: template: metadata: labels: app: jenkins-demo spec: containers: - image: wangxu01/jenkins-demo:<BUILD_TAG> imagePullPolicy: IfNotPresent name: jenkins-demo env: - name: branch value: <BRANCH_NAME>
我們使用一個 Deployment 資源對象來管理 Pod,該 Pod 使用的就是我們上面推送的鏡像,唯一不同的地方是 Docker 鏡像的 tag 不是我們平常見的具體的 tag,而是一個 的標識,實際上如果我們將這個標識替換成上面的 Docker 鏡像的 tag,是不是就是最終我們本次構建需要使用到的鏡像?怎么替換呢?其實也很簡單,我們使用一個sed命令就可以實現了:
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" }
第六步,部署
stage('Deploy') { echo "6. Deploy Stage" sh "kubectl apply -f k8s.yaml" }
人工確認
理論上來說我們上面的6個步驟其實已經完成了,但是一般在我們的實際項目實踐過程中,可能還需要一些人工干預的步驟,這是為什么呢?比如我們提交了一次代碼,測試也通過了,鏡像也打包上傳了,但是這個版本並不一定就是要立刻上線到生產環境的,對吧,我們可能需要將該版本先發布到測試環境、QA 環境、或者預覽環境之類的,總之直接就發布到線上環境去還是挺少見的,所以我們需要增加人工確認的環節,一般都是在 CD 的環節才需要人工干預,比如我們這里的最后兩步,我們就可以在前面加上確認,比如:
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 關鍵字,里面使用一個 Choice 的列表來讓用戶進行選擇,然后在我們選擇了部署環境后,我們當然也可以針對不同的環境再做一些操作,比如可以給不同環境的 YAML 文件部署到不同的 namespace 下面去,增加不同的標簽等等操作:
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('wx-jnlp') { stage('下代碼Clone') { echo "1.Clone Stage" echo "env.BRANCH_NAME" git url: "https://github.com/cnych/jenkins-demo.git" script { build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim() env.BRANCH_NAME = sh(returnStdout: true, script: 'git branch').trim() } } stage('測試Test') { echo "2.Test Stage" } stage('構建Build') { echo "3.Build Docker Image Stage" sh "docker build -t wangxu01/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 wangxu01/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/cnych/wangxu01/' Jenkinsfile" // sh "sed -i 's/cnych/wangxu01/' k8s.yaml" 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 "pwd" // sh "cat k8s.yaml" sh "kubectl apply -f k8s.yaml" } }
執行
現在我們在 Jenkins Web UI 中重新配置 jenkins-demo 這個任務,將上面的腳本粘貼到 Script 區域,重新保存,然后點擊左側的 Build Now,觸發構建,然后過一會兒我們就可以看到 Stage View 界面出現了暫停的情況:

這就是我們上面 Deploy 階段加入了人工確認的步驟,所以這個時候構建暫停了,需要我們人為的確認下,比如我們這里選擇 QA,然后點擊 Proceed,就可以繼續往下走了,然后構建就成功了,我們在 Stage View 的 Deploy 這個階段可以看到如下的一些日志信息:

打印出來了 QA,和我們剛剛的選擇是一致的,現在我們去 Kubernetes 集群中觀察下部署的應用:

這個BRANCH_NAME為空,不過不影響
這個是部署在default命名空間的
Kubernetes 集群中觀察下部署的應用:
[root@k8s-master jenkins-demo]# kubectl get deployments | grep jenkins jenkins-demo 0/1 1 0 8m34s [root@k8s-master jenkins-demo]# kubectl get pod | grep jenkins jenkins-demo-5fc7fd744-t72sv 0/1 Completed 3 56s [root@k8s-master jenkins-demo]# kubectl logs jenkins-demo-5fc7fd744-t72sv Hello, Kubernetes!I'm from Jenkins CI! BRANCH_NAME: <BRANCH_NAME> [root@k8s-master jenkins-demo]#
當然hub也上傳成功了

配置Jenkinsfile
在實際的工作實踐中,我們更多的是將 Pipeline 腳本寫入到 Jenkinsfile 文件中,然后和代碼一起提交到代碼倉庫中進行版本管理。現在我們將上面的 Pipeline 腳本拷貝到一個 Jenkinsfile 中,將該文件放入上面的 git 倉庫中,但是要注意的是,現在既然我們已經在 git 倉庫中了,是不是就不需要 git clone 這一步驟了,所以我們需要將第一步 Clone 操作中的 git clone 這一步去掉,可以參考:https://github.com/cnych/jenkins-demo/
然后我們更改上面的 jenkins-demo 這個任務,點擊 Configure -> 最下方的 Pipeline 區域 -> 將之前的 Pipeline Script 更改成 Pipeline Script from SCM,然后根據我們的實際情況填寫上對應的倉庫配置,要注意 Jenkinsfile 腳本路徑:


這里我在github上fork了一個分支,配置文件都改成我自己的,label標簽、hub地址、github地址等等
最后附一個執行日志
Started by user admin Obtained Jenkinsfile from git https://github.com/wangxu01/jenkins-demo.git Running in Durability level: MAX_SURVIVABILITY [Pipeline] Start of Pipeline [Pipeline] node Still waiting to schedule task ‘jnlp-ps5dp’ is offline Agent jnlp-ps5dp is provisioned from template Kubernetes Pod Template --- apiVersion: "v1" kind: "Pod" metadata: annotations: {} labels: jenkins: "slave" jenkins/wx-jnlp: "true" name: "jnlp-ps5dp" spec: containers: - env: - name: "JENKINS_SECRET" value: "********" - name: "JENKINS_AGENT_NAME" value: "jnlp-ps5dp" - name: "JENKINS_NAME" value: "jnlp-ps5dp" - name: "JENKINS_AGENT_WORKDIR" value: "/home/jenkins/agent" - name: "JENKINS_URL" value: "http://jenkins2.kube-ops.svc.cluster.local:8080/" image: "cnych/jenkins:jnlp6" imagePullPolicy: "IfNotPresent" name: "jnlp" resources: limits: {} requests: {} securityContext: privileged: false tty: true volumeMounts: - mountPath: "/var/run/docker.sock" name: "volume-0" readOnly: false - mountPath: "/root/.kube" name: "volume-1" readOnly: false - mountPath: "/home/jenkins/agent" name: "workspace-volume" readOnly: false workingDir: "/home/jenkins/agent" nodeSelector: {} restartPolicy: "Never" serviceAccount: "jenkins2" volumes: - hostPath: path: "/var/run/docker.sock" name: "volume-0" - hostPath: path: "/root/.kube" name: "volume-1" - emptyDir: medium: "" name: "workspace-volume" Running on jnlp-ps5dp in /home/jenkins/agent/workspace/pipeline-demo [Pipeline] { [Pipeline] stage [Pipeline] { (Prepare) [Pipeline] echo 1.Prepare Stage [Pipeline] checkout No credentials specified Cloning the remote Git repository Cloning repository https://github.com/wangxu01/jenkins-demo.git > git init /home/jenkins/agent/workspace/pipeline-demo # timeout=10 Fetching upstream changes from https://github.com/wangxu01/jenkins-demo.git > git --version # timeout=10 > git fetch --tags --progress -- https://github.com/wangxu01/jenkins-demo.git +refs/heads/*:refs/remotes/origin/* > git config remote.origin.url https://github.com/wangxu01/jenkins-demo.git # timeout=10 > git config --add remote.origin.fetch +refs/heads/*:refs/remotes/origin/* # timeout=10 > git config remote.origin.url https://github.com/wangxu01/jenkins-demo.git # timeout=10 Fetching upstream changes from https://github.com/wangxu01/jenkins-demo.git > git fetch --tags --progress -- https://github.com/wangxu01/jenkins-demo.git +refs/heads/*:refs/remotes/origin/* Checking out Revision 9851bdf7dd63b690cd0a80814a312855c286dcd9 (refs/remotes/origin/master) Commit message: "Update Jenkinsfile" First time build. Skipping changelog. [Pipeline] script [Pipeline] { [Pipeline] sh > git rev-parse refs/remotes/origin/master^{commit} # timeout=10 > git rev-parse refs/remotes/origin/origin/master^{commit} # timeout=10 > git config core.sparsecheckout # timeout=10 > git checkout -f 9851bdf7dd63b690cd0a80814a312855c286dcd9 + git rev-parse --short HEAD [Pipeline] } [Pipeline] // script [Pipeline] } [Pipeline] // stage [Pipeline] stage [Pipeline] { (Test) [Pipeline] echo 2.Test Stage [Pipeline] } [Pipeline] // stage [Pipeline] stage [Pipeline] { (Build) [Pipeline] echo 3.Build Docker Image Stage [Pipeline] sh + docker build -t wangxu01/jenkins-demo:null-9851bdf . Sending build context to Docker daemon 140.3kB Step 1/5 : FROM golang:1.8.0-alpine ---> bcb935bbf1da Step 2/5 : ADD . /go/src/app ---> e12fdc91931c Step 3/5 : WORKDIR /go/src/app ---> Running in 106c10ce573e Removing intermediate container 106c10ce573e ---> a7d41cd09420 Step 4/5 : RUN GOOS=linux GOARCH=386 go build -v -o /go/src/app/jenkins-app ---> Running in a025f9bd3c51 [91mruntime/internal/sys [0m[91mruntime/internal/atomic [0m[91mruntime [0m[91merrors [0m[91minternal/race [0m[91mmath [0m[91msync/atomic [0m[91municode/utf8 [0m[91msync [0m[91mio syscall [0m[91mstrconv [0m[91mreflect [0m[91mtime [0m[91mos [0m[91mfmt [0m[91mapp [0mRemoving intermediate container a025f9bd3c51 ---> f9ba2f994f79 Step 5/5 : CMD ["./jenkins-app"] ---> Running in 51d271e4be69 Removing intermediate container 51d271e4be69 ---> 8155d613d6a5 Successfully built 8155d613d6a5 Successfully tagged wangxu01/jenkins-demo:null-9851bdf [Pipeline] } [Pipeline] // stage [Pipeline] stage [Pipeline] { (Push) [Pipeline] echo 4.Push Docker Image Stage [Pipeline] withCredentials Masking supported pattern matches of $dockerHubUser or $dockerHubPassword [Pipeline] { [Pipeline] sh + docker login -u **** -p **** WARNING! Using --password via the CLI is insecure. Use --password-stdin. WARNING! Your password will be stored unencrypted in /home/jenkins/.docker/config.json. Configure a credential helper to remove this warning. See https://docs.docker.com/engine/reference/commandline/login/#credentials-store Login Succeeded [Pipeline] sh + docker push ****/jenkins-demo:null-9851bdf The push refers to repository [docker.io/****/jenkins-demo] 244b926aac60: Preparing eab6a59b1b75: Preparing a26f54127e87: Preparing d9ec36cf6713: Preparing 5fb532fed28c: Preparing 2d494005645b: Preparing dc364fd32a38: Preparing 23b9c7b43573: Preparing dc364fd32a38: Waiting 23b9c7b43573: Waiting 2d494005645b: Waiting 5fb532fed28c: Layer already exists d9ec36cf6713: Layer already exists a26f54127e87: Layer already exists 2d494005645b: Layer already exists dc364fd32a38: Layer already exists 23b9c7b43573: Layer already exists eab6a59b1b75: Pushed 244b926aac60: Pushed null-9851bdf: digest: sha256:2df27d31591b42274c88e955034336bc7c89c693da11dfa9a2ba27ad355ab6b9 size: 1991 [Pipeline] } [Pipeline] // withCredentials [Pipeline] } [Pipeline] // stage [Pipeline] stage [Pipeline] { (Deploy) [Pipeline] echo 5. Deploy Stage [Pipeline] sh + sed -i s/<BUILD_TAG>/null-9851bdf/ k8s.yaml [Pipeline] sh + sed -i s/<BRANCH_NAME>/null/ k8s.yaml [Pipeline] sh + kubectl apply -f k8s.yaml --record deployment.extensions/jenkins-demo configured [Pipeline] } [Pipeline] // stage [Pipeline] } [Pipeline] // node [Pipeline] End of Pipeline Finished: SUCCESS
