基于Jenkins Pipeline构建企业级CI/CD

开发 前端
Lark Notice Plugin 是一个用于 Jenkins 的构建通知插件,可以将 Jenkins构建过程以及结果通知推送到 Lark、飞书、钉钉 协作平台。 可配置多个的通知时机,包括 构建启动时、构建中断、构建失败、构建成功时、构建不稳定等。

案例介绍

本案例通过若依项目作为案例,通过Jenkins构建企业级CI/CD平台。

若依服务列表:

  • ruoyi-auth
  • ruoyi-system
  • ruoyi-gateway
  • ruoyi-ui

若依环境列表:

  • DEV
  • UAT
  • PROD

环境准备工作:

  • nacos安装并配置完成
  • MySQL部署完成并初始化
  • Redis部署完成
  • Harbor镜像仓库
  • Gitlab部署完成
  • Kubernetes部署完成
  • Ingress部署完成

设计思路

触发构建设计:

本设计通过Jenkins Generic Webhook Trigger 插件实现了基于Webhook自动触发流水线构建。

图片图片

流程说明:

  • 研发项目负责人代码开发完成后进行合并代码并生成Tag
  • Gitlab通过Webhook自动触发Jenkins Pipeline构建

流水线设计:

图片图片

Jenkins流水线完整图:

图片图片

自定义基础镜像

在实际企业环境中,基础镜像都会根据具体得需求定义适合自己得基础镜像。

定义Maven镜像:

用于代码构建编译打包,会把Ruoyi相关依赖包打到基础镜像内,避免分层构建失败。

# 拉取源代码并切换分支
$ https://gitee.com/y_project/RuoYi-Cloud.git
$ git checkout v3.6.3
$ cd ..

# 定义Dockerfile
$ cat Dockerfile
FROM maven:3.8.6-openjdk-8
ADD  RuoYi-Cloud /opt/RuoYi-Cloud
RUN cd /opt/RuoYi-Cloud  && mvn clean install -DskipTests
RUN rm -rf /opt/RuoYi-Cloud

# 构建镜像
$ docker build uhub.service.ucloud.cn/kubesre/maven:jdk8 .
$ docker push uhub.service.ucloud.cn/kubesre/maven:jdk8
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

定义Java基础镜像:

根据需求定义适合自己的基础镜像。通过变量传递让配置变得更灵活!

# 创建个目录
$ mkdir base && cd base

# 创建启动脚本
$ cat docker-entrypoint.sh
#!/bin/sh
java -server -Xms$JVM_XMS -Xmx$JVM_XMX -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/data/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/heapdump.hprof  -jar app.jar --server.port=$SERVICE_PORT --spring.profiles.active=$PROFILES_ACTIVE --spring.cloud.nacos.config.server-addr=$NACOS_ADDRESS --spring.cloud.nacos.config.namespace=$NACOS_NAMESPACE_ID --spring.cloud.nacos.config.username=$NACOS_USERNAME --spring.cloud.nacos.config.password=$NACOS_PASSWORD --spring.cloud.nacos.discovery.server-addr=$NACOS_ADDRESS --spring.cloud.nacos.discovery.namespace=$NACOS_NAMESPACE_ID --spring.cloud.nacos.discovery.username=$NACOS_USERNAME --spring.cloud.nacos.discovery.password=$NACOS_PASSWORD

# 创建down-nacos脚本
cat down-nacos.sh 
#!/bin/sh
ipTrue=false
java_service_ip=""
code=false

getPodIp() {
    java_service_ip=`ip a  | grep inet | grep -v inet6 | grep -v '127.0.0.1' | awk '{print $2}' | awk -F / '{print$1}'`
    grep -w "${java_service_ip}" /etc/hosts > /dev/null
    if [ $? -eq 0 ];then
      echo "get java service ip success"
      ipTrue=true
    else
      echo "get java service ip failed"
    fi
}

downService(){
    accessToken=`curl -s -X POST http://$NACOS_ADDRESS/nacos/v1/auth/users/login --form username=$NACOS_USERNAME --form password=$NACOS_PASSWORD|jq -r .accessToken`
    curl -s -X  PUT "$NACOS_ADDRESS/nacos/v1/ns/instance?language=zh-CN&accessToken=$accessToken&username=$NACOS_USERNAME&serviceName=$JAVA_SERVICE_NAME&ip=$java_service_ip&port=$SERVICE_PORT&enabled=false&namespaceId=$NACOS_NAMESPACE_ID"
    if [ "$code" = "ok" ];then
      echo "java service down from nacos success"
      code=true
    else
      echo "java service down from nacos failed"
    fi
}

start(){
  getPodIp
  if $ipTrue;then
    downService
    sleep 30
  else
    echo "down $JAVA_SERVICE_NAME failed" >> down_service.log
  fi   
}
start


# 定义Dockerfile
$ cat Dockerfile 
FROM  openjdk:8-jre
WORKDIR /data
COPY ./down-nacos.sh .
COPY ./docker-entrypoint.sh .
RUN chmod +x docker-entrypoint.sh && chmod +x  down-nacos.sh
ENTRYPOINT ["./docker-entrypoint.sh"]

# 构建镜像
$ docker build uhub.service.ucloud.cn/kubesre/java-base:v8 .
$ docker push uhub.service.ucloud.cn/kubesre/java-base:v8
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.

