11.Pipeline语法进行持续交付与基础实践

在前两节介绍了使用声明式流水线和脚本式流水线的基本语法,本节通过一些基础实践来加深一下对前两节内容的理解。由于每个人的实际情况和理解程度不一样, 笔者会将要学习的内容从最基础开始演示,采用不断优化代码的方式尽量将在之前pipeline章节介绍的语法通过示例都展示出来。

使用声明式语法进行持续交付与基础实践

Agent代理

有些公司在技术上可能没有达到使用容器作为流水线agent代理的条件,但这并不影响使用pipeline脚本在虚拟机上编译代码构建镜像。所以节内容中使用的agent代理均为虚拟机,至于如何使用容器作为agent代理,将在以后的章节介绍。

配置agent代理没有什么要特别说明的,直接通过agent{}指令配置要使用的代理节点的label或者名称即可.

以一个基础示例开始本节的内容,比如使用master主机执行shell命令

pipeline{
    agent { node { label 'master'} }
    stages{
        stage('test'){
            steps{
                sh "hostname"
            }
        }
    }
}

需要注意的是,在编写声明式脚本时,在stage()中必须加上对该stage的描述,虽然是自定义的内容,但是不添加也会报错

本次pipeline脚本的全部操作在jenkins-slave1节点进行,所以对于agent定义如下

agent { node { label 'jenkins-slave1' } }

使用agent指令还是比较简单的

代码拉取与编译

下面创建一个流水线类型的Jenkins Job,然后编写pipeline脚本如下

pipeline {
    agent { node { label 'jenkins-slave1' } }
    stages {    
        stage('代码拉取并编译'){
            steps {
                sh "git clone http://root:[email protected]/root/base-nop.git"
                echo "开始编译"
                sh ' source /etc/profile && cd fw-base-nop && mvn clean install -DskipTests -Denv=beta'    
            }
        }
    }
}

使用最基础的代码拉取并编译操作只用了两行命令并且拉取代码操作还使用了明文的用户名密码,这显然是不能符合要求的。

使用在”pipeline语法“章节介绍的片段生成器,可以将代码拉取操作换成语法片段(前面有过介绍)

如下所示

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']]])

同样对于在使用mvn命令编译代码时,如果遇到maven命令找不到或者jdk环境变量找不到的情况,除了使用shell命令刷新环境变量的方式,也可以用tools指令定义这些工具的环境变量。

比如:

tools {
      maven 'maven-3.5.4'
      jdk 'jdk-1.8'
}

对于代码编译,如果不想使用cd命令进入指定的目录进行编译,pipeline提供了ws指令用于指定目录进行操作,所以上面的编译代码步骤也可以写成如下示例:

ws('directory'){
    mvn clean install
}

那么上面最初的脚本就可以改成这样

pipeline {
    agent { node { label 'jenkins-slave1' } }
    tools {
      maven 'maven-3.5.4'
      jdk 'jdk-1.8'
    }
    stages {    
        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 "开始打包 "

                ws("$WORKSPACE/fw-base-nop"){
                    sh ' mvn clean install -DskipTests -Denv=beta' 
                }    
            }
        }
    }
}

说明:

  • cd命令的基础路径是当前工作空间(${WORKSPACE})路径
  • ws 指令里使用的目录需要是绝对路径
  • 如果想要在pipeline中测试本地主机或者本地路径是哪个,可通过执行hostname命令或者pwd命令获取

使用sonar

既然是持续交付,那么肯定少不了代码质量分析。上面的示例只是将代码进行了编译,正常流程还需要进行代码质量分析,这就用到了之前搭建的sonarqube平台。在前面《Jenkins插件安装》章节有介绍如何使用代码质量分析工具sonarQube。这里不重复说明,接下来主要看一下在流水线脚本中使用soncarQube以及sonar-scanner

本章节我们使用的节点全是基于vm的机器,所以此次介绍的sonar-scanner工具的使用只是基于在Jenkins UI中配置的sonar-scanner命令工具。

