做云原生开发运维的小伙伴,几乎每天都在做一件事:服务打包、镜像推送、K8s 上线发版

很多团队的现状都是:每个服务一套 Jenkins/GitLab CI 流水线、脚本杂乱、步骤不统一、回滚麻烦、新人上手成本极高。

其实绝大多数 Java/Go/Node 微服务的 K8s 发版流程完全可以标准化

今天给大家分享一套企业级通用 K8s 发版 Pipeline 模板,适配绝大多数业务场景:普通微服务、无状态服务、后端接口服务均可直接套用,支持自动构建、镜像推送、K8s 滚动更新、版本记录、失败回滚

全程无特殊定制,开箱即用,复制改参数就能直接上线。

一、模版适用场景

这套pipline是通用标准版,不绑定特殊业务、但是需要适配一些中间件比如我们的配置中心可以有主流nacos来代替,适合大多数企业微服务:

①java服务:(基于阿里云云效进行CICD)且环境分为 dev/test/pre/

pipeline {
    agent any

    tools {
        jdk 'jdk17'
        maven 'maven3.8.8'
    }

    environment {
        BUILDVERSION = sh(script: "date +%Y%m%d_%H%M", returnStdout: true).trim()
        HARBOR_USER = 'userpush'
        HARBOR_PASSWD = 'xxxxxxx'   #harbor仓库密码
    }

    stages {

        stage('Init Build Workspace') {
            steps {
                script {
                    env.BUILD_WORKSPACE = "${env.WORKSPACE}/${env.BUILD_NUMBER}"
                    echo "当前构建目录: ${env.BUILD_WORKSPACE}"
                }
            }
        }

        stage('Clear workspace') {
            steps {
                deleteDir()
            }
        }

        stage('Parse Parameters') {
            steps {
                ws("${env.BUILD_WORKSPACE}") {
                    script {
                        def paramRegex = /^(\w+:[^;]+;)+$/

                        if (!params.APP_CONFIG || !params.APP_CONFIG.matches(paramRegex)) {
                            error("Invalid APP_CONFIG format. Expected format: 'key1:value1;key2:value2;'")
                        }

                        def configMap = [:]
                        params.APP_CONFIG.split(';').each { entry ->
                            def pair = entry.split(':', 2)
                            if (pair.size() == 2) {
                                configMap[pair[0].trim()] = pair[1].trim()
                            }
                        }

                        env.APP_NAME = configMap['app_name']?: 'default-app'
                        env.GIT_VER = configMap['git_ver']?: 'master_jdk17'
                        env.GIT_GROUP = configMap['git_group']?: 'default-group'
                        env.APP_REPLICAS = configMap['app_replicas']?: '1'
                        env.MEM = configMap['mem']?: '2G'
                        env.MAVEN_API = configMap['maven_api']?: 'false'
                        env.DEPLOY_ENV = configMap['deploy_env']?: 'dev'
                    }
                }
            }
        }

        stage('Check out project branches') {
            steps {
                ws("${env.BUILD_WORKSPACE}") {
                    script {
                        checkout([
                            $class: 'GitSCM',
                            branches: [[name: "*/${GIT_VER}"]],
                            userRemoteConfigs: [[
                                url: "https://xxxxxx.cn/${GIT_GROUP}/${APP_NAME}.git",
                                credentialsId: '323a0e5a-497a-4816-b344-12dba8s56saf5'
                            ]],
                            extensions: [[$class: 'CleanBeforeCheckout']]
                        ])
                    }
                }
            }
        }

        stage('maven api') {
            steps {
                ws("${env.BUILD_WORKSPACE}") {
                    script {
                        if (env.MAVEN_API == 'true') {
                            sh '''
                                api_dir=$(find . -maxdepth 1 -type d -name "*-api" | head -n 1)
                                cd $api_dir
                                mvn clean deploy -Dmaven.test.skip=true
                            '''
                        } else {
                            echo "Skipping maven API deployment because MAVEN_API is set to false"
                        }
                    }
                }
            }
        }

        stage('maven') {
            steps {
                ws("${env.BUILD_WORKSPACE}") {
                    sh '''
                        java --version
                        echo $JAVA_HOME
                        main_dir=$(find . -maxdepth 1 -type d -name "*-web" | head -n 1)
                        if [ -z "$main_dir" ]; then
                          echo "Error: No web module directory found."
                          exit 1
                        fi
                        rm -f $main_dir/src/main/resources/bootstrap.yml
                        rm -f $main_dir/src/main/resources/application.yml
                        mvn clean package -Dmaven.test.skip=true -P${DEPLOY_ENV}
                    '''
                }
            }
        }

        stage('Build Docker Image') {
            steps {
                ws("${env.BUILD_WORKSPACE}") {
                    sh '''
                        main_dir=$(find . -maxdepth 1 -type d -name "*-web" | head -n 1)
                        cd ${main_dir}/target
                        wget http://dockerfile.xxxx.com/download/a${DEPLOY_ENV}/public/Dockerfile

                        sed -i "s?app-name?${APP_NAME}?g" ./Dockerfile

                        if [ "${MEM}" != "" ]; then
                            sed -i "s?2g?${MEM}?g" ./Dockerfile
                        fi

                        docker login http://harbor.xxxx.com/ -u ${HARBOR_USER} -p ${HARBOR_PASSWD}
                        export DOCKER_BUILDKIT=0
                        docker build --no-cache --pull -t harbor.clx.com/${DEPLOY_ENV}/${APP_NAME}-jdk17:${DEPLOY_ENV}_${BUILDVERSION} .
                        docker push harbor.xxxx.com/${DEPLOY_ENV}/${APP_NAME}-jdk17:${DEPLOY_ENV}_${BUILDVERSION}
                    '''
                }
            }
        }

        stage('Deploy to K8S') {
            steps {
                ws("${env.BUILD_WORKSPACE}") {
                    sh """
                        wget http://dockerfile.xxxx.com/yaml/a${DEPLOY_ENV}/public/ackdeployment.yaml -O ./ackdeployment${APP_NAME}.yaml
                        sed -i 's?BUILD_TAG?${DEPLOY_ENV}_${BUILDVERSION}?' ./ackdeployment${APP_NAME}.yaml
                        sed -i 's?app-name?${APP_NAME}?' ./ackdeployment${APP_NAME}.yaml
                        sed -i 's?default?${DEPLOY_ENV}?' ./ackdeployment${APP_NAME}.yaml
                        cat ./ackdeployment${APP_NAME}.yaml
                        kubectl apply -f ./ackdeployment${APP_NAME}.yaml
                        rm -rf ./ackdeployment${APP_NAME}.yaml
                    """
                }
            }
        }
    }
}

