kubesphere的devops流程中一键发布所有后台微服务

发布时间 2023-09-26 15:28:00作者: 万界漂泊者

一:背景

  公司的微服务项目准备上k8s,最近学习了下k8s相关的技术,目前采用的是kubesphere来进行k8s的部署,kubesphere中集成了一站式的devops流程,对于没有devops平台的公司来说很友好,可以很方便的实现代码从检出到构建打包发布等一站式流程。参考官方的文档及样例,可以很容易的搭建出一个简单的流水线,操作过,一个环境建了一个devops项目,在项目中为每个微服务搭建了一个流水线,可以实现服务的流水线发布。发现每个微服务的流水线jenkinsfile中绝大多数都是重复的,但是有一些特定的需要改动,虽然kubesphere流水线有复制的功能,可以很方便的复制一个,在其基础上进行修改,但是感觉还是比较麻烦,只有编辑者可能才清楚怎么修改,换一个来部署可能就不知道了,于是想编写一个jenkinsfile来实现后台所有微服务的发布,使用groovy,并行进行每个步骤,并行检出,并行打包推送镜像,并行发布。

 

二:准备

  本人的操作环境中,有以下需要提前准备

  1.   devops项目凭证,用到了一下三个凭证,需要在kubesphere中预先配置,其中harbor-id是harbor镜像仓库的用户名密码,gitlab-id是gitlab仓库的用户密码

      

  2.  新建的流水线需要两个运行参数,APP_NAME和BRANCH_NAME,其中APP_NAME为微服务的名称,BRANCH_NAME为分支名称

    

 三:环境变量

    在jenkinsfile中预定义了以下环境变量,将每次换环境部署需要修改或者变更的统一在环境变量中,每次部署前只需要关心环境变量中的值是否正确就行了,余下的jenkinsfile脚本不需要再关注。

environment {    
    //镜像仓库地址
    REGISTRY = '127.0.0.1'
    //代码仓库地址前缀
    GIT_PREFIX = 'http://127.0.0.1/Cloud-Platform/micro-services'
    //kubesphere中的项目名称,对应deployment部署文件的metadata.namespace
    NAMESPACE = 'bladex-test'
    DOCKERHUB_NAMESPACE = 'blade'
    VERSION = '3.1.0.RELEASE'
    NACOS_ADDR = '127.0.0.1:8848'
    NACOS_NAMESPACE = '49f83c9e-6e19-4940-8771-a66770f54ddd'
    ACTIVE_PROFILES = 'test'
  }
View Code

 

四:jenkinsfile文件

    以下是完整的jenkinsfile文件,拷贝然后在流水线中点击编辑Jenkinsfile然后粘贴保存就行了。

appNameMap = ["blade-gateway":"BladeX",
    "blade-auth":"BladeX",
    "blade-system":"BladeX",
    "blade-desk":"BladeX",
    "websocket":"Biz-WebSocket",
    "file-service":"Biz-File",
    "drone":"Biz-Api",
    "analysis":"Biz-Analysis",
    "store":"Biz-Store",
    "weather":"Biz-Weather"]
pipeline {
  agent {
    node {
      label 'maven'
    }
  }
  
   environment {
    REGISTRY = '127.0.0.1'
    GIT_PREFIX = 'http://127.0.0.1/Drone-Cloud-Platform/micro-services'
    NAMESPACE = 'bladex-test'
    DOCKERHUB_NAMESPACE = 'blade'
    VERSION = '3.1.0.RELEASE'
    NACOS_ADDR = '127.0.0.1:8848'
    NACOS_NAMESPACE = '49f83c9e-6e19-4940-8771-a66770f54ddd'
    ACTIVE_PROFILES = 'test'
  }

  stages {
  
    stage("checkout"){
        steps{
            script {
                def checkoutStageMap=prepareCheckOutStages()
                if(params.APP_NAME == ''){
                    parallel checkoutStageMap
                }else{
                    String key = getWorkDirBase(appNameMap.get(params.APP_NAME))
                    checkoutStageMap.get('checkout-'+key).call()    
                }
            }
        }
    }

    stage('build and push') {
      steps {
           script {
                    def buildStageMap=prepareBuildStages()
                    if(params.APP_NAME == ''){
                        parallel buildStageMap
                    }else{
                        buildStageMap.get('build-'+params.APP_NAME).call()    
                    }
            }
      }
    }

    stage('Artifacts') {
      steps {
        archiveArtifacts '**/target/*.jar'
      }
    }

    stage('deploy') {
        steps {
            container('maven') {
              withCredentials([kubeconfigContent(credentialsId : 'kubeconfig-id' ,variable : 'KUBECONFIG_CONTENT' ,)]) {
                sh '''mkdir ~/.kube
                echo "$KUBECONFIG_CONTENT" > ~/.kube/config'''
              }
            }
            script {
                def deployStageMap=prepareDeployStages()
                if(params.APP_NAME == ''){
                    parallel deployStageMap
                }else{
                    deployStageMap.get('deploy-'+params.APP_NAME).call()    
                }
            }
      }
    }

  }
} 

