kubesphere的devops流程中一键发布所有后台微服务
一:背景
公司的微服务项目准备上k8s,最近学习了下k8s相关的技术,目前采用的是kubesphere来进行k8s的部署,kubesphere中集成了一站式的devops流程,对于没有devops平台的公司来说很友好,可以很方便的实现代码从检出到构建打包发布等一站式流程。参考官方的文档及样例,可以很容易的搭建出一个简单的流水线,操作过,一个环境建了一个devops项目,在项目中为每个微服务搭建了一个流水线,可以实现服务的流水线发布。发现每个微服务的流水线jenkinsfile中绝大多数都是重复的,但是有一些特定的需要改动,虽然kubesphere流水线有复制的功能,可以很方便的复制一个,在其基础上进行修改,但是感觉还是比较麻烦,只有编辑者可能才清楚怎么修改,换一个来部署可能就不知道了,于是想编写一个jenkinsfile来实现后台所有微服务的发布,使用groovy,并行进行每个步骤,并行检出,并行打包推送镜像,并行发布。
二:准备
本人的操作环境中,有以下需要提前准备
- 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' }
四: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 }
对应的微服务的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
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"]] ])
五:运行效果
生来奔走万山中,踏尽崎岖路自通