背景
在自动化发布场景中,包括代码拉取、测试、编译、发布等阶段,而这些阶段都需要写在Jenkinsfile内。
比如下面这个:
def _dockerUrl='10.3.23.191:9902'
def _dockerNamespace='ks'
def _dockerName='operation-platform'
def _apiFolderName="${_dockerName}-api"
def _jobFolderName="${_dockerName}-job"
def _apiImageName="${_dockerUrl}/${_dockerNamespace}/${_dockerName}-api"
def _jobImageName="${_dockerUrl}/${_dockerNamespace}/${_dockerName}-job"
def createVersion() {
// 版本号
return new Date().format('yyyyMMddHHmmss') + "_${env.BUILD_ID}"
}
pipeline {
options {
timeout(time: 10, unit: 'MINUTES')
}
agent {
docker {
image '10.3.23.191:9902/devops/maven:3.8.2-openjdk-8'
args '-v $HOME/.m2:/root/.m2 -v /root/.ssh:/root/.ssh'
}
}
parameters {
string(name: 'BRANCH', defaultValue: 'dev', description: '要部署的代码分支名称')
choice(name: 'PROFILE', choices:['test', 'pre', 'prod'],description: '要部署的环境')
}
environment {
_DOCKER_TAG=createVersion()
// harbor 登录凭证
_HARBOR_CREDS=credentials('harbor-admin')
// 服务器登录凭证
_APPSERVER_CREDS=credentials('dev_root')
}
stages {
stage('Init') {
steps {
echo "============================================"
sh " echo => 构建的分支: $BRANCH"
sh " echo => 发布的环境: $PROFILE"
echo "============================================"
}
}
stage('Git pull') {
steps {
sh 'date '
sh 'ls -al '
echo '开始拉取代码 ..'
checkout([$class: 'GitSCM', branches: [[name: '*/$BRANCH']], extensions: [], userRemoteConfigs: [[credentialsId: '67f1f5b0-d6fb-40fa-9799-4a6dbc85a328', url: 'https://gitlab.com.cn/operation-platform/operation-platform.git']]])
echo '代码拉取成功'
}
}
stage('Test') {
steps {
echo 'Testing..'
}
}
stage('Build Project') {
steps {
sh 'pwd'
echo '开始编译代码..'
sh "mvn --version"
sh "mvn clean package -D maven.test.skip=true"
echo '代码编译成功'
}
}
stage('Build Docker Image') {
steps {
sh 'pwd'
sh 'ls -al '
echo "===========START,开始编译Docker Image : ${_apiImageName} ==========="
echo "----> 删除历史镜像 ${_apiFolderName} ..."
sh "docker rmi --force ${_apiFolderName} | true"
//only retain last 3 images,自动删除老的容器,只保留最近2个
sh """docker rmi \$(docker images | grep ${_apiFolderName} | sed -n '3,\$p' | awk '{print \$3}') || true"""
echo "----> 编译镜像 ${_apiFolderName} ..."
sh "docker build -f ./${_apiFolderName}/Dockerfile -t ${_apiImageName}:${_DOCKER_TAG} --build-arg JAR_NAME=./${_apiFolderName}/target/*.jar ."
echo "----> docker push 镜像 ${_apiFolderName} ..."
sh "echo ${_HARBOR_CREDS_PSW} | docker login -u ${_HARBOR_CREDS_USR} ${_dockerUrl} --password-stdin"
sh "docker push ${_apiImageName}:${_DOCKER_TAG}"
echo "===========END,编译Docker Image : ${_apiImageName} ==========="
echo "===========START,开始编译Docker Image : ${_jobImageName} ==========="
echo "----> 删除历史镜像 ${_jobFolderName} ..."
sh """docker rmi \$(docker images | grep ${_jobFolderName} | sed -n '3,\$p' | awk '{print \$3}') || true"""
echo "----> 编译镜像 ${_jobFolderName} ..."
sh "docker build -f ./${_jobFolderName}/Dockerfile -t ${_jobImageName}:${_DOCKER_TAG} --build-arg JAR_NAME=./${_jobFolderName}/target/*.jar ."
echo "----> docker push 镜像 ${_jobFolderName} ..."
sh "docker push ${_jobImageName}:${_DOCKER_TAG}"
echo "===========END,编译Docker Image : ${_jobImageName} ==========="
}
}
stage('Deploy test') {
when {
expression { params.PROFILE == "test" }
}
steps {
echo 'Deploying....'
script {
// SSH Pipeline Steps plugin
def remoteServer = [:]
remoteServer.name = 'test'
remoteServer.host = '10.3.23.191'
remoteServer.allowAnyHosts = true
remoteServer.user = "${_APPSERVER_CREDS_USR}"
remoteServer.password = "${_APPSERVER_CREDS_PSW}"
//=========================== deploy api =====================================
sshCommand remote: remoteServer, command: """
pwd
str1=`docker ps -a | grep -w ${_apiImageName} | awk '{print \$1}'`
str2=`docker images | grep -w ${_apiImageName} | awk '{print \$3}'`
if [ "\$str2" != "" ] ; then
if [ "\$str1" != "" ] ; then
docker stop `docker ps -a | grep -w ${_apiImageName} | awk '{print \$1}'`
docker rm `docker ps -a | grep -w ${_apiImageName} | awk '{print \$1}'`
#docker rmi --force `docker images | grep -w ${_apiImageName} | awk '{print \$3}'`
else
docker rmi --force `docker images | grep -w ${_apiImageName} | awk '{print \$3}'`
fi
fi
"""
sshCommand remote: remoteServer, command: """
docker run -d -p 9010:9010 \\
-e JAVA_OPTS="-Xmx1g -Xms1g -Djava.security.egd=file:/dev/.urandom" \\
-e PARAMS="--server.port=9010 --spring.profiles.active=test " \\
--restart always ${_apiImageName}:${_DOCKER_TAG}
"""
//=========================== deploy job =====================================
sshCommand remote: remoteServer, command: """
pwd
str1=`docker ps -a | grep -w ${_jobImageName} | awk '{print \$1}'`
str2=`docker images | grep -w ${_jobImageName} | awk '{print \$3}'`
if [ "\$str2" != "" ] ; then
if [ "\$str1" != "" ] ; then
docker stop `docker ps -a | grep -w ${_jobImageName} | awk '{print \$1}'`
docker rm `docker ps -a | grep -w ${_jobImageName} | awk '{print \$1}'`
#docker rmi --force `docker images | grep -w ${_jobImageName} | awk '{print \$3}'`
else
docker rmi --force `docker images | grep -w ${_jobImageName} | awk '{print \$3}'`
fi
fi
"""
sshCommand remote: remoteServer, command: """
docker run -d -p 9011:9011 -p 9012:9012 \\
-e JAVA_OPTS="-Xmx1g -Xms1g -Djava.security.egd=file:/dev/.urandom" \\
-e PARAMS="--server.port=9011 --spring.profiles.active=test --xxl-job.executor.ip=10.3.23.191 --xxl-job.executor.port=9012" \\
--restart always ${_jobImageName}:${_DOCKER_TAG}
"""
}
}
}
}
post {
always {
// 清理工作区
cleanWs()
}
}
}
- 大量脚本堆砌在Jenkinsfile内,让用户(开发者)感觉非常乱;理论上应该只保留用户关心的东西,其他代码都对用户不可见
- 测试、编译、发布等过程,针对同一种环境,其代码都是一样的。我要想办法让这些共同的过程抽象出来,让所有项目的pipeline共用。这样以后有修改,也只需要修改公共的部分即可。
而Jenkins Shared Library
就可以解决以上问题
安装Pipeline Groovy Libraries插件
具体过程可参考https://www.jianshu.com/p/fb12e47d7b11
其他几个插件因为在groovy脚本中使用到了,所以需要提前安装。
创建 Shared Library Git 仓库
首先创建一个groovy项目,并提交到gitlab中。
groovy项目的目录必须为以下格式:
+- src # Groovy source files
| +- org
| +- foo
| +- Bar.groovy # for org.foo.Bar class
+- vars
| +- foo.groovy # for global 'foo' variable
| +- foo.txt # help for 'foo' variable
+- resources # resource files (external libraries only)
| +- org
| +- foo
| +- bar.json # static helper data for org.foo.Bar
-
src
为源代码目录,执行Pipeline时,该目录将添加到类路径中。 -
vars
目录托管定义可从Pipeline访问的全局脚本(一般我们可以在这里编写标准化脚本)。通常,每个.groovy文件的基本名称应使用驼峰(camelCased)模式 -
resources
目录允许libraryResource从外部库中使用步骤来加载相关联的非Groovy文件,比如一些公共的配置文件(json/yaml等)。
以下就是我定义的全局库目录
+--- resources
| +--- pipeline.yaml
+--- src
| +--- com
| | +--- test
| | | +--- devops
| | | | +--- enums
| | | | | +--- EnvTypeEnum.groovy
| | | | | +--- NetworkEnum.groovy
| | | | | +--- ProjectTypeEnum.groovy
| | | | +--- utils
| | | | | +--- tool.groovy
+--- vars
| +--- buildImage.groovy
| +--- deployImage.groovy
| +--- deployImageBySSH.groovy
| +--- paramPageRender.groovy
| +--- paramPrint.groovy
| +--- pipelineStart.groovy
| +--- pipelineStartMaven.groovy
其中build.gradle内容如下:
plugins {
id 'groovy'
}
group 'com.test.devops'
version '1.0-SNAPSHOT'
sourceSets {
main {
groovy {
srcDir 'src'
srcDir 'vars'
}
resources {
srcDir 'resources'
}
}
test {
groovy {
srcDir 'test'
}
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.apache.groovy:groovy:4.0.2'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
test {
useJUnitPlatform()
}
编写好脚本以后,把脚本提交到gitlab中。
Jenkins中配置全局库
进入Manage Jenkins » Configure System » Global Pipeline Libraries
点新增:
配置完后,点击保存
Jenkinsfile
配置完全局库以后,我们就可以在Jenkinsfile中直接引用此Library
- 首先在Library项目定义了/var/pipelineStart.groovy
其内容如下:
#!groovy
def call(cfg) {
pipeline {
agent {
docker {
image "${cfg.global.dockerAgent}"
args "${cfg.global.dockerAgentArgs}"
}
}
options {
skipDefaultCheckout() //删除隐式checkout scm语句
disableConcurrentBuilds() //禁止并行
timeout(time: 1, unit: 'HOURS') //流水线超时设置1h
timestamps()
}
environment {
// 镜像仓库 登录凭证
_HARBOR_CREDS=credentials("${cfg.global.harborUserId}")
}
stages {
stage('Params') {
steps {
script {
// 渲染参数页面
def pageRender = paramPageRender()
pageRender()
echo '==============================参数配置start================================'
cfg.envType = params.envType
cfg.branch = params.branch
cfg.servers = cfg.servers[params.envType];
cfg.dockerEnvs = cfg.dockerEnvs[params.envType];
cfg.dockerPorts = cfg.dockerPorts[params.envType];
cfg.version = tool.createVersion("${params.envType}","${env.BUILD_ID}")
cfg.fullImageName = "${cfg.global.harbor}/${cfg.imageGroup}/${cfg.imageName}:${cfg.version}"
if(cfg.dockerBuildArg){
cfg.dockerBuildArg = "--build-arg ${cfg.dockerBuildArg}"
}
// 打印参数
paramPrint(cfg)
echo '==============================参数配置end================================'
}
}
}
stage('Checkout') {
steps {
script {
echo '==============================Checkout start================================'
def extensions = []
if(cfg.isSubmodule){
extensions[0] = [$class : 'SubmoduleOption',
disableSubmodules : false,
parentCredentials : true,
recursiveSubmodules: true,
shallow : false,
trackingSubmodules : false]
}
checkout([$class : 'GitSCM',
branches : [[name: "*/${cfg.branch}"]],
extensions : extensions,
userRemoteConfigs: [[credentialsId: "${cfg.global.gitlabUserId}", url: "${cfg.gitUrl}"]]
])
echo '==============================Checkout start================================'
}
}
}
stage('Test') {
steps {
echo '==============================Test start================================'
// skip
echo '==============================Test end================================'
}
}
stage('Build') {
steps {
echo '==============================Build start================================'
buildImage(cfg)
echo '==============================Build end================================'
}
}
stage('Deploy') {
steps {
echo '==============================Deploy start================================'
deployImage(cfg)
echo '==============================Deploy end================================'
}
}
}
post {
always {
echo '==============================Clean Workspace start================================'
cleanWs()
echo '==============================Clean Workspace end================================'
}
}
}
}
其中buildImage、deployImage
等,都是在/vars/文件夹下定义的其他脚本文件。
然后再Jenkinsfile类似这样:
#!groovy
library 'test-library'
def cfg = [:]
// 项目类型。maven
cfg.put('projectType', 'maven')
// git地址
cfg.put('gitUrl',"https://gitlab.xxxx.com/operation-platform/operation-platform.git")
// 工作空间,根目录的相对路径。默认为.
cfg.put('workspace', './operation-platform-api')
// 镜像名称
cfg.put('imageName', 'operation-platform-api')
// 镜像组,请勿随便定义,需要提前在harbor中创建
cfg.put('imageGroup', 'ks')
// docker镜像build时的参数,格式为:'k1=v1 k2=v2 ...',具体值请参考Dockerfile ARG
cfg.put('dockerBuildArg', 'JAR_NAME=target/*.jar')
//........其他配置...
// 启动Pipeline
pipelineStart(cfg)
- Jenkinsfile通过
library 'test-library'
来引用共享库 - Jenkinsfile中只保留用户关心的配置信息,所有的发布过程脚本全都封装在了共享库里。