上篇我們講解了如何將.netCore程序以Docker部署的兩種方法秕衙,http://www.reibang.com/p/a6c78a2c15f2,這是我們本文自動(dòng)化部署k8s的基礎(chǔ)攻礼,便于我們理解自動(dòng)化過程主要的步驟周霉。
今天我們嘗試與Jenkins集成k8s自動(dòng)化發(fā)布K8S集群節(jié)點(diǎn)中。
基本思路:
發(fā)起流水線構(gòu)造時(shí)我們傳入一系列參數(shù)(JSON結(jié)構(gòu))格嘁,然后由jenkins從git拉取流水線腳本温技,腳本根據(jù)我們傳入的參數(shù)對(duì)腳本進(jìn)行動(dòng)態(tài)替換并執(zhí)行革为,構(gòu)建完docker鏡像后,將鏡像推送到私有倉庫舵鳞,然后我們會(huì)根據(jù)json數(shù)據(jù)篷角,docker鏡像路徑等數(shù)據(jù)動(dòng)態(tài)生成k8s的部署yaml文件并交由k8s執(zhí)行部署。
這一系列構(gòu)建過程是由jenkins連接在k8s 某一個(gè)master節(jié)點(diǎn)進(jìn)行執(zhí)行系任。
1.準(zhǔn)備工作
我們沒有搭建自己的GIT代碼倉庫恳蹲,為了測(cè)試我們使用Gitee進(jìn)行托管(github太慢)
Jenkins下載并安裝插件
下載gitee插件,配置gitee憑據(jù)
此外腳本中使用了readJSON,writeJSON
要使用這兩個(gè)方法俩滥,必須安裝插件Pipeline Utility Steps嘉蕾,否則報(bào)錯(cuò):java.lang.NoSuchMethodError: No such DSL method 'readJSON'
另外需要安裝pipline的基本插件:
Pipeline: GitHub
Pipeline: Basic Steps
Jenkins創(chuàng)建gitee帳戶憑據(jù)
然后在jenkins中創(chuàng)建一個(gè)憑據(jù)供使用:
Jenkins添加k8s Master節(jié)點(diǎn),用于部署
jenkins 系統(tǒng)管理--節(jié)點(diǎn)管理霜旧,添加一個(gè)節(jié)點(diǎn)
節(jié)點(diǎn)名稱及標(biāo)簽取為:k8s-master
master節(jié)點(diǎn)安裝依賴項(xiàng)
yum install lttng-ust libcurl openssl-libs krb5-libs libicu zlib -y
master節(jié)點(diǎn)安裝JDK
我們把JDK包直接放到 /root/jenkins/jdk目錄即可错忱,jenkins我們前面指定了節(jié)點(diǎn)工作目錄是/root/jenkins儡率,則添加節(jié)點(diǎn)時(shí)jenkins會(huì)自動(dòng)查找到這個(gè)jdk目錄。
master節(jié)點(diǎn)安裝git
由于我們使用master節(jié)點(diǎn)拉取代碼以清,master節(jié)點(diǎn)需要安裝git儿普,運(yùn)行以下命令安裝
yum install -y git
master節(jié)點(diǎn)放置dotnetsdk3.1包
從微軟下載https://dotnet.microsoft.com/download/dotnet-core/3.1
目錄:/root/jenkins/tools/dotnetsdk3.1
mkdir /root/jenkins/tools/dotnetsdk3.1
cd /root/jenkins/tools
rz 上傳壓縮包dotnet-sdk-3.1.102-linux-x64.tar.gz
tar -zxf dotnet-sdk-3.1.102-linux-x64.tar.gz -C dotnetsdk3.1
2.Jenkins創(chuàng)建流水線構(gòu)建模板
創(chuàng)建模板的好處是后續(xù)其他流水線可以直接使用該模板腳本,不用重復(fù)配置掷倔,實(shí)現(xiàn)標(biāo)準(zhǔn)化眉孩。
Jenkins中新建一個(gè)任務(wù),模板選擇【流水線】
添加一文本參數(shù)
Jenkins中的參數(shù)是勒葱,用于向流水線腳本傳遞動(dòng)態(tài)數(shù)據(jù)浪汪,參數(shù)可以簡單理解為腳本替換用的占位符。
說明:因?yàn)槟0逯皇嵌x了整體執(zhí)行過程凛虽,模板不關(guān)注項(xiàng)目信息死遭,比如項(xiàng)目服務(wù)名,JDK/.net版本凯旋,代碼路徑呀潭,部署資源,發(fā)布后的名稱等信息至非,這些信息我們可以通過參數(shù)形式傳遞進(jìn)來钠署。
通常我們會(huì)定義定義很多個(gè)文本參數(shù),但每一參數(shù)都定義一個(gè)的模式我們傳參麻煩睡蟋,另外也不易擴(kuò)展踏幻,我這里采用一個(gè)文本參數(shù)類型(內(nèi)容用JSON結(jié)構(gòu))解決所有枷颊,避免頻繁的變更模板戳杀,因?yàn)楝F(xiàn)實(shí)中參數(shù)的變化的頻率是比較高的。
參數(shù)名稱我們命名為JSON_BODY夭苗,值隨意寫一個(gè)JSON結(jié)構(gòu)信卡,因?yàn)槭且粋€(gè)模板,我們這里的值只是一個(gè)參考题造,實(shí)際上是會(huì)被使用該模板的流水線重寫掉的傍菇。
流水線腳本我們采用可以采用從GIT拉取,也可以直接編寫腳本界赔,為了便于腳本管理與更新丢习,我們采用GIT來管理,目前測(cè)試我們托管到Gitee平臺(tái)淮悼。
流水線腳本參考
println("#############################################開始流水線##################################################")
//env.JOB_NAME ***
//env.WORKSPACE /var/jenkins_home/workspace/***
//env.K8S_TYPE="$params.K8S_TYPE"
//env.DOCKER_TYPE="$params.DOCKER_TYPE"
def jobName = "${JOB_NAME}"
def defaultEnv,defaultApiHost,defaultImageHost,defaultApolloMeta, imageVersion,versionTimestamp, codeUrl, branch, appId, serviceAndversion, sdkVersion, namespace, nodes, host, replicas, cpu, memory, sonarUrl, sonarKey, sonarToken, sonarResult, devopsUrl, version, service, imageName,nodePort
try {
def paraBodyJson = readJSON text: "${params.JSON_BODY}"
defaultImageHost = paraBodyJson.defaultImageHost
if (!defaultImageHost?.trim()) {
println("defaultImageHost 為空")
defaultImageHost = "192.168.101.101:30083/janet/";
}
service = paraBodyJson.service
if (!service?.trim()) {
println("service 不能為空")
sh "exit 1"
}
version = paraBodyJson.version
if (!version?.trim()) {
println("version 不能為空")
sh "exit 1"
}
serviceAndversion = service + "-" + version
println("service-version:"+serviceAndversion)
appId = paraBodyJson.appId
if (!appId?.trim()) {
println("appId 不能為空")
sh "exit 1"
}
nodePort=paraBodyJson.nodePort
if (!nodePort?.trim()) {
println("nodePort 不能為空")
sh "exit 1"
}
codeUrl = paraBodyJson.codeUrl
imageVersion = paraBodyJson.imageVersion
if (!codeUrl?.trim() && !imageVersion?.trim()) {
println("codeUrl和imageVersion 不能同時(shí)為空")
sh "exit 1"
}
//分支
branch = paraBodyJson.branch
if (!branch?.trim()) {
branch = "master"
}
namespace = paraBodyJson.namespace
if (!namespace?.trim()) {
println("namespace 不能為空")
sh "exit 1"
}
//版本
versionTimestamp = paraBodyJson.versionTimestamp
if (!versionTimestamp?.trim()) {
versionTimestamp = version + "." + System.currentTimeMillis()
}
//sdk版本咐低,java/.net均有不同的SDK版本,如果不填寫默認(rèn)jdk8
sdkVersion = paraBodyJson.sdkVersion
if (!sdkVersion?.trim()) {
sdkVersion = "openjdk8"
}
defaultApolloMeta=paraBodyJson.defaultApolloMeta;
nodes = paraBodyJson.node
if (!nodes?.trim()) {
nodes = ""
}
replicas = paraBodyJson.replicas
if (!replicas?.trim()) {
replicas = "1"
}
cpu = paraBodyJson.cpu
if (!cpu?.trim()) {
cpu = "0"
}
memory = paraBodyJson.memory
if (!memory?.trim()) {
memory = "0"
}
defaultApiHost = paraBodyJson.defaultApiHost
if (!defaultApiHost?.trim()) {
println("defaultApiHost 為空")
defaultApiHost = "api.test.com";
}
host = paraBodyJson.host
if (!host?.trim()) {
host = defaultApiHost
}
} catch (errx) {
println("參數(shù)解析錯(cuò)誤" + errx)
sh "exit 1"
}
println("參數(shù)解析完畢袜腥,開始構(gòu)建準(zhǔn)備")
//使用k8s節(jié)點(diǎn)執(zhí)行
node('k8s-master') {
imageName = defaultImageHost + service + ":" +versionTimestamp
stage('Clone Code') {
println("#############################################開始拉取代碼##################################################")
sh 'find /root/.m2/repository/ -name "*lastUpdated*" | xargs rm -rf'
git branch: branch, url: codeUrl
println("#############################################拉取代碼成功##################################################")
}
stage('Dotnet Build') {
println("#############################################開始打包##################################################")
def dockerfile = """
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
ENV ASPNETCORE_URLS http://+:8020
EXPOSE 8020
COPY ./build .
RUN sed -i 's/TLSv1.2/TLSv1.0/g' /etc/ssl/openssl.cnf
ENTRYPOINT ["dotnet", "{{dllname}}"]
"""
if ("dotnetsdk3.1".equals(sdkVersion.toString())) {
dockerfile = dockerfile.replace("aspnet:3.1", "aspnet:3.1")
}
else if("dotnetsdk2.0".equals(sdkVersion.toString())) {
dockerfile = dockerfile.replace("aspnet:3.1", "aspnet:2.0")
}
withEnv(["DOTNET_HOME=/root/jenkins/tools/${sdkVersion}"]) {
sh '"$DOTNET_HOME/dotnet" --version'
def csprojname = sh(script: 'echo *.csproj', returnStdout: true).replace('\n', "")
def out=sh(script:"ls "+csprojname,returnStatus:true)
if(out == 2){
println("文件:" + csprojname+" 不存在")
sh "exit 1"
}
// sh '"$DOTNET_HOME/dotnet" restore '+csprojname+' -s http://xxxx.com/repository/nuget-group' //如果有私有nuget倉庫可以加上-s 倉庫地址
sh '"$DOTNET_HOME/dotnet" restore '+csprojname
sh '"$DOTNET_HOME/dotnet" build '+csprojname+' -c Release -o ./build '
sh 'rm -rf ${WORKSPACE}/docker'
sh 'mkdir -p ${WORKSPACE}/docker'
sh 'cp -r ${WORKSPACE}/build/ ./docker/'
def dllname = csprojname.replace("csproj","dll")
out=sh(script:"ls ./build/"+dllname,returnStatus:true)
if(out == 2){
println("文件:" + dllname+" 不存在")
sh "exit 1"
}
dockerfile = dockerfile.replace("{{dllname}}", dllname)
sh "echo '${dockerfile}' >./docker/Dockerfile"
}
println("#############################################打包成功##################################################")
}
stage('Build Image') {
println("#############################################開始build docker鏡像##################################################")
sh "docker build -t ${imageName} ${WORKSPACE}/docker/."
sh "docker push ${imageName}"
println("#############################################build docker鏡像件成功##################################################")
}
stage('K8S Deploy') {
println("#############################################開始部署到集群##################################################")
def yamldir = "/root/jenkins/deploy/deploy-"
def yamlTemplatedir = "/root/jenkins/deploy-template/deploy-project.yaml"
println(yamlTemplatedir)
def fileContents = readFile file: yamlTemplatedir, encoding: "UTF-8"
println("開始處理yaml模板文件");
fileContents = fileContents.replace("{{namespace}}", namespace)
fileContents = fileContents.replace("{{name}}", serviceAndversion)
fileContents = fileContents.replace("{{replicas}}", replicas)
fileContents = fileContents.replace("{{cpu}}", cpu)
fileContents = fileContents.replace("{{host}}", host)
fileContents = fileContents.replace("{{memory}}", memory)
//println("yaml02...")
fileContents = fileContents.replace("{{image}}", imageName)
fileContents = fileContents.replace("{{appId}}", appId)
// println("yaml03...")
fileContents = fileContents.replace("{{apolloMeta}}", defaultApolloMeta)
fileContents = fileContents.replace("{{nodePort}}", nodePort)
println("yaml模板處理完畢")
println "${yamldir}${serviceAndversion}.yaml"
sh "rm -rf ${yamldir}${serviceAndversion}.yaml"
sh "echo '${fileContents}' >${yamldir}${serviceAndversion}.yaml"
println "kubectl apply -f ${yamldir}${serviceAndversion}.yaml"
//sh "kubectl apply -f /root/jenkins/deployment/deployment-${serviceAndversion}.yaml"
def consoleApply = sh(script: 'kubectl apply -f '+yamldir + serviceAndversion + '.yaml', returnStdout: true)
String[] consoleArr = consoleApply.split("\n|\r")
for (console in consoleArr) {
/* if(console.startsWith("deployment.apps") && console.endsWith("unchanged")){
println("#############################################部署到集群沒有變化见擦,流水線退出##################################################")
sh "exit 1"
}*/
}
sleep 10
println("#############################################部署到集群成功##################################################")
}
println("#############################################流水線執(zhí)行成功##################################################")
}
/root/jenkins/deploy-template/deploy-project.yaml文件模板參考:
這個(gè)Yaml文件就是一個(gè)yaml程序完整部署的模板,我們通過JSON傳入,然后進(jìn)行替換鲤屡,再交由k8s執(zhí)行损痰。
#create namespace
apiVersion: v1
kind: Namespace
metadata:
name: {{namespace}}
spec:
finalizers:
- kubernetes
---
#deploy
apiVersion: apps/v1
kind: Deployment
#kind: StatefulSet
metadata:
name: {{name}}
namespace: {{namespace}}
spec:
selector:
matchLabels:
app: {{name}}
replicas: {{replicas}}
#serviceName: {{name}}
template:
metadata:
labels:
app: {{name}}
spec:
containers:
- name: {{name}}
image: {{image}}
imagePullPolicy: Always
env:
- name: image
value: "{{name}}>{{image}}"
- name: app.id
value: "{{appId}}"
- name: TZ
value: Asia/Shanghai
ports:
- containerPort: 8020
resources:
limits:
cpu: {{cpu}}
memory: {{memory}}
requests:
cpu: {{cpu}}
memory: {{memory}}
livenessProbe:
httpGet:
path: /healthy
port: 8020
scheme: HTTP
initialDelaySeconds: 30
periodSeconds: 60
failureThreshold: 2
successThreshold: 1
timeoutSeconds: 30
readinessProbe:
httpGet:
path: /healthy
port: 8020
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 2
successThreshold: 1
timeoutSeconds: 10
---
#service
apiVersion: v1
kind: Service
metadata:
name: {{name}}
namespace: {{namespace}}
spec:
ports:
- port: 80
protocol: TCP
targetPort: 8020
nodePort: {{nodePort}}
selector:
app: {{name}}
type: NodePort
sessionAffinity: ClientIP
---
#router 配合kong/nginx等任一網(wǎng)關(guān)使用,可以對(duì)外暴露統(tǒng)一API
#如service1 api.test.com/server1/user/gettoken
#如service2 api.test.com/service2/bill/getBill
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: {{name}}
namespace: {{namespace}}
spec:
rules:
#host 網(wǎng)關(guān)域名或IP
- host: {{host}}
http:
paths:
#path路徑酒来,如service1
- path: /{{name}}/
backend:
serviceName: {{name}}
servicePort: 80
2.使用模板創(chuàng)建一個(gè)流水線
新建一流水線卢未,取名netCore01,復(fù)制自template-netCore模板
點(diǎn)擊保存役首,到下一步尝丐,無需任何修改,直接保存即可衡奥。
3 . netCore環(huán)境準(zhǔn)備
** 下載.netcore sdk 3.1**
https://dotnet.microsoft.com/download/dotnet-core/3.1
下載到本地
上傳sdk包并解壓
master節(jié)點(diǎn)
mkdir /root/jenkins/tools/dotnetsdk3.1
cd /root/jenkins/tools/
rz 上傳壓縮包
tar -zxf dotnet-sdk-3.1.102-linux-x64.tar.gz -C dotnetsdk3.1
查看及安裝依賴項(xiàng)
yum install lttng-ust libcurl openssl-libs krb5-libs libicu zlib -y
4. 執(zhí)行自動(dòng)化部署
代碼我們提交到git爹袁,然后Jenkins選擇netCore01流水線,選擇使用參數(shù)構(gòu)建矮固,輸入JSON參數(shù)失息,如下圖:
構(gòu)建過程會(huì)下載aspnet:3.1-buster-slim,建議提前docker pull下載好档址,這樣構(gòu)建會(huì)快很多盹兢。
jenkins自動(dòng)下載有點(diǎn)慢
Step 1/6 : FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
3.1-buster-slim: Pulling from dotnet/core/aspnet
68ced04f60ab: Pulling fs layer
4ddb1a571238: Pulling fs layer
我們通過jenkins控制臺(tái)輸出查看構(gòu)建過程,已經(jīng)成功守伸。
[Pipeline] echo
#############################################部署到集群成功##################################################
[Pipeline] }
[Pipeline] // stage
[Pipeline] echo
#############################################流水線執(zhí)行成功##################################################
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
訪問測(cè)試下:
kubectl get pods --all-namespaces -owide
http://ip:31001/default/gettime #我們自己寫的一個(gè)測(cè)試api
另外我們看下鏡像倉庫中绎秒,該鏡像已經(jīng)存在
k8s查看pod信息
[root@k8s-master Controllers]# kubectl get pods -n mydemos
NAME READY STATUS RESTARTS AGE
netcore-01-blue-7fdff4f9f7-9ll6p 1/1 Running 0 64s
附:JSON中主要參數(shù)解讀:
codeUrl:當(dāng)前項(xiàng)目git地址
sdkVersion:使用的.netcoreSDK版本
replicas:部署幾個(gè)pod
branch:拉取代碼的哪個(gè)分支
defaultImageHost:鏡像倉庫地址,鏡像構(gòu)建完成后需要推送到倉庫尼摹,供pod所在節(jié)點(diǎn)獲取生成容器见芹。
nodePort 映射到主機(jī)的端口,如果你搭建了網(wǎng)關(guān)蠢涝,可以不將端口映射到主機(jī)玄呛,可配合API網(wǎng)關(guān)+域名實(shí)現(xiàn)動(dòng)態(tài)路由。