这里我弄了一个nginx 作为本地拉取公司内部文件地址下载通用模板这个过程

图片

图片

图片

我的Dockerfile和yaml如下

详细请参考我之前的定制化Dockerfile介绍

运维小邵,公众号:运维小邵微服务部署极简实战-企业配置定制化Dockerfile

[root@one-tech-prod-jump-06-240 public]# cat Dockerfile
FROM harbor.xxxx.com/public/java-jdk-17-0-6:v1.2.0

ENV NAME="app-name"

ENV JAVA_OPTS="\
-Xms2g -Xmx2g -Xmn1g -XX:+UseG1GC -XX:MaxGCPauseMillis=200  -XX:+UseContainerSupport -XX:MaxRAMPercentage=80.0 \
--add-opens java.base/java.lang=ALL-UNNAMED \
"


# 配置nacos地址
ENV NACOS_ADDR="nacos-0.nacos-headless.pre.svc.cluster.local:8848 nacos-1.nacos-headless.pre.svc.cluster.local:8848 nacos-2.nacos-headless.pre.svc.cluster.local:8848"
ENV ACTIVE="dev"  # 根据环境去配置

ENV ENV_ARGS="--server.port=80 \
--server.servlet.context-path=/${NAME} \
--spring.main.allow-bean-definition-overriding=true \
--spring.profiles.active=${ACTIVE} \
--spring.application.name=${NAME} \
--spring.cloud.nacos.discovery.server-addr=${NACOS_ADDR} \
--spring.cloud.nacos.discovery.username=nacos \
--spring.cloud.nacos.discovery.password=&umS74RkrTnsAhnD \
--spring.cloud.nacos.config.server-addr=${NACOS_ADDR} \
--spring.cloud.nacos.config.username=nacos \
--spring.cloud.nacos.config.password=&umS74RkrTnsAhnD \
--spring.cloud.nacos.config.file-extension=yml \
--spring.main.allow-circular-references=true \
--spring.cloud.refresh.never-refreshable=com.zaxxer.hikari.HikariDataSource,com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceWrapper \
--spring.cloud.nacos.config.shared-configs[0].dataId=common-redis-${ACTIVE}.yml \
--spring.cloud.nacos.config.shared-configs[0].refresh=true \
--spring.cloud.nacos.config.shared-configs[1].dataId=common-mq-${ACTIVE}.yml \
--spring.cloud.nacos.config.shared-configs[1].refresh=true \
--spring.cloud.nacos.config.shared-configs[2].dataId=common-satoken-${ACTIVE}.yml \
--spring.cloud.nacos.config.shared-configs[2].refresh=true \
--spring.cloud.nacos.config.shared-configs[3].dataId=common-druid-${ACTIVE}.yml \
--spring.cloud.nacos.config.shared-configs[3].refresh=true \
--spring.cloud.nacos.config.shared-configs[4].dataId=common-swagger-${ACTIVE}.yml \
--spring.cloud.nacos.config.shared-configs[4].refresh=true \
--spring.cloud.nacos.config.shared-configs[5].dataId=common-easy-es-${ACTIVE}.yml \
--spring.cloud.nacos.config.shared-configs[5].refresh=true \
--spring.cloud.nacos.config.shared-configs[6].dataId=common-esign-${ACTIVE}.yml \
--spring.cloud.nacos.config.shared-configs[6].refresh=true \
--spring.cloud.nacos.config.shared-configs[7].dataId=common-xxl-${ACTIVE}.yml \
--spring.cloud.nacos.config.shared-configs[7].refresh=true \
--spring.cloud.nacos.config.shared-configs[8].dataId=common-mongo-${ACTIVE}.yml \
--spring.cloud.nacos.config.shared-configs[8].refresh=true \
--spring.cloud.nacos.config.shared-configs[9].dataId=common-snail-job-${ACTIVE}.yml \
--spring.cloud.nacos.config.shared-configs[9].refresh=true \
--spring.cloud.nacos.config.shared-configs[10].dataId=common-prometheus-${ACTIVE}.yml \
--spring.cloud.nacos.config.shared-configs[10].refresh=true \
"

