文章目录
前言
在现代化前端工程中,高效的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
[root@k8s-node ~]# mc config host add myminio http://192.168.56.102:8021 OpsMinIO OpsAdmin081524
minio服务器设置前端制品库桶
2、Jenkins凭据
minio账密凭据--usernamePassword类型
服务器账密凭据--usernamePassword类型
3、注意事项
1、服务器账密保持一致,因为后续pipeline中连接部署服务器会使用
2、Jenkins服务器需要安装nodejs、yarn等编译前端代码的组件
3、Jenkins需要安装nodejs插件、ssh相关插件,并在全局工具配置中设置npm路径
二、设计思想
1. 模块化设计
采用共享库模式将功能解耦为独立模块:
build.groovy :封装构建逻辑,支持前端不同构建工具(npm、yarn)
tools.groovy :提供统一的日志输出和可视化工具
toemailF.groovy :处理通知机制,实现标准化的邮件模板
2.多环境支持
通过环境变量实现配置与逻辑分离:
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作为制品仓库,实现版本追踪:
// 保存部署信息
env.DEPLOY_INFO = """
应用: ${JOB_NAME}
版本: ${BUILD_TIME}-${env.GIT_COMMIT}
包路径: ${MINIO_BUCKET}/${JOB_NAME}/${env.ARTIFACT_NAME}
"""
4. 安全部署机制
- 凭据管理:通过 withCredentials 安全使用SSH和MinIO密钥
- 签名验证:动态生成AWS签名头保障MinIO访问安全
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. 回滚机制
实现完整的版本追溯和回滚流程:
- 从MinIO获取历史版本列表
- 交互式选择回滚目标
- 保持与部署相同的安全机制
三、CI阶段
1、构建节点选择
核心思想:
1、后端服务采用Jenkins动态slave-pod的方式,将其部署到k8s
2、前端服务采用宿主机Jenkins服务将其构建部署
设置构建节点
pipeline{
agent {
label 'master' #此名称跟上述图片中的名称保持一致
}
options {
timestamps()
skipDefaultCheckout() // 禁用隐式 Checkout
timeout(time: 1, unit: 'HOURS') //设置流水线超时
}
}
2、代码拉取
#!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
@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
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阶段
1、Jenkins凭据添加服务器ssh账密、minio账密
2、将多个destIp按逗号分割成数组,使用each 循环遍历每个服务器IP
3、动态生成minio签名并结合curl命令从minio下载部署包
4、通过sshpass命令连接单个服务器IP
a、删除源部署路径下的文件,然后将从minio下载的部署包解压到指定目录
b、删除多余目录和下载的tar包
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
"""
}
}
}
}
}
五、回滚阶段
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包
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
"""
}
}
}
}
}
}
六、构建通知
1、不管构建成功还是失败,都发送对应的邮件给接收者
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、执行回滚
1、构建rollback选项
2、部署路径不变
3、勾选回滚机器IP destIp
八、完整pipeline
#!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}"
)
}
}
}
}