13.使用 Docker Pipeline插件动态生成Jenkins Slave 实践

前面有提到过使用docker pipeline插件的主要目的是为了动态生成Jenkins的slave节点作为pipeline执行的环境。本节根据上一节的语法示例,通过一个实践案例来加深一下理解。

构建镜像

首先需要说明的是,无论使用哪种类型的语法、是使用一个镜像还是多个镜像作为流水线stage的agent代理,首先要做的是构建好这些提供流水线脚本执行的镜像。

经过前面一系列实践,可以发现本系列文章用到工具主要包括jdk、git、maven、docker和ansible等。所以我们需要自定义一个包含以上一个多个工具的镜像,dockerfile如下:

FROM alpine:latest

RUN apk update && apk add openjdk8 git maven

COPY settings.xml /usr/share/java/maven-3/conf/settings.xml
COPY docker/docker /usr/bin/docker

其中:

  • settings.xml文件为maven配置文件,里面配置了Maven私有仓库管理器的地址及认证信息,这里需要根据你实际情况考虑要不要添加,在后面的章节中会介绍其它方法来自定义该文件的使用。

  • docker/docker:docker的二进制文件,这里需要注意一点,如果想要将docker可执行文件构建到镜像中或者在启动时挂载docker可执行文件,一定要使用二进制可执行文件,不要使用yum安装生成的二进制文件,否则在使用时会提示docker命令找不到。这是因为使用yum安装生成的docker二进制文件,挂载到容器内目录或者构建到其它镜像中时,必须保证宿主机和镜像内的OS相同,否则无法执行另一个系统平台上的docker二进制文件。

  • docker/docker从官方下载的docker tar包文件解压后为一个docker目录。

通过build命令构建:

$ docker build -t alpine-cicd:latest .

构建好以后测试一下,pipeline脚本如下:

pipeline {
    agent { 
        docker{
            image 'alpine-cicd:latest'
            args '-v /var/run/docker.sock:/var/run/docker.sock'
        }
    }

    stages('build') {    
        stage('测试'){
            steps {
                sh """
                docker version
                mvn --version
                git --version
                """
            }
        }
    }
}

流水线正常执行表示镜像构建成功。

一个统一的镜像构建好了,如果遇到使用多个agent的情况,也可以使用这个统一的镜像,或者分别构建特定工具的镜像,这里不再进行演示,有兴趣的可以自己尝试一下。

声明式语法

构建完基础镜像,就可以正式学习如何使用pipeline与docker插件集成了。在前面章节介绍了使用vm作为Jenkins的slave节点的实践示例,使用docker容器作为Jenkins的slave节点与使用vm有很多相同的地方,比如使用共享存储保存构建后的产物,使用pipeline方法实现对私有仓库的认证等,学会了前面章节的内容,对于本章的学习还是相对较简单的。

Agent代理

首先来回顾一下使用docker容器作为agent代理的基本语法。

使用一个全局agent

pipeline {
    agent { docker 'maven:3-alpine' } 
    stages {
        stage('build') {
            steps {
                sh 'mvn clean install'
            }
        }
    }
}

使用stage级别的agent:

pipeline {
    agent none 
    stages {
        stage('Build') {
            agent { docker 'maven:3-alpine' } 
            steps {
                echo 'Hello, Maven'
                sh 'mvn --version'
            }
        }
        stage('Test') {
            agent { docker 'openjdk:8-jre' } 
            steps {
                echo 'Hello, JDK'
                sh 'java -version'
            }
        }
    }
}

而在学习了脚本式语法以及docker pipeline插件语法后,使用多agent也可以这样写:在不同的代理节点启动不同的容器。

pipeline {
    agent none 
    stages {
        stage('Build') {
            agent { node { label ‘slave1’ } } 

            steps {
                script{
                    docker.image('maven').inside(){
                        sh 'hostname'
                    }
                }
            }
        }
        stage('Test') {
           agent { node { label ‘slave2’ } } 
            steps {
                script{
                    docker.image('nginx').inside(){
                        sh 'hostname'
                    }
                }
            }
        }
    }
}

