原文鏈接:
上節課我們完成了最基本的流水線流程,接下來的工作就是來實現之前的具體 Pipeline 腳本了。
第一個階段:單元測試,我們可以在這個階段是運行一些單元測試或者靜態代碼分析的腳本,我們這裡直接忽略。
第二個階段:代碼編譯打包,我們可以看到我們是在一個maven的容器中來執行的,所以我們只需要在該容器中獲取到代碼,然後在代碼目錄下面執行 maven 打包命令即可,如下所示:
maven
stage(代碼編譯打包) { try { container(maven) { echo "2. 代碼編譯打包階段" sh "mvn clean package -Dmaven.test.skip=true" } } catch (exc) { println "構建失敗 - ${currentBuild.fullDisplayName}" throw(exc) } }
第三個階段:構建 Docker 鏡像,要構建 Docker 鏡像,就需要提供鏡像的名稱和 tag,要推送到 Harbor 倉庫,就需要提供登錄的用戶名和密碼,所以我們這裡使用到了withCredentials方法,在裡面可以提供一個credentialsId為dockerhub的認證信息,如下:
withCredentials
credentialsId
dockerhub
container(構建 Docker 鏡像) { withCredentials([[$class: UsernamePasswordMultiBinding, credentialsId: dockerhub, usernameVariable: DOCKER_HUB_USER, passwordVariable: DOCKER_HUB_PASSWORD]]) { container(docker) { echo "3. 構建 Docker 鏡像階段" sh """ docker login ${dockerRegistryUrl} -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD} docker build -t ${image}:${imageTag} . docker push ${image}:${imageTag} """ } } }
其中 ${image} 和 ${imageTag} 我們可以在上面定義成全局變數:
def imageTag = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() def dockerRegistryUrl = "registry.qikqiak.com" def imageEndpoint = "course/polling-app-server" def image = "${dockerRegistryUrl}/${imageEndpoint}"
docker 的用戶名和密碼信息則需要通過憑據來進行添加,進入 jenkins 首頁 -> 左側菜單憑據 -> 添加憑據,選擇用戶名和密碼類型的,其中 ID 一定要和上面的credentialsId的值保持一致:
憑據
添加憑據
第四個階段:運行 kubectl 工具,其實在我們當前使用的流水線中是用不到 kubectl 工具的,那麼為什麼我們這裡要使用呢?這還不是因為我們暫時還沒有去寫應用的 Helm Chart 包嗎?所以我們先去用原始的 YAML 文件來編寫應用部署的資源清單文件,這也是我們寫出 Chart 包前提,因為只有知道了應用如何部署才可能知道 Chart 包如何編寫,所以我們先編寫應用部署資源清單。
首先當然就是 Deployment 控制器了,如下所示:(k8s.yaml)
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: polling-server namespace: course labels: app: polling-server spec: strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 1 type: RollingUpdate template: metadata: labels: app: polling-server spec: restartPolicy: Always imagePullSecrets: - name: myreg containers: - image: <IMAGE>:<IMAGE_TAG> name: polling-server imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: api env: - name: DB_HOST value: mysql - name: DB_PORT value: "3306" - name: DB_NAME value: polling_app - name: DB_USER value: polling - name: DB_PASSWORD value: polling321
---
kind: Service apiVersion: v1 metadata: name: polling-server namespace: course spec: selector: app: polling-server type: ClusterIP ports: - name: api-port port: 8080 targetPort: api
--- apiVersion: extensions/v1beta1 kind: Deployment metadata: name: mysql namespace: course spec: template: metadata: labels: app: mysql spec: restartPolicy: Always containers: - name: mysql image: mysql:5.7 imagePullPolicy: IfNotPresent ports: - containerPort: 3306 name: dbport env: - name: MYSQL_ROOT_PASSWORD value: rootPassW0rd - name: MYSQL_DATABASE value: polling_app - name: MYSQL_USER value: polling - name: MYSQL_PASSWORD value: polling321 volumeMounts: - name: db mountPath: /var/lib/mysql volumes: - name: db hostPath: path: /var/lib/mysql
--- kind: Service apiVersion: v1 metadata: name: mysql namespace: course spec: selector: app: mysql type: ClusterIP ports: - name: dbport port: 3306 targetPort: dbport
可以看到我們上面的 YAML 文件中添加使用的鏡像是用標籤代替的:<IMAGE>:<IMAGE_TAG>,這是因為我們的鏡像地址是動態的,下依賴我們在上一個階段打包出來的鏡像地址的,所以我們這裡用標籤代替,然後將標籤替換成真正的值即可,另外為了保證應用的穩定性,我們還在應用中添加了健康檢查,所以需要在代碼中添加一個健康檢查的 Controller:(src/main/java/com/example/polls/controller/StatusController.java)
<IMAGE>:<IMAGE_TAG>
package com.example.polls.controller;
import org.springframework.web.bind.annotation.*;
@RestController @RequestMapping("/api/_status/healthz") public class StatusController {
@GetMapping public String healthCheck() { return "UP"; }
}
最後就是環境變數了,還記得前面我們更改了資源文件中資料庫的配置嗎?(src/main/resources/application.properties)因為要盡量通用,我們在部署應用的時候很有可能已經有一個外部的資料庫服務了,所以這個時候通過環境變數傳入進來即可。另外由於我們這裡使用的是私有鏡像倉庫,所以需要在集群中提前創建一個對應的 Secret 對象:
$ kubectl create secret docker-registry myreg --docker-server=registry.qikqiak.com --docker-username=DOCKER_USER --docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAIL --namespace course
在代碼根目錄下面創建一個 manifests 的目錄,用來存放上面的資源清單文件,正常來說是不是我們只需要在鏡像構建成功後,將上面的 k8s.yaml 文件中的鏡像標籤替換掉就 OK,所以這一步的動作如下:
stage(運行 Kubectl) { container(kubectl) { echo "查看 K8S 集群 Pod 列表" sh "kubectl get pods" sh """ sed -i "s/<IMAGE>/${image}" manifests/k8s.yaml sed -i "s/<IMAGE_TAG>/${imageTag}" manifests/k8s.yaml kubectl apply -f k8s.yaml """ } }
第五階段:運行 Helm 工具,就是直接使用 Helm 來部署應用了,現在有了上面的基本的資源對象了,要創建 Chart 模板就相對容易了,Chart 模板倉庫地址:https://github.com/cnych/polling-helm,我們可以根據values.yaml文件來進行自定義安裝,模板中我們定義了可以指定使用外部資料庫服務或者內部獨立的資料庫服務,具體的我們可以去看模板中的定義。首先我們可以先使用這個模板在集群中來測試下。首先在集群中 Clone 上面的 Chart 模板:
values.yaml
$ git clone https://github.com/cnych/polling-helm.git
然後我們使用內部的資料庫服務,新建一個 custom.yaml 文件來覆蓋 values.yaml 文件中的值:
persistence: enabled: true persistentVolumeClaim: database: storageClass: "database"
database: type: internal internal: database: "polling" # 資料庫用戶 username: "polling" # 資料庫用戶密碼 password: "polling321"
可以看到我們這裡使用了一個名為database的 StorgeClass 對象,所以還得創建先創建這個資源對象:
database
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: database provisioner: fuseim.pri/ifs
然後我們就可以在 Chart 根目錄下面安裝應用,執行下面的命令:
$ helm upgrade --install polling -f custom.yaml . --namespace course Release "polling" does not exist. Installing it now. NAME: polling LAST DEPLOYED: Sat May 4 23:31:42 2019 NAMESPACE: course STATUS: DEPLOYED
RESOURCES: ==> v1/Pod(related) NAME READY STATUS RESTARTS AGE polling-polling-api-6b699478d6-lqwhw 0/1 ContainerCreating 0 0s polling-polling-ui-587bbfb7b5-xr2ff 0/1 ContainerCreating 0 0s polling-polling-database-0 0/1 Pending 0 0s
==> v1/Secret NAME TYPE DATA AGE polling-polling-database Opaque 1 0s
==> v1/Service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE polling-polling-api ClusterIP 10.109.19.220 <none> 8080/TCP 0s polling-polling-database ClusterIP 10.98.136.190 <none> 3306/TCP 0s polling-polling-ui ClusterIP 10.108.170.43 <none> 80/TCP 0s
==> v1beta2/Deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE polling-polling-api 1 1 1 0 0s polling-polling-ui 1 1 1 0 0s
==> v1/StatefulSet NAME DESIRED CURRENT AGE polling-polling-database 1 1 0s
==> v1beta1/Ingress NAME HOSTS ADDRESS PORTS AGE polling-polling-ingress ui.polling.domain 80 0s
NOTES: 1. Get the application URL by running these commands: http://ui.polling.domain
You have new mail in /var/spool/mail/root
注意我們這裡安裝也是使用的helm upgrade命令,這樣有助於安裝和更新的時候命令統一。
helm upgrade
安裝完成後,查看下 Pod 的運行狀態:
$ kubectl get pods -n course NAME READY STATUS RESTARTS AGE polling-polling-api-6b699478d6-lqwhw 1/1 Running 0 3m polling-polling-database-0 1/1 Running 0 3m polling-polling-ui-587bbfb7b5-xr2ff 1/1 Running 0 3m
然後我們可以在本地/etc/hosts裡面加上http://ui.polling.domain的的映射,這樣我們就可以通過這個域名來訪問我們安裝的應用了,可以註冊、登錄、發表投票內容了:
/etc/hosts
http://ui.polling.domain
這樣我們就完成了使用 Helm Chart 安裝應用的過程,但是現在我們使用的包還是直接使用的 git 倉庫中的,平常我們正常安裝的時候都是使用的 Chart 倉庫中的包,所以我們需要將該 Chart 包上傳到一個倉庫中去,比較幸運的是我們的 Harbor 也是支持 Helm Chart 包的。我們可以選擇手動通過 Harbor 的 Dashboard 將 Chart 包進行上傳,也可以通過使用Helm Push插件:
Helm Push
$ helm plugin install https://github.com/chartmuseum/helm-push Downloading and installing helm-push v0.7.1 ... https://github.com/chartmuseum/helm-push/releases/download/v0.7.1/helm-push_0.7.1_linux_amd64.tar.gz
Installed plugin: push
當然我們需要首先將 Harbor 提供的倉庫添加到 helm repo 中,由於是私有倉庫,所以在添加的時候我們需要添加用戶名和密碼:
$ helm repo add course https://registry.qikqiak.com/chartrepo/course --username=<harbor用戶名> --password=<harbor密碼> "course" has been added to your repositories
這裡的 repo 的地址是<Harbor URL>/chartrepo/<Harbor中項目名稱>,Harbor 中每個項目是分開的 repo,如果不提供項目名稱,則默認使用library這個項目。
<Harbor URL>/chartrepo/<Harbor中項目名稱>
library
需要注意的是如果你的 Harbor 是採用的自建的 https 證書,這裡就需要提供 ca 證書和私鑰文件了,否則會出現證書校驗失敗的錯誤x509: certificate signed by unknown authority。我們這裡是通過cert-manager為 Harbor 提供的一個信任的 https 證書,所以沒有指定 ca 證書相關的參數。
x509: certificate signed by unknown authority
cert-manager
然後我們將上面的polling-helm這個 Chart 包上傳到 Harbor 倉庫中去:
polling-helm
$ helm push polling-helm course Pushing polling-0.1.0.tgz to course... Done.
這個時候我們登錄的 Harbor 倉庫中去,查看 course 這個項目下面的Helm Charts就可以發現多了一個 polling 的應用了:
Helm Charts
我們也可以在右下角看到有添加倉庫和安裝 Chart 的相關命令。
到這裡 Helm 相關的工作就準備好了。那麼我們如何在 Jenkins Pipeline 中去使用 Helm 呢?我們可以回顧下,我們平時的一個 CI/CD 的流程:開發代碼 -> 提交代碼 -> 觸發鏡像構建 -> 修改鏡像tag -> 推送到鏡像倉庫中去 -> 然後更改 YAML 文件鏡像版本 -> 使用 kubectl 工具更新應用。
現在我們是不是直接使用 Helm 了,就不需要去手動更改 YAML 文件了,也不需要使用 kubectl 工具來更新應用了,而是只需要去覆蓋下 helm 中的鏡像版本,直接 upgrade 是不是就可以達到應用更新的結果了。我們可以去查看下 chart 包的 values.yaml 文件中關於 api 服務的定義:
api: image: repository: cnych/polling-api tag: 0.0.7 pullPolicy: IfNotPresent
我們是不是只需要將上面關於 api 服務使用的鏡像用我們這裡 Jenkins 構建後的替換掉就可以了,這樣我們更改上面的最後運行 Helm的階段如下:
運行 Helm
stage(運行 Helm) { container(helm) { echo "更新 polling 應用" sh """ helm upgrade --install polling polling --set persistence.persistentVolumeClaim.database.storageClass=database --set database.type=internal --set database.internal.database=polling --set database.internal.username=polling --set database.internal.password=polling321 --set api.image.repository=${image} --set api.image.tag=${imageTag} --set imagePullSecrets[0].name=myreg --namespace course """ } }
當然我們可以將需要更改的值都放入一個 YAML 之中來進行修改,我們這裡通過--set來覆蓋對應的值,這樣整個 API 服務的完整 Jenkinsfile 文件如下所示:
--set
def label = "slave-${UUID.randomUUID().toString()}"
def helmLint(String chartDir) { println "校驗 chart 模板" sh "helm lint ${chartDir}" }
def helmInit() { println "初始化 helm client" sh "helm init --client-only --stable-repo-url https://mirror.azure.cn/kubernetes/charts/" }
def helmRepo(Map args) { println "添加 course repo" sh "helm repo add --username ${args.username} --password ${args.password} course https://registry.qikqiak.com/chartrepo/course"
println "更新 repo" sh "helm repo update"
println "獲取 Chart 包" sh """ helm fetch course/polling tar -xzvf polling-0.1.0.tgz """ }
def helmDeploy(Map args) { helmInit() helmRepo(args)
if (args.dry_run) { println "Debug 應用" sh "helm upgrade --dry-run --debug --install ${args.name} ${args.chartDir} --set persistence.persistentVolumeClaim.database.storageClass=database --set database.type=internal --set database.internal.database=polling --set database.internal.username=polling --set database.internal.password=polling321 --set api.image.repository=${args.image} --set api.image.tag=${args.tag} --set imagePullSecrets[0].name=myreg --namespace=${args.namespace}" } else { println "部署應用" sh "helm upgrade --install ${args.name} ${args.chartDir} --set persistence.persistentVolumeClaim.database.storageClass=database --set database.type=internal --set database.internal.database=polling --set database.internal.username=polling --set database.internal.password=polling321 --set api.image.repository=${args.image} --set api.image.tag=${args.tag} --set imagePullSecrets[0].name=myreg --namespace=${args.namespace}" echo "應用 ${args.name} 部署成功. 可以使用 helm status ${args.name} 查看應用狀態" } }
podTemplate(label: label, containers: [ containerTemplate(name: maven, image: maven:3.6-alpine, command: cat, ttyEnabled: true), containerTemplate(name: docker, image: docker, command: cat, ttyEnabled: true), containerTemplate(name: helm, image: cnych/helm, command: cat, ttyEnabled: true) ], volumes: [ hostPathVolume(mountPath: /root/.m2, hostPath: /var/run/m2), hostPathVolume(mountPath: /home/jenkins/.kube, hostPath: /root/.kube), hostPathVolume(mountPath: /var/run/docker.sock, hostPath: /var/run/docker.sock) ]) { node(label) { def myRepo = checkout scm def gitCommit = myRepo.GIT_COMMIT def gitBranch = myRepo.GIT_BRANCH def imageTag = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() def dockerRegistryUrl = "registry.qikqiak.com" def imageEndpoint = "course/polling-api" def image = "${dockerRegistryUrl}/${imageEndpoint}"
stage(單元測試) { echo "1.測試階段" } stage(代碼編譯打包) { try { container(maven) { echo "2. 代碼編譯打包階段" sh "mvn clean package -Dmaven.test.skip=true" } } catch (exc) { println "構建失敗 - ${currentBuild.fullDisplayName}" throw(exc) } } container(構建 Docker 鏡像) { withCredentials([[$class: UsernamePasswordMultiBinding, credentialsId: dockerhub, usernameVariable: DOCKER_HUB_USER, passwordVariable: DOCKER_HUB_PASSWORD]]) { container(docker) { echo "3. 構建 Docker 鏡像階段" sh """ docker login ${dockerRegistryUrl} -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD} docker build -t ${image}:${imageTag} . docker push ${image}:${imageTag} """ } } } stage(運行 Helm) { withCredentials([[$class: UsernamePasswordMultiBinding, credentialsId: dockerhub, usernameVariable: DOCKER_HUB_USER, passwordVariable: DOCKER_HUB_PASSWORD]]) { container(helm) { // todo,也可以做一些其他的分支判斷是否要直接部署 echo "4. [INFO] 開始 Helm 部署" helmDeploy( dry_run : false, name : "polling", chartDir : "polling", namespace : "course", tag : "${imageTag}", image : "${image}", username : "${DOCKER_HUB_USER}", password : "${DOCKER_HUB_PASSWORD}" ) echo "[INFO] Helm 部署應用成功..." } } } } }
由於我們沒有將 chart 包放入到 API 服務的代碼倉庫中,這是因為我們這裡使用的 chart 包涉及到兩個應用,一個 API 服務,一個是前端展示的服務,所以我們這裡是通過腳本裡面去主動獲取到 chart 包來進行安裝的,如果 chart 包跟隨代碼倉庫一起管理當然就要簡單許多了。
現在我們去更新 Jenkinsfile 文件,然後提交到 gitlab 中,然後去觀察下 Jenkins 中的構建是否成功,我們重點觀察下 Helm 階段:
當然我們還可以去做一些必要的判斷工作,比如根據分支判斷是否需要自動部署等等,同樣也可以切換到 Blue Occean 界面查看構建結果。
現在大家可以嘗試去修改下代碼,然後提交代碼到 gitlab 上,觀察下 Jenkins 是否能夠自動幫我們完成整個 CI/CD 的過程。
作業:現在還有一個前端展示的項目:https://github.com/cnych/polling-app-client,大家針對這個項目使用上面的 gitlab + jenkins + harbor + helm 來完成一個 Jenkins Pipeline 流水線的編寫,嘗試去修改下前端頁面內容,看是否能夠生效。
最後打個廣告,給大家推薦一個本人精心打造的一個精品課程,現在限時優惠中:從 Docker 到 Kubernetes 進階
微信搜索k8s技術圈關注我們的微信公眾帳號,在微信公眾帳號中回復 加群 即可加入到我們的 kubernetes 討論群裡面共同學習。
k8s技術圈
推薦閱讀: