《企业级前端部署方案:Jenkins+MinIO+SSH+Gitee+Jenkinsfile自动化实践》

文章目录

前言

在现代化前端工程中,高效的CI/CD流程已成为团队标配。本文将详细解析如何通过Jenkins Pipeline实现从代码提交到自动化部署的全流程,重点分享多服务器并行部署、MinIO制品管理以及一键回滚等核心功能的实现方案。文中提供的Jenkinsfile模板可直接用于生产环境,助你快速搭建企业级部署平台。

前端项目CICD时序图

一、环境准备

1、服务器相关

ip 部署
192.168.56.101 nginx1
192.168.56.102 nignx2、Jenkins、nodejs(18.16.0)、minio(minio-RELEASE_2023_05_18)

minio服务器设置myminio

shell 复制代码
[root@k8s-node ~]# mc config host add myminio http://192.168.56.102:8021 OpsMinIO OpsAdmin081524

minio服务器设置前端制品库桶

2、Jenkins凭据

minio账密凭据--usernamePassword类型

服务器账密凭据--usernamePassword类型

3、注意事项

yaml 复制代码
1、服务器账密保持一致,因为后续pipeline中连接部署服务器会使用
2、Jenkins服务器需要安装nodejs、yarn等编译前端代码的组件
3、Jenkins需要安装nodejs插件、ssh相关插件,并在全局工具配置中设置npm路径

二、设计思想

1. 模块化设计

采用共享库模式将功能解耦为独立模块:

  • build.groovy :封装构建逻辑,支持前端不同构建工具(npm、yarn)

  • tools.groovy :提供统一的日志输出和可视化工具

  • toemailF.groovy :处理通知机制,实现标准化的邮件模板

2.多环境支持

通过环境变量实现配置与逻辑分离:

groovy 复制代码
String Tenv="${env.Tenv}"
environment {
    BUILD_TIME = sh(script: "date '+%Y%m%d_%H%M%S'", returnStdout: true).trim()
    MINIO_BUCKET = 'frontend-artifacts' 
}
复制代码
构建参数如 buildType 、 buildshell、Tenv 等通过Jenkins job参数动态注入

3. 制品管理

采用MinIO作为制品仓库,实现版本追踪:

groovy 复制代码
// 保存部署信息
env.DEPLOY_INFO = """
    应用: ${JOB_NAME}
    版本: ${BUILD_TIME}-${env.GIT_COMMIT}
    包路径: ${MINIO_BUCKET}/${JOB_NAME}/${env.ARTIFACT_NAME}
"""

4. 安全部署机制

  • 凭据管理:通过 withCredentials 安全使用SSH和MinIO密钥
  • 签名验证:动态生成AWS签名头保障MinIO访问安全
groovy 复制代码
DATE_VALUE_REMOTE=\$(date -R)
SIGNATURE_REMOTE=\$(echo -en "GET\\n\\n\\n\${DATE_VALUE_REMOTE}\\n/${MINIO_BUCKET}/${JOB_NAME}/${env.ARTIFACT_NAME}" | 
    openssl sha1 -hmac "\${MINIO_SECRET_KEY}" -binary | base64)

5. 回滚机制

实现完整的版本追溯和回滚流程:

  1. 从MinIO获取历史版本列表
  2. 交互式选择回滚目标
  3. 保持与部署相同的安全机制

三、CI阶段

1、构建节点选择

yaml 复制代码
核心思想:
	1、后端服务采用Jenkins动态slave-pod的方式,将其部署到k8s
	2、前端服务采用宿主机Jenkins服务将其构建部署

设置构建节点

groovy 复制代码
pipeline{
    agent {
        label 'master' #此名称跟上述图片中的名称保持一致
    }
    options {
        timestamps()
        skipDefaultCheckout()  // 禁用隐式 Checkout
        timeout(time: 1, unit: 'HOURS') //设置流水线超时
    }
}

2、代码拉取


groovy 复制代码
#!groovy
@Library("jenkinslib") _

//func from sharelibrary调用共享库
def build=new org.devops.build()
def tools=new org.devops.tools()
def toemailF=new org.devops.toemailF()

//调用Jenkins构建参数
String Tenv="${env.Tenv}"
String srcURL="${env.SrcURL}"
String branch="${env.branchName}"

pipeline{
	stages{
        stage("CheckOut"){
            when { expression { !rollback } }  // 非回滚时执行
            steps{
                script{
                    tools.PrintMsg("获取分支: ${branch}","checkout")
                    tools.PrintMsg("获取代码","checkout")
                    checkout([$class: 'GitSCM', branches: [[name: "${branch}"]], 
                        extensions: [], 
                        userRemoteConfigs: [[credentialsId: 'gitee_registry_ssh', url: "${srcURL}"]]])
                    // 记录当前commit信息用于追踪
                    env.GIT_COMMIT = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
                }
            }
        }
    }
}

3、代码编译

groovy 复制代码
#!groovy
@Library("jenkinslib") _

//func from sharelibrary调用共享库
def build=new org.devops.build()
def tools=new org.devops.tools()
def toemailF=new org.devops.toemailF()

String Tenv="${env.Tenv}"
String buildType="${env.buildType}"
String buildshell="${env.buildshell}"

pipeline{
    environment {
        BUILD_TIME = sh(script: "date '+%Y%m%d_%H%M%S'", returnStdout: true).trim()
        MINIO_BUCKET = 'frontend-artifacts'
    }
	stage("代码编译"){
            when { expression { !rollback } }  // 非回滚时执行
            steps{
                script{
                    tools.PrintMsg("代码编译","build")
                    // 使用共享库中的构建方法,会自动处理依赖安装和构建
                    build.Builds(buildType,buildshell)
                    // 生成带版本号的构建产物名称
                    env.ARTIFACT_NAME = "${JOB_NAME}-${BUILD_TIME}-${env.GIT_COMMIT}.tar.gz"
                }
            }
        }
}

4、打包并上传至minio

groovy 复制代码
pipeline{
    environment {
        BUILD_TIME = sh(script: "date '+%Y%m%d_%H%M%S'", returnStdout: true).trim()
        MINIO_BUCKET = 'frontend-artifacts'
    }
	stage("打包并上传至minio"){
            when { expression { !rollback } }  // 非回滚时执行
            steps{
                script{
                    tools.PrintMsg("构建好的包上传至minio","image_tag")
                    sh """
                        tar -czf ${env.ARTIFACT_NAME} dist/
                        mc cp ${env.ARTIFACT_NAME} myminio/${MINIO_BUCKET}/${JOB_NAME}/
                    """
                    // 保存部署信息
                    env.DEPLOY_INFO = """
                        应用: ${JOB_NAME}
                        版本: ${BUILD_TIME}-${env.GIT_COMMIT}
                        包路径: ${MINIO_BUCKET}/${JOB_NAME}/${env.ARTIFACT_NAME}
                    """
                }
            }
    }
}

四、CD阶段

yaml 复制代码
1、Jenkins凭据添加服务器ssh账密、minio账密
2、将多个destIp按逗号分割成数组,使用each 循环遍历每个服务器IP
3、动态生成minio签名并结合curl命令从minio下载部署包
4、通过sshpass命令连接单个服务器IP
	a、删除源部署路径下的文件,然后将从minio下载的部署包解压到指定目录
	b、删除多余目录和下载的tar包
groovy 复制代码
        stage("部署"){
            when { expression { !rollback } }
            steps{
                script {
                    tools.PrintMsg("开始部署", "deploy")
                    withCredentials([
                        usernamePassword(
                            credentialsId: 'target-server-credential',
                            usernameVariable: 'SSH_USER',
                            passwordVariable: 'SSH_PASS'
                        ),
                        usernamePassword(
                            credentialsId: 'minio-credentials',
                            usernameVariable: 'MINIO_ACCESS_KEY',
                            passwordVariable: 'MINIO_SECRET_KEY'
                        )
                    ]) {
                        // 将destIp按逗号分割成数组
                        def servers = destIp.split(',')
                        servers.each { server ->
                            sh """
                                DATE_VALUE=\$(date -R)
                                SIGNATURE=\$(echo -en "GET\\n\\n\\n\${DATE_VALUE}\\n/${MINIO_BUCKET}/${JOB_NAME}/${env.ARTIFACT_NAME}" | 
                                    openssl sha1 -hmac "\${MINIO_SECRET_KEY}" -binary | base64)
                                
                                # 直接在SSH会话中生成签名和下载
                                sshpass -p \${SSH_PASS} ssh -o StrictHostKeyChecking=no \${SSH_USER}@${server} <<'EOS'
                                    cd ${destPath}
                                    rm -rf ${destPath}/*
                                    
                                    # 在远程服务器上重新生成签名
                                    DATE_VALUE_REMOTE=\$(date -R)
                                    SIGNATURE_REMOTE=\$(echo -en "GET\\n\\n\\n\${DATE_VALUE_REMOTE}\\n/${MINIO_BUCKET}/${JOB_NAME}/${env.ARTIFACT_NAME}" | 
                                        openssl sha1 -hmac "${MINIO_SECRET_KEY}" -binary | base64)
                                    
                                    curl -v -X GET -H "Date: \${DATE_VALUE_REMOTE}" \\
                                        -H "Authorization: AWS ${MINIO_ACCESS_KEY}:\${SIGNATURE_REMOTE}" \\
                                        -o ${env.ARTIFACT_NAME} \\
                                        "http://192.168.56.102:8021/${MINIO_BUCKET}/${JOB_NAME}/${env.ARTIFACT_NAME}"
                                    
                                    if [ -s ${env.ARTIFACT_NAME} ] && file ${env.ARTIFACT_NAME} | grep -q 'gzip compressed data'; then
                                        tar xzf ${env.ARTIFACT_NAME} -C ${destPath}/
                                        mv ${destPath}/dist/* ${destPath}
                                        rm -rf ${destPath}/dist ${destPath}/${env.ARTIFACT_NAME}
                                    else
                                        echo "下载的文件无效或不是gzip压缩包"
                                        exit 1
                                    fi
EOS
                            """
                        }
                    }
                }
            }
        }

五、回滚阶段

yaml 复制代码
1、Jenkins凭据添加服务器ssh账密、minio账密
2、通过mc ls myminio结合awk命令获取到对应桶中目录下所有的tar包
3、手动选择要回滚的包
4、将多个destIp按逗号分割成数组,使用each 循环遍历每个服务器IP
5、通过sshpass命令连接单个服务器IP,将选择的tar包传入curl下载命令中
    a、动态生成minio签名并结合curl命令从minio下载回滚的包
	b、删除源部署路径下的文件,然后将从minio下载的回滚包解压到指定目录
	c、删除多余目录和下载的tar包
groovy 复制代码
        stage("回滚"){
            when { expression { rollback } }
            steps{
                script {
                    tools.PrintMsg("执行回滚", "rollback")
                    
                    // 获取可用版本列表
                    def versions = sh(script: "mc ls myminio/${MINIO_BUCKET}/${JOB_NAME}/ | awk '{print \$6}'", returnStdout: true).trim().split(',')
                    def selectedVersion = input(
                        message: '选择要回滚的版本', 
                        parameters: [
                            choice(name: 'selectedVersion', choices: versions.join(','), description: '可用的构建版本')
                        ]
                    )
                    // 设置回滚部署信息
                    env.DEPLOY_INFO = """
                        版本: ${selectedVersion}
                    """
                    withCredentials([
                        usernamePassword(
                            credentialsId: 'target-server-credential',
                            usernameVariable: 'SSH_USER',
                            passwordVariable: 'SSH_PASS'
                        ),
                        usernamePassword(
                            credentialsId: 'minio-credentials',
                            usernameVariable: 'MINIO_ACCESS_KEY',
                            passwordVariable: 'MINIO_SECRET_KEY'
                        )
                    ]) {
                        // 将destIp按逗号分割成数组
                        def servers = destIp.split(',')
                        servers.each { server ->
                            sh """
                                sshpass -p \${SSH_PASS} ssh -o StrictHostKeyChecking=no \${SSH_USER}@${server} <<'EOS'
                                    cd ${destPath}
                                    rm -rf ${destPath}/*
                                    
                                    # 在远程服务器上生成签名
                                    DATE_VALUE_REMOTE=\$(date -R)
                                    SIGNATURE_REMOTE=\$(echo -en "GET\\n\\n\\n\${DATE_VALUE_REMOTE}\\n/${MINIO_BUCKET}/${JOB_NAME}/${selectedVersion}" | 
                                        openssl sha1 -hmac "${MINIO_SECRET_KEY}" -binary | base64)
                                    
                                    curl -v -X GET -H "Date: \${DATE_VALUE_REMOTE}" \\
                                        -H "Authorization: AWS ${MINIO_ACCESS_KEY}:\${SIGNATURE_REMOTE}" \\
                                        -o ${selectedVersion} \\
                                        "http://192.168.56.102:8021/${MINIO_BUCKET}/${JOB_NAME}/${selectedVersion}"
                                    
                                    if [ -s ${selectedVersion} ] && file ${selectedVersion} | grep -q 'gzip compressed data'; then
                                        tar xzf ${selectedVersion} -C ${destPath}/
                                        mv ${destPath}/dist/* ${destPath}/
                                        rm -rf ${destPath}/dist ${destPath}/${selectedVersion}
                                    else
                                        echo "下载的文件无效或不是gzip压缩包"
                                        exit 1
                                    fi
EOS
                            """
                        }
                    }
                }
            }
        }
    }