在前面的章节有介绍sonar-scanner的命令以及参数,在pipeline中使用时,我们只需要根据片段生成生成sonar-scanner的语法片段即可。如下所示

选择好凭据以后生成语法片段,然后将在之前章节中使用的sonar-scanner命令以及参数粘贴到withSonarQubeEnv(){}代码块里即可,如下所示:

stage('sonar'){
    steps{
        script{
            def sonarqubeScannerHome = tool name: 'sonar-scanner-4.2.0'
            withSonarQubeEnv(credentialsId: 'sonarqube') {
                sh "${sonarqubeScannerHome}/bin/sonar-scanner -X "+
                   "-Dsonar.login=admin " +
                   "-Dsonar.language=java " + 
                   "-Dsonar.projectKey=${JOB_NAME} " + 
                   "-Dsonar.projectName=${JOB_NAME} " + 
                   "-Dsonar.projectVersion=${BUILD_NUMBER} " + 
                   "-Dsonar.sources=${WORKSPACE}/fw-base-nop " + 
                   "-Dsonar.sourceEncoding=UTF-8 " + 
                   "-Dsonar.java.binaries=${WORKSPACE}/fw-base-nop/target/classes " + 
                   "-Dsonar.password=admin " 
           }
       }
    }

}

这样就配置好了在pipeline中使用sonarqube与sonar-scanner。在执行完代码编译步骤后就会自动执行sonar-scanner命令。

构建并推送镜像

基础版

上面示例使用pipeline脚本就完成了代码的编译工作,下面看一下将代码构建成镜像并推送到私有仓库的操作,还是先看一下基础的代码。

stage('构建镜像'){
      steps {
         sh "cp $WORKSPACE/fw-base-nop/target/fw-base-nop.jar /data/fw-base-nop/"
         sh "cd /data/fw-base-nop && docker build -t fw-base-nop:${BUILD_ID} ."
      }
}

stage('上传镜像'){
     steps {
        sh "docker tag fw-base-nop:${BUILD_ID} 192.168.176.155/library/fw-base-nop:${BUILD_ID}"
        sh "docker login 192.168.176.155 -u admin -p da88e43d88722c2c9ca09da644eeb015"
        sh "docker push 192.168.176.155/library/fw-base-nop:${BUILD_ID}"
     }
}

该基础代码虽然可以实现我们的目的,但是对于有想法的年轻人,对于示例中的纯命令式操作还是不能接受的,对于部分步骤还是能够优化的。比如

构建镜像阶段:

1、对于构建产物(jar包)的引用使用了cp命令拷贝到agent代理节点上指定的目录,如果遇到添加其他项目时要使用这个pipeline的模板,还要去频繁修改jar包路径和名称,所以这里还需要简单的优化一下。

2、对于构建镜像的命令也可以直接通过命令docker build -t fw-base-nop:${BUILD_ID} /data/fw-base-nop实现。

3、需要在指定的目录(此示例为/data/fw-base-nop/),预先存放Dockerfile文件(可参考第四章节中的Dockerfile)。

上传镜像阶段:

1、需要先将上一步骤构建的镜像添加tag以后,才能上传到指定私有仓库。在构建镜像时,我们可以直接将镜像构建成想要通过tag命令标记的镜像名。

2、上传到私有仓库之前还需要对私有仓库进行认证,简单的几个步骤就使用了如上示例这么多命令。

针对上面列出的问题,部分步骤可以通过使用插件或指令进行优化:

比如,使用find命令从当前路径中查找构建的产物,并且将镜像直接构建成名称为Docker Registry地址/仓库名:标签的格式,可以修改成如下:

  stage('构建镜像'){
        steps {
           script{
              jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name fw-base-nop.jar |head -1").trim()
           }
           sh """
              cp $jar_file /data/fw-base-nop/
              docker build -t 192.168.176.155/library/fw-base-nop:${BUILD_ID} /data/fw-base-nop/.
           """
       }
  }

