前言
前面我们介绍了Jenkins多分支流水线、Jenkins流水线即代码之扩展共享库,其实都是“流水线即代码”的体现。我们将Jenkinsfile纳入项目版本库中统一管理,实现了“谁构建、谁运行”的理念。
但是在实际项目中,CI/CD其实是由运维来管理的,这样就会导致运维、开发都要通过版本库去修改Jenkinsfile、项目代码。
试想下运维在调试流水线频繁提交版本,导致远程分支不断更新,如果有钩子触发自动发版,势必会引起开发的烦感。
为了避免这个情况的放生,我们引入了Jenkins扩展共享库,即将流水线操作拆分为两块:
- Jenkinsfile定义流水线步骤、环境变量、参数等与项目相关的一切变量;
- 扩展共享库定义流水线调用的方法、函数、类库等与构建相关的具体操作;
由于一旦流水线步骤及变量确定一般就不会改动了,而扩展共享库的方法等具体操作实现我们可以以代码的方式放入远程版本中,修改提交后Jenkinsfile构建自动加载共享库,获取最新的构建修改。
另,通过扩展共享库我们可以提高构建操作的复用,有效减少构建代码量;Jenkinsfile、扩展库还可以作为备份托管在版本库中,可谓是两全其美啊。
下面我们对多分支流水线、扩展共享库结合实现Vue项目的发版、回滚来具体讲解下扩展共享库的使用。
注:多分支流水线可以有效将多个分支放到一个项目下统一管理,避免因分支导致的项目分散。
Vue场景
Jenkins 远程web服务器
功能实现:
- 参数化构建:deploy-发版,rollback-回滚。
- 发版:判断git版本是否更新,若更新则在Jenkins上打包,并将dist包分发到远程web服务器上;若未更新,则停止构建。
- 回滚:回滚archiveArtifacts的版本包,分发到远程web服务器上。
注意:我们使用archiveArtifacts来归档版本包,回滚时可从归档路径中获取。
扩展共享库
一、添加扩展共享库
Manage Jenkins--Configure System--Global Pipeline Libraries中添加
二、代码库结构
代码语言:javascript复制pipeline-shared-library/
├── resources #空
├── src #空
└── vars
└── vueUpdate.groovy
vueUpdate.groovy 是所有vue项目的构建具体操作。
注意:由于所有的vue项目构建由共享库中的统一的方法实现,因此不同分支对应的环境要高度一致,这样才能最大限度的实现代码复用。
三、具体实现
我们将代码分为三部分,即deploy、rollback、update。
1.deploy-发版
(1)判断版本是否更新
我们通过将本次git的版本id存入文件,以便下次构建时将其与GIT_COMMIT进行比较,实现版本是否更新。
注意:由于第一次构建时,流水线报错“
代码语言:javascript复制No such property: GIT_PREVIOUS_SUCCESSFUL_COMMIT for class: groovy.lang.Binding
”。此时是无法通过GIT_PREVIOUS_SUCCESSFUL_COMMIT变量来获取上一次版本的,因此只能将其写入文件存放。
(2)打包
通过npm 打包vue项目生成dist。
注意:我们将dist压缩并改名为dist_temp.zip 作为我们分发到项目的版本包。其目的是作为中间临时文件,用于和项目的实际dist目录进行替换,更新后销毁即可。
另最终归档的版本包也为dist_temp.zip。
2.回滚-rollback
回滚的版本存在于archiveArtifacts归档后的构建目录中,在此目录中
代码语言:javascript复制${JENKINS_HOME}/jobs/`echo ${JOB_NAME}|awk -F'/' '{print $1}'`/branches/${BRANCH_NAME}/builds/${version}/archive/
多分支流水线的目录以分支名区分子目录。
3.分发更新
Jenkins通过sshpublisher将版本包dist_temp.zip 分发到远程web服务器上,通过rsync对项目目录dist进行更新,最后销毁dist_temp.zip。
注意:归档dist_temp.zip 及 邮件通知由Jenkinsfile定义,不放在共享库中。
具体代码如下:
所有的变量由跟随项目的Jenkinsfile提供。
代码语言:javascript复制def deploy() {
sh """
#新建commitid文件,初次写入0
if [ ! -f commitid ];then
echo 0 > commitid
fi
#从commitid获取上次版本id
previous_id=`cat commitid`
if [ $previous_id != $GIT_COMMIT ];then
#写入本次git commit,用于下次判断
echo $GIT_COMMIT > commitid
echo "start npm install&build"
/usr/local/nodejs/bin/npm install
/usr/local/nodejs/bin/npm run build
[ -f ${ZIP_NAME}.zip ] && rm -rfv ${ZIP_NAME}.zip
[ -d ${ZIP_NAME} ] && rm -rfv ${ZIP_NAME}
echo -e "\033[34m将dist改名为${ZIP_NAME}:\033[0m"
mv -v dist ${ZIP_NAME} && echo -e "\033[32mdist改名为${ZIP_NAME}成功。\033[0m" || {
echo -e "\033[32mdist改名为${ZIP_NAME}失败。\033[0m"
exit 1
}
zip -r ${ZIP_NAME}.zip ${ZIP_NAME} && echo -e "\033[32m${ZIP_NAME}压缩成功。\033[0m" || {
echo -e "\033[32m${ZIP_NAME}压缩失败。\033[0m"
exit 1
}
else
echo -e "\033[31mRepositories not update, stop deploy\033[0m"
exit 1
fi
"""
}
def rollback() {
sh """
[ ${version} -eq 0 ] && {
echo -e "\033[31m版本号非0。\033[0m"
exit 1
} || echo -e "\033[34mAction:${deploy_env}\033[0m"
echo -e "\033[34mRollback version:${version}\033[0m"
rm -rfv ${ZIP_NAME}.zip
cp -Rv ${JENKINS_HOME}/jobs/`echo ${JOB_NAME}|awk -F'/' '{print $1}'`/branches/${BRANCH_NAME}/builds/${version}/archive/${ZIP_NAME}.zip .
[ $? -eq 0 ] && echo -e "\033[32m指定版本号${version}的${ZIP_NAME}.zip复制到当前部署目录成功。\033[0m" || {
echo -e "\033[31m指定版本号${version}的${ZIP_NAME}.zip复制到当前部署目录失败。\033[0m"
exit 1
}
"""
}
def update(host) {
for(i in host){
sshPublisher(
publishers: [sshPublisherDesc(configName: "$i", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: """
IN_Face=`/sbin/route -n |awk '{if($4~/UG/){print $8}}'|head -n 1`
Local_IP=`/sbin/ip addr show "${IN_Face}" | grep -w 'inet' | awk '{print $2}'`
cd /App/htdocs/${APP_NAME}
echo -e "\033[34m${Local_IP}节点解压新版压缩包${ZIP_NAME}.zip:\033[0m"
unzip -o ${ZIP_NAME}.zip && echo -e "\033[32m${Local_IP}节点${ZIP_NAME}.zip解压成功。\033[0m" || {
echo -e "\033[31m${Local_IP}节点${ZIP_NAME}.zip解压失败。\033[0m"
exit 1
}
public_dir=`echo ${ZIP_NAME} | awk -F'_' '{print $1}'`
echo -e "\033[34m${Local_IP}节点将${ZIP_NAME}目录下数据同步到生产目录${public_dir}内:\033[0m"
[ -d ${ZIP_NAME} ] && rsync -vrl --delete ${ZIP_NAME}/ ${public_dir:-/tmp/aaa}/
[ $? -eq 0 ] && echo -e "\033[32m${Local_IP}节点数据同步成功。\033[0m" || {
echo -e "\033[31m${Local_IP}节点数据同步失败。\033[0m"
exit 1
}
echo -e "\033[34m${Local_IP}节点将${ZIP_NAME}.zip删除:\033[0m"
rm -rfv ${ZIP_NAME}.zip ${ZIP_NAME}
echo -e "\033[32m${JOB_NAME}发布成功。\033[0m"
""", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ] ', remoteDirectory: "htdocs/${APP_NAME}", remoteDirectorySDF: false, removePrefix: '', sourceFiles: "${ZIP_NAME}.zip")], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)]
)
}
}
Jenkinsfile
1.参数化构建
通过parameters 定义构建的参数:deploy、rollback
回滚通过BUILD_NUMBER传输历史版本的归档。
2.环境变量
我们定义两个环境变量:
APP_NAME 项目所在目录,如/App/${APP_NAME}/dist
ZIP_NAME 版本包
3.流水线步骤
发版:调用共享库deploy方法;
回滚:调用共享库rollback方法;
测试部署:调用共享库update方法,传入测试环境服务器列表;
生产部署:调用共享库update方法,传入生产环境服务器列表;
归档:不管构建状态,总是归档版本包dist_temp.zip;
邮件通知:构建不稳定、成功、失败发送邮件通知;
注意:流水线中我们使用when来匹配参数化构建,这样可以比避免在sh中使用case或if 判断,减少代码量。
具体代码如下:
代码语言:javascript复制//引入jenkins扩展共享库
@Library('shared-library') _
pipeline {
agent any
options {
ansiColor('xterm')
timestamps()
}
parameters {
choice choices: ['deploy', 'rollback'], description: '''deploy:发布
rollback:回滚''', name: 'deploy_env'
string defaultValue: '0', description: '''回滚版本号,发布时忽略该参数
版本号为jenkins job BUILD_NUMBER''', name: 'version', trim: false
}
environment {
APP_NAME="www.test.cn"
ZIP_NAME="dist_temp"
}
stages {
stage('发版') {
//匹配发版参数
when {
expression { params.deploy_env ==~ 'deploy' }
}
steps {
script {
vueUpdate.deploy()
}
}
}
stage('回滚') {
//匹配回滚参数
when {
expression { params.deploy_env ==~ 'rollback' }
}
steps {
script {
vueUpdate.rollback()
}
}
}
stage('测试部署') {
//匹配develop分支
when {
branch 'develop'
}
steps {
script {
def host = ["test-10.13", "test-10.14"]
vueUpdate.update(host)
}
}
stage('生产部署') {
//匹配master分支
when {
branch 'master'
}
steps {
script {
def host = ["prod-10.11", "prod-10.12"]
vueUpdate.update(host)
}
}
}
}
post {
always {
archiveArtifacts "${ZIP_NAME}.zip"
}
unstable {
emailext (
body: """项目名称:${JOB_NAME}n构建编号:${BUILD_NUMBER}n构建日志:${BUILD_URL}console""",
subject: '【Jenkins构建通知】:$JOB_NAME - Build # $BUILD_NUMBER - Unstable!',
to: 'xx@test.cn',
from: 'test@test.cn'
)
}
success {
emailext (
body: """项目名称:${JOB_NAME}n构建编号:${BUILD_NUMBER}n构建日志:${BUILD_URL}console""",
subject: '【Jenkins构建通知】:$JOB_NAME - Build # $BUILD_NUMBER - Successful!',
to: 'xx@test.cn',
from: 'test@test.cn'
)
}
failure {
emailext (
body: """项目名称:${JOB_NAME}n构建编号:${BUILD_NUMBER}n构建日志:${BUILD_URL}console""",
subject: '【Jenkins构建通知】:$JOB_NAME - Build # $BUILD_NUMBER - Failure!',
to: 'xx@test.cn',
from: 'test@test.cn'
)
}
}
}
最终结果如下:
总结
Jenkins扩展共享库 多分支流水线一方面可以简化CI/CD过程中的项目管理,一方面可以驱动我们各个环境的标准化,为实现自动化做好铺垫。
反过来环境标准化是我们灵活应用Jenkins扩展共享库的前提,没有足够的标准化,那么我们就需要增加代码量去适配各个环境。
总之,在运维的过程中,你会发现标准化和规范化越来越重要。