從0到1企業(yè)級(jí)DevOps實(shí)踐

基于.net core+Docker+jenkins pipeline+harbor+helm+Kubernetes的企業(yè)級(jí)DevOps實(shí)踐(2020.07)

前言

作為白嫖滴神,一是白嫖多了也不好意思,二是自己做過(guò)的東西,理解了的知識(shí)點(diǎn)總是忘,寫(xiě)點(diǎn)東西記錄下。當(dāng)然我也不是完全白嫖,主要是因?yàn)樘F了,卑微打工仔,只能投幣三連,最大極限了。.net很多資料都是隔靴搔癢,講的太淺未深入原理,又或者人云亦云,自己都沒(méi)搞明白就照著抄,發(fā)博客,再就是好點(diǎn)的都收費(fèi)。所以希望能反饋點(diǎn)東西為.net生態(tài)做點(diǎn)貢獻(xiàn)。這個(gè)雖然由于我做了脫敏處理沒(méi)辦法直接跑起來(lái),但是學(xué)東西重要的是理解,不是背題和抄,相信看完后,你能夠按自己的想法完整的搭建一套。
原本想的將代碼和yaml以及jenkinsfile一起放git上的,但是一是因?yàn)閭€(gè)人太懶了,二是其實(shí)我講的這個(gè)內(nèi)容跟代碼沒(méi)多大關(guān)系,還是等我微服務(wù)的項(xiàng)目寫(xiě)完后,再把代碼放上去。

實(shí)踐準(zhǔn)備

  1. 硬件資源
    測(cè)試環(huán)境: CentOS7.4版本以上 虛擬機(jī)3臺(tái)(4C+8G+50G),內(nèi)網(wǎng)互通,可訪(fǎng)問(wèn)外網(wǎng)
    生產(chǎn)環(huán)境: 騰訊云服務(wù)器 CentOS7.6(2C+4G)+CentOs7.5(1C+1G)

  2. 網(wǎng)絡(luò)環(huán)境
    最好能科學(xué)上網(wǎng),很多鏡像需要從國(guó)外鏡像源下載

  3. git倉(cāng)庫(kù)
    https://gitee.com/Gao06/hello-world.git

4.教學(xué)視頻
Docker+K8S+Jenkins項(xiàng)目實(shí)戰(zhàn)視頻教程
老男孩Kubernetes教程 k8s企業(yè)級(jí)DevOps實(shí)踐
尚硅谷Kubernetes教程(17h深入掌握k8s)
Docker最新超詳細(xì)版教程通俗易懂

5.教學(xué)文檔
《kubernetes權(quán)威指南》
《阿里巴巴DevOps實(shí)踐手冊(cè)》

上述資料需要整體閱讀,并不會(huì)看了某一個(gè)就能實(shí)現(xiàn)完整CI/CD落地,具體落地實(shí)踐方案參考本文章
上述資料實(shí)踐部分,會(huì)有一部分坑,但不影響這些資料整體質(zhì)量,具體踩坑位置,會(huì)在本文章指出
本文章實(shí)踐環(huán)境,以上述 硬件資源->實(shí)際環(huán)境為標(biāo)準(zhǔn)來(lái)講解,個(gè)人可以自己做相應(yīng)調(diào)整,相信看完本文章再進(jìn)行調(diào)整也很簡(jiǎn)單。
其他一些方案、ppt資料為私人資料,能分享的都會(huì)放git上,有問(wèn)題的可以留言,基本都會(huì)回答。文中IP密碼等敏感內(nèi)容都是自己瞎寫(xiě)的。

6.工作計(jì)劃

序號(hào) 工作內(nèi)容 人天
1 Jenkins安裝配置(docker安裝)
2 k8s集群安裝
3 Helm安裝
4 Baget安裝配置及nuget包遷移)
5 制品庫(kù)harbor安裝
6 SonarQuebe和SonarScanner安裝配置
7 編寫(xiě)k8s相關(guān)yaml文件
8 修改dockerfile(指定docker上下文)
9 編寫(xiě)jenkinsfile相關(guān)流水線(xiàn)腳本
10 測(cè)試調(diào)整

7.軟件版本

序號(hào) 名稱(chēng) 版本 鏡像
1 Docker 19.03.12
2 Docker-Compose 1.26.0
3 Helm v3.2.1 dtzar/helm-kubectl:latest
4 K8s 服務(wù)端 v1.16.3-tke.9
5 Progeresql Postgres:latest
6 SonarQube Sonarqube:latest
7 SonarScaner nosinovacao/dotnet-sonar:latest
8 Baget loicsharma/baget:latest
9 Jenkins jenkinsci/blueocean:latest

軟件版本中,含鏡像的內(nèi)容都是在docker中部署,含版本的直接在centos上安裝
軟件版本可以根據(jù)自己實(shí)際情況而定,老一點(diǎn)新一點(diǎn)沒(méi)關(guān)系
關(guān)于Sonar代碼質(zhì)量掃描的內(nèi)容,因?yàn)槿鄙傧嚓P(guān)服務(wù)器,暫未落地,后續(xù)會(huì)補(bǔ)上,目前缺少這個(gè)也不影響整體閱讀