使用withCredentials方法对私有仓库进行认证

  stage('上传镜像'){
      steps {
          withCredentials([usernamePassword(credentialsId: 'auth_harbor', passwordVariable: 'dockerHubPassword', usernameVariable: 'dockerHubUser')]) {
              sh "“”
              docker login -u ${env.dockerHubUser} -p ${env.dockerHubPassword} 192.168.176.155
              docker push 192.168.176.155/library/fw-base-nop:${BUILD_ID}
              """
          }
      }
  }

因为是在虚拟机主机上进行镜像构建操作,为了避免job执行次数过多导致构建的镜像在宿主机保存数量过多的情况,还需要在最后阶段执行删除镜像的操作,比如:

  stage('删除本地镜像'){
       steps{
            sh "docker rmi -f  192.168.176.155/library/fw-base-nop:${BUILD_ID}"
       }
  }

根据上面的更改,完整的pipeline脚本如下:

pipeline {
    agent { node { label 'jenkins-slave1'}}
    tools {
      maven 'maven-3.5.4'
      jdk 'jdk-1.8'
    }
    stages {    
        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 "开始编译 "
                ws("${WORKSPACE}/fw-base-nop"){
                    sh ' mvn clean install -DskipTests -Denv=beta' 
                }    
            }
        }
        stage('构建镜像'){
            steps {
                script{
                    jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name fw-base-nop.jar |head -1").trim()
                }
                sh """
                cp $jar_file /data/fw-base-nop/
                docker build -t 192.168.176.155/library/fw-base-nop:${BUILD_ID} /data/fw-base-nop/.
                """
            }
        }

        stage('上传镜像'){
            steps {
                withCredentials([usernamePassword(credentialsId: 'auth_harbor', passwordVariable: 'dockerHubPassword', usernameVariable: 'dockerHubUser')]) {
                    sh "docker login -u ${env.dockerHubUser} -p ${env.dockerHubPassword} 192.168.176.155"
                    sh 'docker push 192.168.176.155/library/fw-base-nop:${BUILD_ID}'
                }
            }
        }

        stage('删除本地镜像'){
            steps{
                sh "docker rmi -f  192.168.176.155/library/fw-base-nop:${BUILD_ID}"
            }
        }
    }
}

执行结果如下:- - -

这样,基础版的pipeline脚本算是完成了。

进阶版

虽然脚本是完成了,但是回顾前面pipeline章节介绍的相关语法,还是能够发现一些存在的问题并且有些步骤还可以进行优化,对部分步骤的代码还能进行缩减。

比如:

1、 在镜像构建步骤中使用的存放dockerfile和jar包的目录,可以通过挂载共享存储的方式挂载到agent代理节点指定的目录下,这样如果Dockerfile发生变更或者要拷贝镜像到指定目录的路径发生变更时只需要修改一次即可,并且在任何slave节点都可以通过挂载共享存储的方式使用,不会限制job只使用固定的agent代理节点。

2、对于”上传镜像“步骤中对私有仓库认证的操作,除了使用withCredentials方法外,也可以直接在agent指定代理节点上对私有仓库进行认证,这样在上传镜像时就可以直接略过向私有仓库认证操作。

3、对于”删除本地镜像“步骤中的删除操作,可以直接放到”上传镜像“步骤中。

下面针对上面列出的问题进行简单的优化。

1、对于使用共享存储和在代理节点对私有仓库认证的操作在代码中不会体现,共享存储我这里使用nfs模拟代替,目录没有变;

2、仓库认证直接在agent代理节点上通过docker login命令登录即可,这样就可以将上传镜像和删除镜像两个stage和并为一个,同时为了规范操作,将在代码编译步骤中的构建镜像操作也放到上传镜像操作stage中。

如下所示:

stage('上传并删除本地镜像'){
     steps {
       sh """
       docker build 192.168.176.155/library/fw-base-nop:${BUILD_ID}
       docker push 192.168.176.155/library/fw-base-nop:${BUILD_ID}
       docker rmi -f  192.168.176.155/library/fw-base-nop:${BUILD_ID}
       """        
    }
}

对于项目数量较大以及需要对项目分组管理,并且想要使用同一套pipeline脚本作为模板的时候,可以将该pipeline脚本中使用的部分通用配置设置成变量,每次新建项目只更改变量的值即可。

比如:

  • 对于项目名称,项目构建的产物名称,项目所属的组可以设置成变量
  • 对于私有仓库的地址以及项目所属组也可以通过变量的方式配置
  • 对于镜像的版本,同样可以通过变量的方式配置

根据上面列出的优化思路,后续的脚本修改如下:

pipeline {
    agent { node { label 'jenkins-slave1'}}
    tools {
      maven 'maven-3.5.4'
      jdk 'jdk-1.8'
    }
    environment {
        project_name = 'fw-base-nop'
        jar_name = 'fw-base-nop.jar'
        registry_url = '192.168.176.155'
        project_group = 'base'
    }
    stages {    
        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 "开始打包 "
                ws("${WORKSPACE}/fw-base-nop"){
                    sh ' 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/
                docker build -t $registry_url/$project_group/$project_name:${BUILD_ID} /data/$project_group/$project_name/.
                """
            }
        }
        stage('上传镜像'){
            steps {
                sh """
                docker push $registry_url/$project_group/$project_name:${BUILD_ID}
                docker rmi -f $registry_url/$project_group/$project_name:${BUILD_ID}
                """
            }
        }
    }
}

