12.Jenkins Docker Pipeline插件动态生成Slave节点语法剖析

越来越多的公司和用户在实际工作中引入Jenkins,在使用Jenkins自动化构建部署的过程中,使用动态容器作为构建部署工作的执行环境的方案也越来越多的被采纳,不仅提高了资源利用率,同时还降低配置成本和维护管理成本。

在pipeline语法章节介绍Jenkins agent代理的时候有提到可以使用Docker容器作为流水线的执行环境。对于可以在Linux上运行的构建步骤,同样可以在容器中进行。每个流水线项目仅需要选择一个包含所需要使用的工具和库的镜像即可。而docker插件设计的目的除了操作容器以外,另一个比较重要的功能是通过指定的镜像启动容器作为单个stage或全局流水线的执行环境。

为了方便学习,同样将两种类型的语法分开介绍。

脚本式语法

在文章的开始首先介绍一下Docker pipeline插件的使用,毕竟无论是声明式语法还是脚本式语法都是围绕该插件来启动代理节点。该插件在脚本式流水线中使用比较简单,下面介绍一下基本语法。

docker-pipeline插件在安装后,会产生一系列的变量,要获取这些变量的使用方法的详细信息,可以在任何一个pipeline类型的job里,通过pipeline syntax(流水线语法)来查看,在跳转的界面中点击”全局变量参考菜单

如下所示-

更多变量内容,可去Jenkins pipeline项目里查看,下面对一些经常用到的变量方法进行简单的介绍

image

image方法用于根据镜像名称或者镜像id定义一个镜像对象,然后根据该方法下的多个属性来执行不同的操作

比如,根据镜像名称定义对象,通过inside属性启动一个容器

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

说明

  • image方法根据image名称从Docker Hub拉取镜像并定义对象,通过inside属性启动容器。默认使用jenkins所在主机运行该容器,如果要在其他节点可以在node里添加节点名称,如下所示

    node('jenkins-slave1') {
       docker.image('maven:3.3.3-jdk-8').inside('-v $HOME/:/.m2/') {
            git '…your-sources…'
            sh 'mvn -B clean install'
     }
    }
    

    该示例会在jenkins的系统配置里寻找label或者name为jenkins-slave1的节点拉取镜像并启动容器- 1、 inside属性用于启动容器并添加容器启动时的参数,该属性可以没有参数,上面示例添加了一个volume挂载的参数- 2、 镜像也可以从私有仓库拉取,同时,也可以使用image_id启动一个容器,但该方法用的不多

上面示例使用inside属性启动了一个容器,除了使用这种方式外,还可以通过将image定义的对象赋予变量的方式使用,比如

node('jenkins-slave1') {
  def maven = docker.image('maven:latest')
  maven.pull() 
  maven.inside {
    sh 'hostname'
  }
}

说明

  • pull方法用于确保该镜像在镜像仓库里的版本是最新的
  • inside属性启动容器

build

在前面pipeline章节的声明式语法中有介绍可以使用docker和dockrefile来启动一个容器。在脚本式语法中除了根据镜像名称或者镜像id启动一个容器以外,也可以通过dockerfile方式构建一个容器并启动。为了构建 Docker 镜像,Docker pipeline插件提供了一个 build() 方法用于在流水线运行之前根据源码库中提供的Dockerfile 构建一个新的镜像。

使用语法 docker.build("my-image-name") 可以构建一个镜像,使用定义变量的方式的好处在于在下面能够使用该变量的其他属性方法

比如:

node {
    checkout scm
    def customImage = docker.build("my-image:${env.BUILD_ID}")
    customImage.inside {
        sh 'make test'
    }
}

默认情况下, build() 方法是根据拉取的源码当前目录下的Dockerfile文件构建镜像。

build方法也可以通过提供一个包含 Dockerfile文件的目录路径作为build() 方法的第二个参数 ,用来在指定的目录下构建镜像

比如:

node {
    checkout scm
    def testImage = docker.build("test-image", "./dockerfiles/test") 

    testImage.inside {
        sh 'make test'
    }
}

说明:

  • 通过在 ./dockerfiles/test/目录下发现的Dockerfile文件构建test-image
  • checkout scm表示从源码仓库拉取Dockerfile文件,可以单独放置,可以与应用代码放到一起 。需要注意的是,该配置可以通过git clone url(checkout)命令直接代替(包括下面所有示例中的checkout scm

也可以通过传递 -f参数覆盖默认的 Dockerfile ,当使用这种方法传递参数时, 该字符串的最后一个值必须是Dockerfile文件的路径

比如

node {
    checkout scm
    def dockerfile = 'Dockerfile.test'
    def customImage = docker.build("my-image:${env.BUILD_ID}", "-f ${dockerfile} ./dockerfiles") 
}

说明:

  • 从在./dockerfiles/目录发现的Dockerfile.test文件构建 my-image:${env.BUILD_ID}

上面的示例中,通过def指令定义了构建镜像操作的变量名称,该变量可以用于通过 push() 方法将Docker 镜像推送到私有仓库

比如

node {
    checkout scm
    def customImage = docker.build("my-image:${env.BUILD_ID}")
    customImage.push()
}

说明

  • push方法除了可以将自定义执行流水线的docker镜像推送到镜像仓库以外,也可以将应用代码经过代码编译和镜像构建后推送到私有仓库。

push方法在推送镜像时也可以自定义镜像的tag

如下所示

node {
    checkout scm
    def customImage = docker.build("my-image:${env.BUILD_ID}")
    customImage.push()

    customImage.push('latest')
}

使用多个容器

在前面的pipeline章节有介绍在脚本式流水线语法中使用多个agent代理,这里在来回顾一下

node('jenkins-slave1') {

    stage('Back-end') {
        docker.image('maven:3-alpine').inside {
            sh 'mvn --version'
        }
    }

    stage('Front-end') {
        docker.image('node:7-alpine').inside { 
            sh "node --version"
        }
    }
}

结合前两节学过的内容,可以在上面语法的基础上对其进行扩展。

比如,在不同的阶段,使用不同的slave节点启动不同容器作为流水线执行的环境

如下示例

node() {

    stage('Back-end') {
        node('slave1'){
            stage("test1"){
                docker.image('maven:3-alpine').inside {
                    sh 'mvn --version'
                }
            }
        }
    }

    stage('Front-end') {
        node('slave2'){
            stage('test2'){
                docker.image('node:7-alpine').inside { 
                    sh "node --version"
                }
            }

     }

   }
}

这样,针对不同的步骤就会在不同的slave节点上启动容器执行流水线

使用远程docker服务器

除了使用jenkins节点宿主机上的docker进程外,还可以使用其他服务器上的docker进程。docker pipeline提供了withServer方法用于连接远程的docker服务器。

要使用远程Docker服务器上的进程需要Docker服务器开启2375端口,如下所示

修改/usr/lib/systemd/system/docker.service 配置文件

#在ExecStart=/usr/bin/docker daemon 后追加 -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock

#如:
ExecStart=/usr/bin/docker daemon -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock

#修改完后重启docker
$ systemctl daemon-reload
$ systemctl restart docker

#查看进程
$ netstat -ntlp|grep 2375
tcp6       0      0 :::2375                 :::*                    LISTEN      5554/dockerd 

配置好后修改pipeline脚本,如下

node {
    docker.withServer('tcp://192.168.176.160:2375',) {
        docker.image('maven:latest').inside {
           sh "mvn --version"
        }
    }
}

这样,就会在远程服务器上启动该容器,可以执行想要执行的命令,比较简单。

withRegistry(使用私有仓库)

默认情况下, Docker 流水线 插件集成了 Docker Hub的私有仓库。 在编写流水线脚本时,直接使用image方式拉取的镜像一般都是从Docker Hub拉取的。在上面的声明式流水线语法中介绍了在执行流水线过程中对私有仓库服务的认证的几种方法。对于脚本式流水线,这就简单的多了,那就是使用withRegistry()方法

例如

node {
    checkout scm
    docker.withRegistry('https://registry.example.com') {

        docker.image('my-custom-image').inside {
            sh 'make test'
        }
    }
}

此时是没有对镜像仓库服务进行认证的。

而如果想使用自定义的仓库怎么办?使用vm作为agent代理时,在声明式流水线语法可以使用凭据,挂载目录等方式。而使用docker插件就没这么复杂了,通过docker插件的withRegistry() 方法,在使用时通过指定的参数对私有仓库认证,认证方式也是通过jenkins系统里的凭据。

例如,对于需要身份验证的Docker 私有仓库,直接在Jenkins凭据中添加一个 “Username/Password” 凭据, 然后可以通过片段生成器,使用凭据ID作为 withRegistry()的第二个参数,生成语法片段

比如

node {
    checkout scm

    docker.withRegistry('https://registry.example.com', 'credentials-id') {

        def customImage = docker.build("my-image:${env.BUILD_ID}")

        customImage.push()
    }
}

声明式语法

首先回顾一下在pipeline语法章节介绍声明式语法时使用docker启动agent代理的一些使用方法,然后在做一下补充

首先来看一下在声明式脚本中使用docker作为agent的范例

例如,启动一个容器并执行一条命令

pipeline {
    agent {
        docker { image 'node:7-alpine' }
    }
    stages {
        stage('Test') {
            steps {
                sh 'node --version'
            }
        }
    }
}

当执行上面的流水线脚本时, Jenkins将会自动地启动指定的镜像(如果镜像不存在会自动下载)并在其中执行指定的stage,当stage执行完好,该容器会销毁。

除了使用该方式生成容器以外,也可以使用docker pipeline插件,例如

pipeline {
    agent any
    stages {
        stage('Test') {
            steps {
                script {
                    docker.image('maven:latest').inside(){
                        sh 'mvn --version'
                    }
                }
            }
        }
    }
}

需要使用script{}块将pipeline语法块包含起来

使用Dockerfile

除了使用上面指定容器镜像的方法启动一个容器外,还可以使用自定义的dockerfile启动容器。这种情况就需要将jenkinsfile文件和dockerfile文件放到源码仓库中了。

pipeline支持从源代码仓库的Dockerfile中构建和运行容器。使用 agent { dockerfile true } 语法会从 Dockerfile 中构建一个镜像并启动容器,而不是从 Docker Hub或者私有仓库中拉取。

如下示例

$ cat Dockerfile
FROM maven:latest
RUN apt-get update \
    && apt-get install lsof

$ cat Jenkinsfile
pipeline {
    agent { dockerfile true }
    stages {
        stage('Test') {
            steps {
                sh 'lsof'
            }
        }
    }
}

将上面的Dockerfile和Jenkinsfile文件一起放到源码仓库中,然后配置jenkins项目时使用Pipeline script from SCM方式,配置该源码仓库的地址即可-

说明:

  • Script Path 指定文件名要与jenkinsfile的文件名一样
  • 使用dockerfile的其他参数可以参考pipeline章节中关于agent的介绍,这里不再多说

容器的缓存数据

使用容器作为流水线的执行环境,当流水线执行完以后,容器就会销毁,流水线构建的产物也会随之丢失。对于使用maven构建工具编译代码的时候,默认会下载外部依赖包并将它们缓存到本地.m2仓库中,以便于再次编译或者其他项目编译的的时候使用。 同时对于编译好的服务包有时候也需要做一次备份,所以我们需要将一些需要重复利用依赖包缓存到本地。

流水线支持向Docker中添加自定义的参数, 允许用户在启动容器时自定义挂载volume用来缓存数据。

下面的示例将会在流水线运行期间缓存maven仓库目录( ~/.m2), 从而避免了项目在从新构建时重新下载依赖导致部署效率低下的问题。

pipeline {
    agent {
        docker {
            image 'maven:3-alpine'
            args '-v $HOME/.m2:/root/.m2'
        }
    }
    stages {
        stage('Build') {
            steps {
                sh 'mvn -version'
            }
        }
    }
}

说明:

  • 上面流水线示例使用args指令传递容器运行时的参数,-v指定的目录默认挂载到宿主机的目录上;如果宿主机节点过多的情况下,可以使用共享存储目录来代替宿主机目录

使用多个容器

在构建项目的时候如果遇到多种语言写的代码,需要使用不同的编译工具进行代码的编译;又或者对于一套流水线脚本对于不同功能的stage需要使用不同的镜像时,就可以在一个流水线脚本中启动多个容器镜像

例如,一个应用既有基于Java的后端API实现又有基于JavaScript的前端实现,这种情况就可以通过agent {} 指令在不同的stage使用不同的容器进行编译操作。

如下示例

pipeline {
    agent none
    stages {
        stage('Back-end') {
            agent {
                docker { image 'maven:3-alpine' }
            }
            steps {
                sh 'mvn --version'
            }
        }
        stage('Front-end') {
            agent {
                docker { image 'node:7-alpine' }
            }
            steps {
                sh 'node --version'
            }
        }
    }
}

说明:

  • 示例中在顶层的agent指令里定义代理主机为none,就必须要在每个stage里设置独立的agent代理
  • 分别对后端和前端使用不同的镜像启动构建环境

如果要在指定的node节点上启动容器,或者在不同节点上分别启动不同的容器,又该如何操作?这里就要使用到docker pipeline插件了。

如下所示,在slave1节点启动两个容器

pipeline {
    agent { node { label 'slave1' } }

stages {
    stage('Build') {
        steps {
            echo 'Building..'
            sh 'npm install'
        }
    }
    stage('Test') {
        steps {
            echo 'Testing..'
            script {
                docker.image('selenium:latest').inside(){
                    .......
                }
            }
        }
    }
    stage('build') {
        steps {
            script {
                docker.image('maven:lastet').inside(){
                    docker build ..
                }
            }
        }
    }
 }
}

使用多个node启动多个容器配置如下

pipeline {
    agent none
stages {
    stage('Test') {
        agent { node { label 'jenkins-slave1'} }
        steps {
            echo 'Testing..'
            script {
                docker.image('selenium:latest').inside(){
                    .......
                }
            }
        }
    }
    stage('build') {
        agent { node { label 'jenkins-slave169'} }
        steps {
            script {
                docker.image('maven:lastet').inside(){
                    docker build ..
                }
            }
        }
    }
 }
}

虽然可以实现,但是该方案可能用的比较少。有兴趣的自己可以尝试一下

使用私有仓库

将应用代码编译完成并构建成镜像以后,需要将该镜像推送到指定的私有仓库上去,这个流程中涉及到一个向私有仓库服务认证的过程,那么在声明式流水线语法中如何向私有仓库服务认证呢?可以参考如下方法:

使用docker login命令

如果你怕麻烦,并且对于Jenkins的安全设置也比较有把握,那么你可以选择用docker login命令加明文密码。如下示例

stage('Docker Push') {
   steps {
       sh "docker login -u xxx -p xxx registry_url"
       sh 'docker push 192.168.176.154/base/nop:latest'
   }
}

该示例在流水线中直接使用docker login命令向私有仓库服务认证。

如果觉得明文密码太过显眼,也可以使用”参数化构建过程“选项添加一个password类型的参数,然后使用login命令登录时-p参数直接使用变量名称即可。但是此方法每次构建时都要输入一下密码,显得有些笨拙。此时,在之前的章节中介绍到的凭据就派上用场了。

除了使用”参数化构建过程“选项提供参数以外,还可以使用Jenkins凭据提供私有仓库服务认证的用户名和密码。

stage('Docker Push') {
      steps {
        withCredentials([usernamePassword(credentialsId: 'dockerregistry', passwordVariable: 'dockerHubPassword', usernameVariable: 'dockerHubUser')]) {
          sh "docker login -u ${env.dockerHubUser} -p ${env.dockerHubPassword}"
          sh 'docker push 192.168.176.155/library/base-nop:latest'
        }
      }
    }

说明:

  • 该语法片段可以通过片段生成器生成

也可以直接使用参数指定仓库的url和凭据id,示例如下

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'
       }
}

这样便实现了与上面withCredentials方式同样的效果。不过需要说明的是,在使用Docker插件时,建议还是使用脚本式语法,该插件与脚本式语法集成使用会更加方便。

使用目录挂载

如果不想用Jenkins里的凭据怎么办,可以使用挂载docker配置文件的方法。使用docker命令登录私有仓库的时候,默认会将认证信息保存在docker本地服务器指定的文件里。这是可以使用args参数时,将该文件挂载到容器内指定的文件上。

示例如下

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 -v /root/.docker/config.json:/root/.docker/config.json'
       }
}

说明:

  • docker向私有仓库认证的配置文件根据docker安装方式的不同会有所差异。yum方式安装的docker使用的认证的文件为/root/.docker/config.json,该文件存储了认证信息,至于如何获取该文件的具体路径,可以通过docker login registry_url命令,认证成功后就会提示存放认证文件的路径。

使用Docker pipeline插件

Docker pipeline实际使用的时候在脚本式语法中应用的相对简单,但也可以在声明式流水线中使用,只不过需要将插件的方法通过script{}块包起来。

比如使用私有仓库的方法,在声明式流水线中可以写成如下

pipeline{
    agent ...
    stages(){
        stage(){
            steps{
                ....
            }
        }
        stage(){
            steps{
                script{
                    docker.withRegistry('https://registry.example.com', 'credentials-id') {
                        def customImage = docker.build("my-image:${env.BUILD_ID}")
            customImage.push()
          }
                }
            }
        }
    }
}

该方式同样适用于docker插件的其它属性方法。

有关Pipeline与Docker集成的语法说明就简单的介绍到这里。在下面的章节中,会通过一些示例来演示通过jenkins与Docker集成实现持续交付和持续部署的过程。