部署架構(gòu)

系統(tǒng)部署架構(gòu).jpg

我們準(zhǔn)備了兩臺(tái)服務(wù)器,一臺(tái)服務(wù)器(10.129.55.18)僅部署docker和Baget,只是單純做一個(gè)nuget私有服務(wù)器使用。另一臺(tái)服務(wù)器(10.129.55.110)作為運(yùn)行我們整個(gè)CI/CD的服務(wù)器,部署整個(gè)k8s集群、鏡像倉(cāng)庫(kù)(騰訊云或harbor二選一)、helm、jenkins等。圖中沒(méi)有IP的服務(wù)器,是單純作為私有g(shù)ithub代碼倉(cāng)庫(kù)。(這個(gè)IP是我隨便寫(xiě)的)

此處并沒(méi)做k8s和jenkins高可用集群,一是因?yàn)榉?wù)器資源有限,二是單節(jié)點(diǎn)容易理解學(xué)習(xí)。如果需要部署成高可用集群,參考下列部署文檔即可,高可用集群和單節(jié)點(diǎn)其實(shí)思路差不多,相信單節(jié)點(diǎn)完整跑完后,部署高可用也可以很快實(shí)現(xiàn)落地。
此處沒(méi)有部署sonar,因?yàn)閟onar很吃?xún)?nèi)存和硬盤(pán)資源,我這兩臺(tái)服務(wù)器資源不夠部署不了,所以文章會(huì)缺少集成sonar代碼掃描的內(nèi)容,不過(guò)整體不影響CI/CD流程

流程示意

1596183240(1).jpg

整個(gè)流程,我們?nèi)肟邳c(diǎn)為git私有倉(cāng)庫(kù)。首先從git倉(cāng)庫(kù)拉取(checkout)項(xiàng)目代碼,然后將代碼通過(guò)sonar進(jìn)行掃描,查看是否有不合規(guī)范的代碼,如果掃描通過(guò),則用docker build構(gòu)建鏡像。構(gòu)建鏡像時(shí),從baget拉取還原nuget包,當(dāng)鏡像構(gòu)建成功后,將鏡像打tag,再把鏡像推送到私有鏡像倉(cāng)庫(kù)(harbor或騰訊云),最后通過(guò)helm把我們推送的鏡像發(fā)布到k8s集群中。這樣我們的應(yīng)用就發(fā)布成功了。
我們的用戶(hù)有管理員和普通用戶(hù),管理員可以用圖上三種方式訪(fǎng)問(wèn)控制k8s集群。生產(chǎn)環(huán)境下用戶(hù)通過(guò)nginx反向代理訪(fǎng)問(wèn)到k8s集群,這樣保障安全性。

我們有兩個(gè)jenkins任務(wù) 一個(gè)“HelloWorld”,一個(gè)“HelloWorld - 預(yù)發(fā)”。分別對(duì)應(yīng)發(fā)布到不同k8s集群
推送代碼時(shí)自動(dòng)觸發(fā)jenkins的任務(wù)->拉取代碼->代碼掃描->鏡像構(gòu)建->鏡像推送->部署到測(cè)試環(huán)境k8s集群
測(cè)試完成后,手動(dòng)點(diǎn)擊觸發(fā)jenkins任務(wù)->部署到生產(chǎn)環(huán)境k8s集群(從鏡像倉(cāng)庫(kù)拉取剛推送的鏡像)

jenkins安裝

#執(zhí)行命令
docker run \
  -u root \
  -d \
  -p 8080:8080 \
  -p 50000:50000 \
  -v $(which docker):/bin/docker \
  -v /var/jenkins_home:/var/jenkins_home \
  -v /var/run/docker.sock:/var/run/docker.sock \
  --restart=always \
  --name=jenkinsci \
  jenkinsci/blueocean

這里相對(duì)于網(wǎng)上的使用 Docker 安裝 Jenkins 的方式有一個(gè)避坑點(diǎn),因?yàn)槲覀兊乃姓麄€(gè)CI/CD流程是用jenkinsfile寫(xiě)的腳本,這些腳本在用docker安裝的jenkins里執(zhí)行,這樣就容易發(fā)生“docker in docker”的情況,所以加上了“ -v /var/run/docker.sock:/var/run/docker.sock \”,具體原理參考博客,docker的/var/run/docker.sock參數(shù)

1596187735(1).jpg

jenkins安裝完成后,我們?cè)L問(wèn)可能會(huì)出現(xiàn)jenkins離線(xiàn)的頁(yè)面,原因是在我們jenkins里有一個(gè)配置文件default.json,這個(gè)文件默認(rèn)會(huì)根據(jù)地址www.google.com去檢測(cè)你的網(wǎng)絡(luò)狀況,因?yàn)闆](méi)配置科學(xué)上網(wǎng),你訪(fǎng)問(wèn)不了google,然后就網(wǎng)絡(luò)不通,我們通過(guò)修改這個(gè)配置,并設(shè)置jenkins鏡像加速。就解決了離線(xiàn)問(wèn)題,而且下插件也改成了國(guó)內(nèi)的清華的源。

