阿里雲Kubernetes實戰3–DevOps
前言:
在上一篇文章中,我們已經在K8S叢集部署了Jenkins、Harbor和EFK。作為本系列最後一篇文章,將通過實際案例串聯所有的基礎軟體服務,基於K8S做DevOps。
整體的業務流程如下圖所示:

一、一機多Jenkins Slave
由於業務需要,我們的自動化測試需要基於windows做web功能測試,每一個測試任務獨佔一個windows使用者桌面,所以我們首先要給Jenkins配置幾個Windows的Slave Node.在我之前的post ofollow,noindex">《持續整合CI實施指南三–jenkins整合測試》 中詳細講解了給Jenkins新增Node的方法步驟。 本篇無需重複,但這裡主要講的是,如何在一臺Windows伺服器上搭建多個Jenkins Node,供多使用者使用。
- 在目標機上建立多個使用者,如下圖所示:
- 用Administrator使用者安裝JDK
- 在Jenkins的節點管理建立三個Node,分別為WinTester01、WinTester02、WinTester03,配置如下
- 在目標機的Administrator,用IE開啟Jenkins並進入節點管理,在WinTester01、WinTester02、WinTester03中分別點選“Launch”啟動Slave
- 確認啟動成功後,點選“File”下的“Install as service”
- 三個Slave都啟動後,可以在服務管理器看到
- 除了Jenkins Slave1無需配置,Slave2和Slave3都需要右鍵進入屬性,修改登入使用者分別為JenkinsSlave2和JenkinsSlave3
通過上面的配置,可以在一臺目標機部署三個使用者對應三個Jenkins Slave以滿足我們的業務需求。
二、 二次開發Jenkins 釘釘通知外掛
在整個DevOps的業務流程圖上,我們想使用釘釘作為通知方式,相比郵件而言,實時性和擴充套件性都很高。在2018年4月,Jenkins的釘釘通知外掛有兩款,分別是 Dingding JSON Pusher 和 Dingding notification plugin ,前者長期未更新,已經不能使用,後者可以在非Pipeline模式下使用,對於Pipeline則有一些問題。雖然目前,Dingding notification plugin已經更新到1.9版本並支援了Pipeline,但在當時,我們不得不在1.4版本的基礎上做二次開發。
整體開發經過參考 《Jenkins專案實戰之-釘釘提醒外掛二次開發舉例》 ,總體來說還是比較簡單:
- 修改”src/main/java/com/ztbsuper/dingtalk/DingTalkNotifier.java”,釘釘的訊息API型別有文字、link、markdown、card等,我們這裡把通知介面改成文字型別
public class DingTalkNotifier extends Notifier implements SimpleBuildStep { private String accessToken;private String message;private String imageUrl;private String messageUrl; @DataBoundConstructorpublic DingTalkNotifier(String accessToken, String message, String imageUrl, String messageUrl) {this.accessToken = accessToken; //釘釘的accesstokenthis.message = message; //訊息主體this.imageUrl = imageUrl; //縮圖this.messageUrl = messageUrl; //訊息的連結來源,一般是jenkins的build url} public String getAccessToken() {return accessToken;}public String getMessage() {return message;}public String getImageUrl() {return imageUrl;}public String getMessageUrl() {return messageUrl;} @Overridepublic void perform(@Nonnull Run<?, ?> run, @Nonnull FilePath filePath, @Nonnull Launcher launcher, @Nonnull TaskListener taskListener) throws InterruptedException, IOException {String buildInfo = run.getFullDisplayName();if (!StringUtils.isBlank(message)) {sendMessage(LinkMessage.builder().title(buildInfo).picUrl(imageUrl).text(message).messageUrl(messageUrl).build());}} private void sendMessage(DingMessage message) {DingTalkClient dingTalkClient = DingTalkClient.getInstance();try {dingTalkClient.sendMessage(accessToken, message);} catch (IOException e) {e.printStackTrace();}} @Overridepublic BuildStepMonitor getRequiredMonitorService() {return BuildStepMonitor.NONE;} @Symbol("dingTalk")@Extensionpublic static final class DescriptorImpl extends BuildStepDescriptor<Publisher> { @Overridepublic boolean isApplicable(Class<? extends AbstractProject> aClass) {return true;} @Nonnull@Overridepublic String getDisplayName() {return Messages.DingTalkNotifier_DescriptorImpl_DisplayName();}} }
-
用maven打包
maven需要安裝java環境,為了方便,我直接run一個maven的docker image,編譯完成後把hpi檔案send出來
- 在jenkins的外掛管理頁面上傳hpi檔案
- 在釘釘群中開啟自定義機器人
- 找到accesstoken
- 在jenkins pipeline中可以使用以下命令傳送資訊到釘釘群
dingTalk accessToken:"2fccafaexxxx",message:"資訊",imageUrl:"圖片地址",messageUrl:"訊息連結"
三、 DevOps解決方案
針對每一個軟體專案增加部署目錄,目錄結構如下:
- _deploy
- master
- deployment.yaml
- Dockerfile
- other files
- test
- deployment.yaml
- Dockerfile
- other files
- master
master和test資料夾用於區分測試環境與生產環境的部署配置
Dockerfile和other files用於生成應用或服務的映象
如前端vue和nodejs專案的Dockerfile:
# 前端專案執行環境的Image,從Harbor獲取 FROM xxx/xxx/frontend:1.0.0RUN mkdir -p /workspace/build && mkdir -p /workspace/run COPY . /workspace/build # 編譯,生成執行檔案,並刪除原始檔 RUN cd /workspace/build/frontend && \cnpm install && \npm run test && \cp -r /workspace/build/app/* /workspace/run && \rm -rf /workspace/build && \cd /workspace/run && \cnpm install# 執行專案,用npm run test或run prod區分測試和生產環境 CMD cd /workspace/run && npm run test
又如dotnet core專案的Dockerfile:
# dotnet專案編譯環境的Image,從Harbor獲取 FROM xxx/xxx/aspnetcore-build:2 AS builder WORKDIR /app COPY . . # 編譯 RUN cd /app/xxx RUN pwd && ls -al && dotnet restore RUN dotnet publish -c Release -o publish # dotnet專案執行環境的Image,從Harbor獲取 FROM xxx/xxx/aspnetcore:2 WORKDIR /publish COPY --from=builder /app/xxx/publish . # 重新命名配置檔案,中綴test、prod用於區分測試環境和生產環境 RUN mv appsettings.test.json appsettings.json # 執行 ENTRYPOINT ["dotnet", "xxx.dll"]
deployent.yaml用於執行應用或服務在k8s上的部署
由於deployment有很多配置項可以抽離成公共配置,所以deployment的配置有很多佔位變數,佔位變數用兩個#中間加變數名錶示,如下所示:
apiVersion: v1kind: Namespacemetadata:name: #namespace#labels:name: #namespace#--- apiVersion: v1 data:.dockerconfigjson: xxxxxxxxxxxxxxxxxxxxxx kind: Secret metadata:name: regcrednamespace: #namespace#type: kubernetes.io/dockerconfigjson --- apiVersion: extensions/v1beta1 kind: Deployment metadata:name: #app#-deploynamespace: #namespace#labels:app: #app#-deploy spec:replicas: #replicas#strategy:type: Recreatetemplate:metadata:labels:app: #app#spec:containers:- image: #image#name: #app#ports:- containerPort: #port#name: #app#securityContext:privileged: #privileged#volumeMounts:- name: log-volumemountPath: #log#- image: #filebeatImage#name: filebeatargs: ["-c", "/etc/filebeat.yml"]securityContext:runAsUser: 0volumeMounts:- name: configmountPath: /etc/filebeat.ymlreadOnly: truesubPath: filebeat.yml- name: log-volumemountPath: /var/log/container/volumes:- name: configconfigMap:defaultMode: 0600name: filebeat-config- name: log-volumeemptyDir: {}imagePullSecrets:- name: regcred --- apiVersion: v1 kind: ConfigMap metadata:name: filebeat-confignamespace: #namespace#labels:app: filebeat data:filebeat.yml: |-filebeat.inputs:- type: logenabled: truepaths:- /var/log/container/*.logoutput.elasticsearch:hosts: ["#es#"]tags: ["#namespace#-#app#"] --- apiVersion: v1 kind: Service metadata:name: #app#-servicenamespace: #namespace#labels:app: #app#-service spec:ports:- port: 80targetPort: #port#selector:app: #app# --- apiVersion: extensions/v1beta1 kind: Ingress metadata:name: #app#-ingressnamespace: #namespace#annotations:nginx.ingress.kubernetes.io/proxy-body-size: "0"nginx.ingress.kubernetes.io/rewrite-target: / spec:rules:- host: #host#http:paths:- path: #urlPath#backend:serviceName: #app#-serviceservicePort: 80
其中幾個關鍵變數的解釋如下:
- dockerconfigjson:因為所有的映象需要從Harbor獲取,而Harbor的映象如果設定為私有許可權,就需要提供身份驗證,這裡的dockerconfigjson就是Harbor的身份資訊。生成dockerconfigjson的方法如下:
- 進入K8S任何一個節點,刪除” ~/.docker/config.json ” 檔案
- 使用命令” docker login harbor地址”登入harbor
- 通過命令” cat ~/.docker/config.json “可以看到harbor的身份驗證資訊
- 使用命令” cat /root/.docker/config.json | base64 -w 0 “對資訊編碼,將生成後的編碼填寫到deployment.yaml的dockerconfigjson節點即可
- namespace:同一個專案的不同k8s元件應置於同一個namespace,所以namespace可統一配置,在我們的專案實踐中,生產環境的namespace為” 專案名 “,測試環境的namespace為” 專案名-test “
- app:應用或服務名稱
- image:應用或服務的映象地址
- replicas:副本數量
- port:應用或服務的Pod開放埠
- log:應用或服務的日誌路徑,在本系列的第二篇文章中,提到我們的日誌方案是給每個應用或服務配一個filebeat,放在同一Pod中,這裡只需告知應用或服務的日誌的絕對路徑,filebeat就能將日誌傳遞到ES中,日誌的tag命名方式為” namespace-app”
- host:在本系列的第一篇文章中,講了使用nginx ingress做服務暴露與負載。這裡的host就是給nginx ingress設定的域名,埠預設都是80,如果需要https,則在外層使用阿里雲SLB轉發
- urlPath:很多情況下,如微服務,需要通過相同的域名,不同的一級目錄將請求分發到不同的後臺,在nginx中,就是location的配置與反向代理,比如host的配置是確定了域名aaa.bbb.com,而urlPath的配置是確定aaa.bbb.com/user/getuser將會被轉發到使用者服務podIP:podPort/getuser中
以上所有的佔位變數都是在Pipeline Script中賦值,關於Jenkins Pipeline的相關內容介紹這裡不再多講,還是去看 官方文件 靠譜。我們這裡將k8s的部署檔案deployment.yaml與Jenkinsfile結合,即可做到一個deployment.yaml能適配所有專案,一個Pipeline Script模板能適配所有專案,針對不同的專案,只需在Pipeline Script中給佔位變數賦值,大大降低了配置複雜度。下面是一個專案的Jenkins配置示例:

對於一個專案,我們只需配置Trigger和Pipeline,上圖“Do not allow concurrent builds ”也是通過Pipeline的配置生成的。Pipeline Script示例如下:
pipeline {// 指定專案在label為jnlp-agent的節點上構建,也就是Jenkins Slave in Podagent { label 'jnlp-agent' }// 對應Do not allow concurrent buildsoptions {disableConcurrentBuilds()}environment {// ------ 以下內容,每個專案可能均有不同,按需修改 ------//author:用於釘釘通知author="張三"// branch: 分支,一般是test、 master,對應git從哪個分支拉取程式碼,也對應究竟執行_deploy資料夾下的test配置還是master配置branch = "test"// namespace: myproject-test, myproject,名稱空間一般是專案名稱,測試環境加testnamespace = "myproject-test"// hostname:對應deployment中的hosthost = "test.aaa.bbb.com"// appname:對應deployment中的appapp = "myserver"// port:對應deployment中的portport= "80"// replicas:對應deployment中的replicasreplicas = 2//git repo path:git的地址git="[email protected]/xxx.git"//log:對應deployment中的loglog="/publish/logs/"// ------ 以下內容,一般所有的專案都一樣,不經常修改 ------// harbor inner addressrepoHost = "192.168.0.1:23280"// harbor的賬號密碼資訊,在jenkins中配置使用者名稱/密碼形式的認證資訊,命名成harbor即可harborCreds = credentials('harbor')// filebeat的映象地址filebeatImage="${repoHost}/common/filebeat:6.3.1"// es的內網訪問地址es="elasticsearch-logging.kube-system:9200"}// ------ 以下內容無需修改 ------stages {// 開始構建前清空工作目錄stage ("CleanWS"){steps {script {try{deleteDir()}catch(err){echo "${err}"sh 'exit 1'}}}}// 拉取stage ("CheckOut"){steps {script {try{checkout([$class: 'GitSCM', branches: [[name: "*/${branch}"]], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'gitlab', url: "${git}"]]])}catch(err){echo "${err}"sh 'exit 1'}}}}// 構建stage ("Build"){steps {script {try{// 登入 harborsh "docker login -u ${harborCreds_USR} -p ${harborCreds_PSW} ${repoHost}"sh "date +%Y%m%d%H%m%S > timestamp"// 映象tag用時間戳代表tag = readFile('timestamp').replace("\n", "").replace("\r", "")repoPath = "${repoHost}/${namespace}/${app}:${tag}"// 根據分支,進入_deploy下對應的不同資料夾,通過dockerfile打包映象sh "cp _deploy/${branch}/* ./"sh "docker login -u ${harborCreds_USR} -p ${harborCreds_PSW} ${repoHost}"sh "docker build -t ${repoPath} ."}catch(err){echo "${err}"sh 'exit 1'}}}}// 映象推送到harborstage ("Push"){steps {script {try{sh "docker push ${repoPath}"}catch(err){echo "${err}"sh 'exit 1'}}}}// 使用pipeline script中複製的變數替換deployment.yaml中的佔位變數,執行deployment.yaml進行部署stage ("Deploy"){steps {script {try{sh "sed -i 's|#namespace#|${namespace}|g' deployment.yaml"sh "sed -i 's|#app#|${app}|g' deployment.yaml"sh "sed -i 's|#image#|${repoPath}|g' deployment.yaml"sh "sed -i 's|#port#|${port}|g' deployment.yaml"sh "sed -i 's|#host#|${host}|g' deployment.yaml"sh "sed -i 's|#replicas#|${replicas}|g' deployment.yaml"sh "sed -i 's|#log#|${log}|g' deployment.yaml"sh "sed -i 's|#filebeatImage#|${filebeatImage}|g' deployment.yaml"sh "sed -i 's|#es#|${es}|g' deployment.yaml"sh "sed -i 's|#redisImage#|${redisImage}|g' deployment.yaml"sh "cat deployment.yaml"sh "kubectl apply -f deployment.yaml"}catch(err){echo "${err}"sh 'exit 1'}}}}}post {// 使用釘釘外掛進行通知always {script {def msg = "【${author}】你把伺服器搞掛了,老詹喊你回家改BUG!"def imageUrl = "https://www.iconsdb.com/icons/preview/red/x-mark-3-xxl-2.png"if (currentBuild.currentResult=="SUCCESS"){imageUrl= "http://icons.iconarchive.com/icons/paomedia/small-n-flat/1024/sign-check-icon-2.png"msg ="【${author}】釋出成功,幹得不錯!"}dingTalk accessToken:"xxxx",message:"${msg}",imageUrl:"${imageUrl}",messageUrl:"${BUILD_URL}"}}} }
釋出完成後,可以參考 《持續整合CI實施指南三–jenkins整合測試》 ,做持續測試,測試結果也可通過釘釘通知。最後我們利用自建的運維平臺,監控阿里雲ECS狀態、K8S各元件狀態、監控ES中的日誌並做異常抓取和報警。形成一整套DevOps模式。
綜上,對於每個專案,我們只需維護Dockerfile,並在Jenkins建立持續整合專案時,填寫專案所需的引數變數。進階情況下,也可定製性的修改deployment檔案與pipeline script,滿足不同的業務需要。至此,完結,撒花!
本文轉自中文社群- 阿里雲Kubernetes實戰3–DevOps