六、构建通知

yaml 复制代码
1、不管构建成功还是失败,都发送对应的邮件给接收者
groovy 复制代码
    post {
        always {
            script {
                TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"))
                env.BUILD_TIME = new Date().format("yyyyMMdd_HHmmss")
                def buildTime = env.BUILD_TIME ?: "N/A"
                def buildDuration = currentBuild.durationString ?: "N/A"
                toemailF.Email(
                    currentBuild.currentResult,
                    "${Tenv}",
                    "${env.emailUser}",
                    "${JOB_NAME}",
                    "${branch}",
                    "${env.BUILD_USER}",
                    buildTime,
                    buildDuration,
                    rollback,
                    "服务器: ${destIp}",
                    env.DEPLOY_INFO ?: "无部署信息",
                    "${srcURL}"
                )
            }
        }
    }

七、实战演示--发布/回滚前端项目

1、Jenkins创建流水线项目

2、执行构建


3、执行回滚

yaml 复制代码
1、构建rollback选项
2、部署路径不变
3、勾选回滚机器IP destIp

八、完整pipeline

groovy 复制代码
#!groovy
@Library("jenkinslib") _

//func from sharelibrary调用共享库
def build=new org.devops.build()
def tools=new org.devops.tools()
def toemailF=new org.devops.toemailF()