$ cd  /var/jenkins_home/updates  #進(jìn)入更新配置位置
sed -i 's/http:\/\/updates.jenkins-ci.org\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g' default.json && sed -i 's/http:\/\/www.google.com/https:\/\/www.baidu.com/g' default.json

然后我們解決完離線(xiàn)問(wèn)題后,出現(xiàn)頁(yè)面


1596188771(1).jpg
#執(zhí)行命令
cat /var/jenkins_home/secrets/initialAdminPassword

將得到的結(jié)果,輸入到j(luò)enkins解鎖,我們jenkins就安裝好了,剩下的填用戶(hù)密碼什么的就參考網(wǎng)上教程,自己填。也沒(méi)啥踩坑的點(diǎn)了。針對(duì)通過(guò)配置jenkins,連接k8s、git等內(nèi)容,先不著急配,后面再處理。

k8s集群安裝

參考教程使用kubeadm安裝kubernetes_v1.18.x,基本可以全程無(wú)坑安裝。如果對(duì)k8s很熟,可以考慮使用二進(jìn)制安裝。

Helm安裝

Helm的安裝很簡(jiǎn)單,這里安裝建議是安裝Helm3及以上版本,因?yàn)镠elm2和Helm3與k8s通信的方式改變了。本文內(nèi)容“軟件版本”中,既在cenos中直接安裝了,又用docker鏡像安裝了。因?yàn)閏enos中直接安裝,對(duì)于整個(gè)CI/CD流程沒(méi)有任何影響,僅僅就是為了有個(gè)客戶(hù)端工具,可以手動(dòng)輸入命令與k8s集群交互。docker中安裝helm是因?yàn)?,我們的jenkinsfile在jenkins里執(zhí)行,但是jenkins沒(méi)有Helm客戶(hù)端,沒(méi)辦法執(zhí)行Helm命令發(fā)布到K8s集群,所以在jenkinsfile中使用agent,動(dòng)態(tài)的將Helm安裝到docker中,當(dāng)jenkinsfile中這個(gè)發(fā)布操作執(zhí)行結(jié)束后,刪除這個(gè)docker容器。

#下載Helm客戶(hù)端
$ wget https://get.helm.sh/helm-v3.2.1-linux-amd64.tar.gz
#解壓 Helm
$ tar -zxvf helm-v3.2.1-linux-amd64.tar.gz
#復(fù)制客戶(hù)端執(zhí)行文件到 bin 目錄下,方便在系統(tǒng)下能執(zhí)行 helm 命令
$ cp linux-amd64/helm /usr/local/bin/

執(zhí)行完上述命令,我們的Helm就安裝好了,但是這里有個(gè)坑。我們思考下,kubectl作為k8s客戶(hù)端工具,是怎么跟k8s交互的?怎么驗(yàn)證身份信息的呢?
默認(rèn)情況下,kubectl$HOME/.kube 目錄下查找名為 config 的文件(安裝目錄/root/.kube/)。這個(gè)config文件中的內(nèi)容就是相關(guān)的用戶(hù)權(quán)限、集群地址、CA證書(shū)相關(guān)信息,這樣你根據(jù)這個(gè)文件就可以訪(fǎng)問(wèn)相應(yīng)的k8s集群了。但是我想自己設(shè)置可訪(fǎng)問(wèn)的K8S集群怎么做呢,可以通過(guò)設(shè)置 KUBECONFIG 環(huán)境變量或者設(shè)置 [--kubeconfig]參數(shù)來(lái)指定其他 kubeconfig 文件。我們的helm也是一樣,通過(guò)訪(fǎng)問(wèn)環(huán)境變量KUBECONFIG,獲取到相應(yīng)的kubeconfig文件,訪(fǎng)問(wèn)到相應(yīng)的集群。
說(shuō)明: 用于配置集群訪(fǎng)問(wèn)的文件稱(chēng)為 kubeconfig 文件。這是引用配置文件的通用方法。這并不意味著有一個(gè)名為 kubeconfig 的文件

#執(zhí)行命令 設(shè)置環(huán)境變量
export KUBECONFIG=/root/.kube/config

如果想配置訪(fǎng)問(wèn)多k8s集群,可參考使用 kubeconfig 文件組織集群訪(fǎng)問(wèn)

Baget安裝

安裝Baget是因?yàn)槲覀僯enkins和原有nuget兩個(gè)服務(wù)器網(wǎng)絡(luò)不通,沒(méi)辦法,只能遷移私有nuget倉(cāng)庫(kù),私有nuget倉(cāng)庫(kù)有很多,對(duì)比下來(lái)這個(gè)Baget是最好的,開(kāi)源簡(jiǎn)單,就功能支持少一點(diǎn),不過(guò)可以自己寫(xiě)。這個(gè)Baget安裝可以忽略,如果不需要搭建私有nuget庫(kù)的話(huà)。
搭建過(guò)程參考在Linux上搭建基于開(kāi)源技術(shù)的nuget私人保密倉(cāng)庫(kù),無(wú)坑搭建。說(shuō)明一點(diǎn)就是,baget目前不支持用戶(hù)權(quán)限這塊,所以用的nginx的auth做的用戶(hù)認(rèn)證。github上作者說(shuō),這個(gè)支持用戶(hù)權(quán)限這個(gè)功能,后續(xù)版本即將實(shí)現(xiàn),看了下PR已經(jīng)有相關(guān)功能的代碼了。不過(guò)目前還是不支持的。

