Jenkins+Gitlab+Harbor+Helm+Kubernetes
https://www.qikqiak.com/k8s-book/docs/66.devops.html
使用 Jenkins + Gitlab + Harbor + Helm + Kubernetes 來實現一個完整的 CI/CD 流水線作業。
修改polling-app-server代碼———提交gitlab——Jenkins構建新的鏡像提交給harbor——再從harbor拉取鏡像——helm進行更新——helm檢查
1 流程
- 開發人員提交代碼到 Gitlab 代碼倉庫
- 通過 Gitlab 配置的 Jenkins Webhook 觸發 Pipeline 自動構建
- Jenkins 觸發構建構建任務,根據 Pipeline 腳本定義分步驟構建
- 先進行代碼靜態分析,單元測試
- 然后進行 Maven 構建(Java 項目)
- 根據構建結果構建 Docker 鏡像
- 推送 Docker 鏡像到 Harbor 倉庫
- 觸發更新服務階段,使用 Helm 安裝/更新 Release
- 查看服務是否更新成功。
2 項目
本次示例項目是一個完整的基於 Spring Boot、Spring Security、JWT、React 和 Ant Design 構建的一個開源的投票應用,項目地址:https://github.com/callicoder/spring-security-react-ant-design-polls-app。 我也fork一個https://github.com/wangxu01/spring-security-react-ant-design-polls-app 會在該項目的基礎上添加部分代碼,並實踐 CI/CD 流程。
3 服務端
首先需要更改的是服務端配置,我們需要將數據庫鏈接的配置更改成環境變量的形式,寫死了的話就沒辦法進行定制了,修改服務端文件src/main/resources/application.properties
,將下面的數據庫配置部分修改成如下形式:
spring.datasource.url= jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:polling_app}?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false spring.datasource.username= ${DB_USER:root} spring.datasource.password= ${DB_PASSWORD:root}
當環境變量中有上面的數據配置的時候,就會優先使用環境變量中的值,沒有的時候就會用默認的值進行數據庫配置。
由於我們要將項目部署到 Kubernetes 集群中去,所以我們需要將服務端進行容器化,所以我們在項目根目錄下面添加一個Dockerfile
文件進行鏡像構建:
FROM openjdk:8-jdk-alpine MAINTAINER wangxu <314144952@qq.com> ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en ENV LC_ALL en_US.UTF-8 ENV TZ=Asia/Shanghai RUN mkdir /app WORKDIR /app COPY target/polls-0.0.1-SNAPSHOT.jar /app/polls.jar EXPOSE 8080 ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar","/app/polls.jar"]
由於服務端代碼是基於Spring Boot
構建的,所以我們這里使用一個openjdk
的基礎鏡像,將打包過后的jar
包放入鏡像之中,然后用過java -jar
命令直接啟動即可,這里就會存在一個問題了,我們是在 Jenkins 的 Pipeline 中去進行鏡像構建的,這個時候項目中並沒有打包好的jar
包文件,那么我們應該如何獲取打包好的jar
包文件呢?這里我們可以使用兩種方法:
第一種就是如果你用於鏡像打包的 Docker 版本大於17.06
版本的話,那么我牆裂推薦你使用 Docker 的多階段構建功能來完成鏡像的打包過程,我們只需要將上面的Dockerfile
文件稍微更改下即可,將使用maven
進行構建的工作放到同一個文件中:
FROM maven:3.6-alpine as BUILD COPY src /usr/app/src COPY pom.xml /usr/app RUN mvn -f /usr/app/pom.xml clean package -Dmaven.test.skip=true FROM openjdk:8-jdk-alpine MAINTAINER cnych <icnych@gmail.com> ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en ENV LC_ALL en_US.UTF-8 ENV TZ=Asia/Shanghai RUN mkdir /app WORKDIR /app COPY --from=BUILD /usr/app/target/polls-0.0.1-SNAPSHOT.jar /app/polls.jar EXPOSE 8080 ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar","/app/polls.jar"]
定義了兩個階段,第一個階段利用maven:3.6-alpine
這個基礎鏡像將我們的項目進行打包,然后將該階段打包生成的jar
包文件復制到第二階段進行最后的鏡像打包,這樣就可以很好的完成我們的 Docker 鏡像的構建工作。
第二種方式就是我們傳統的方式,在 Jenkins Pipeline 中添加一個maven
構建的階段,然后在第二個 Docker 構建的階段就可以直接獲取到前面的jar
包了,也可以很方便的完成鏡像的構建工作,為了更加清楚的說明 Jenkins Pipeline 的用法,我們這里采用這種方式,所以 Dockerfile 文件還是使用第一個就行。
同樣fork的dithub地址是 https://github.com/wangxu01/polling-app-server
現在我們可以將服務端的代碼推送到 Gitlab 上去,我們這里的倉庫地址為:
http://gitlab.wangxu.com/root/polling-app-server
注意,這里我們只推送的服務端代碼,持續集成的也只是服務端
[root@k8s-master test]# pwd /root/ops/test [root@k8s-master test]# [root@k8s-master test]# git clone https://github.com/wangxu01/polling-app-server 正克隆到 'polling-app-server'... remote: Enumerating objects: 222, done. remote: Counting objects: 100% (222/222), done. remote: Compressing objects: 100% (170/170), done. remote: Total 222 (delta 83), reused 170 (delta 39), pack-reused 0 接收對象中: 100% (222/222), 92.49 KiB | 0 bytes/s, done. 處理 delta 中: 100% (83/83), done. [root@k8s-master test]# ls gitlab-ci-k8s-demo gitlab-demo polling-app-server [root@k8s-master test]# cd polling-app-server/ [root@k8s-master polling-app-server]# git remote set-url origin ssh://git@gitlab.wangxu.com:30022/root/polling-app-server.git[root@k8s-master polling-app-server]# git push -u origin master Counting objects: 222, done. Delta compression using up to 4 threads. Compressing objects: 100% (126/126), done. Writing objects: 100% (222/222), 92.49 KiB | 0 bytes/s, done. Total 222 (delta 83), reused 222 (delta 83) remote: Resolving deltas: 100% (83/83), done. remote: remote: The private project root/polling-app-server was successfully created. remote: remote: To configure the remote, run: remote: git remote add origin git@gitlab.wangxu.com:root/polling-app-server.git remote: remote: To view the project, visit: remote: http://gitlab.wangxu.com/root/polling-app-server remote: To ssh://git@gitlab.wangxu.com:30022/root/polling-app-server.git * [new branch] master -> master 分支 master 設置為跟蹤來自 origin 的遠程分支 master。 [root@k8s-master polling-app-server]#
4 客戶端
客戶端我們需要修改 API 的鏈接地址,修改文件src/constants/index.js中API_BASE_URL的地址,我們同樣通過環境變量來進行區分,如果有環境變量APISERVER_URL,則優先使用這個環境變量來作為 API 請求的地址:
let API_URL = 'http://localhost:8080/api'; if (process.env.APISERVER_URL) { API_URL = `${process.env.APISERVER_URL}/api`; } export const API_BASE_URL = API_URL;
這里的項目使用的就是前后端分離的架構,所以我們同樣需要將前端代碼進行單獨的部署,同樣我們要將項目部署到 Kubernetes 環境中,所以也需要做容器化,同樣在項目根目錄下面添加一個Dockerfile文件:
FROM nginx:1.15.10-alpine ADD build /usr/share/nginx/html ADD nginx.conf /etc/nginx/conf.d/default.conf
由於前端頁面是單純的靜態頁面,所以一般我們使用一個nginx
鏡像來運行,所以我們提供一個nginx.conf
配置文件:
server { gzip on; listen 80; server_name localhost; root /usr/share/nginx/html; location / { try_files $uri /index.html; expires 1h; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } }
這里我們可以看到我們需要將前面頁面打包到一個build
目錄,然后將改目錄添加到 nginx 鏡像中的/usr/share/nginx/html
目錄,這樣當 nginx 鏡像啟動的時候就是直接使用的改文件夾下面的文件。
所以現在我們需要獲取打包后的目錄build
,同樣的,和上面服務端項目一樣,我們可以使用兩種方式來完成這個工作。
第一種方式自然是推薦的 Docker 的多階段構建,我們在一個node
鏡像的環境中就可以打包我們的前端項目了,所以我們可以更改下Dockerfile
文件,先進行 node 打包,然后再進行 nginx 啟動:
FROM node:alpine as BUILD WORKDIR /usr/src/app RUN mkdir -p /usr/src/app ADD . /usr/src/app RUN npm install && \ npm run build FROM nginx:1.15.10-alpine MAINTAINER wangxu <314144952@qq.com> COPY --from=BUILD /usr/src/app/build /usr/share/nginx/html ADD nginx.conf /etc/nginx/conf.d/default.conf
第二種方式和上面一樣在 Jenkins Pipeline 中添加一個打包構建的階段即可,我們這里采用這種方式,所以 Dockerfile 文件還是使用第一個就行。
我們也fork了一個https://github.com/wangxu01/polling-app-client
現在我們可以將客戶端的代碼推送到 Gitlab 上去,我們這里的倉庫地址為:
http://gitlab.wangxu.com/root/polling-app-client
[root@k8s-master test]# git clone https://github.com/wangxu01/polling-app-client 正克隆到 'polling-app-client'... remote: Enumerating objects: 88, done. remote: Counting objects: 100% (88/88), done. remote: Compressing objects: 100% (85/85), done. remote: Total 88 (delta 3), reused 85 (delta 0), pack-reused 0 Unpacking objects: 100% (88/88), done. [root@k8s-master test]# cd polling-app-client/ [root@k8s-master polling-app-client]# git remote set-url origin ssh://git@gitlab.wangxu.com:30022/root/polling-app-client.git [root@k8s-master polling-app-client]# git push -u origin master Counting objects: 88, done. Delta compression using up to 4 threads. Compressing objects: 100% (85/85), done. Writing objects: 100% (88/88), 254.11 KiB | 0 bytes/s, done. Total 88 (delta 20), reused 0 (delta 0) remote: remote: The private project root/polling-app-client was successfully created. remote: remote: To configure the remote, run: remote: git remote add origin git@gitlab.wangxu.com:root/polling-app-client.git remote: remote: To view the project, visit: remote: http://gitlab.wangxu.com/root/polling-app-client remote: To ssh://git@gitlab.wangxu.com:30022/root/polling-app-client.git * [new branch] master -> master 分支 master 設置為跟蹤來自 origin 的遠程分支 master。 [root@k8s-master polling-app-client]#
5 Jenkins
現在項目准備好了,接下來我們可以開始 Jenkins 的配置,還記得前面在 Pipeline 結合 Kubernetes 的課程中我們使用了一個kubernetes
的 Jenkins 插件,但是之前使用的方式有一些不妥的地方,我們 Jenkins Pipeline 構建任務綁定到了一個固定的 Slave Pod 上面,這樣就需要我們的 Slave Pod 中必須包含一系列構建所需要的依賴,比如 docker、maven、node、java 等等,這樣就難免需要我們自己定義一個很龐大的 Slave 鏡像,我們直接直接在 Pipeline 中去自定義 Slave Pod 中所需要用到的容器模板,這樣我們需要什么鏡像只需要在 Slave Pod Template 中聲明即可,完全不需要去定義一個龐大的 Slave 鏡像了。
首先去掉 Jenkins 中 kubernetes 插件中的 Pod Template 的定義,Jenkins -> 系統管理 -> 系統設置 -> 雲 -> Kubernetes區域,刪除下方的Kubernetes Pod Template
-> 保存。
然后新建一個名為polling-app-server類型為流水線(Pipeline)的任務:
然后在這里需要勾選觸發遠程構建的觸發器,其中令牌我們可以隨便寫一個字符串,然后記住下面的 URL,將 JENKINS_URL 替換成 Jenkins 的地址,我們這里的地址就是:http://10.6.76.25:30002/job/polling-app-server/build?token=server321
然后在下面的流水線區域我們可以選擇Pipeline script然后在下面測試流水線腳本,我們這里選擇Pipeline script from SCM,意思就是從代碼倉庫中通過Jenkinsfile文件獲取Pipeline script腳本定義,然后選擇 SCM 來源為Git,在出現的列表中配置上倉庫地址http://gitlab.wangxu.com/course/polling-app-server.git,由於我們是在一個 Slave Pod 中去進行構建,所以如果使用 SSH 的方式去訪問 Gitlab 代碼倉庫的話就需要頻繁的去更新 SSH-KEY,所以我們這里采用直接使用用戶名和密碼的形式來方式:
我們的gitlab沒有解析,Jenkins不能解析,用這個 http://gitlab.kube-ops.svc.cluster.local/root/polling-app-server.git
在Credentials區域點擊添加按鈕添加我們訪問 Gitlab 的用戶名和密碼:
root/ admin321
然后需要我們配置用於構建的分支,如果所有的分支我們都想要進行構建的話,只需要將Branch Specifier區域留空即可,一般情況下不同的環境對應的分支才需要構建,比如 master、develop、test 等,平時開發的 feature 或者 bugfix 的分支沒必要頻繁構建,我們這里就只配置 master 和 develop 兩個分支用戶構建:
然后前往 Gitlab 中配置項目polling-app-server Webhook,settings -> Integrations,填寫上面得到的 trigger 地址:
保存后,可以直接點擊Test -> Push Event測試是否可以正常訪問 Webhook 地址,這里需要注意的是我們需要配置下 Jenkins 的安全配置,否則這里的觸發器沒權限訪問 Jenkins,系統管理 -> 全局安全配置:取消防止跨站點請求偽造,勾選上匿名用戶具有可讀權限:
如果測試出現了Hook executed successfully: HTTP 201
則證明 Webhook 配置成功了,否則就需要檢查下 Jenkins 的安全配置是否正確了。
配置成功后我們只需要往 Gitlab 倉庫推送代碼就會觸發 Pipeline 構建了。接下來我們直接在服務端代碼倉庫根目錄下面添加Jenkinsfile
文件,用於描述流水線構建流程。
這里我們使用podTemplate
來定義不同階段使用的的容器,有哪些階段呢?
Clone 代碼 -> 代碼靜態分析 -> 單元測試 -> Maven 打包 -> Docker 鏡像構建/推送 -> Helm 更新服務。
Clone 代碼在默認的 Slave 容器中即可;靜態分析和單元測試我們這里直接忽略,有需要這個階段的自己添加上即可;Maven 打包肯定就需要 Maven 的容器了;Docker 鏡像構建/推送是不是就需要 Docker 環境了呀;最后的 Helm 更新服務是不是就需要一個有 Helm 的容器環境了,所以我們這里就可以很簡單的定義podTemplate
了,如下定義:(添加一個kubectl
工具用於測試)
def label = "slave-${UUID.randomUUID().toString()}" 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: 'kubectl', image: 'cnych/kubectl', 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 stage('單元測試') { echo "測試階段" } stage('代碼編譯打包') { container('maven') { echo "代碼編譯打包階段" } } stage('構建 Docker 鏡像') { container('docker') { echo "構建 Docker 鏡像階段" } } stage('運行 Kubectl') { container('kubectl') { echo "查看 K8S 集群 Pod 列表" sh "kubectl get pods" } } stage('運行 Helm') { container('helm') { echo "查看 Helm Release 列表" sh "helm list" } } } }
需要注意的是volumes區域的定義,將容器中的/root/.m2目錄掛載到宿主機上是為了給Maven構建添加緩存的,不然每次構建的時候都需要去重新下載依賴,這樣就非常慢了;掛載.kube目錄是為了能夠讓kubectl和helm兩個工具可以讀取到 Kubernetes 集群的連接信息,不然我們是沒辦法訪問到集群的;最后掛載/var/run/docker.sock文件是為了能夠讓我們的docker這個容器獲取到Docker Daemon的信息的,因為docker這個鏡像里面只有客戶端的二進制文件,我們需要使用宿主機的Docker Daemon來構建鏡像,當然我們也需要在運行 Slave Pod 的節點上擁有訪問集群的文件,然后在每個Stage階段使用特定需要的容器來進行任務的描述即可,所以這幾個volumes都是非常重要的
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') ]
另外一個值得注意的就是label
標簽的定義,我們這里使用 UUID 生成一個隨機的字符串,這樣可以讓 Slave Pod 每次的名稱都不一樣,而且這樣就不會被固定在一個 Pod 上面了,以后有多個構建任務的時候就不會存在等待的情況了,這和我們之前的課程中講到的固定在一個 label 標簽上有所不同。
然后我們將上面的Jenkinsfile
文件提交到 Gitlab 代碼倉庫上
項目已經有了,我們Jenkinsfile替換提交觸發一下
[root@k8s-master polling-app-server]# vim Jenkinsfile [root@k8s-master polling-app-server]# git add . [root@k8s-master polling-app-server]# git commit -m "修改 Jenkinsfile 文件" [master 13ce99f] 修改 Jenkinsfile 文件 1 file changed, 1 insertion(+), 1 deletion(-) [root@k8s-master polling-app-server]# git push origin master Counting objects: 5, done. Delta compression using up to 4 threads. Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 307 bytes | 0 bytes/s, done. Total 3 (delta 2), reused 0 (delta 0) To ssh://git@gitlab.wangxu.com:30022/root/polling-app-server.git a883f85..13ce99f master -> master [root@k8s-master polling-app-server]#
然后切換到 Jenkins 頁面上,正常情況就可以看到我們的流水線任務polling-app-server已經被觸發構建了,然后回到我們的 Kubernetes 集群中可以看到多了一個 slave 開頭的 Pod,里面有5個容器,就是我們上面 podTemplate 中定義的4個容器,加上一個默認的 jenkins slave 容器,同樣的,構建任務完成后,這個 Pod 也會被自動銷毀掉:
[root@k8s-master polling-app-server]# kubectl get pods -n kube-ops NAME READY STATUS RESTARTS AGE …… slave-8f3bb83c-042c-4c7e-8caf-ab2c62aa6544-12b3n-8fqct 5/5 Running 0 12s
由於這個namespace權限的問題 我們把 sh "kubectl get pods "和 helm list先注釋,先走通流程
接下來的工作就是來實現上面具體的 Pipeline 腳本了。
6 Pipeline
6.1 單元測試
第一個階段:單元測試,我們可以在這個階段是運行一些單元測試或者靜態代碼分析的腳本,我們這里直接忽略。
6.2 代碼編譯
第二個階段:代碼編譯打包,我們可以看到我們是在一個maven
的容器中來執行的,所以我們只需要在該容器中獲取到代碼,然后在代碼目錄下面執行 maven 打包命令即可,如下所示:
stage('代碼編譯打包') { try { container('maven') { echo "2. 代碼編譯打包階段" sh "mvn clean package -Dmaven.test.skip=true" } } catch (exc) { println "構建失敗 - ${currentBuild.fullDisplayName}" throw(exc) } }
6.3 構建 Docker 鏡像
第三個階段:構建 Docker 鏡像,要構建 Docker 鏡像,就需要提供鏡像的名稱和 tag,要推送到 Harbor 倉庫,就需要提供登錄的用戶名和密碼,所以我們這里使用到了withCredentials
方法,在里面可以提供一個credentialsId
為dockerhub
的認證信息,如下:
stage('構建 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 = "harbor.wangxu.com" def imageEndpoint = "root/polling-app-server" def image = "${dockerRegistryUrl}/${imageEndpoint}"
docker 的用戶名和密碼信息則需要通過憑據來進行添加,進入 jenkins 首頁 -> 左側菜單憑據 -> 添加憑據,選擇用戶名和密碼類型的,其中 ID 一定要和上面的credentialsId的值保持一致:
admin/Harbor12345
6.4 構建 Docker 鏡像
第四個階段:運行 kubectl 工具,其實在我們當前使用的流水線中是用不到 kubectl 工具的,那么為什么我們這里要使用呢?這還不是因為我們暫時還沒有去寫應用的 Helm Chart 包嗎?所以我們先去用原始的 YAML 文件來編寫應用部署的資源清單文件,這也是我們寫出 Chart 包前提,因為只有知道了應用如何部署才可能知道 Chart 包如何編寫,所以我們先編寫應用部署資源清單。
k8s.yaml
首先當然就是 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)
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 ns course #創建命名空間
kubectl create secret docker-registry myreg --docker-server=harbor.wangxu.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 """ } }
6.5 運行 Helm 工具
第五階段:運行 Helm 工具,就是直接使用 Helm 來部署應用了,現在有了上面的基本的資源對象了,要創建 Chart 模板就相對容易了,Chart 模板倉庫地址:https://github.com/cnych/polling-helm,我們可以根據values.yaml
文件來進行自定義安裝,模板中我們定義了可以指定使用外部數據庫服務或者內部獨立的數據庫服務,具體的我們可以去看模板中的定義。首先我們可以先使用這個模板在集群中來測試下。首先在集群中 Clone 上面的 Chart 模板:
gitclone https://github.com/cnych/polling-helm.git
我也fork
了一個 https://github.com/wangxu01/polling-helm
然后我們使用內部的數據庫服務,新建一個 custom.yaml 文件來覆蓋 values.yaml 文件中的值:
persistence: enabled: true persistentVolumeClaim: database: storageClass: "database" database: type: internal internal: database: "polling" # 數據庫用戶 username: "polling" # 數據庫用戶密碼 password: "polling321"
可以看到我們這里使用了一個名為database
的 StorgeClass 對象,所以還得創建先創建這個資源對象:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: database
provisioner: fuseim.pri/ifs
然后我們就可以在 Chart 根目錄下面安裝應用,執行下面的命令:
[root@k8s-master test]# git clone https://github.com/cnych/polling-helm.git 正克隆到 'polling-helm'... remote: Enumerating objects: 115, done. remote: Counting objects: 100% (115/115), done. remote: Compressing objects: 100% (112/112), done. remote: Total 115 (delta 63), reused 49 (delta 0), pack-reused 0 接收對象中: 100% (115/115), 14.08 KiB | 0 bytes/s, done. 處理 delta 中: 100% (63/63), done. [root@k8s-master test]# [root@k8s-master test]# ls custom.yaml gitlab-ci-k8s-demo gitlab-demo polling-app-client polling-app-server polling-helm s-sc.yaml [root@k8s-master test]# rm -f custom.yaml s-sc.yaml ^C [root@k8s-master test]# cd polling-helm/ [root@k8s-master polling-helm]# ls Chart.yaml README.md templates values.yaml [root@k8s-master polling-helm]# mv ../custom.yaml . [root@k8s-master polling-helm]# mv ../s-sc.yaml mv: 在"../s-sc.yaml" 后缺少了要操作的目標文件 Try 'mv --help' for more information. [root@k8s-master polling-helm]# mv ../s-sc.yaml . [root@k8s-master polling-helm]# helm upgrade --install polling -f custom.yaml . --namespace course^C [root@k8s-master polling-helm]# ls Chart.yaml custom.yaml README.md s-sc.yaml templates values.yaml [root@k8s-master polling-helm]# helm upgrade --install polling -f custom.yaml . --namespace course Release "polling" does not exist. Installing it now. NAME: polling LAST DEPLOYED: Thu Oct 24 15:42:56 2019 NAMESPACE: course STATUS: DEPLOYED RESOURCES: ==> v1/Pod(related) NAME READY STATUS RESTARTS AGE polling-polling-api-7d6575ff64-9jcs2 0/1 ContainerCreating 0 1s polling-polling-database-0 0/1 Pending 0 0s polling-polling-ui-84b74859f-svhkt 0/1 ContainerCreating 0 0s ==> v1/Secret NAME TYPE DATA AGE polling-polling-database Opaque 1 1s ==> v1/Service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE polling-polling-api ClusterIP 10.104.180.131 <none> 8080/TCP 1s polling-polling-database ClusterIP 10.101.246.251 <none> 3306/TCP 1s polling-polling-ui ClusterIP 10.105.223.7 <none> 80/TCP 1s ==> v1/StatefulSet NAME READY AGE polling-polling-database 0/1 1s ==> v1beta1/Ingress NAME HOSTS ADDRESS PORTS AGE polling-polling-ingress ui.polling.domain 80 0s ==> v1beta2/Deployment NAME READY UP-TO-DATE AVAILABLE AGE polling-polling-api 0/1 1 0 1s polling-polling-ui 0/1 1 0 1s NOTES: 1. Get the application URL by running these commands: http://ui.polling.domain [root@k8s-master polling-helm]#
#注意我們這里安裝也是使用的helm upgrade
命令,這樣有助於安裝和更新的時候命令統一。
安裝完成后,查看下 Pod 的運行狀態:
# kubectl get pods -n courseNAME READY STATUS RESTARTS AGE
polling-polling-api-7d6575ff64-cc2fv 1/1 Running 0 44s
polling-polling-database-0 1/1 Running 0 52m
polling-polling-ui-84b74859f-svhkt 1/1 Running 0 52m
然后我們可以在本地/etc/hosts
里面加上http://ui.polling.domain
的的映射,這樣我們就可以通過這個域名來訪問我們安裝的應用了,可以注冊、登錄、發表投票內容了:
# kubectl get ingresses -n course NAME HOSTS ADDRESS PORTS AGE polling-polling-ingress ui.polling.domain 80 56m
10.6.76.23 ui.polling.domain 10.6.76.24 ui.polling.domain
不能中文,啊哈哈
這樣我們就完成了使用 Helm Chart 安裝應用的過程,但是現在我們使用的包還是直接使用的 git 倉庫中的,平常我們正常安裝的時候都是使用的 Chart 倉庫中的包,所以我們需要將該 Chart 包上傳到一個倉庫中去,比較幸運的是我們的 Harbor 也是支持 Helm Chart 包的。我們可以選擇手動通過 Harbor 的 Dashboard 將 Chart 包進行上傳,也可以通過使用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://harbor.wangxu.com/chartrepo/course --username=admin --password=Harbor12345 "course" has been added to your repositories
這里的 repo 的地址是<Harbor URL>/chartrepo/
<Harbor中項目名稱>,Harbor 中每個項目是分開的 repo,如果不提供項目名稱,則默認使用library
這個項目。
需要注意的是如果你的 Harbor 是采用的自建的 https 證書,這里就需要提供 ca 證書和私鑰文件了,否則會出現證書校驗失敗的錯誤x509: certificate signed by unknown authority
。我們這里是通過cert-manager
為 Harbor 提供的一個信任的 https 證書,所以沒有指定 ca 證書相關的參數。
harbor證書x509: certificate signed by unknown authority
[root@k8s-master test]# helm push polling-helm course Pushing polling-0.1.0.tgz to course... Done.
在右下角看到有添加倉庫和安裝 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 構建后的替換掉就可以了
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 文件如下所示:
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 http://harbor-harbor-core.kube-ops.svc.cluster.local/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 api.image.repository=${args.image} --set api.image.tag=${args.tag} --set imagePullSecrets[0].name=myreg --namespace=course" } else { println "部署應用" sh "helm upgrade --install ${args.name} ${args.chartDir} --set persistence.persistentVolumeClaim.database.storageClass=database --set api.image.repository=${args.image} --set api.image.tag=${args.tag} --set imagePullSecrets[0].name=myreg --namespace=course" 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 = "harbor.wangxu.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) } } stage('構建 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 部署應用成功..." } } } } }
改了幾處
1 harbor的地址不能解析,改成了k8s內部dns解析地址
2 下面這個rbac權限錯誤,我直接指定的namespace
部署應用 [Pipeline] sh + helm upgrade --install polling polling --set 'persistence.persistentVolumeClaim.database.storageClass=database' --set 'api.image.repository=harbor.wangxu.com/course/polling-api' --set 'api.image.tag=fadff33' --set 'imagePullSecrets[0].name=myreg' '--namespace=course' Error: pods is forbidden: User "system:serviceaccount:kube-ops:default" cannot list resource "pods" in API group "" in the namespace "kube-system" [Pipeline] }
這里只是對后端API進行了更新
[root@k8s-master test]# kubectl -n course get pod,svc NAME READY STATUS RESTARTS AGE pod/polling-polling-api-86c788465f-trjcx 1/1 Running 0 83s pod/polling-polling-database-0 1/1 Running 0 21h pod/polling-polling-ui-744798989-jb6lg 1/1 Running 0 46m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/polling-polling-api ClusterIP 10.104.180.131 <none> 8080/TCP 21h service/polling-polling-database ClusterIP 10.101.246.251 <none> 3306/TCP 21h service/polling-polling-ui ClusterIP 10.105.223.7 <none> 80/TCP 21h [root@k8s-master test]# [root@k8s-master test]#
我想在gitlab中把副本數調整成3,提交,Jenkins自動發布
構建鏡像,發給harbor
再從harbor拉取鏡像去部署
但是 很抱歉,這里的helm-chart只是polling-app-server的鏡像倉庫,副本數不是在這里控制的,所以每次更新的都是server 也就是polling-polling-api ,也就是說我們實現了polling-app-server代碼———提交gitlab——Jenkins構建新的鏡像提交給harbor——再從harbor拉取鏡像——helm進行更新——helm檢查,別暈。
但也再次測試了,修改代碼的自動構建發布
[root@k8s-master test]# cd polling-helm/ [root@k8s-master polling-helm]# ls Chart.yaml custom.yaml README.md s-sc.yaml templates values.yaml [root@k8s-master polling-helm]# grep rep values.yaml repository: cnych/polling-ui replicas: 1 repository: cnych/polling-api replicas: 1 repository: mysql [root@k8s-master polling-helm]#