Jenkins Shared Library - pipeline代码共用

背景

在自动化发布场景中,包括代码拉取、测试、编译、发布等阶段,而这些阶段都需要写在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()
        }
    }
}
  1. 大量脚本堆砌在Jenkinsfile内,让用户(开发者)感觉非常乱;理论上应该只保留用户关心的东西,其他代码都对用户不可见
  2. 测试、编译、发布等过程,针对同一种环境,其代码都是一样的。我要想办法让这些共同的过程抽象出来,让所有项目的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

image.png

点新增:
image.png

image.png

配置完后,点击保存

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中只保留用户关心的配置信息,所有的发布过程脚本全都封装在了共享库里。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容