harbor安裝

harbor無(wú)論是http還是https安裝都很簡(jiǎn)單,沒(méi)有什么坑,參考Docker 企業(yè)級(jí)鏡像倉(cāng)庫(kù) Harbor 的搭建與維護(hù) ,企業(yè)級(jí)鏡像倉(cāng)庫(kù) Harbor 的安裝與配置

SonarQuebe安裝

SonarQube整個(gè)架構(gòu)由4個(gè)組件組成:

  • SonarQube 服務(wù)端,包括web服務(wù)界面,elasticsearch搜索引擎,計(jì)算引擎三個(gè)主要部分。
  • SonarQube 數(shù)據(jù)庫(kù)存儲(chǔ)端,用于SonarQube 服務(wù)端數(shù)據(jù)。
  • SonarQube插件,可能包括語(yǔ)言,SCM,集成,身份驗(yàn)證和治理插件等,用于jenkins中集成sonarqube。
  • SonarScanner客戶(hù)端,開(kāi)發(fā)人員或持續(xù)集成服務(wù)器通過(guò)SonarScanner進(jìn)行項(xiàng)目代碼分析。


    image.png

    要安裝使用SonarQube服務(wù)端需要一些前提要求,由于SonarQube服務(wù)端需要安裝elasticsearch作為搜索引擎,所以主要端要求的限制大多在elasticsearch。
    Linux系統(tǒng),需要確保一些內(nèi)核參數(shù),主要是為了滿(mǎn)足Elasticsearch的運(yùn)行,如官方文檔所示,需要設(shè)置幾個(gè)內(nèi)核參數(shù),查看命令也列出,使用其中的命令,查看是否滿(mǎn)足。


    image.png

    這些參數(shù)保證后,開(kāi)始安裝sonarquebe數(shù)據(jù)庫(kù)和服務(wù)端。
#安裝數(shù)據(jù)庫(kù)
docker run -d -p 6000:5432 -v /var/sonar/db:/var/lib/postgresql/data \
-e POSTGRES_USER=sonar -e POSTGRES_PASSWORD=sonar \
--name=sonar-postgres --restart=unless-stopped postgres:latest
#安裝服務(wù)端
 docker run -d --name sonarqube \
    -p 9000:9000 \
    -e sonar.jdbc.url="jdbc:postgresql://192.888.10.122:6000/sonar" \
    -e sonar.jdbc.username=sonar \
    -e sonar.jdbc.password=sonar \
    -v sonarqube_conf:/opt/sonarqube/conf \
    -v sonarqube_extensions:/opt/sonarqube/extensions \
    -v sonarqube_logs:/opt/sonarqube/logs \
    -v sonarqube_data:/opt/sonarqube/data \
    --restart unless-stopped \
    sonarqube

編寫(xiě)dockerfile文件

對(duì)于dockerfile的編寫(xiě)可以參考教程,尚硅谷Docker核心技術(shù)。對(duì)于.net core使用VS開(kāi)發(fā),通常會(huì)自動(dòng)生成一個(gè)Dockerfile,但有些時(shí)候這個(gè)dockerfile文件是不適用的,需要針對(duì)不同情況調(diào)整。
這里還有個(gè)坑就是,我們根據(jù)dockerfile進(jìn)行build生成鏡像時(shí),經(jīng)常會(huì)報(bào)錯(cuò)“找不到文件路徑”,這是因?yàn)槲覀儾煌捻?xiàng)目代碼結(jié)構(gòu)可能不一樣,docker build時(shí),會(huì)根據(jù)上下文,把當(dāng)前上下文的內(nèi)容傳輸?shù)絛ocker容器中,如果上下文指定不正確就容易找不到相應(yīng)的文件。具體參考深入理解 Docker 構(gòu)建上下文
以采集報(bào)告為例,編寫(xiě)dockerfile如下,然后執(zhí)行命令。

#dockerfile
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM ccr.ccs.tencentyun.com/demaxiya/dotnet-core-sdk:3.1-buster-auth-nuget AS build
WORKDIR /src
COPY . .
RUN dotnet restore "Gy.HelloWorld.Api.csproj" -s http://nuget.demaxiya.com/v3/index.json -s https://api.nuget.org/v3/index.json
RUN dotnet build "Gy.HelloWorld.Api.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "Gy.HelloWorld.Api.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Gy.HelloWorld.Api.dll"]
#指定上下文構(gòu)建
docker build --add-host nuget.demaxiya.com:192.222.222.22 \
-t ccr.ccs.tencentyun.com/demaxiya/helloworld:v1.0.0.7232 src/Gy.HelloWorld.Api