COPY ./web.jar /app
ENTRYPOINT cd / && java  ${JAVA_OPTS} -jar /app/web.jar ${ENV_ARGS}

当然开发环境还可简化下yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-name-jdk17
  namespace: default
  labels:
    name: app-name-jdk17
spec:
  replicas: 1
  selector:
    matchLabels:
      name: app-name-jdk17
  template:
    metadata:
      labels:
        app: app-name-jdk17
        name: app-name-jdk17
    spec:
      imagePullSecrets:
        - name: harbor-secret
      containers:
      - name: app-name
        image: harbor.xxx.com/dev/app-name-jdk17:BUILD_TAG
        imagePullPolicy: Always
        resources:
          limits:
            memory: 3072Mi
          requests:
            memory: 1024Mi
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  revisionHistoryLimit: 7
  progressDeadlineSeconds: 600

测试预发布生产可以优化yaml

[root@one-tech-prod-jump-06-240 public]# cat ackdeployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-name-jdk17
  namespace: default
  labels:
    name: app-name-jdk17
spec:
  replicas: 1
  selector:
    matchLabels:
      name: app-name-jdk17
  template:
    metadata:
      labels:
        app: app-name-jdk17
        name: app-name-jdk17
        version: v1
    spec:
      containers:
      - name: app-name
        image: harbor.xxxx.com/default/app-name-jdk17:BUILD_TAG
        imagePullPolicy: Always
        env:
        - name: ACCESS_KEY
          valueFrom:
            secretKeyRef:
              name: app-name-jdk17
              key: ACCESS_KEY
              optional: true
        - name: SECRET_KEY
          valueFrom:
            secretKeyRef:
              name: app-name-jdk17
              key: SECRET_KEY
              optional: true
        livenessProbe:
          httpGet:
            path: /app-name/actuator/health/liveness
            port: 80
            scheme: HTTP
          initialDelaySeconds: 240
          periodSeconds: 30
          successThreshold: 1
          timeoutSeconds: 10
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /app-name/actuator/health/readiness
            port: 80
            scheme: HTTP
          initialDelaySeconds: 240
          periodSeconds: 30
          successThreshold: 1
          timeoutSeconds: 10
          failureThreshold: 3
        lifecycle:
          preStop:
            exec:
              command:
                 - /bin/bash
                 - '-c'
                 - >-
                   curl 'http://localhost/app-name/appServer/deregister' && sleep 30 && PID=`pidof java` && kill -SIGTERM $PID && while ps -p $PID > /dev/null; do sleep 1; done;
        resources:
          limits:
            memory: 3072Mi
          requests:
            memory: 1024Mi
        volumeMounts:
        - mountPath: /logs
          name: logm
      - name: filebeat
        image: harbor.xxx.com/public/filebeat:v1.1.7
        imagePullPolicy: Always
        env:
        - name: ENV
          value: default
        - name: PROJ_NAME
          value: app-name
        - name: REDIS_ADDR
          value: '192.168.10.61:7008'
        volumeMounts:
        - mountPath: /logm
          name: logm
      volumes:
      - emptyDir: {}
        name: logm
      imagePullSecrets:
      - name: harbor
      terminationGracePeriodSeconds: 240
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  revisionHistoryLimit: 7
  progressDeadlineSeconds: 600