该方式用的情况可能不多,但在这里还是有必要说一下:

使用全局agent

使用docker容器作为agent只要修改agent指令的配置即可,配置比较简单,示例如下:

pipeline {
    agent { 
        docker{
            image 'alpine-cicd:latest'
            args '-v /var/run/docker.sock:/var/run/docker.sock -v /data/:/data/ -v /root/.docker/config.json:/root/.docker/config.json -v /root/.m2:/root/.m2'
        }
    }

    environment {
        project_name = 'fw-base-nop'
        jar_name = 'fw-base-nop.jar'
        registry_url = '192.168.176.155'
        project_group = 'base'
    }

    stages('build') {    
        stage('代码拉取并打包'){
            steps {
                checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
                echo "开始打包 "
                sh "cd $project_name && mvn clean install -DskipTests -Denv=beta"
            }
        }

        stage('构建镜像'){
            steps {
                    script{
                        jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name $jar_name |head -1").trim()
                    }
                    sh """
                    cp $jar_file /data/$project_group/$project_name/
                    """
            }
        }
        stage('上传镜像'){
            steps {
                script {
                    docker.withRegistry('http://192.168.176.155', 'auth_harbor') {
                        def customImage=docker.build("${registry_url}/${project_group}/${project_name}:${env.BUILD_ID}","/data/${project_group}/${project_name}/")
                        customImage.push()
                    }
                }
            }
        }
    }
    post {
        always {
            script{
                sh 'docker rmi -f $registry_url/$project_group/$project_name:${BUILD_ID}'

                if (currentBuild.currentResult == "FAILURE" || currentBuild.currentResult == "UNSTABLE" ){
                    emailext (
                        subject: "'${env.JOB_NAME} [${env.BUILD_NUMBER}]' 构建结果",
                        body: """
                        详情:\n<br>
                        Jenkins 构建 ${currentBuild.result} '\n'<br>
                        项目名称 :${env.JOB_NAME} "\n"
                        项目构建id:${env.BUILD_NUMBER} "\n"
                        URL :${env.BUILD_URL} \n
                        构建日志:${env.BUILD_URL}console

                        """,
                        to: "[email protected]",  
                        recipientProviders: [[$class: 'CulpritsRecipientProvider'],
                                             [$class: 'DevelopersRecipientProvider'],
                                             [$class: 'RequesterRecipientProvider']]
                    )
                }else{
                    echo "构建成功"
                }
            }

        }

    }

}

说明:

  • 相对于使用vm作为agent代理,流水线核心代码没变,只是修改了agent的配置。

  • image 参数指定了在上一个小节中构建的镜像名称。

  • args 定义了启动镜像的参数:

-v /var/run/docker.sock:/var/run/docker.sock用于挂载宿主机docker服务的sock文件到容器中,如果不挂载该文件使用docker时会提示docker进程没有启动,并无法进行镜像构建工作;

-v /data/:/data/用于将共享存储的data目录挂载到容器中,该目录包含了项目组文件夹,项目文件夹以及dockerfile文件(目录结构根据自己实际情况自定义),代码编译的产物会放到指定的项目文件夹中,构建镜像时会使用项目文件夹路径作为上下文路径进行构建;

-v /root/.docker/config.json:/root/.docker/config.json 用于将宿主机上docker进程向私有仓库认证的文件挂载到容器内指定的文件上,这里需要注意的使用宿主机当前用户(需要对docker命令有操作权限),至于该容器在构建镜像时默认是root用户;

流水线执行时,默认会在jenkins的所有节点中选择一台节点,并启动容器,虽然是在容器中进行镜像构建与推送到私有仓库操作,但使用的各种配置大多都依赖于宿主机。

使用多个agent

使用多个agent与使用全局agent没有明显的区别,只是将最顶层的agent关键字的值变为none,在不同的步骤通过agent关键字指定不同的镜像即可。

如下示例:

pipeline {
    agent none

    environment {
        project_name = 'fw-base-nop'
        jar_name = 'fw-base-nop.jar'
        registry_url = '192.168.176.155'
        project_group = 'base'
    }

    stages('build') {    
        stage('代码拉取并打包'){
            agent { 
                docker {
                    image 'alpine-cicd:latest'
                    args '-v /root/.m2:/root/.m2'
                } 
            }
            steps {
                checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
                echo "开始打包 "
                sh "cd $project_name && mvn clean install -DskipTests -Denv=beta"
            }
        }

        stage('构建镜像'){
            agent { 
                docker {
                    image 'alpine-cicd:latest'
                    args ' -v /var/run/docker.sock:/var/run/docker.sock -v /data/:/data/'
                } 

            }
            steps {
                script{
                    jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name $jar_name |head -1").trim()
                }
                sh """
                cp $jar_file /data/$project_group/$project_name/
                """
            }
        }
        stage('上传镜像'){
            agent { 
                docker {
                    image 'alpine-cicd:latest'
                    args ' -v /data/:/data/ -v /var/run/docker.sock:/var/run/docker.sock‘
                } 
            }
            steps {
                script {
                    docker.withRegistry('http://192.168.176.155', 'auth_harbor') {
                        def customImage=docker.build("${registry_url}/${project_group}/${project_name}:${env.BUILD_ID}","/data/${project_group}/${project_name}/")
                        customImage.push()
                    }
                }
            }
        }
    }
    post {
        always {
            node (null){
                script{
                    sh 'docker rmi -f $registry_url/$project_group/$project_name:${BUILD_ID}'

                    if (currentBuild.currentResult == "ABORTED" || currentBuild.currentResult == "FAILURE" || currentBuild.currentResult == "UNSTABLE" ){
                        emailext (
                            subject: "'${env.JOB_NAME} [${env.BUILD_NUMBER}]' 构建结果",
                            body: """
                            详情:<br>
                            Jenkins 构建 ${currentBuild.result} <br>
                            项目名称 :${env.JOB_NAME} <br>
                            项目构建id:${env.BUILD_NUMBER} <br>
                            URL :${env.BUILD_URL} <br>
                            构建日志:${env.BUILD_URL}console

                            """,
                            to: "[email protected]",  
                            recipientProviders: [[$class: 'CulpritsRecipientProvider'],
                                                 [$class: 'DevelopersRecipientProvider'],
                                                 [$class: 'RequesterRecipientProvider']]
                        )
                    }else{
                        echo "构建成功"
                    }

                }
            }

        }

    }

}

说明:

  • 有一点与使用全局agent不同,在最后post指令中,需要单独设定node节点。此处不能用agent指令,但可以通过node('slave-label')的方式指定一个节点,slave的标签也可以设定为null,此时默认会自动选择一个slave节点进行post下的操作。这种情况如果要部署该镜像,可直接通过宿主机的脚本进行部署。

  • 另一点需要注意的是,代码编译与镜像构建要挂载相同的数据目录来使用同一个构建产物。

私有仓库认证

前面的章节中,在声明式脚本中对私有仓库的认证介绍了使用withCredentials()withRegistry<br/>(),对于在声明式语法中使用docker容器作为agent对私有仓库进行认证时,也可以通过在agent指令中添加对私有仓库认证的附加参数,比如:

docker {
        image '192.168.176.155/library/maven:v2'
        args '-v $HOME/.m2:/root/.m2  -v /data/fw-base-nop:/data  -v /var/run/docker.sock:/var/run/docker.sock'
        registryUrl 'http://192.168.176.155'
        registryCredentialsId 'auth_harbor'
}