ccr.ccs.tencentyun.com/demaxiya/dotnet-core-sdk:3.1-buster-auth-nuget這個(gè)鏡像是自己制作的鏡像,基于.net core sdk:3.1-buster,內(nèi)置了http://nuget.demaxiya.com/這個(gè)私有nuget服務(wù)器的賬戶(hù)信息,避免每個(gè)dockerfile都要寫(xiě)nuget認(rèn)證信息。

編寫(xiě)k8s相關(guān)yaml文件

首先我們參考閱讀一下 ASP.NET Core on K8S學(xué)習(xí)初探(3)部署API到K8S。從文章中我們可以看出,我們部署應(yīng)用到k8s集群時(shí),需要寫(xiě)一個(gè)yaml文件,然后執(zhí)行“kubectl create -f 文件名.yaml”,這樣就把應(yīng)用發(fā)布到k8s集群了。從博客中的示例我們看到y(tǒng)aml文件有一個(gè)"kind: Deployment"和“kind: Service”,我們執(zhí)行命令“kubectl create -f 文件名.yaml”時(shí),會(huì)根據(jù)這個(gè)yaml文件在k8s集群中創(chuàng)建一個(gè)Deployment對(duì)象和Service對(duì)象。這兩個(gè)對(duì)象的作用、原理參考Kubernetes中文手冊(cè)名詞解釋。

可是直接用命令“kubectl create -f 文件名.yaml”有幾個(gè)問(wèn)題,第一,yaml文件中的內(nèi)容參數(shù)都寫(xiě)死了,我每次修改都得改yaml文件,沒(méi)辦法用一個(gè)變量來(lái)表示具體的參數(shù)值。第二,假設(shè)我有很多不同的對(duì)象,每個(gè)對(duì)象我都單獨(dú)寫(xiě)一個(gè)yaml文件,這樣我執(zhí)行命令“kubectl create ”要執(zhí)行很多次很麻煩。于是我們就將這個(gè)yaml文件改為Helm方式。Helm教程參考 Helm 用戶(hù)指南

我們首先參考k8s發(fā)布 以“采集報(bào)告服務(wù)為例”,可以大體看到helm文件夾中的結(jié)構(gòu)。當(dāng)我們Helm文件編寫(xiě)好后,執(zhí)行命令“Helm upgrade -參數(shù) 你的helm文件夾路徑”,就會(huì)根據(jù)文件夾中的yaml文件生成各個(gè)對(duì)象。首先 chart.yaml 作為這個(gè)chart的說(shuō)明,描述應(yīng)用的版本信息和應(yīng)用名稱(chēng)等等。然后values.yaml,configmap.yaml配置應(yīng)用和k8s的一些參數(shù)。_helpers.tpl設(shè)置一些公共設(shè)置如應(yīng)用的名稱(chēng)、label、賬戶(hù)等等。然后deployment.yaml讀取values.yaml,_helpers.tpl等等這些配置的值,生成deployment對(duì)象,生成和管理Pod,然后用戶(hù)通過(guò)訪(fǎng)問(wèn)service.yaml的對(duì)象,找到對(duì)應(yīng)的Pod,訪(fǎng)問(wèn)pod中的應(yīng)用程序。

image.png

以HelloWorld項(xiàng)目為例,HelloWorld項(xiàng)目git地址。(我沒(méi)有寫(xiě)代碼)

  1. 下載代碼到本地,將下載的代碼拷貝到安裝了helm的服務(wù)器
  2. 執(zhí)行helm upgrade命令
#創(chuàng)建名稱(chēng)空間
kubectl create namespace ${kube_namespace}
#鏡像倉(cāng)庫(kù)為私有鏡像倉(cāng)庫(kù) 需要再k8s集群中設(shè)置secret信息
kubectl create secret docker-registry qcloudregistrykey --docker-server=ccr.ccs.tencentyun.com --docker-username=demaxiya --docker-password=helloworld  -n ${kube_namespace}
#執(zhí)行安裝
helm upgrade -i  --namespace=${kube_namespace}  --set image.repository=${DOCKER_IMAGE} \
--set image.tag=${GIT_TAG} --set replicaCount=${params.replica_count} --set nameOverride=${GIT_REPO}  ${GIT_REPO} ./deploy/helm/

成功運(yùn)行后可以看到,這里ready是1/1,假如出現(xiàn)錯(cuò)誤可以使用命令kubectl -n 名稱(chēng)空間 describe pod pod的name來(lái)查看詳細(xì)的錯(cuò)誤,或者通過(guò)kubectl -n 名稱(chēng)空間 logs pod的name查看錯(cuò)誤日志排錯(cuò)。使用helm upgrade成功發(fā)布應(yīng)用到k8s集群后,我們準(zhǔn)備工作算是做完了,開(kāi)始正式使用jenkins實(shí)現(xiàn)CI/CD.


image.png

使用jenkins實(shí)現(xiàn)CI/CD

參考上文的流程示意,安裝上文的步驟我們一步步實(shí)現(xiàn)流水線(xiàn)。