def prepareDeployStages() {
    def deployStageMap= [:]
    for(appName in appNameMap.keySet()){
        String workDir = getWorkDir(appName)
        String app = appName
        String svcsh = app.equals('blade-gateway')?"envsubst < $workDir/deploy/$app-svc.yaml | kubectl apply -f -":''
        def onedeploy = {
            container('maven') {
              withCredentials([kubeconfigContent(credentialsId : 'kubeconfig-id' ,variable : 'KUBECONFIG_CONTENT' ,)]) {
                sh """export APP_NAME=$app
                $svcsh
                envsubst < $workDir/deploy/${app}.yaml | kubectl apply -f -"""
              }
            }
        }
        deployStageMap.put("deploy-"+appName,onedeploy)    
    }
  return deployStageMap
}




def prepareBuildStages() {
    def buildStageMap= [:]
    for(appName in appNameMap.keySet()){
        String workDir = getWorkDir(appName)
        String app = appName
        def oneBuild = {
            container('maven') {
                sh """cd $workDir
                    mvn -Dmaven.test.skip=true clean package"""
                sh """cd $workDir
                    docker build -f Dockerfile -t $REGISTRY/$DOCKERHUB_NAMESPACE/$app:$VERSION-$BRANCH_NAME ."""
                    withCredentials([usernamePassword(credentialsId : 'harbor-id' ,usernameVariable : 'HARBOR_USERNAME' ,passwordVariable : 'HARBOR_PASSWORD' ,)]) {
                sh 'echo "$HARBOR_PASSWORD" | docker login $REGISTRY -u "$HARBOR_USERNAME" --password-stdin'
                sh "docker push $REGISTRY/$DOCKERHUB_NAMESPACE/$app:$VERSION-$BRANCH_NAME"
              }
            }
        }
        buildStageMap.put("build-"+appName,oneBuild)    
    }
  return buildStageMap
}
  
  
def prepareCheckOutStages() {
    def checkoutStageMap= [:]
    for(appName in appNameMap.keySet()){
        String value = appNameMap.get(appName)
        String workDir= getWorkDirBase(value)
        if(appName.startsWith('blade')){
            //bladex项目在一个git仓库中,只用检出一次
            appName = 'BladeX'
        }
        if(!checkoutStageMap.containsKey('checkout-'+value)){
            checkoutStageMap.put("checkout-"+value,getCheckOutStage(appName,workDir))
        }
    }
  return checkoutStageMap
}

//根据项目名称获取单个项目检出stage
def getCheckOutStage(appName,workDir){
    return{
          checkout([$class: 'GitSCM', branches: [[name: "${params.BRANCH_NAME}"]], 
                extensions: [[$class: 'RelativeTargetDirectory',relativeTargetDir: "${workDir}"]], 
                userRemoteConfigs: [[credentialsId: 'gitlab-id', url: "${env.GIT_PREFIX}/${workDir}.git"]]
            ])
    }
}


def getWorkDirBase(appName){
    return appName.startsWith('blade') ?'BladeX' : appName
}

