通過Jenkins流水線自動部署.NetCore應(yīng)用到K8S集群

上篇我們講解了如何將.netCore程序以Docker部署的兩種方法,http://www.itdecent.cn/p/a6c78a2c15f2,這是我們本文自動化部署k8s的基礎(chǔ),便于我們理解自動化過程主要的步驟。
今天我們嘗試與Jenkins集成k8s自動化發(fā)布K8S集群節(jié)點中。

基本思路:
發(fā)起流水線構(gòu)造時我們傳入一系列參數(shù)(JSON結(jié)構(gòu)),然后由jenkins從git拉取流水線腳本,腳本根據(jù)我們傳入的參數(shù)對腳本進行動態(tài)替換并執(zhí)行,構(gòu)建完docker鏡像后,將鏡像推送到私有倉庫,然后我們會根據(jù)json數(shù)據(jù),docker鏡像路徑等數(shù)據(jù)動態(tài)生成k8s的部署yaml文件并交由k8s執(zhí)行部署。
這一系列構(gòu)建過程是由jenkins連接在k8s 某一個master節(jié)點進行執(zhí)行。

1.準(zhǔn)備工作

我們沒有搭建自己的GIT代碼倉庫,為了測試我們使用Gitee進行托管(github太慢)
Jenkins下載并安裝插件
下載gitee插件,配置gitee憑據(jù)
此外腳本中使用了readJSON,writeJSON
要使用這兩個方法,必須安裝插件Pipeline Utility Steps,否則報錯:java.lang.NoSuchMethodError: No such DSL method 'readJSON'
另外需要安裝pipline的基本插件:
Pipeline: GitHub
Pipeline: Basic Steps
Jenkins創(chuàng)建gitee帳戶憑據(jù)

然后在jenkins中創(chuàng)建一個憑據(jù)供使用:

Jenkins添加k8s Master節(jié)點,用于部署
jenkins 系統(tǒng)管理--節(jié)點管理,添加一個節(jié)點
節(jié)點名稱及標(biāo)簽取為:k8s-master


master節(jié)點安裝依賴項
yum install lttng-ust libcurl openssl-libs krb5-libs libicu zlib -y
master節(jié)點安裝JDK
我們把JDK包直接放到 /root/jenkins/jdk目錄即可,jenkins我們前面指定了節(jié)點工作目錄是/root/jenkins,則添加節(jié)點時jenkins會自動查找到這個jdk目錄。
master節(jié)點安裝git
由于我們使用master節(jié)點拉取代碼,master節(jié)點需要安裝git,運行以下命令安裝
yum install -y git
master節(jié)點放置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ù)配置,實現(xiàn)標(biāo)準(zhǔn)化。
Jenkins中新建一個任務(wù),模板選擇【流水線】


添加一文本參數(shù)

Jenkins中的參數(shù)是,用于向流水線腳本傳遞動態(tài)數(shù)據(jù),參數(shù)可以簡單理解為腳本替換用的占位符。
說明:因為模板只是定義了整體執(zhí)行過程,模板不關(guān)注項目信息,比如項目服務(wù)名,JDK/.net版本,代碼路徑,部署資源,發(fā)布后的名稱等信息,這些信息我們可以通過參數(shù)形式傳遞進來。
通常我們會定義定義很多個文本參數(shù),但每一參數(shù)都定義一個的模式我們傳參麻煩,另外也不易擴展,我這里采用一個文本參數(shù)類型(內(nèi)容用JSON結(jié)構(gòu))解決所有,避免頻繁的變更模板,因為現(xiàn)實中參數(shù)的變化的頻率是比較高的。
參數(shù)名稱我們命名為JSON_BODY,值隨意寫一個JSON結(jié)構(gòu),因為是一個模板,我們這里的值只是一個參考,實際上是會被使用該模板的流水線重寫掉的。

流水線腳本我們采用可以采用從GIT拉取,也可以直接編寫腳本,為了便于腳本管理與更新,我們采用GIT來管理,目前測試我們托管到Gitee平臺。

流水線腳本參考

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 "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ù)解析錯誤" + errx)
    sh "exit 1"
}
 println("參數(shù)解析完畢,開始構(gòu)建準(zhǔn)備")
//使用k8s節(jié)點執(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文件模板參考:
這個Yaml文件就是一個yaml程序完整部署的模板,我們通過JSON傳入,然后進行替換,再交由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)使用,可以對外暴露統(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)建一個流水線

新建一流水線,取名netCore01,復(fù)制自template-netCore模板


點擊保存,到下一步,無需任何修改,直接保存即可。

3 . netCore環(huán)境準(zhǔn)備

** 下載.netcore sdk 3.1**
https://dotnet.microsoft.com/download/dotnet-core/3.1


下載到本地
上傳sdk包并解壓

master節(jié)點
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

查看及安裝依賴項


yum install lttng-ust libcurl openssl-libs krb5-libs libicu zlib -y

4. 執(zhí)行自動化部署

代碼我們提交到git,然后Jenkins選擇netCore01流水線,選擇使用參數(shù)構(gòu)建,輸入JSON參數(shù),如下圖:


構(gòu)建過程會下載aspnet:3.1-buster-slim,建議提前docker pull下載好,這樣構(gòu)建會快很多。

jenkins自動下載有點慢
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控制臺輸出查看構(gòu)建過程,已經(jīng)成功。

[Pipeline] echo
#############################################部署到集群成功##################################################
[Pipeline] }
[Pipeline] // stage
[Pipeline] echo
#############################################流水線執(zhí)行成功##################################################
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

訪問測試下:
kubectl get pods --all-namespaces -owide
http://ip:31001/default/gettime #我們自己寫的一個測試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)前項目git地址
sdkVersion:使用的.netcoreSDK版本
replicas:部署幾個pod
branch:拉取代碼的哪個分支
defaultImageHost:鏡像倉庫地址,鏡像構(gòu)建完成后需要推送到倉庫,供pod所在節(jié)點獲取生成容器。
nodePort 映射到主機的端口,如果你搭建了網(wǎng)關(guān),可以不將端口映射到主機,可配合API網(wǎng)關(guān)+域名實現(xiàn)動態(tài)路由。

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

相關(guān)閱讀更多精彩內(nèi)容

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