通过在容器启动参数中加入registryUrlregistryCredentialsId参数,在容器启动后对私有仓库认证。除了可以在拉取工作镜像时使用,也可以在构建应用镜像后推送私到私有仓库时使用。

完整版的配置如下:

pipeline{
    agent { 
        docker {
            image '192.168.176.155/library/jenkins-slave'
            args '-v $HOME/.m2:/root/.m2  -v /data/fw-base-nop:/data -v /var/run/docker.sock:/var/run/docker.sock'
            registryUrl 'http://192.168.176.155'
            registryCredentialsId 'auth_harbor'
        }
    }
    stages {
        stage('clone') {
            steps('clone code'){
                checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
                script {
                  imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
                }
            }
        }

        stage("mvn") {
            steps('build'){
                sh """
                    cd fw-base-nop && mvn clean install -DskipTests -Denv=beta
                    cp ${WORKSPACE}/fw-base-nop/target/fw-base-nop.jar /data/
                    hostname && pwd
                """
            }
        }
        stage("build and push to registry") {
            environment { 
                dockerfile = '/data/Dockerfile'
            }
            steps('push'){
                script {
                    docker.withRegistry('http://192.168.176.155', 'auth_harbor') {
                        def customImage=docker.build("${registry_url}/${project_group}/${project_name}:${env.BUILD_ID}","/data/${project_group}/${project_name}/")
                        customImage.push()
                    }
                }
            }
        }
    }
}

有关在声明式语法中使用Docker插件的内容就简单的介绍到这里,下面重点介绍一下在脚本式语法中使用Docker pipeline插件。

脚本式语法

使用脚本式语法动态生成slave工作节点,开始之前首先来回顾一下使用docker pipeline插件启动容器的语法,如下所示:

node {
     docker.image('maven:3.3.3-jdk-8').inside {
        git '…your-sources…'
        sh 'mvn -B clean install'
     }
}

通过image属性指定一个容器,然后通过inside属性启动一个容器。

下面分多个步骤介绍该插件的使用:

  • 版本一:在容器内进行代码编译,在宿主机进行镜像构建并推送至私有仓库。
  • 版本二:在容器内编译构建并上传到私有仓库

版本一

使用脚本式语法相对于使用声明式语法在指定的slave节点上启动容器时相对简单的多,比如在jenkins-slave1主机上启动容器。

node('jenkins-slave1') {
    docker.image('192.168.176.155/library/maven:v2').inside('-v $HOME/.m2:/root/.m2')    { 
        stage('checkout'){
            checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false,  userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
        }
        stage('编译'){
            sh 'cd fw-base-nop && mvn clean install -DskipTests -Denv=beta'
            sh 'hostname && pwd'    
        }
    }
}

说明

  • 通过在node()中指定slave节点的标签,可以设置在指定的节点上启动容器进行工作。

  • 该示例会在jenkins-slave1节点上使用inside属性启动一个maven容器用来进行代码编译工作,需要注意的是,即便不传参数,也要保证inside属性存在。

执行日志如下:

由上面的jenkins执行结果可以看到,在job执行后,如果本地没有设定的maven镜像,会先拉取maven镜像,并将该镜像启动。启动的方法和参数可以通过上面的截图看到。

执行结果如下:-

由上图可看到,通过容器拉取代码并编译,编译完成以后容器会自动销毁,所以编译后的产物也会随之消失。此时,在上一章节中使用的共享存储就到了彰显自我价值的时候了,只需要将编译好的包保存到共享存储上即可,一个比较简单的方法,通过在宿主机挂载共享存储目录并将该目录映射到容器中指定的目录下。

如下示例:

node('jenkins-slave1') {
    docker.image('192.168.176.155/library/maven:v2').inside('-v $HOME/.m2:/root/.m2 -v /data/base/fw-base-nop:/data')    { 
        stage('checkout'){
            checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
        }
        stage('编译'){
            sh 'cd fw-base-nop && mvn clean install -DskipTests -Denv=beta'
            sh 'hostname && pwd'    
        }
        stage('拷贝jar包'){
            sh 'cp ${WORKSPACE}/fw-base-nop/target/*.jar /data/fw-base-nop.jar'
        }
    }
}