def getWorkDir(appName){
    String workDirBase = getWorkDirBase(appNameMap.get(appName))
    String workDir = "./$workDirBase/blade-service/$appName"
    if(appName.equals('blade-gateway') || appName.equals('blade-auth')){
        workDir = "./$workDirBase/$appName"
    }
    return workDir
}
jenkinsfile
    对应的微服务的deployment配置文件如下,里面的变量都依赖于Jenkinsfile中传入,所以每个微服务除了端口号不同,其他的完全一样
kind: Deployment
apiVersion: apps/v1
metadata:
    name: $APP_NAME
    namespace: $NAMESPACE
    labels:
        app: $APP_NAME
    annotations:
        deployment.kubernetes.io/revision: '1'
        kubesphere.io/creator: titan-admin
spec:
    replicas: 1
    selector:
        matchLabels:
            app: $APP_NAME
    template:
        metadata:
            creationTimestamp: null
            labels:
                app: $APP_NAME
            annotations:
                kubesphere.io/creator: titan-admin
                kubesphere.io/imagepullsecrets: '{"$APP_NAME":"harbor"}'
        spec:
            volumes:
                - name: host-time
                  hostPath:
                      path: /etc/localtime
                      type: ''
            containers:
                - name: $APP_NAME
                  image: $REGISTRY/$DOCKERHUB_NAMESPACE/$APP_NAME:$VERSION-$BRANCH_NAME
                  volumeMounts:
                      - name: host-time
                        mountPath: /etc/localtime
                        readOnly: true
                  readinessProbe:
                      httpGet:
                          scheme: HTTP
                          path: /actuator/health
                          port: 80
                      initialDelaySeconds: 10
                      timeoutSeconds: 5
                      periodSeconds: 10
                      successThreshold: 1
                      failureThreshold: 3
                  args:
                      - '--spring.profiles.active=$ACTIVE_PROFILES'
                      - '--spring.cloud.nacos.discovery.server-addr=$NACOS_ADDR'
                      - '--spring.cloud.nacos.discovery.namespace=$NACOS_NAMESPACE'
                      - '--spring.cloud.nacos.config.namespace=$NACOS_NAMESPACE'
                      - '--spring.cloud.nacos.config.server-addr=$NACOS_ADDR'
                  ports:
                      - name: http-9999
                        containerPort: 9999
                        protocol: TCP
                  resources: {}
                  terminationMessagePath: /dev/termination-log
                  terminationMessagePolicy: File
                  imagePullPolicy: IfNotPresent
            restartPolicy: Always
            terminationGracePeriodSeconds: 30
            dnsPolicy: ClusterFirst
            serviceAccountName: default
            serviceAccount: default
            securityContext: {}
            imagePullSecrets:
                - name: harbor
            schedulerName: default-scheduler
    strategy:
        type: RollingUpdate
        rollingUpdate:
            maxUnavailable: 25%
            maxSurge: 25%
    revisionHistoryLimit: 10
    progressDeadlineSeconds: 600
deployment.yaml

 

appNameMap是预先定义的一个映射,包含了所有要部署的微服务,键为微服务名称,值为对应的git项目名称,其中,blade-开头的项目都在BladeX项目中,blade-gateway和blade-auth在根目录下,其余的blade-微服务在blade-service目录下,其余非blade-开头的项目都和BladeX项目平级,具体目录如下:
                 
      主要的groovy脚本如下,params.APP_NAME,即流水线构建时选择的参数,如果是空,就构建全部微服务,使用parallel map来进行并行构建,否则就构建单个微服务
script {
def checkoutStageMap=prepareCheckOutStages()
if(params.APP_NAME == ''){
parallel checkoutStageMap
}else{
String key = getWorkDirBase(appNameMap.get(params.APP_NAME))
checkoutStageMap.get('checkout-'+key).call()
}
}

      并行检出多个git仓库代码使用relativeTargetDir来指定签出的文件夹

checkout([$class: 'GitSCM', branches: [[name: "${params.BRANCH_NAME}"]], 
                extensions: [[$class: 'RelativeTargetDirectory',relativeTargetDir: "${workDir}"]], 
                userRemoteConfigs: [[credentialsId: 'gitlab-id', url: "${env.GIT_PREFIX}/${workDir}.git"]]
            ])

五:运行效果