通过Jenkins流水线自动部署.NetCore应用到K8S集群

上篇我们讲解了如何将.netCore程序以Docker部署的两种方法,https://www.jianshu.com/p/a6c78a2c15f2,这是我们本文自动化部署k8s的基础,便于我们理解自动化过程主要的步骤。
今天我们尝试与Jenkins集成k8s自动化发布K8S集群节点中。

基本思路:
发起流水线构造时我们传入一系列参数(JSON结构),然后由jenkins从git拉取流水线脚本,脚本根据我们传入的参数对脚本进行动态替换并执行,构建完docker镜像后,将镜像推送到私有仓库,然后我们会根据json数据,docker镜像路径等数据动态生成k8s的部署yaml文件并交由k8s执行部署。
这一系列构建过程是由jenkins连接在k8s 某一个master节点进行执行。

1.准备工作

我们没有搭建自己的GIT代码仓库,为了测试我们使用Gitee进行托管(github太慢)
Jenkins下载并安装插件
下载gitee插件,配置gitee凭据
此外脚本中使用了readJSON,writeJSON
要使用这两个方法,必须安装插件Pipeline Utility Steps,否则报错:java.lang.NoSuchMethodError: No such DSL method 'readJSON'
另外需要安装pipline的基本插件:
Pipeline: GitHub
Pipeline: Basic Steps
Jenkins创建gitee帐户凭据

然后在jenkins中创建一个凭据供使用:

Jenkins添加k8s Master节点,用于部署
jenkins 系统管理--节点管理,添加一个节点
节点名称及标签取为:k8s-master


master节点安装依赖项
yum install lttng-ust libcurl openssl-libs krb5-libs libicu zlib -y
master节点安装JDK
我们把JDK包直接放到 /root/jenkins/jdk目录即可,jenkins我们前面指定了节点工作目录是/root/jenkins,则添加节点时jenkins会自动查找到这个jdk目录。
master节点安装git
由于我们使用master节点拉取代码,master节点需要安装git,运行以下命令安装
yum install -y git
master节点放置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创建流水线构建模板

创建模板的好处是后续其他流水线可以直接使用该模板脚本,不用重复配置,实现标准化。
Jenkins中新建一个任务,模板选择【流水线】


添加一文本参数

Jenkins中的参数是,用于向流水线脚本传递动态数据,参数可以简单理解为脚本替换用的占位符。
说明:因为模板只是定义了整体执行过程,模板不关注项目信息,比如项目服务名,JDK/.net版本,代码路径,部署资源,发布后的名称等信息,这些信息我们可以通过参数形式传递进来。
通常我们会定义定义很多个文本参数,但每一参数都定义一个的模式我们传参麻烦,另外也不易扩展,我这里采用一个文本参数类型(内容用JSON结构)解决所有,避免频繁的变更模板,因为现实中参数的变化的频率是比较高的。
参数名称我们命名为JSON_BODY,值随意写一个JSON结构,因为是一个模板,我们这里的值只是一个参考,实际上是会被使用该模板的流水线重写掉的。

流水线脚本我们采用可以采用从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版本,如果不填写默认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("参数解析错误" + errx)
    sh "exit 1"
}
 println("参数解析完毕,开始构建准备")
//使用k8s节点执行
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("#############################################流水线执行成功##################################################")
}

/root/jenkins/deploy-template/deploy-project.yaml文件模板参考:
这个Yaml文件就是一个yaml程序完整部署的模板,我们通过JSON传入,然后进行替换,再交由k8s执行。

#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等任一网关使用,可以对外暴露统一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 网关域名或IP
  - host: {{host}}
    http:
      paths:
      #path路径,如service1
      - path: /{{name}}/
        backend:
          serviceName: {{name}}
          servicePort: 80

2.使用模板创建一个流水线

新建一流水线,取名netCore01,复制自template-netCore模板


点击保存,到下一步,无需任何修改,直接保存即可。

3 . netCore环境准备

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


下载到本地
上传sdk包并解压

master节点
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. 执行自动化部署

代码我们提交到git,然后Jenkins选择netCore01流水线,选择使用参数构建,输入JSON参数,如下图:


构建过程会下载aspnet:3.1-buster-slim,建议提前docker pull下载好,这样构建会快很多。

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控制台输出查看构建过程,已经成功。

[Pipeline] echo
#############################################部署到集群成功##################################################
[Pipeline] }
[Pipeline] // stage
[Pipeline] echo
#############################################流水线执行成功##################################################
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

访问测试下:
kubectl get pods --all-namespaces -owide
http://ip:31001/default/gettime #我们自己写的一个测试api


另外我们看下镜像仓库中,该镜像已经存在

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中主要参数解读:
codeUrl:当前项目git地址
sdkVersion:使用的.netcoreSDK版本
replicas:部署几个pod
branch:拉取代码的哪个分支
defaultImageHost:镜像仓库地址,镜像构建完成后需要推送到仓库,供pod所在节点获取生成容器。
nodePort 映射到主机的端口,如果你搭建了网关,可以不将端口映射到主机,可配合API网关+域名实现动态路由。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容