这里模拟/data/目录为共享存储挂载到宿主机的目录。编译好后将构建产物拷贝到容器的/data目录就相当于拷贝到宿主机/data/base/fw-base-nop目录下,然后在该目录事先创建Dockerfile文件就可以构建镜像了。

而在宿主机进行镜像构建与推送到私有仓库操作,直接参考上一章节相关的操作即可。

node('jenkins-slave1') {
    docker.image('192.168.176.155/library/maven:v2').inside('-v $HOME/.m2:/root/.m2 -v /data/base/fw-base-nop:/data')    { 
        stage('checkout'){
            checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
        }
        stage('maven'){
            sh 'cd fw-base-nop && mvn clean install -DskipTests -Denv=beta'
            sh 'hostname && pwd'    
        }
        stage('拷贝jar包'){
            sh 'cp ${WORKSPACE}/xxx-management/xxx-admin/target/*.jar /data/admin.jar'
        }

    }
    stage('build and push'){
            docker.withRegistry('http://192.168.176.155','auth_harbor') {
                def dockerfile = '/data/bsse/fw-base-nop/Dockerfile'
                def customImage = docker.build("192.168.176.155/library/admin:v1", "-f ${dockerfile} /data/bsse/fw-base-nop/.")
                customImage.push()
            }
    }
}

构建效果图这里不展示了,有兴趣的可以自己进行扩展并测试。如果你的slave节点配置了私有仓库认证,这里你可以不用withRegistry方法。

版本二

上面已经完成了在容器内的代码编译并在宿主机构建镜像。下面只需要在此基础上将镜像构建操作放到容器中即可。

直接编写pipeline script:

node ('jenkins-slave1') {
    docker.withRegistry('http://192.168.176.155','auth_harbor') { 
        docker.image('alpine-cicd:latest').inside('-v $HOME/.m2:/root/.m2  -v /data/base/fw-base-nop:/data  -v /var/run/docker.sock:/var/run/docker.sock)    {  
            stage('checkout'){
                checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
                script {
                  imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
                }
            }
            stage('打包'){
                sh 'cd fw-base-nop && mvn clean install -DskipTests -Denv=beta'

                sh 'hostname && pwd'    
            }
            stage('copy jar file'){
                sh 'cp ${WORKSPACE}/fw-base-nop/target/fw-base-nop.jar /data/'
            }
            stage('build and push'){
                    def dockerfile = '/data/Dockerfile'
                    def customImage = docker.build("192.168.176.155/library/fw-base-nop:${imageTag}", "-f ${dockerfile} /data/.")
                    customImage.push()
            }

            stage('delete image'){
                sh "docker rmi 192.168.176.155/library/fw-base-nop:${imageTag}"
            }

        }
    }

}

说明

  • 在版本一中我将withRegistry方法放到了指定的stage下,还可以将该方法放到pipeline脚本的最顶层。

  • script 包含的命令用来执行命令获取提交的代码的commit 的short id作为镜像的版本号。

  • 脚本最后不要忘记加上删除刚刚构建的镜像的操作,这里再说说明一下该操作是因为构建的镜像会存在宿主机上,而且每个镜像的tag不一样,如果构建次数多了会导致镜像数量庞大,占用空间;并且该镜像已经上传到私有仓库,这里就没必要在保存在宿主机上了。

  • 有一点需要注意,就是jenkins slave节点的docker版本问题,本例docker版本为docker-ce-18.06,非ce版本可能会出现如下问题:

    /usr/bin/docker: .: line 2: can't open '/etc/sysconfig/docker'
    
    
    或者
    
    
    You don't have either docker-client or docker-client-latest installed. Please install either one and retry.
    

执行结果如下:-

使用多个agent代理

在脚本式语法中使用多个agent的配置比较灵活,比如:

示例一:在同一个node节点上启动多个容器执行流水线。

node ('jenkins-slave1') {
    docker.withRegistry('http://192.168.176.155','auth_harbor') {
        docker.image('xxx').inside('-v $HOME/.m2:/root/.m2  -v /data/base/fw-base-nop:/data ')    {  
            stage('checkout'){
                checkout(xxxx)
                script {
                  imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
                }
            }
            stage('编译'){
                sh 'xxx'    
            }
            stage('拷贝jar包'){
                sh 'cp ${WORKSPACE}/fw-base-nop/target/fw-base-nop.jar /data/'
            }
        }

        docker.image('xxx').inside('-v /data/base/fw-base-nop:/data -v /var/run/docker.sock:/var/run/docker.sock') {
           stage('构建') {
            def dockerfile = '/data/Dockerfile'
            def customImage = docker.build("192.168.176.155/library/fw-base-nop:${imageTag}", "-f ${dockerfile} /data/.")
            customImage.push()
           }
        }
    }

}

示例二:在不同的node节点上进行不同的操作,比如在slave1节点上进行代码编译工作,在slave2节点上进行镜像构建与推送到私有仓库操作(这种一般使用共享存储来共享工作目录)。

node {

    stage('working in slave1'){
        node('jenkins-slave1'){
            docker.image('alpine-cicd:latest').inside('-v $HOME/.m2:/root/.m2  -v /data/base/fw-base-nop:/data  -v /var/run/docker.sock:/var/run/docker.sock')    {
                stage('checkout in slave1'){
                    checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
                    script {
                      imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
                    }
                }

                stage('build in slave1'){
                    sh 'cd fw-base-nop && mvn clean install -DskipTests -Denv=beta'

                    sh 'hostname && pwd'    
                }

                stage('copy jar file in slave1'){
                    sh 'cp ${WORKSPACE}/fw-base-nop/target/fw-base-nop.jar /data/'
                }
            }
        }
    }

    stage('working in slave169'){
        node('jenkins-slave169'){
            stage('build and push in slave169'){
                docker.withRegistry('http://192.168.176.155','auth_harbor') {
                    def dockerfile = '/data/base/fw-base-nop/Dockerfile'
                    def customImage = docker.build("192.168.176.155/library/fw-base-nop:${imageTag}", "-f ${dockerfile} /data/base/fw-base-nop/.")
                    customImage.push()
                }
            }
        }
    }

}

此示例分别在两台机器上进行代码编译和镜像构建,需要注意的是,两个slave节点用到的共同的目录(比如maven私有库存放目录,构建产物的存放目录,dockerfile的存放目录等)需要使用共享存储挂载,才能保证数据的一致性。

构建效果这里就不贴图了,执行结果大致都相同。

Pipeline script from SCM

除了上面使用 pipeline script的方式直接在jenkins项目里编写pipeline脚本外,还可以将写好pipeline脚本放到源代码仓库里,即Pipeline script from SCM

将上面pipeline脚本写到文件Jenkinsfile中,然后提交到源代码仓库,然后在jenkins项目中通过选择Pipeline script from SCM,然后配置该代码仓库的地址,用户名和密码构建即可。- - 上面配置保存即可。使用这种方式构建项目,官方建议使用声明式语法编写脚本。

异常处理

有关异常处理的内容,在上一章节以及pipeline实践章节中已经有过介绍,在接下来的内容只要将其拿过来直接使用即可。

首先回顾一下在脚本式语法中使用try/catch/finally指令的语法:

node ('jenkins-slave1') {        
            try {
                stage('exec command'){
                    sh 'hostnamae && pwd'
                    currentBuild.currentresult = 'SUCCESS'
                }
            }catch(e) {
                currentBuild.currentresult = 'FAILURE'
                def errorMsg = "================== 错误信息 START ===================\n"
                errorMsg += "${e.toString()}|${e.message}\n"
                errorMsg += "==================== 错误信息 END ======================"
                echo "errrormsg is ${errorMsg}"
            }

            finally{
                if (currentBuild.result == 'SUCCESS') {
                    echo "---currentBuild.currentresult is:${currentBuild.currentresult}---"
                }
                else {
                    echo "---currentBuild.currentresult is:${currentBuild.currentresult}
                }

           }

        }
    }
}

说明

  • try 指令包含要执行的任务,try指令后面必须跟随catch或者finally指令,catch和finally可以不用同时存在,但当有try指令时,这俩指令必须有一个存在。
  • 该示例根据job执行的结果分别做出不同的操作。

根据上面的语法,就可以将上面持续交付的代码套进这个模板里,如下所示:

node ('jenkins-slave1') {

    docker.withRegistry('http://192.168.176.155','auth_harbor') {
        docker.image('192.168.176.155/library/jenkins-slave').inside('-v $HOME/.m2:/root/.m2  -v /data/fw-base-nop:/data  -v /var/run/docker.sock:/var/run/docker.sock -v /usr/bin/docker:/usr/bin/docker')    {  
            stage('checkout'){
                checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false,  userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
                script {
                  imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
                }
            }

            try {
                stage('maven'){
                    sh 'cd fw-base-nop && mvn clean install -DskipTests -Denv=beta'
                    sh 'hostname && pwd'
                    currentBuild.currentresult = 'SUCCESS'
                }
            }catch(e) {
                currentBuild.currentresult = 'FAILURE'
                def errorMsg = "================= 错误信息 START ======================\n"
                errorMsg += "${e.toString()}|${e.message}\n"
                errorMsg += "=================== 错误信息 END ======================"
                echo "${errorMsg}"
                emailext (
                    subject: "'${env.JOB_NAME} [${env.BUILD_NUMBER}]' 构建异常",
                    body: """
                    详情:\n
                        failure: Job ${env.JOB_NAME} [${env.BUILD_NUMBER}] \n
                        状态:${env.JOB_NAME} jenkins 构建异常  \n
                        URL :${env.BUILD_URL} \n
                        项目名称 :${env.JOB_NAME} \n
                        项目构建id:${env.BUILD_NUMBER} \n
                        错误信息: ${errorMsg} \n

                    """,
                    to: "[email protected]",  
                    recipientProviders: [[$class: 'DevelopersRecipientProvider']]
                )
            }
            if (currentBuild.currentresult == 'SUCCESS') {
                    echo "---currentBuild.result is:${currentBuild.result}------"
                    stage('拷贝拷贝jar包'){
                        sh 'cp ${WORKSPACE}/fw-base-nop/target/fw-base-nop.jar /data/'
                    }
                    stage('build and push'){
                        def dockerfile = '/data/Dockerfile'
                        def customImage = docker.build("192.168.176.155/library/fw-base-nop:${imageTag}", "-f ${dockerfile} /data/.")
                        customImage.push()
                    }
                }else {
                    echo "---currentBuild.currentresult is:${currentBuild.currentresult}"
                }

            }

        }
    }
}

说明:

上面使用了emailext方法用于在指定的步骤发送邮件,可以在多个步骤设置不同的邮件模板,用于快速定位异常问题。

currentBuild为默认自带的全局变量,该变量下有多个方法,可以通过在${YOUR_JENKINS_URL}/pipeline-syntax/globals#env中获取到。

对于流水线中的所有核心步骤都可以通过try指令去捕捉流水线各阶段执行的结果,配置比较灵活,此示例仅做演示。

有关pipeline与docker插件集成的进行持续交付内容到这里就结束了。在以后的章节中会详细介绍如何进行持续部署,此处先略过。在下一节的内容会介绍一下jenkins与kubernetes的集成配置。