文章目錄
- 前言
- 前端項目CICD時序圖
- 一、環境準備
- 1、服務器相關
- 2、Jenkins憑據
- 3、注意事項
- 二、設計思想
- 1. 模塊化設計
- 2.多環境支持
- 3. 制品管理
- 4. 安全部署機制
- 5. 回滾機制
- 三、CI階段
- 1、構建節點選擇
- 2、代碼拉取
- 3、代碼編譯
- 4、打包并上傳至minio
- 四、CD階段
- 五、回滾階段
- 六、構建通知
- 七、實戰演示--發布/回滾前端項目
- 1、Jenkins創建流水線項目
- 2、執行構建
- 3、執行回滾
- 八、完整pipeline
前言
在現代化前端工程中,高效的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的方式,將其部署到k8s2、前端服務采用宿主機Jenkins服務將其構建部署
設置構建節點
pipeline{agent {label 'master' #此名稱跟上述圖片中的名稱保持一致}options {timestamps()skipDefaultCheckout() // 禁用隱式 Checkouttimeout(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命令連接單個服務器IPa、刪除源部署路徑下的文件,然后將從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'; thentar xzf ${env.ARTIFACT_NAME} -C ${destPath}/mv ${destPath}/dist/* ${destPath}rm -rf ${destPath}/dist ${destPath}/${env.ARTIFACT_NAME}elseecho "下載的文件無效或不是gzip壓縮包"exit 1fi
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'; thentar xzf ${selectedVersion} -C ${destPath}/mv ${destPath}/dist/* ${destPath}/rm -rf ${destPath}/dist ${destPath}/${selectedVersion}elseecho "下載的文件無效或不是gzip壓縮包"exit 1fi
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() // 禁用隱式 Checkouttimeout(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'; thentar xzf ${env.ARTIFACT_NAME} -C ${destPath}/mv ${destPath}/dist/* ${destPath}rm -rf ${destPath}/dist ${destPath}/${env.ARTIFACT_NAME}elseecho "下載的文件無效或不是gzip壓縮包"exit 1fi
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'; thentar xzf ${selectedVersion} -C ${destPath}/mv ${destPath}/dist/* ${destPath}/rm -rf ${destPath}/dist ${destPath}/${selectedVersion}elseecho "下載的文件無效或不是gzip壓縮包"exit 1fi
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}")}}}
}