变量说明:

  • JVM_XMS:最小JVM堆栈内存
  • JVM_XMX:最大JVM堆栈内存
  • SERVICE_PORT:应用服务端口
  • NACOS_ADDRESS:Nacos地址
  • NACOS_USERNAME:Nacos用户名
  • NACOS_PASSWORD:Nacos密码
  • NACOS_NAMESPACE_ID:Nacos命名空间ID
  • PROFILES_ACTIVE:环境名称

Dockerfile编写

分层构建好处:

  • 不依赖本地环境
  • 减小容器镜像大小

Java Dockerfile(分层构建):

FROM uhub.service.ucloud.cn/kubesre/maven:jdk8 AS build
COPY  src /opt/src/
COPY pom.xml /opt/
RUN  cd /opt/ && mvn clean install -DskipTests

FROM  uhub.service.ucloud.cn/kubesre/java-base:v8
# 复制jar文件到路径
COPY  --from=build /opt/target/*.jar /data/app.jar
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

Vue Dockerfile(分层构建):

FROM node:16 AS builder

# 设置工作目录
WORKDIR /usr/src/app

# 将项目文件复制到 Docker 镜像中
COPY . .

# 安装项目依赖
RUN npm install --registry=https://registry.npmmirror.com

# 构建静态文件
RUN npm run build:prod

# 使用 Nginx 镜像作为基础镜像,用于托管静态文件
FROM nginx:stable-alpine

WORKDIR /home/ruoyi/projects/ruoyi-ui
# 将 VuePress 构建的静态文件复制到 Nginx 的网站目录
COPY --from=builder /usr/src/app/dist /home/ruoyi/projects/ruoyi-ui
COPY ./nginx/conf/nginx.conf /etc/nginx/nginx.conf

# 暴露 80 端口
EXPOSE 80

# 启动 Nginx
CMD ["nginx", "-g", "daemon off;"]
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

Pipeline编写

如下所有Pipeline文件,需要自行修改内容:

  • credentialsId
  • robot
  • 镜像仓库地址

如何查找credentialsId:

图片图片

如何查找robot:

图片图片

Java Pipeline:

pipeline {
    agent any
    triggers {
        GenericTrigger(
            genericVariables: [
                [key: 'ref', value: '$.ref'],  //获取分支
                [key: 'user_username', value: '$.user_username'],     //获取自动构建用户名
                [key: 'GitRepository', value: '$.project.git_http_url'],  //获取gitlab ssh项目地址
                [key: 'project', value: '$.project.name'],      //获取项目名称
                [key: 'repository', value: '$.repository.name'],
            ],
            token: "$JOB_NAME",
            causeString: 'Triggered on $branch',
            printContributedVariables: true,
            printPostContent: true,
            silentResponse: false,
        )
    }
    environment {
        // pipeline配置路径
        pipeline_dir="/var/lib/jenkins/workspace/pipeline"
        // 项目版本
        Tag=sh(script: 'echo "${ref}" | awk -F"/" \'{print $3}\'', returnStdout: true).trim()
        // 项目名称
        Project_Name="${project}"
        // 上一次版本
        Revsion_Prod=''
        //Depolyment名称
        DeploymentName=''
        // 生产名称空间
        Namespace_Prod=''
        // 灰度模式
        GrayHeaderMode=''
        // 灰度Depolyment名称
        GrayDeploymentName=''
        // 灰度Service名称
        GrayServiceName=''
        // 灰度Ingress名称
        GrayIngressName=''
        // 是否灰度
        GrayEnable='yes'
    }
    options {
        // 表示保留10次构建历史
        buildDiscarder(logRotator(numToKeepStr: '10'))
    }
    stages {
        stage('Pull Code') {
            // 拉取代码
            steps {
                checkout([
                    $class: 'GitSCM', 
                    branches: [[name: "$ref"]],
                    doGenerateSubmoduleConfigurations: false,
                    extensions: [], 
                    userRemoteConfigs: [[
                        credentialsId: 'ac66550d-6999-485c-af3a-7e6189f765f0',
                        url: "$GitRepository"
                    ]]
                ])

                script{
                    currentBuild.displayName = "#${BUILD_NUMBER} - ${Project_Name} - ${Tag}"
                }
            }
        }
        //     // 代码构建
        //   stage('Build Code') {
        //         steps {
        //              sh '/application/maven/bin/mvn -f pom.xml -s settings.xml clean package -DskipTests'
        //         }
        //     } 
        // 镜像构建
        stage('Build Image') {
            steps {
                sh '''
                /usr/bin/docker build -t uhub.service.ucloud.cn/kubesre/$Project_Name:$Tag .
                /usr/bin/docker push  uhub.service.ucloud.cn/kubesre/$Project_Name:$Tag
                '''
            }
            post {
                success {
                    wrap([$class: 'BuildUser']) {
                        lark (
                            robot: "9f7c94cd-491e-4309-83b4-9290d01fc285",
                            type: "CARD",
                            title: "📢  Jenkins 镜像构建成功",
                            text: [
                                "📋 **任务名称**:[${JOB_NAME}](${JOB_URL})",
                                "🔢 **任务编号**:[${BUILD_DISPLAY_NAME}](${BUILD_URL})",
                                "🌟 **构建状态**: <font color='green'>成功</font>",
                                "🤩 **镜像版本**: $Tag",
                                "😎 **镜像仓库**: uhub.service.ucloud.cn/kubesre/$Project_Name",
                                "🕐 **构建用时**: ${currentBuild.duration} ms",
                                "👤 **执  行 者**: ${env.BUILD_USER}",
                                "<at id=all></at>"
                            ],
                            buttons: [
                                [
                                    title: "更改记录",
                                    url: "${BUILD_URL}changes"
                                ],
                                [
                                    title: "控制台",
                                    type: "danger",
                                    url: "${BUILD_URL}console"
                                ]
                            ]
                        )}
                }
            }
        }
        stage('DeployDev'){
            steps {
                echo "部署开发环境"
                script {
                    def userInput = input (
                        message: '确定要发布到DEV环境吗?',
                        parameters:[
                            choice(name: '操作', choices: ['发布', '跳过'])
                        ],
                        ok: '确定',
                        submitter: 'admin',
                        submitterParameter: 'APPROVER'
                    )
                    if (userInput['操作'] == '发布'){
                        echo "部署Dev环境开始"
                        sh '''
                        echo $pipeline_dir
                        echo "打印编排文件详细信息"
                        if [ -e "$pipeline_dir/dev/$Project_Name/deployment.yml" ]; then
                        cat $pipeline_dir/dev/$Project_Name/deployment.yml | sed  "s/TAG/${Tag}/g"
                        cat $pipeline_dir/dev/$Project_Name/deployment.yml | sed  "s/TAG/${Tag}/g" | /usr/bin/kubectl apply -f  -
                        fi

                        if [ -e "$pipeline_dir/dev/$Project_Name/service.yml" ]; then
                        cat $pipeline_dir/dev/$Project_Name/service.yml
                        cat $pipeline_dir/dev/$Project_Name/service.yml  | /usr/bin/kubectl apply -f  -
                        fi

                        if [ -e "$pipeline_dir/dev/$Project_Name/ingress.yml" ]; then
                        cat $pipeline_dir/dev/$Project_Name/ingress.yml 
                        cat $pipeline_dir/dev/$Project_Name/ingress.yml  | /usr/bin/kubectl apply -f  -
                        fi
                        '''
                    } else {
                        echo "不发布"
                    }
                }
            }
        } 
        stage('DeployUat'){
            steps {
                echo "部署测试环境"
                script {
                    def userInput = input (
                        message: '确定要发布到UAT环境吗?',
                        parameters:[
                            choice(name: '操作', choices: ['发布', '跳过'])
                        ],
                        ok: '确定',
                        submitter: 'admin',
                        submitterParameter: 'APPROVER'
                    )
                    if (userInput['操作'] == '发布'){
                        echo "发布"
                        sh '''
                        echo $pipeline_dir
                        echo "打印编排文件详细信息"

                        if [ -e "$pipeline_dir/uat/$Project_Name/deployment.yml" ]; then
                        cat $pipeline_dir/uat/$Project_Name/deployment.yml | sed  "s/TAG/${Tag}/g"
                        cat $pipeline_dir/uat/$Project_Name/deployment.yml | sed  "s/TAG/${Tag}/g" | /usr/bin/kubectl apply -f  -
                        fi

                        if [ -e "$pipeline_dir/uat/$Project_Name/service.yml" ]; then
                        cat $pipeline_dir/uat/$Project_Name/service.yml
                        cat $pipeline_dir/uat/$Project_Name/service.yml  | /usr/bin/kubectl apply -f  -
                        fi

                        if [ -e "$pipeline_dir/uat/$Project_Name/ingress.yml" ]; then
                        cat $pipeline_dir/uat/$Project_Name/ingress.yml 
                        cat $pipeline_dir/uat/$Project_Name/ingress.yml  | /usr/bin/kubectl apply -f  -
                        fi
                        '''
                    } else {
                        echo "不发布"
                    }
                }
            }
        } 
        stage('DeployGray'){
            steps {
                echo "部署灰度环境"
                script {
                    def GraysMode = input (
                        message: '确定要灰度验证吗?',
                        parameters:[
                            choice(name: 'operation', choices: ['基于权重灰度','基于请求头灰度','跳过'])
                        ],
                        ok: '确定',
                        submitter: 'admin',
                        submitterParameter: 'APPROVER'
                    )
                    if (GraysMode['operation'] == '基于权重灰度'){
                        def WeightMode = input (
                            message: '请输入权重比例!',
                            parameters:[
                                string(name: 'workload_weight',defaultValue: '',description: ''),
                                string(name: 'grayload_weight',defaultValue: '',description: '')
                            ],
                            ok: '确定',
                            submitter: 'admin',
                            submitterParameter: 'APPROVER'
                        )
                        sh """
                        echo $pipeline_dir
                        echo "打印编排文件详细信息"
                        if [ -e "$pipeline_dir/prod/$Project_Name/deployment-gray.yml" ]; then
                        cat $pipeline_dir/prod/$Project_Name/deployment-gray.yml | sed  "s/TAG/${Tag}/g" 
                        cat $pipeline_dir/prod/$Project_Name/deployment-gray.yml | sed  "s/TAG/${Tag}/g" | /usr/bin/kubectl apply -f  -
                        fi

                        echo "配置权重"

                        echo ${WeightMode['grayload_weight']}
                        if [ -e "$pipeline_dir/prod/$Project_Name/ingress-gray-weight.yml" ]; then
                        cat $pipeline_dir/prod/$Project_Name/ingress-gray-weight.yml | sed  "s/WEIGHT-VALUE/${WeightMode['grayload_weight']}/g" 
                        cat $pipeline_dir/prod/$Project_Name/ingress-gray-weight.yml | sed  "s/WEIGHT-VALUE/${WeightMode['grayload_weight']}/g" | /usr/bin/kubectl apply -f  -
                        fi
                        """
                    }
                    if (GraysMode['operation'] == '基于请求头灰度'){
                        GrayHeaderMode = input (
                            message: '请输入请求头!',
                            parameters:[
                                string(name: 'header_key',defaultValue: '',description: ''),
                                string(name: 'header_value',defaultValue: '',description: '')
                            ],
                            ok: '确定',
                            submitter: 'admin',
                            submitterParameter: 'APPROVER'
                        )

                        sh """
                        echo ${GrayHeaderMode['header_value']}
                        echo $pipeline_dir
                        echo "打印编排文件详细信息"

                        if [ -e "$pipeline_dir/prod/$Project_Name/deployment-gray.yml" ]; then
                        cat $pipeline_dir/prod/$Project_Name/deployment-gray.yml | sed  "s/TAG/${Tag}/g" 
                        cat $pipeline_dir/prod/$Project_Name/deployment-gray.yml | sed  "s/TAG/${Tag}/g" | /usr/bin/kubectl apply -f  -
                        fi

                        echo "配置请求头"
                        echo ${GrayHeaderMode['header_key']}
                        echo ${GrayHeaderMode['header_value']}

                        if [ -e "$pipeline_dir/prod/$Project_Name/ingress-gray-header.yml" ]; then
                        cat $pipeline_dir/prod/$Project_Name/ingress-gray-header.yml | sed  "s/header-key/${GrayHeaderMode['header_key']}/g" | sed  "s/header-value/${GrayHeaderMode['header_value']}/g"
                        cat $pipeline_dir/prod/$Project_Name/ingress-gray-header.yml | sed  "s/header-value/${GrayHeaderMode['header_key']}/g" | sed  "s/header-value/${GrayHeaderMode['header_value']}/g" | /usr/bin/kubectl apply -f  -
                        fi
                        """
                    }
                    // 默认模式为yes,如果跳过为no
                    if (GraysMode['operation'] == '跳过'){
                        GrayEnable='no'
                    }
                }
            }
        } 
        stage('DeployProd'){
            steps {
                echo "部署生产环境"
                script {
                    def userInput = input (
                        message: '确定要发布到生产环境吗?',
                        parameters:[
                            choice(name: '操作', choices: ['发布', '跳过'])
                        ],
                        ok: '确定',
                        submitter: 'test',
                        submitterParameter: 'APPROVER'
                    )
                    if (userInput['操作'] == '发布'){
                        echo "发布"
                        Namespace_Prod = sh(script: "cat $pipeline_dir/prod/$Project_Name/deployment.yml | grep namespace | awk -F ':' '{print \$2}'", returnStdout: true).trim()
                        DeploymentName = sh(script: "cat $pipeline_dir/prod/$Project_Name/deployment.yml | grep name: |  head -n 1 | awk -F ':' '{print \$2}'", returnStdout: true).trim()
                        Revsion_Prod = sh(script: "kubectl get deployment $DeploymentName -n ${Namespace_Prod} -o=jsnotallow='{.spec.template.spec.containers[*].image}' | awk -F ':' '{print \$NF}'", returnStdout: true).trim()
                        GrayDeploymentName = sh(script: "cat $pipeline_dir/prod/$Project_Name/deployment-gray.yml | grep name: |  head -n 1 | awk -F ':' '{print \$2}'", returnStdout: true).trim()
                        GrayServiceName = sh(script: "cat $pipeline_dir/prod/$Project_Name/service-gray.yml | grep name: |  head -n 1 | awk -F ':' '{print \$2}'", returnStdout: true).trim()
                        GrayIngressName = sh(script: "cat $pipeline_dir/prod/$Project_Name/ingress-gray-header.yml | grep name: |  head -n 1 | awk -F ':' '{print \$2}'", returnStdout: true).trim()

                        sh '''
                        echo $pipeline_dir
                        echo "开始部署生产环境"
                        echo "打印编排文件详细信息"

                        if [ -e "$pipeline_dir/prod/$Project_Name/deployment.yml" ]; then
                          cat $pipeline_dir/prod/$Project_Name/deployment.yml | sed  "s/TAG/${Tag}/g"
                          cat $pipeline_dir/prod/$Project_Name/deployment.yml | sed  "s/TAG/${Tag}/g" | /usr/bin/kubectl apply -f  -
                        fi

                        if [ -e "$pipeline_dir/prod/$Project_Name/service.yml" ]; then
                           cat $pipeline_dir/prod/$Project_Name/service.yml
                           cat $pipeline_dir/prod/$Project_Name/service.yml  | /usr/bin/kubectl apply -f  -
                        fi

                        if [ -e "$pipeline_dir/prod/$Project_Name/ingress.yml" ]; then
                          cat $pipeline_dir/prod/$Project_Name/ingress.yml
                          cat $pipeline_dir/prod/$Project_Name/ingress.yml  | /usr/bin/kubectl apply -f  -
                        fi
                        '''
                        if (GrayEnable == 'yes'){
                            sh """
                        kubectl delete deployment ${GrayDeploymentName} -n ${Namespace_Prod}
                        kubectl delete service  ${GrayServiceName} -n ${Namespace_Prod}
                        kubectl delete ingress  ${GrayIngressName} -n ${Namespace_Prod}
                        """
                        }

                    } else {
                        echo "不发布"
                    }
                }
            }
            post {
                success {
                    wrap([$class: 'BuildUser']) {
                    lark (
                        robot: "9f7c94cd-491e-4309-83b4-9290d01fc285",
                        type: "CARD",
                        title: "📢  Jenkins 应用发布成功",
                        text: [
                            "😋 **应用名称**:[${JOB_NAME}](${JOB_URL})",
                            "😜 **应用环境**:Prod",
                            "🤪 **任务编号**:[${BUILD_DISPLAY_NAME}](${BUILD_URL})",
                            "😘 **发布状态**: <font color='green'>成功</font>",
                            "🤩 **镜像版本**: $Tag",
                            "😎 **镜像仓库**: harbor.kubesre.com:8443/kubesre/$Project_Name",
                            "😝 **执  行 者**: ${env.BUILD_USER}",
                            "<at id=all></at>"
                        ],
                        buttons: [
                           [
                              title: "更改记录",
                              url: "${BUILD_URL}changes"
                           ],
                           [
                              title: "控制台",
                              type: "danger",
                              url: "${BUILD_URL}console"
                           ]
                        ]
                    )}
                }
            }
        } 

        stage('RollBack'){
            steps {
                echo "版本回滚操作"
                script {
                  def RollBack = input (
                        message: '确定要执行回滚操作吗?',
                        parameters:[
                            choice(name: '是否回滚', choices: ['是', '否']),
                            string(name: '回滚版本', defaultValue: Revsion_Prod,description: '默认上一个版本')
                        ],
                        ok: '确定',
                        submitter: 'admin',
                        submitterParameter: 'APPROVER'
                    )
                  if (RollBack['是否回滚'] == '是'){
                        echo "版本回滚成功"
                        echo RollBack['回滚版本']
                        sh """
                        if [ -e "$pipeline_dir/prod/$Project_Name/deployment.yml" ]; then
                          cat $pipeline_dir/prod/$Project_Name/deployment.yml | sed  "s/TAG/${RollBack['回滚版本']}/g"
                          cat $pipeline_dir/prod/$Project_Name/deployment.yml | sed  "s/TAG/${RollBack['回滚版本']}/g" | /usr/bin/kubectl apply -f  -
                        fi
                        """
                  } else {
                         echo "放弃版本回滚"
                         echo RollBack['回滚版本']
                  }
            }
         }
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.
  • 194.
  • 195.
  • 196.
  • 197.
  • 198.
  • 199.
  • 200.
  • 201.
  • 202.
  • 203.
  • 204.
  • 205.
  • 206.
  • 207.
  • 208.
  • 209.
  • 210.
  • 211.
  • 212.
  • 213.
  • 214.
  • 215.
  • 216.
  • 217.
  • 218.
  • 219.
  • 220.
  • 221.
  • 222.
  • 223.
  • 224.
  • 225.
  • 226.
  • 227.
  • 228.
  • 229.
  • 230.
  • 231.
  • 232.
  • 233.
  • 234.
  • 235.
  • 236.
  • 237.
  • 238.
  • 239.
  • 240.
  • 241.
  • 242.
  • 243.
  • 244.
  • 245.
  • 246.
  • 247.
  • 248.
  • 249.
  • 250.
  • 251.
  • 252.
  • 253.
  • 254.
  • 255.
  • 256.
  • 257.
  • 258.
  • 259.
  • 260.
  • 261.
  • 262.
  • 263.
  • 264.
  • 265.
  • 266.
  • 267.
  • 268.
  • 269.
  • 270.
  • 271.
  • 272.
  • 273.
  • 274.
  • 275.
  • 276.
  • 277.
  • 278.
  • 279.
  • 280.
  • 281.
  • 282.
  • 283.
  • 284.
  • 285.
  • 286.
  • 287.
  • 288.
  • 289.
  • 290.
  • 291.
  • 292.
  • 293.
  • 294.
  • 295.
  • 296.
  • 297.
  • 298.
  • 299.
  • 300.
  • 301.
  • 302.
  • 303.
  • 304.
  • 305.
  • 306.
  • 307.
  • 308.
  • 309.
  • 310.
  • 311.
  • 312.
  • 313.
  • 314.
  • 315.
  • 316.
  • 317.
  • 318.
  • 319.
  • 320.
  • 321.
  • 322.
  • 323.
  • 324.
  • 325.
  • 326.
  • 327.
  • 328.
  • 329.
  • 330.
  • 331.
  • 332.
  • 333.
  • 334.
  • 335.
  • 336.
  • 337.
  • 338.
  • 339.
  • 340.
  • 341.
  • 342.
  • 343.
  • 344.
  • 345.
  • 346.
  • 347.
  • 348.
  • 349.
  • 350.
  • 351.
  • 352.
  • 353.
  • 354.
  • 355.
  • 356.
  • 357.
  • 358.
  • 359.
  • 360.
  • 361.
  • 362.
  • 363.
  • 364.
  • 365.
  • 366.
  • 367.
  • 368.
  • 369.
  • 370.
  • 371.
  • 372.
  • 373.
  • 374.
  • 375.
  • 376.
  • 377.
  • 378.
  • 379.
  • 380.
  • 381.
  • 382.
  • 383.
  • 384.
  • 385.
  • 386.
  • 387.
  • 388.
  • 389.
  • 390.
  • 391.

Vue Pipeline:

pipeline {
    agent any
    triggers {
            GenericTrigger(
                genericVariables: [
                    [key: 'ref', value: '$.ref'],  //获取分支
                    [key: 'user_username', value: '$.user_username'],     //获取自动构建用户名
                    [key: 'GitRepository', value: '$.project.git_http_url'],  //获取gitlab ssh项目地址
                    [key: 'project', value: '$.project.name'],      //获取项目名称
                    [key: 'repository', value: '$.repository.name'],
                ],
                token: "$JOB_NAME",
                causeString: 'Triggered on $branch',
                printContributedVariables: true,
                printPostContent: true,
                silentResponse: false,
            )
        }
    environment {
        // pipeline配置路径
        pipeline_dir="/var/lib/jenkins/workspace/pipeline"
        // 项目版本
        Tag=sh(script: 'echo "${ref}" | awk -F"/" \'{print $3}\'', returnStdout: true).trim()
        // 项目名称
        Project_Name="${project}"
        // 上一次版本
        Revsion_Prod=''
        //Depolyment名称
        DeploymentName=''
        // 生产名称空间
        Namespace_Prod=''
        // 灰度模式
        GrayHeaderMode=''
        // 灰度Depolyment名称
        GrayDeploymentName=''
        // 灰度Service名称
        GrayServiceName=''
        // 灰度Ingress名称
        GrayIngressName=''
        // 是否灰度
        GrayEnable='yes'
    }
    options {
  // 表示保留10次构建历史
  buildDiscarder(logRotator(numToKeepStr: '10'))
    }
    stages {
        stage('Pull Code') {
             // 拉取代码
            steps {
                checkout([
                    $class: 'GitSCM', 
                    branches: [[name: "$ref"]],
                    doGenerateSubmoduleConfigurations: false,
                    extensions: [], 
                    userRemoteConfigs: [[
                        credentialsId: 'ac66550d-6999-485c-af3a-7e6189f765f0',
                        url: "$GitRepository"
                    ]]
                ])

                script{
                    currentBuild.displayName = "#${BUILD_NUMBER} - ${Project_Name} - ${Tag}"
                }
            }
        }
    //     // 代码构建
    //   stage('Build Code') {
    //         steps {
    //              sh '/application/maven/bin/mvn -f pom.xml -s settings.xml clean package -DskipTests'
    //         }
    //     } 
         // 镜像构建
        stage('Build Image') {
            steps {
                 sh '''
                   /usr/bin/docker build -t uhub.service.ucloud.cn/kubesre/$Project_Name:$Tag .
                   /usr/bin/docker push  uhub.service.ucloud.cn/kubesre/$Project_Name:$Tag
                 '''
            }
            post {
                success {
                    wrap([$class: 'BuildUser']) {
                    lark (
                        robot: "9f7c94cd-491e-4309-83b4-9290d01fc285",
                        type: "CARD",
                        title: "📢  Jenkins 镜像构建成功",
                        text: [
                            "📋 **任务名称**:[${JOB_NAME}](${JOB_URL})",
                            "🔢 **任务编号**:[${BUILD_DISPLAY_NAME}](${BUILD_URL})",
                            "🌟 **构建状态**: <font color='green'>成功</font>",
                            "🤩 **镜像版本**: $Tag",
                            "😎 **镜像仓库**: uhub.service.ucloud.cn/kubesre/$Project_Name",
                            "🕐 **构建用时**: ${currentBuild.duration} ms",
                            "👤 **执  行 者**: ${env.BUILD_USER}",
                            "<at id=all></at>"
                        ],
                        buttons: [
                           [
                              title: "更改记录",
                              url: "${BUILD_URL}changes"
                           ],
                           [
                              title: "控制台",
                              type: "danger",
                              url: "${BUILD_URL}console"
                           ]
                        ]
                    )}
                }
            }
        }
        stage('DeployDev'){
            steps {
                echo "部署开发环境"
                script {
                    def userInput = input (
                        message: '确定要发布到DEV环境吗?',
                        parameters:[
                            choice(name: '操作', choices: ['发布', '跳过'])
                        ],
                        ok: '确定',
                        submitter: 'admin',
                        submitterParameter: 'APPROVER'
                    )
                    if (userInput['操作'] == '发布'){
                        echo "部署Dev环境开始"
                        sh '''
                        echo $pipeline_dir
                        echo "打印编排文件详细信息"
                        if [ -e "$pipeline_dir/dev/$Project_Name/deployment.yml" ]; then
                            cat $pipeline_dir/dev/$Project_Name/deployment.yml | sed  "s/TAG/${Tag}/g"
                            cat $pipeline_dir/dev/$Project_Name/deployment.yml | sed  "s/TAG/${Tag}/g" | /usr/bin/kubectl apply -f  -
                        fi
                        
                        if [ -e "$pipeline_dir/dev/$Project_Name/service.yml" ]; then
                            cat $pipeline_dir/dev/$Project_Name/service.yml
                            cat $pipeline_dir/dev/$Project_Name/service.yml  | /usr/bin/kubectl apply -f  -
                        fi

                        if [ -e "$pipeline_dir/dev/$Project_Name/ingress.yml" ]; then
                            cat $pipeline_dir/dev/$Project_Name/ingress.yml 
                            cat $pipeline_dir/dev/$Project_Name/ingress.yml  | /usr/bin/kubectl apply -f  -
                        fi
                        '''
                    } else {
                        echo "不发布"
                    }
                }
            }
        } 
        stage('DeployUat'){
            steps {
                echo "部署测试环境"
                 script {
                    def userInput = input (
                        message: '确定要发布到UAT环境吗?',
                        parameters:[
                            choice(name: '操作', choices: ['发布', '跳过'])
                        ],
                        ok: '确定',
                        submitter: 'admin',
                        submitterParameter: 'APPROVER'
                    )
                    if (userInput['操作'] == '发布'){
                        echo "发布"
                         sh '''
                        echo $pipeline_dir
                        echo "打印编排文件详细信息"

                        if [ -e "$pipeline_dir/uat/$Project_Name/deployment.yml" ]; then
                            cat $pipeline_dir/uat/$Project_Name/deployment.yml | sed  "s/TAG/${Tag}/g"
                            cat $pipeline_dir/uat/$Project_Name/deployment.yml | sed  "s/TAG/${Tag}/g" | /usr/bin/kubectl apply -f  -
                        fi
                        
                        if [ -e "$pipeline_dir/uat/$Project_Name/service.yml" ]; then
                            cat $pipeline_dir/uat/$Project_Name/service.yml
                            cat $pipeline_dir/uat/$Project_Name/service.yml  | /usr/bin/kubectl apply -f  -
                        fi

                        if [ -e "$pipeline_dir/uat/$Project_Name/ingress.yml" ]; then
                            cat $pipeline_dir/uat/$Project_Name/ingress.yml 
                            cat $pipeline_dir/uat/$Project_Name/ingress.yml  | /usr/bin/kubectl apply -f  -
                        fi
                        '''
                    } else {
                        echo "不发布"
                    }
                }
            }
        } 
        stage('DeployProd'){
            steps {
                echo "部署生产环境"
                 script {
                    def userInput = input (
                        message: '确定要发布到生产环境吗?',
                        parameters:[
                            choice(name: '操作', choices: ['发布', '跳过'])
                        ],
                        ok: '确定',
                        submitter: 'test',
                        submitterParameter: 'APPROVER'
                    )
                    if (userInput['操作'] == '发布'){
                        echo "发布"
                        Namespace_Prod = sh(script: "cat $pipeline_dir/prod/$Project_Name/deployment.yml | grep namespace | awk -F ':' '{print \$2}'", returnStdout: true).trim()
                        DeploymentName = sh(script: "cat $pipeline_dir/prod/$Project_Name/deployment.yml | grep name: |  head -n 1 | awk -F ':' '{print \$2}'", returnStdout: true).trim()
                        Revsion_Prod = sh(script: "kubectl get deployment $DeploymentName -n ${Namespace_Prod} -o=jsnotallow='{.spec.template.spec.containers[*].image}' | awk -F ':' '{print \$NF}'", returnStdout: true).trim()
                        GrayDeploymentName = sh(script: "cat $pipeline_dir/prod/$Project_Name/deployment-gray.yml | grep name: |  head -n 1 | awk -F ':' '{print \$2}'", returnStdout: true).trim()
                        GrayServiceName = sh(script: "cat $pipeline_dir/prod/$Project_Name/service-gray.yml | grep name: |  head -n 1 | awk -F ':' '{print \$2}'", returnStdout: true).trim()
                        GrayIngressName = sh(script: "cat $pipeline_dir/prod/$Project_Name/ingress-gray-header.yml | grep name: |  head -n 1 | awk -F ':' '{print \$2}'", returnStdout: true).trim()

                        sh '''
                        echo $pipeline_dir
                        echo "开始部署生产环境"
                        echo "打印编排文件详细信息"

                        if [ -e "$pipeline_dir/prod/$Project_Name/deployment.yml" ]; then
                          cat $pipeline_dir/prod/$Project_Name/deployment.yml | sed  "s/TAG/${Tag}/g"
                          cat $pipeline_dir/prod/$Project_Name/deployment.yml | sed  "s/TAG/${Tag}/g" | /usr/bin/kubectl apply -f  -
                        fi

                        if [ -e "$pipeline_dir/prod/$Project_Name/service.yml" ]; then
                           cat $pipeline_dir/prod/$Project_Name/service.yml
                           cat $pipeline_dir/prod/$Project_Name/service.yml  | /usr/bin/kubectl apply -f  -
                        fi

                        if [ -e "$pipeline_dir/prod/$Project_Name/ingress.yml" ]; then
                          cat $pipeline_dir/prod/$Project_Name/ingress.yml
                          cat $pipeline_dir/prod/$Project_Name/ingress.yml  | /usr/bin/kubectl apply -f  -
                        fi
                        '''

                    } else {
                        echo "不发布"
                    }
                }
            }
            post {
                success {
                    wrap([$class: 'BuildUser']) {
                    lark (
                        robot: "9f7c94cd-491e-4309-83b4-9290d01fc285",
                        type: "CARD",
                        title: "📢  Jenkins 应用发布成功",
                        text: [
                            "😋 **应用名称**:[${JOB_NAME}](${JOB_URL})",
                            "😜 **应用环境**:Prod",
                            "🤪 **任务编号**:[${BUILD_DISPLAY_NAME}](${BUILD_URL})",
                            "😘 **发布状态**: <font color='green'>成功</font>",
                            "🤩 **镜像版本**: $Tag",
                            "😎 **镜像仓库**: harbor.kubesre.com:8443/kubesre/$Project_Name",
                            "😝 **执  行 者**: ${env.BUILD_USER}",
                            "<at id=all></at>"
                        ],
                        buttons: [
                           [
                              title: "更改记录",
                              url: "${BUILD_URL}changes"
                           ],
                           [
                              title: "控制台",
                              type: "danger",
                              url: "${BUILD_URL}console"
                           ]
                        ]
                    )}
                }
            }
        } 
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.
  • 194.
  • 195.
  • 196.
  • 197.
  • 198.
  • 199.
  • 200.
  • 201.
  • 202.
  • 203.
  • 204.
  • 205.
  • 206.
  • 207.
  • 208.
  • 209.
  • 210.
  • 211.
  • 212.
  • 213.
  • 214.
  • 215.
  • 216.
  • 217.
  • 218.
  • 219.
  • 220.
  • 221.
  • 222.
  • 223.
  • 224.
  • 225.
  • 226.
  • 227.
  • 228.
  • 229.
  • 230.
  • 231.
  • 232.
  • 233.
  • 234.
  • 235.
  • 236.
  • 237.
  • 238.
  • 239.
  • 240.
  • 241.
  • 242.
  • 243.
  • 244.
  • 245.
  • 246.
  • 247.
  • 248.
  • 249.
  • 250.
  • 251.
  • 252.
  • 253.
  • 254.
  • 255.
  • 256.
  • 257.
  • 258.
  • 259.
  • 260.
  • 261.
  • 262.
  • 263.
  • 264.
  • 265.
  • 266.
  • 267.
  • 268.
  • 269.
  • 270.
  • 271.
  • 272.
  • 273.

配置Jenkins

依赖的组件(自行安装):

  • Generic Webhook Trigger
  • Pipeline(所有以Pipeline开头的组件)
  • build user vars
  • Blue Ocean
  • Lark Notice(通过上传文件的方式安装)https://721806280.github.io/lark-notice-plugin-doc/

配置Lark Notice:

Lark Notice Plugin 是一个用于 Jenkins 的构建通知插件,可以将 Jenkins构建过程以及结果通知推送到 Lark、飞书、钉钉 协作平台。 可配置多个的通知时机,包括 构建启动时、构建中断、构建失败、构建成功时、构建不稳定 等。 支持多种不同类型的消息,包括 文本消息、图片消息, 群名片消息、富文本消息、卡片消息; 同时该插件还提供了自定义模板和变量的功能,使您能够根据自己的需求来定制通知消息的内容和格式。(本次案例是基于飞书进行验证)

准备工作,在飞书群新建一个机器人(https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot):

在飞书群,点击设置:

图片图片

然后点击群机器人:

图片图片

在飞书群,选择添加机器人

图片图片

填写相应配置信息并点击保存:

图片图片

图片图片

选择系统管理-Lark Notice:

图片图片

通知时机全部勾选:

图片图片

配置机器人信息并保存

图片图片

配置Java Pipeline

新建任务:

图片图片

填写任务名称,并选择流水线,点击确定:

图片图片

配置Pipeline SCM:

图片图片

修改脚本路径,点击确定:

图片图片

点击构建让配置生效:

图片图片

其他Java项目配置都一样!

配置Vue Pipeline

新建任务:

图片图片

填写任务名称,选择流水线:

图片图片

配置Pipeline SCM:

图片图片

修改脚本路径,点击确定:

图片图片

点击构建让配置生效:

图片图片

其他Vue项目配置都一样!

配置Gitlab Webhook

进入项目,选择webhook:

图片图片

选择添加Webhook:

图片图片

配置webhook URL,Token以Job Name:

图片图片

勾选标签推送事件,也就是说只有标签推送事件才会触发流水线:

图片图片

到此为止,Webook已配置完毕!所有项目配置都一样。

触发验证

触发Java Pipeline:

进入标签管理:

图片图片

新建标签:

图片图片

填写信息并点击创建标签(此标签名称也是容器镜像的Tag):

图片图片

进入Jenkins可以看到Gateway Pipeline已经触发了:

图片图片

图片图片

选择发布,并点击确定,将新版本发布到Dev环境:

图片图片

选择发布,并点击确定,将新版本发布到Uat环境:

图片图片

选择对应的灰度发布方式或者跳过:

图片图片

选择发布,并点击确定,将新版本发布到Prod环境:

图片图片

也可以回滚,默认是上一个版本也可修改成想要回滚到的版本:

图片图片

触发 Vue流水线:

进入标签管理:

图片图片

创建标签:

图片图片

填写信息并点击创建标签(此标签名称也是容器镜像的Tag):

图片图片

图片图片

进入Jenkins可以看到ruoyi-ui Pipeline已经触发了:

图片图片

选择发布,并点击确定,将新版本发布到Dev环境:

图片图片

选择发布,并点击确定,将新版本发布到Uat环境:

图片图片

选择发布,并点击确定,将新版本发布到Prod环境:

图片图片

构建通知

图片图片

责任编辑:武晓燕 来源: 云原生运维圈
相关推荐

2020-10-21 14:10:28

工具测试开发

2022-08-31 22:25:53

微服务架构DevOPs

2019-11-07 09:00:39

Jenkins流水线开源

2022-02-22 09:00:00

软件开发CI/CD 管道工具

2021-10-11 14:28:25

TypeScript企业级应用

2009-01-03 14:54:36

ibmdwWebSphere

2009-06-03 14:24:12

ibmdwWebSphere

2015-11-10 15:14:20

CIO时代网

2020-06-05 07:20:41

测试自动化环境

2023-02-19 15:28:39

CI/CD 管道集成开发

2021-07-27 08:01:22

CICD平台

2021-08-31 09:00:00

开发Azure DevOp集成

2020-10-12 07:00:00

JenkinsDevOps测试工具

2023-03-13 14:46:32

CI/CD工具软件开发

2018-08-24 09:00:00

DevOps持续集成连续部署

2023-09-11 12:57:00

大数据大数据中台

2010-04-07 08:55:00

OSGiSpring

2016-10-12 17:18:26

私有云持续交付华为

2014-09-09 14:10:01

企业级HadoopSpark

2018-02-02 11:21:25

云计算标准和应用大会
点赞
收藏

51CTO技术栈公众号