jenkins配置

  • jenkins安裝插件Git Parameter、Environment Injector Plugin、Generic Webhook Trigger Plugin
  • 新建任務(wù)->填寫(xiě)任務(wù)名稱(chēng)->選擇“流水線(xiàn)”(并非多分支流水線(xiàn))->確定
  • 勾選Prepare an environment for the run,配置自己需要使用的內(nèi)置的環(huán)境變量
  • 勾選參數(shù)化構(gòu)建,配置用戶(hù)自己填寫(xiě)的變量
  • 勾選Generic Webhook Trigger,填寫(xiě)token值,使私有g(shù)it倉(cāng)庫(kù)能發(fā)送請(qǐng)求到j(luò)enkins,使jenkins實(shí)現(xiàn)自動(dòng)構(gòu)建
  • 配置流水線(xiàn)scm


    image.png

這里內(nèi)置了GIT_REPO是為了設(shè)置鏡像地址、service的名稱(chēng)等,具體參考jenkinsfile。
這里內(nèi)置了kafka、mongo信息,因?yàn)椴煌h(huán)境,以來(lái)的服務(wù)地址不一樣,內(nèi)置在這里方便改動(dòng)(但安全性會(huì)相應(yīng)降低)


image.png

這里的git參數(shù)必須對(duì)應(yīng)SCM配置為git才能生效

image.png

image.png

因?yàn)槲覀兯接袀}(cāng)庫(kù)是bitbucket,因此它必須配置token,不然會(huì)報(bào)錯(cuò)404.在我們jenkins這里配置后,去自己的git倉(cāng)庫(kù)配置webhook,url填:http://jenkins賬戶(hù):jenkins密碼@jenkins地址:jenkins端口/generic-webhook-trigger/invoke?token=jenkins中配置的token值
image.jpg

這里的Credentials來(lái)自配置,系統(tǒng)管理->manager credentials->全局->添加憑據(jù)。這里配置git倉(cāng)庫(kù)的賬戶(hù)密碼。
這樣我們整個(gè)流水線(xiàn)就配置好了,可能你會(huì)有疑惑,怎么沒(méi)有連接鏡像倉(cāng)庫(kù)和k8s集群。怎么和網(wǎng)上的教程不一樣,下面我們看下整個(gè)CI/CD的核心,jenkinsfile pipeline。

jenkinsfile解析

首先看下jenkinfile的代碼