配置比较简单,学会使用environment关键字即可。

除了使用environment参数以外,也可以在job构建时传入参数用来代替部分变量,这就需要用到前面学过的parameters指令了。

比如,对于设定的jar包名称和所属项目组,分别添加一个string类型的参数和choice类型的参数。

如下代码:

parameters {
  string defaultValue: 'fw-base-nop.jar', description: 'jar包名称,必须以.jar后缀结尾', name: 'jar_name', trim: false
  choice choices: ['base', 'open', 'tms'], description: '服务所属项目组', name: 'project_group'
}

既然定义了外部参数传入变量,对于通过environment指令定义的这两个变量也就没必要存在了。

这样在执行pipeline job时就会变成参数化构建类型的job,如下图所示:-

构建时输入想要的参数值即可,使用parameters这种方式个人觉得比较臃肿,可能使用的场景不对,所以对于parameters指令要根据实际情况考虑要不要使用。

成型版

本次版本对于进阶版的示例没有什么明显的改动,只是将与docker相关的操作通过docker pipeline插件去执行。有关Docker pipeline 插件相关的内容还没有介绍,此处通过示例先简单演示一下。

对于stages中的代码变化如下:

stages {    
        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 "开始编译 "

                ws("${WORKSPACE}/fw-base-nop"){
                    sh ' mvn clean install -DskipTests -Denv=beta' 
                } 

                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 {
                    def customImage = docker.build("$registry_url/${project_group}/$project_name:${BUILD_ID}", "/data/${project_group}/$project_name/.")
                    customImage.push()
                }

                sh """
                docker rmi -f $registry_url/${project_group}/$project_name:${BUILD_ID}
                """
            }
        }
    }

这样使用docker pipeline插件中的build和push方法就简单实现了镜像的构建和推送。从上面的脚本可以看到,在使用插件方法时使用了script{}块包含,是因为Jenkins中的插件基本上都是适配与脚本式语法,而对于声明式语法,需要使用script块来声明使用脚本式语法的部分功能。

在以后的章节会详细介绍docker pipeline插件的使用语法,这里先简单演示一下如何使用。

使用多agent

对于分工比较明确的agent节点,比如某些agent代理节点只能进行代码编译操作,某些agent代理节点只能进行docker镜像构建操作;或者对于同一个项目有不同的脚本语言编写的代码,需要使用使用不同的编译工具构建时时,对于这些特殊情况,可以使用多agent构建方式,在不同的步骤使用不同的agent节点进行项目构建操作。