图片

后续发版我们可以在这去choose你的环境去打包了比较方便,开发测试预发布也可搭配云效去操作 加入审核人 开发测试等可以自己打包发版 不需要去麻烦运维同事帮忙 是不是很方便 我们运维只管生产即可 大大减少我们工作量

②前端项目pipline(基于阿里云oss部署前端项目)

pipeline {
    agent any
    tools {
        nodejs 'node18'
    }

    options {
        ansiColor('xterm') // 启用 AnsiColor,支持颜色输出
    }

    stages {
        stage('Parse Parameters') {
            steps {
                script {
                    // 检查参数格式的正则表达式
                    def paramRegex = /^(\w+:[^;]+;)+$/
                    // 判断参数是否符合正则
                    if (!params.APP_CONFIG || !params.APP_CONFIG.matches(paramRegex)) {
                        error("Invalid APP_CONFIG format. Expected format: 'key1:value1;key2:value2;'")
                    }

                    // 将参数按分号拆分成键值对
                    def configMap = [:]
                    params.APP_CONFIG.split(';').each { entry ->
                        def pair = entry.split(':', 2) // 按冒号拆分
                        if (pair.size() == 2) {
                            configMap[pair[0].trim()] = pair[1].trim()
                        }
                    }

                    // 提取具体变量并设置到环境变量中
                    env.APP_NAME = configMap['app_name']?: 'default-app'
                    env.GIT_VER = configMap['git_ver']?: 'master_jdk17'
                    env.GIT_GROUP = configMap['git_group']?: 'default-group'
                    env.REGION = configMap['region']?: '1'
                    env.OSS_BUCKET = configMap['oss_bucket']?: 'pm-pc-html-dev'
                        def suffix = env.OSS_BUCKET.tokenize('-')[-1]
                            env.OSS_BUCKET_SUFFIX = suffix.capitalize()
                }
            }
        }
        stage('Check out project branches') {
            steps {
                script {
                    def scm = [
                        $class: 'GitSCM',
                        branches: [[name: "*/${GIT_VER}"]],
                        userRemoteConfigs: [[
                            url: "https://t.xxxxx.cn/${GIT_GROUP}/${APP_NAME}.git",
                            credentialsId: '323a0e5a-497a-4816-b344-1assasassd29f5'
                        ]],
                        extensions: [
                            [$class: 'CleanBeforeCheckout'], // 清理旧代码
                            [$class: 'SubmoduleOption', 
                                recursiveSubmodules: true,   // 递归更新子模块
                                trackingSubmodules: true,    // 跟踪子模块分支
                                parentCredentials: true      // 使用主项目的凭据
                            ]
                        ]
                    ]
                    checkout(scm)
                }
            }
        }
        stage('npm build') {
            steps {
                sh '''
                    npm -v 
                    node -v
                    export NODE_OPTIONS="--max-old-space-size=4096"
                    npm install
                    npm run lint || true
                    npm run build${OSS_BUCKET_SUFFIX}
                    '''
            }
        } 
        stage('oss push') {
            steps {
                sh '''
                    /var/lib/jenkins/ossutil64 rm -e ${REGION} oss://${OSS_BUCKET} -rf
                    /var/lib/jenkins/ossutil64 cp -e ${REGION} -r -f dist oss://${OSS_BUCKET}
                    '''
            }
        } 
    }
}

图片

这里前端项目也创建好了,这样手动改来改去也比较麻烦 怎么办呢,我们可以使用云效的流水线 去传参 把参数传进去,开发传开发 测试传测试就行,这里不做详细介绍

这样做的目的是什么?

  • 环境区分:通过分支区分测试/预发/生产环境,不同环境推送不同仓库、不同命名空间

  • 手动确认卡点:生产环境新增人工确认步骤,防止自动误发

  • 发版钉钉/企业微信通知:流水线结束推送结果,成功/失败实时提醒

  • 自动回滚机制:Pod 启动失败、就绪检测失败,自动触发上一版本回滚

  • 镜像清理:定时清理仓库冗余镜像,节省存储资源

总结:

K8s 发版的核心不是写复杂脚本,而是标准化、统一化、可追溯、可回滚

统一团队发版规范、减少人为事故、提升迭代效率。

需要带手动确认、自动回滚、消息通知的增强版模板,可以留言,后续更新进阶版本!

Logo

脑启社区是一个专注类脑智能领域的开发者社区。欢迎加入社区,共建类脑智能生态。社区为开发者提供了丰富的开源类脑工具软件、类脑算法模型及数据集、类脑知识库、类脑技术培训课程以及类脑应用案例等资源。

更多推荐