def createTag() {
    // 定義一個(gè)版本號(hào)作為當(dāng)次構(gòu)建的版本,輸出結(jié)果 20191210175842_69
    def yesterday= sh(returnStdout: true,script: 'date  +"%Y-%m-%d" -d  "-1 days"').trim()
    def commitCount= sh(returnStdout: true,script: "git rev-list HEAD --count --since=${yesterday}").trim()
    def dateNow= sh(returnStdout: true,script: 'date "+%-m%d"').trim()   
    return  "v1.0.${dateNow}.${commitCount}"
}
image_tag = "default"  //定一個(gè)全局變量,存儲(chǔ)Docker鏡像的tag(版本)
pipeline {
    agent any
    environment {
        GIT_BRANCH = "${env.gitTargetBranch}"  //項(xiàng)目的分支
        GIT_TAG = createTag()
        GIT_REPO= "${env.GIT_REPO}"
        DOCKER_CONTEXT_PATH="${env.CONTEXT_PATH}"
        DOCKER_REGISTER_CREDS = credentials('e2e1cb5f-5538-43d7-2-14561') //docker registry憑證
        DOCKER_REGISTRY = "fwafa.421.42.com" //Docker倉(cāng)庫(kù)地址
        DOCKER_NAMESPACE = "321"  //命名空間
        DOCKER_IMAGE = "${DOCKER_REGISTRY}/${DOCKER_NAMESPACE}/${GIT_REPO}" //Docker鏡像地址
        KUBECONFIG="/root/.kube/config:/root/.kube/develop-config" //k8s集群信息
    }
    parameters {
        string(name: 'replica_count', defaultValue: '2', description: '容器副本數(shù)量')
    }
    stages {
        stage('Test Pipeline Stages') {
            when {
                 environment name: 'deploy_env', value: 'test'
            }
            stages {
                stage('Params Analyze') {
                    agent any
                    steps {
                    echo "1. 構(gòu)建參數(shù)檢查"
                    echo "項(xiàng)目名稱(chēng) ${env.GIT_REPO}"
                    
                    //    script {
                    //         if (env.gitTargetBranch.indexOf("develop") == -1&&"${env.deploy_env}"=="develop") {
                    //           error "當(dāng)前分支,非開(kāi)發(fā)分支,不允許構(gòu)建!"
                    //         } else if (env.gitTargetBranch.indexOf("release") == -1&&"${env.deploy_env}"=="pre-release") {
                    //             error "當(dāng)前分支,非預(yù)發(fā)布分支,不允許構(gòu)建!"
                    //         } else if (env.gitTargetBranch.indexOf("master") == -1&&"${env.deploy_env}"=="prod"){
                    //            error "當(dāng)前分支,非生產(chǎn)分支,不允許構(gòu)建!"
                    //         }                  
                    //     }  
                    }
                }
                stage('Code Pull') {
                    steps {
                        echo "2. 代碼拉取"
                        script {
                            checkout([$class: 'GitSCM', branches: [[name: '$gitTargetBranch']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: "09d880cf-48af-4ce1-ac85-5f0c223e2559", url: "${env.GIT_URL}"]]])
                            }               
                                    
                    }
                }
                stage('Code Analyze') {
                    agent any
                    steps {
                    echo "3. 代碼靜態(tài)檢查"
                    }
                }
                stage('Docker Build') {
                    steps {
                        echo "4. 構(gòu)建Docker鏡像"
                        echo "鏡像地址: ${DOCKER_IMAGE}"
                        echo "鏡像標(biāo)簽: ${GIT_TAG}"
                        script {
                            //登錄Docker倉(cāng)庫(kù)
                            sh "docker login -u ${DOCKER_REGISTER_CREDS_USR} -p ${DOCKER_REGISTER_CREDS_PSW} ${DOCKER_REGISTRY}"  
                            //通過(guò)--build-arg將profile進(jìn)行設(shè)置,以區(qū)分不同環(huán)境進(jìn)行鏡像構(gòu)建
                            sh "docker build --add-host nuget.degea31.com:182.132.313.37 -t ${DOCKER_IMAGE}:${GIT_TAG} ${DOCKER_CONTEXT_PATH}"
                            sh "docker push ${DOCKER_IMAGE}:${GIT_TAG}"
                            sh "docker rmi -f ${DOCKER_IMAGE}:${GIT_TAG}"
                        }
                    }
                }
            }
        }
        //發(fā)布部分 測(cè)試、預(yù)發(fā)、生產(chǎn)都執(zhí)行
        stage('Helm Deploy') {
            agent {
                docker {
                    image 'wzrdtales/helm-kubectl'
                    args '-u root:root -v /root/.kube:/root/.kube'
                }
            }
            steps {
                echo "5. 部署到K8s"  
                script {
                    //設(shè)置k8s上下文
                    def kube_context = ""
                    def kube_namespace = ""
                    if ("${env.deploy_env}"== "test") {
                        kube_context = "kubernetes-admin@kubernetes"
                        kube_namespace = "test"
                    } else if ("${env.deploy_env}" == "pre-release") {
                        kube_context = "master"
                        kube_namespace = "kube-pre-release"
                    } else if ("${env.deploy_env}" == "master"){
                         kube_context = "kubernetes-admin@kubernetes"
                    }
                    //多集群時(shí) 設(shè)置上下文
                    sh "kubectl config use-context ${kube_context}"
                    //cat將文件重定向輸出到臨時(shí)文件 envsubst替換環(huán)境變量
                    sh "cat ./deploy/helm/values.yaml > tmp.yaml"
                    sh "envsubst < tmp.yaml > ./deploy/helm/values.yaml"
                     //根據(jù)不同環(huán)境將服務(wù)部署到不同的namespace下,這里使用分支名稱(chēng)
                    sh "helm upgrade -i  --namespace=${kube_namespace}  --set image.repository=${DOCKER_IMAGE} --set image.tag=${GIT_TAG} --set replicaCount=${params.replica_count} --set nameOverride=${GIT_REPO}  ${GIT_REPO} ./deploy/helm/"                
                }
            } 
        }
    }
}

jenkins pipeline用的是Groovy寫(xiě)的,跟java使用同樣的jvm運(yùn)行,而jvm和CLR又很相似,所以這些代碼理解起來(lái)也很簡(jiǎn)單。所以只講幾個(gè)不容易理解的點(diǎn)。

            when {
                 environment name: 'deploy_env', value: 'develop'
            }

還記得我們說(shuō)的有兩個(gè)jenkins任務(wù),一個(gè)對(duì)應(yīng)測(cè)試環(huán)境k8s,一個(gè)對(duì)應(yīng)生產(chǎn)環(huán)境k8s。這里是因?yàn)镻arams Analyze、Code Pull、Code Analyze、Docker Build這幾個(gè)步驟都僅在測(cè)試環(huán)境那個(gè)jenkins任務(wù)執(zhí)行,所以這里根據(jù)環(huán)境設(shè)置了執(zhí)行條件(Params Analyze應(yīng)該生產(chǎn)環(huán)境的jenkins任務(wù)也需要這個(gè)步驟,但是為了方便調(diào)試,就直接把Params Analyze中的內(nèi)容全屏蔽了,等同于兩個(gè)jenkins任務(wù)都沒(méi)有這個(gè)步驟)

 sh "docker build --add-host nuget.3131.com:182.2fa.2wq312.37 -t ${DOCKER_IMAGE}:${GIT_TAG} ${DOCKER_CONTEXT_PATH}"