以上面的代码为示例,在代码拉取和编译步骤使用master节点,在镜像构建和push步骤使用jenkins-slave1节点,代码如下:

pipeline {
    agent none
    tools {
      maven 'maven-3.5.4'
      jdk 'jdk-1.8'
    }
    environment {
        project_name = 'fw-base-nop'
        jar_name = 'fw-base-nop.jar'
        registry_url = '192.168.176.155'
        project_group = 'base'
    }
    }
    stages {    
        stage('代码拉取并打包'){
            agent { node { label 'master'} }

            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 "开始打包 "
                ws("${WORKSPACE}/fw-base-nop"){
                    sh ' mvn clean install -DskipTests -Denv=beta' 
                }

                script{
                    jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name $jar_name |head -1").trim()
                }
                sh "scp $jar_file root@jenkins-slave1:/data/$project_group/$project_name/"
            }
        }

        stage('上传镜像'){
            agent { node { label 'jenkins-slave1'}}
            steps {
                script {
                    def customImage = docker.build("$registry_url/${project_group}/$project_name:${BUILD_ID}", "/data/${project_group}/$project_name/.")
                    customImage.push()
                }

                sh """
                docker rmi -f $registry_url/${project_group}/$project_name:${BUILD_ID}
                """

            }
        }
    }
}

说明:

1、 使用多agent代理节点进行构建时,最顶部的agent需要指定为none。

2、 由于使用了不同的agent节点,对于构建产物需要从master主机拷贝到jenkins-slave1主机上,相对于使用同一个agent,这应该算是比较复杂的操作了吧,但是如果用上共享存储或使用同一个agent,这应该不算是问题。

3、 对于代码编译和镜像构建分别使用不同的工具进行不同类型的操作,所以使用两个stage即可完成所有操作。

异常处理

在使用pipeline脚本执行任务的时候,难免遇到执行出现异常的情况,并且出现异常后流水线会自动终止退出,为了第一时间获取流水线执行的结果或者状态,就需要我们在脚本中加入异常处理的代码。

前面介绍过声明式语法使用post指令根据任务执行状态进行下一步的处理操作,下面简单看一下post在该示例中的应用。

post {
        always {
            echo "xxx"
        }
        success {
            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']]
            )
        }
        failure {
            echo "构建日志:${env.BUILD_URL}console"
        }
        unstable {
            echo "构建日志:${env.BUILD_URL}console"
        }
        changed {
            echo "changed"
        }
    }

将post指令放到全局stages步骤下面,该post指令对于流水线脚本执行的多种状态结果通过使用emailext插件发送邮件通知管理人员。邮件内容可以根据自己实际情况定义,可以参考上面的示例。

如果觉得将每种状态结果分别单独处理比较麻烦,可以根据job执行状态进行分组处理,比如将状态分为成功和失败两种状态,可以修改脚本如下:

post {
        always {
            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: """
                        详情:\n<br>
                        Jenkins 构建 ${currentBuild.currentresult} '\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 "构建成功"
                }
            }

        }

    }
}

使用判断语句对执行结果进行判断并分别处理。有关currentBuild变量的介绍,可以参考”全局变量“界面中currentbuild方法。

Parallel(并行执行)

在实际工作中可能会遇到并行执行一些job的情况。比如同属一个项目组的多个应用服务依赖同一个或多个基础服务,如果要构建应用服务时需要优先构建基础服务,这种情况先我们就可以使用并行构建的方案。

例如,我们有基础服务service1和service2,应用服务app1、app2和app3,有些情况下,开发提交了基础服务代码到service1或(和)service2,同时提交了应用服务(app1 app2 app3)的代码需要构建,这种情况下如果先手动构建service1和service2,等着两个都构建完了,再去手动构建三个应用服务,无疑是繁琐的。

所以本小节就用pipeline中的Parallel关键字来解决类似的问题,代码如下:

pipeline {
    agent any
    parameters {
      extendedChoice description: '是否构建基础服务', descriptionPropertyValue: 'base_service1,base_service2', multiSelectDelimiter: ',', name: 'base_service', quoteValue: false, saveJSONParameterToFile: false, type: 'PT_CHECKBOX', value: 'service1,service2', visibleItemCount: 2
      extendedChoice description: '是否构建应用服务', descriptionPropertyValue: 'app_service1,app_service2,app_service3', multiSelectDelimiter: ',', name: 'app_service', quoteValue: false, saveJSONParameterToFile: false, type: 'PT_CHECKBOX', value: 'app1,app2,app3', visibleItemCount: 3
    }

    stages(){
        stage('deploy'){
            parallel{
                stage('deploy service1'){
                    when {
                        expression {
                            return params.base_service =~ /service1/
                        }
                    }
                    steps{
                        sh "echo build job base_service1"
                        // build job: 'service1'
                    }
                }
                stage('deploy service2'){
                    when {
                        expression {
                            return params.base_service =~ /service2/
                        }
                    }
                    steps{
                        sh "echo build job base_service2"
                        // build job: 'service2'
                    }
                }

            }
        }

        stage('build job'){
            parallel{
                stage('构建应用服务1') {
                    when {
                        expression {
                            return params.app_service =~ /app1/
                        }
                    }

                    steps('build') {
                        sh "echo build job app1"
                        // build job: 'app1'

                    }

                }
                stage('构建应用服务2') {
                    when {
                        expression {
                            return params.app_service =~ /app2/
                        }
                    }

                    steps('build') {
                        sh "echo build job app2"
                        // build job: 'app2'

                    }

                } 
                stage('构建应用服务3') {
                    when {
                        expression {
                            return params.app_service =~ /app3/
                        }
                    }

                    steps('build') {
                        sh "echo build job app3"
                        // build job: 'app3'

                    }

                } 
            }
        }
    }
}

说明:

1 、在该示例中构建job操作使用shell命令代替,在实际工作中,需要使用build job指令来构建一个job,示例中被注释的部分为构建job的代码。

2、 除了使用parameters指令指定参数外,也可以通过在”General“步骤中勾选This project is parameterized来自定义参数,使用方法在前面章节有介绍,此处不再重复说明。

有关使用声明式语法进行持续交付与部署的实践到此结束。

使用脚本式语法进行持续交付与部署实践

通过在声明式语法章节的学习,对于代码编译,镜像构建整个流程应该都不用重复介绍了,本节与使用声明式语法实践一样,也是只通过使用虚拟机作为agent代理节点来执行流水线脚本,对于部分重复的内容会一笔带过,如果有疑问,可以随时联系笔者。

node代理

声明式语法通过agent关键字使用代理,脚本式语法通过node关键字使用代理。

根据声明式语法中的流程以及示例,使用脚本式语法的基础示例代码如下:

node('jenkins-slave1'){
    stage('代码编译'){
        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 "开始编译 "
        ws("$WORKSPACE/fw-base-nop"){
            sh ' mvn clean install -DskipTests -Denv=beta' 
        } 
    }

    stage('镜像构建'){
        script{
            jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name fw-base-nop.jar |head -1").trim()
        }
        sh """
        cp $jar_file /data/base/fw-base-nop/
        docker build -t 192.168.176.155/library/fw-base-nop:${BUILD_ID} /data/base/fw-base-nop/.
        """
    }
}

代码编译

在上面基础示例中简单实现了一下使用脚本式语法进行代码拉取、编译、镜像构建的操作,下面根据在声明式脚本中的示例,将部分代码通过使用脚本式语法指令的方式实现,对于一些可能存在的问题以及先关优化方案在前面已经做过介绍,这里就不在重复介绍了。只需要关注一下脚本的变更即可。

定义工具

比如,对于代码编译时用到的工具的环境变量的定义,在脚本式语法中可以使用tool和withEnv指令设置jdk和maven工具的环境变量,代替source /etc/profile命令,代码如下所示:

    def jdk = tool name: 'jdk-1.8'
    env.PATH = "${jdk}/bin:${env.PATH}"
    stage('代码编译'){
        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 "开始打包 "
        withEnv(["PATH+MAVEN=${tool 'maven-3.5.4'}/bin"]) {
            ws("$WORKSPACE/fw-base-nop"){
                sh ' mvn clean install -DskipTests -Denv=beta' 
            } 
        }

    }

该脚本使用def指令和env命令定义了jdk的环境变量,使用withenv指令定义了maven的环境变量。

上面定义变量的方式也可以写成如下:

node {
    def jdk=tool name: 'jdk-1.8'
    def mvn=tool name:'maven-3.5.4'
    env.PATH = "${jdk}/bin:${mvn}/bin:${env.PATH}"

    stage('代码编译'){
        ......
        echo "开始打包 "
        ws("$WORKSPACE/fw-base-nop"){
            sh ' mvn clean install -DskipTests -Denv=beta' 
        } 
    }
}

构建并推送镜像

在构建并推送镜像阶段,同样可以使用docker pipeline插件实现镜像的构建与推送操作。

代码如下:

stage('镜像构建'){
        script{
            jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name fw-base-nop.jar |head -1").trim()
        }
        sh """
        cp $jar_file /data/base/fw-base-nop/
        """
    }

    stage('推送镜像到仓库'){
        def customImage=docker.build("192.168.176.155/base/fw-base-nop:${env.BUILD_ID}",'/data/base/fw-base-nop/')
        customImage.push()
    }

说明

1、 如果dockerfile的名字不是”Dockerfile或者dockerfile“,还需要通过-f参数指定dockefile的名称。

2、 推送镜像阶段的默认条件是agent代理节点已经对私有仓库做了认证,如果agent节点没有对私有仓库做认证,这里还需要加上认证的方法,参考下面的“对私有仓库服务认证”。

3、 由于docker pipeline插件没有删除镜像的属性,所以本地构建的镜像还是要使用shell命令去删除,与在声明式语法中使用相同的命令即可,这里不再演示。

私有仓库服务认证

使用脚本式语法对私有仓库服务认证,相对于声明式语法就更简单了,只需要使用docker pipeline插件中的with_registry方法,即可轻松解决认证问题,如下代码:

stage("构建并推送镜像"){
    docker.withRegistry('http://192.168.176.155', 'auth_harbor') {
        def customImage=docker.build("${vars.registry_url}/${vars.project_group}/${vars.project_name}:${env.BUILD_ID}","/data/${vars.project_group}/${vars.project_name}/")
        customImage.push()
    }
}

与在声明式语法中使用docker插件的build方法、push方法一样,在声明式语法中使用该方法同样需要用script{}块包含起来,代码示例如下:

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()
                    }
                }
            }
        }

使用变量

在声明式语法中使用environment来定义变量,在脚本式语法中使用def指令来定义变量,如下所示:

def vars=[project_name:'fw-base-nop',jar_name:'fw-base-nop.jar',registry_url:'192.168.176.155',project_group:'base']

使用时,通过${vars.project_name}来引用该值,在该示例中配置如下:

node('jenkins-slave1'){
    def jdk = tool name: 'jdk-1.8'
    env.PATH = "${jdk}/bin:${env.PATH}"
    def vars=[project_name:'fw-base-nop',jar_name:'fw-base-nop.jar',registry_url:'192.168.176.155',project_group:'base']

    stage('代码编译'){

        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 "开始打包 "
        withEnv(["PATH+MAVEN=${tool 'maven-3.5.4'}/bin"]) {
            ws("$WORKSPACE/fw-base-nop"){
                sh ' mvn clean install -DskipTests -Denv=beta' 
            } 
        }
    }

    stage('镜像构建'){
        script{
            jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name ${vars.jar_name} |head -1").trim()
        }
        sh """
        cp $jar_file /data/${vars.project_group}/${vars.project_name}/
        """
    }

    stage('推送镜像到仓库'){
        docker.withRegistry('http://192.168.176.155', 'auth_harbor') {
            def customImage=docker.build("${vars.registry_url}/${vars.project_group}/${vars.project_name}:${env.BUILD_ID}","/data/${vars.project_group}/${vars.project_name}/")
            customImage.push()
        }
    }
}

使用多agent

使用多agent的方式在语法章节中已经做了简单介绍,首先回顾一下基本语法。

node() {
    stage('test-node'){
        node('jenkins-slave1'){
            stage('test1'){
                sh 'hostname'
            }   
        }
    }
    stage('test-node2'){
        node('jenkins-slave169'){
            stage('test2'){
                sh 'hostname'
            }
        }
    }
}

对于核心脚本代码的实现,只需要将在声明式语法示例中的代码复制过来放到stage下即可,还是比较简单的。

异常处理

在前面的pipeline章节中有介绍,脚本式语法使用try/catch/finally关键字来捕捉异常并对异常进行处理。使用try指令比较灵活,它可以用来包含全局的stage,也可以包含特定的stage中,try{}后边必须包含catch或者finally关键字,也可以同时都包含。

代码如下:

node('jenkins-slave1'){
    def jdk = tool name: 'jdk-1.8'
    env.PATH = "${jdk}/bin:${env.PATH}"
    def vars=[project_name:'fw-base-nop',jar_name:'fw-base-nop.jar',registry_url:'192.168.176.155',project_group:'base']

    try{
        stage('代码编译'){

            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 "开始打包 "
            withEnv(["PATH+MAVEN=${tool 'maven-3.5.4'}/bin"]) {
                sh "cd ${vars.project_name}/ && mvn clean install -DskipTests -Denv=beta"
            }   
        }
        stage('镜像构建'){
            script{
                jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name ${vars.jar_name} |head -1").trim()
            }
            sh """
            cp $jar_file /data/${vars.project_group}/${vars.project_name}/
            """
        }

        stage('推送镜像到仓库'){
            def customImage=docker.build("${vars.registry_url}/${vars.project_group}/${vars.project_name}:${env.BUILD_ID}","/data/${vars.project_group}/${vars.project_name}/")
            customImage.push()
        }
    }catch(all){
        currentBuild.result = 'FAILURE'
    }
    if(currentBuild.currentResult == "ABORTED" || currentBuild.currentResult == "FAILURE" || currentBuild.currentResult == "UNSTABLE" ) {
        echo "---currentBuild.result is:${currentBuild.currentResult}"
    }
    else {
        echo "---currentBuild.result is:${currentBuild.currentResult}"
    }
}

对于if和else判断后的步骤,同样可以使用emailext方法将构建结果发送邮件给管理员,参考声明式语法中发送邮件的配置即可。

删除工作空间

有些项目为了防止缓存可能会需要在构建完成后删除当前的工作目录。比较简单的方式是使用shell命令直接删除。但是既然是使用pipeline,这里就用pipeline指令删除,此时也许你已经想到方法了,没错,就是使用dir和deleteDir指令。

针对上面的示例,可以在”推送镜像到仓库“步骤后添加一个新的stage,通过dir命令进入目录,然后通过deleteDir指令删除。具体的代码如下:

stage('清除工作空间'){
    dir('$WORKSPACE'){
        deleteDir()
    }
}

这样在执行该stage的时候就会删除当前的工作目录。

代码部署

前面的内容介绍的都是对代码的编译以及构建等操作,关于部署的操作没有提及。如何部署可以参考之前ansible章节编写的playbook,只需要做简单的修改即可。当然,如果你不会也没关系,在后面的章节中我会专门用一个章节来写一下如何部署。

有关使用pipeline语法进行持续交付与部署的基础实践就介绍到这里,在下一节中将介绍一下常用的Docker pipeline的语法。