String Tenv="${env.Tenv}"
String buildType="${env.buildType}"
String buildshell="${env.buildshell}"
String srcURL="${env.SrcURL}"
String branch="${env.branchName}"
String destPath="${env.destPath}"
String destIp="${env.destIp}"
Boolean rollback = (env.rollback == 'true')
pipeline{
    agent {
        label 'master'
    }
    options {
        timestamps()
        skipDefaultCheckout()  // 禁用隐式 Checkout
        timeout(time: 1, unit: 'HOURS') //设置流水线超时
    }
    environment {
        BUILD_TIME = sh(script: "date '+%Y%m%d_%H%M%S'", returnStdout: true).trim()
        MINIO_BUCKET = 'frontend-artifacts'
    }
    stages{
        stage("CheckOut"){
            when { expression { !rollback } }  // 非回滚时执行
            steps{
                script{
                    tools.PrintMsg("获取分支: ${branch}","checkout")
                    tools.PrintMsg("获取代码","checkout")
                    checkout([$class: 'GitSCM', branches: [[name: "${branch}"]], 
                        extensions: [], 
                        userRemoteConfigs: [[credentialsId: 'gitee_registry_ssh', url: "${srcURL}"]]])
                    // 记录当前commit信息用于追踪
                    env.GIT_COMMIT = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
                }
            }
        }
        stage("代码编译"){
            when { expression { !rollback } }  // 非回滚时执行
            steps{
                script{
                    tools.PrintMsg("代码编译","build")
                    // 使用共享库中的构建方法,会自动处理依赖安装和构建
                    build.Builds(buildType,buildshell)
                    // 生成带版本号的构建产物名称
                    env.ARTIFACT_NAME = "${JOB_NAME}-${BUILD_TIME}-${env.GIT_COMMIT}.tar.gz"
                }
            }
        }
        stage("打包并上传至minio"){
            when { expression { !rollback } }  // 非回滚时执行
            steps{
                script{
                    tools.PrintMsg("构建好的包上传至minio","image_tag")
                    sh """
                        tar -czf ${env.ARTIFACT_NAME} dist/
                        mc cp ${env.ARTIFACT_NAME} myminio/${MINIO_BUCKET}/${JOB_NAME}/
                    """
                    // 保存部署信息
                    env.DEPLOY_INFO = """
                        应用: ${JOB_NAME}
                        版本: ${BUILD_TIME}-${env.GIT_COMMIT}
                        包路径: ${MINIO_BUCKET}/${JOB_NAME}/${env.ARTIFACT_NAME}
                    """
                }
            }
        }
        stage("部署"){
            when { expression { !rollback } }
            steps{
                script {
                    tools.PrintMsg("开始部署", "deploy")
                    withCredentials([
                        usernamePassword(
                            credentialsId: 'target-server-credential',
                            usernameVariable: 'SSH_USER',
                            passwordVariable: 'SSH_PASS'
                        ),
                        usernamePassword(
                            credentialsId: 'minio-credentials',
                            usernameVariable: 'MINIO_ACCESS_KEY',
                            passwordVariable: 'MINIO_SECRET_KEY'
                        )
                    ]) {
                        // 将destIp按逗号分割成数组
                        def servers = destIp.split(',')
                        servers.each { server ->
                            sh """
                                # 直接在SSH会话中生成签名和下载
                                sshpass -p \${SSH_PASS} ssh -o StrictHostKeyChecking=no \${SSH_USER}@${server} <<'EOS'
                                    cd ${destPath}
                                    rm -rf ${destPath}/*
                                    
                                    # 在远程服务器上重新生成签名
                                    DATE_VALUE_REMOTE=\$(date -R)
                                    SIGNATURE_REMOTE=\$(echo -en "GET\\n\\n\\n\${DATE_VALUE_REMOTE}\\n/${MINIO_BUCKET}/${JOB_NAME}/${env.ARTIFACT_NAME}" | 
                                        openssl sha1 -hmac "${MINIO_SECRET_KEY}" -binary | base64)
                                    
                                    curl -v -X GET -H "Date: \${DATE_VALUE_REMOTE}" \\
                                        -H "Authorization: AWS ${MINIO_ACCESS_KEY}:\${SIGNATURE_REMOTE}" \\
                                        -o ${env.ARTIFACT_NAME} \\
                                        "http://192.168.56.102:8021/${MINIO_BUCKET}/${JOB_NAME}/${env.ARTIFACT_NAME}"
                                    
                                    if [ -s ${env.ARTIFACT_NAME} ] && file ${env.ARTIFACT_NAME} | grep -q 'gzip compressed data'; then
                                        tar xzf ${env.ARTIFACT_NAME} -C ${destPath}/
                                        mv ${destPath}/dist/* ${destPath}
                                        rm -rf ${destPath}/dist ${destPath}/${env.ARTIFACT_NAME}
                                    else
                                        echo "下载的文件无效或不是gzip压缩包"
                                        exit 1
                                    fi
EOS
                            """
                        }
                    }
                }
            }
        }
        // 5. 回滚机制
        stage("回滚"){
            when { expression { rollback } }
            steps{
                script {
                    tools.PrintMsg("执行回滚", "rollback")
                    
                    // 获取可用版本列表
                    def versions = sh(script: "mc ls myminio/${MINIO_BUCKET}/${JOB_NAME}/ | awk '{print \$6}'", returnStdout: true).trim().split(',')
                    def selectedVersion = input(
                        message: '选择要回滚的版本', 
                        parameters: [
                            choice(name: 'selectedVersion', choices: versions.join(','), description: '可用的构建版本')
                        ]
                    )
                    // 设置回滚部署信息
                    env.DEPLOY_INFO = """
                        版本: ${selectedVersion}
                    """
                    
                    withCredentials([
                        usernamePassword(
                            credentialsId: 'target-server-credential',
                            usernameVariable: 'SSH_USER',
                            passwordVariable: 'SSH_PASS'
                        ),
                        usernamePassword(
                            credentialsId: 'minio-credentials',
                            usernameVariable: 'MINIO_ACCESS_KEY',
                            passwordVariable: 'MINIO_SECRET_KEY'
                        )
                    ]) {
                        // 将destIp按逗号分割成数组
                        def servers = destIp.split(',')
                        servers.each { server ->
                            sh """
                                sshpass -p \${SSH_PASS} ssh -o StrictHostKeyChecking=no \${SSH_USER}@${server} <<'EOS'
                                    cd ${destPath}
                                    rm -rf ${destPath}/*
                                    
                                    # 在远程服务器上生成签名
                                    DATE_VALUE_REMOTE=\$(date -R)
                                    SIGNATURE_REMOTE=\$(echo -en "GET\\n\\n\\n\${DATE_VALUE_REMOTE}\\n/${MINIO_BUCKET}/${JOB_NAME}/${selectedVersion}" | 
                                        openssl sha1 -hmac "${MINIO_SECRET_KEY}" -binary | base64)
                                    
                                    curl -v -X GET -H "Date: \${DATE_VALUE_REMOTE}" \\
                                        -H "Authorization: AWS ${MINIO_ACCESS_KEY}:\${SIGNATURE_REMOTE}" \\
                                        -o ${selectedVersion} \\
                                        "http://192.168.56.102:8021/${MINIO_BUCKET}/${JOB_NAME}/${selectedVersion}"
                                    
                                    if [ -s ${selectedVersion} ] && file ${selectedVersion} | grep -q 'gzip compressed data'; then
                                        tar xzf ${selectedVersion} -C ${destPath}/
                                        mv ${destPath}/dist/* ${destPath}/
                                        rm -rf ${destPath}/dist ${destPath}/${selectedVersion}
                                    else
                                        echo "下载的文件无效或不是gzip压缩包"
                                        exit 1
                                    fi
EOS
                            """
                        }
                    }
                }
            }
        }
    }
    post {
        always {
            script {
                TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"))
                env.BUILD_TIME = new Date().format("yyyyMMdd_HHmmss")
                def buildTime = env.BUILD_TIME ?: "N/A"
                def buildDuration = currentBuild.durationString ?: "N/A"
                toemailF.Email(
                    currentBuild.currentResult,
                    "${Tenv}",
                    "${env.emailUser}",
                    "${JOB_NAME}",
                    "${branch}",
                    "${env.BUILD_USER}",
                    buildTime,
                    buildDuration,
                    rollback,
                    "服务器: ${destIp}",
                    env.DEPLOY_INFO ?: "无部署信息",
                    "${srcURL}"
                )
            }
        }
    }
}
相关推荐
如意.75917 小时前
【Linux开发工具实战】Git、GDB与CGDB从入门到精通
linux·运维·git
运维小欣17 小时前
智能体选型实战指南
运维·人工智能
yy552717 小时前
Nginx 性能优化与监控
运维·nginx·性能优化
踩着两条虫18 小时前
VTJ.PRO 核心架构全公开!从设计稿到代码,揭秘AI智能体如何“听懂人话”
前端·vue.js·ai编程
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ18 小时前
Linux 查询某进程文件所在路径 命令
linux·运维·服务器
jzlhll12319 小时前
kotlin Flow first() last()总结
开发语言·前端·kotlin
蓝冰凌19 小时前
Vue 3 中 defineExpose 的行为【defineExpose暴露ref变量】详解:自动解包、响应性与实际使用
前端·javascript·vue.js
奔跑的呱呱牛20 小时前
generate-route-vue基于文件系统的 Vue Router 动态路由生成工具
前端·javascript·vue.js
05大叔20 小时前
网络基础知识 域名,JSON格式,AI基础
运维·服务器·网络