docker build時(shí),加了參數(shù) --add-host nuget.bazhuayu.com:182.254.222.37這是因?yàn)槲覀兊腄ockerfile中的基礎(chǔ)鏡像沒(méi)有相應(yīng)host,當(dāng)docker restore時(shí),無(wú)法識(shí)別私有nuget服務(wù)器域名,這里還指定“”${DOCKER_CONTEXT_PATH}"為構(gòu)建上下文,至于構(gòu)建上下文,之前的內(nèi)容也說(shuō)過(guò)了,具體參考相關(guān)博客。

      agent {
                docker {
                    image 'wzrdtales/helm-kubectl'
                    args '-u root:root -v /root/.kube:/root/.kube'
                }
            }

這里agent docker表示當(dāng)流水線(xiàn)執(zhí)行這一步時(shí),在基于鏡像wzrdtales/helm-kubectl生成的容器中執(zhí)行,當(dāng)steps 中內(nèi)容執(zhí)行完成后,自動(dòng)銷(xiāo)毀這個(gè)容器。這個(gè)鏡像wzrdtales/helm-kubectl是含有kubectl、helm、gettext這三個(gè)客戶(hù)端工具的鏡像。使我們能使用helm命令連接k8s集群、envsubst 命令替換values.yaml中環(huán)境變量。wzrdtales/helm-kubectl這個(gè)鏡像的dockerfile可以看下
這里 -v /root/.kube:/root/.kube是因?yàn)闉榱吮苊狻癲ocker in docker”,具體參考前文。

#wzrdtales/helm-kubectl的dockerfile
FROM dtzar/helm-kubectl #基礎(chǔ)鏡像
MAINTAINER Tobias Gurtzick <magic@wizardtales.com> 
RUN apk add --update --no-cache gettext && rm -fr /var/cache/apk/* #安裝gettext 
#dtzar/helm-kubectl 的dockerfile
FROM alpine:3.11

ARG VCS_REF
ARG BUILD_DATE

# Metadata
LABEL org.label-schema.vcs-ref=$VCS_REF \
      org.label-schema.name="helm-kubectl" \
      org.label-schema.url="https://hub.docker.com/r/dtzar/helm-kubectl/" \
      org.label-schema.vcs-url="https://github.com/dtzar/helm-kubectl" \
      org.label-schema.build-date=$BUILD_DATE

# Note: Latest version of kubectl may be found at:
# https://github.com/kubernetes/kubernetes/releases
ENV KUBE_LATEST_VERSION="v1.18.2"
# Note: Latest version of helm may be found at
# https://github.com/kubernetes/helm/releases
ENV HELM_VERSION="v3.2.1"

RUN apk add --no-cache ca-certificates bash git openssh curl \
    && wget -q https://storage.googleapis.com/kubernetes-release/release/${KUBE_LATEST_VERSION}/bin/linux/amd64/kubectl -O /usr/local/bin/kubectl \
    && chmod +x /usr/local/bin/kubectl \
    && wget -q https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz -O - | tar -xzO linux-amd64/helm > /usr/local/bin/helm \
    && chmod +x /usr/local/bin/helm

WORKDIR /config

CMD bash

最后這一段就是將服務(wù)發(fā)布到k8s集群中了,因?yàn)槲覀兪莾蓚€(gè)jenkin任務(wù)發(fā)布到不同的k8s集群,但是需要使用同一個(gè)jenkinsfile。因此根據(jù)環(huán)境判斷,設(shè)置不同的k8s上下文,使用 Kubernetes API 訪(fǎng)問(wèn)集群
。然后envsubst 命令替換掉之前設(shè)置的kafka、mongo等環(huán)境變量,最后根據(jù)helm upgrade -i命令發(fā)布到k8s集群。

 environment {
               KUBECONFIG="/root/.kube/config:/root/.kube/develop-config" //k8s集群信息
         }
               //多集群時(shí) 設(shè)置上下文
                    sh "kubectl config use-context ${kube_context}"
                    //cat將文件重定向輸出到臨時(shí)文件 envsubst替換環(huán)境變量
                    sh "cat ./deploy/helm/values.yaml > tmp.yaml"
                    sh "envsubst < tmp.yaml > ./deploy/helm/values.yaml"
                     //根據(jù)不同環(huán)境將服務(wù)部署到不同的namespace下,這里使用分支名稱(chēng)
                    sh "helm upgrade -i  --namespace=${kube_namespace}  --set image.repository=${DOCKER_IMAGE} --set image.tag=${GIT_TAG} --set replicaCount=${params.replica_count} --set nameOverride=${GIT_REPO}  ${GIT_REPO} ./deploy/helm/"

然后我們構(gòu)建多了很可能會(huì)出現(xiàn)很多的構(gòu)建記錄,感覺(jué)寫(xiě)代碼多了后,人多少有點(diǎn)強(qiáng)迫癥,沒(méi)用的東西就喜歡刪除,這個(gè)就是刪除構(gòu)建歷史的腳本

def jobName = "hello"
def maxNumber = 1888
  
Jenkins.instance.getItemByFullName(jobName).builds.findAll {
  it.number <= maxNumber
}.each {
  it.delete()
}

然后記起來(lái),遷移nuget也是用腳本,網(wǎng)上可以找到,腳本大概就是循環(huán)推送到nuget倉(cāng)庫(kù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容