前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Jenkins 配合Pipeline使用Docker

Jenkins 配合Pipeline使用Docker

作者头像
授客
发布2022-05-06 15:50:54
3.3K0
发布2022-05-06 15:50:54
举报
文章被收录于专栏:授客的专栏

配合Pipeline使用Docker

许多组织使用Docker跨机器统一构建和测试环境,并为部署应用程序提供高效机制。从Pipeline 2.5及更高版本开始,Pipeline内置了从Jenkinsfile中与Docker交互的支持。下文将介绍从Jenkinsfile中使用Docker的基础知识

定制执行环境

Pipeline的设计可以轻松地使用Docker镜像作为单个Stage或整个 Pipeline 的执行环境。这意味着用户可以定义管道所需的工具,而无需手动配置代理。

代码语言:javascript
复制
pipeline {
    agent {
        docker { image 'node:16.13.1-alpine' }
    }
    stages {
        stage('Test') {
            steps {
                sh 'node --version'
            }
        }
    }
}

当Pipeline执行时,Jenkins将自动启动指定的容器并在其中执行预定义的步骤:

代码语言:javascript
复制
...略
+ docker inspect -f . node:16.13.1-alpine

Error: No such object: node:16.13.1-alpine
[Pipeline] isUnix
[Pipeline] sh
+ docker pull node:16.13.1-alpine
16.13.1-alpine: Pulling from library/node
59bf1c3509f3: Pulling fs layer
...略
503741069fa0: Pull complete
88b2b4880461: Pull complete
Digest: sha256:0e071f3c5c84cffa6b1035023e1956cf28d48f4b36e229cef328772da81ec0c5
Status: Downloaded newer image for node:16.13.1-alpine
[Pipeline] withDockerContainer
Jenkins does not seem to be running inside a container
$ docker run -t -d -u 0:0 -w /var/lib/jenkins/workspace/CI-Builder_testBranch -v /var/lib/jenkins/workspace/CI-Builder_testBranch:/var/lib/jenkins/workspace/CI-Builder_testBranch2:rw,z -v /var/lib/jenkins/workspace/CI-Builder_testBranch2@tmp:/var/lib/jenkins/workspace/CI-Builder_testBranch2@tmp:rw,z -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** node:16.13.1-alpine cat
$ docker top 90c6a3a70e38ad03c8318cedcf78df34e5b8533d634ec2356e8babc32742a74c -eo pid,comm
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Test)
[Pipeline] sh
+ node --version
v16.13.1
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
$ docker stop --time=1 90c6a3a70e38ad03c8318cedcf78df34e5b8533d634ec2356e8babc32742a74c
$ docker rm -f 90c6a3a70e38ad03c8318cedcf78df34e5b8533d634ec2356e8babc32742a74c
[Pipeline] // withDockerContainer
[Pipeline] }
[Pipeline] // withEnv
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

从输出可知,Jenkins自动创建了指定镜像的容器,并且在容器中执行指定Step,最后,停止并强制删除创建的容器

工作空间同步

如果保持工作区与其他Stage同步很重要,请使用reuseNode true。否则,除了临时工作空间,Docker化的Stage还可以在任何其他任意代理或同一代理上运行

默认的,对于容器化的Stage, Jenkin会执行以下动作:

  • 任选一个代理
  • 创建临时工作空间
  • 克隆pipeline代码到该工作空间
  • 加载该工作空间到容器

如果你有多个Jenkins代理,你的容器化Stage可以在其中任何一个代理上启动

当设置reuseNode设置为true时:不会创建新的工作区,当前代理的当前工作区将被装入容器,且将在同一节点上启动该容器,所以整体数据将被同步

代码语言:javascript
复制
pipeline {
    agent any
    stages {
        stage('Build') {
            agent {
                docker {
                    image 'gradle:6.7-jdk11'
                    // Run the container on the node specified at the top-level of the Pipeline, in the same workspace, rather than on a new node entirely:
                    reuseNode true
                }
            }
            steps {
                sh 'gradle --version'
            }
        }
    }
}

为容器缓存数据

许多构建工具会下载外部依赖项并为了后续重用,会在本地缓存它们,以备将来重用。由于容器最初是用“干净”的文件系统创建的,这可能会导致Pipeline运行速度变慢,因为它们可能无法利用后续Pipeline运行之间的磁盘缓存。

Pipeline支持添加传递给Docker的自定义参数,允许用户指定要加载的自定义Docker 卷,该卷可用于在Pipeline运行之间缓存agent上的数据。下面的示例将在Pipeline运行之间为maven容器缓存~/.m2,从而避免了为后续Pipeline运行重新下载依赖项的需要

代码语言:javascript
复制
pipeline {
    agent {
        docker {
            image 'maven:3.8.1-adoptopenjdk-11'
            args '-v $HOME/.m2:/root/.m2'
        }
    }
    stages {
        stage('Build') {
            steps {
                sh 'mvn -B'
            }
        }
    }
}

使用多个容器

代码库依赖多种不同的技术已经变得越来越普遍。例如,源码库可能既有基于Java的后端API实现,也有基于JavaScript的前端实现。Docker和Pipeline的结合允许Jenkinsfile通过在不同stage使用不同的 agent {}指令来使用多种技术。

代码语言:javascript
复制
pipeline {
    agent none
    stages {
        stage('Back-end') {
            agent {
                docker { image 'maven:3.8.1-adoptopenjdk-11' }
            }
            steps {
                sh 'mvn --version'
            }
        }
        stage('Front-end') {
            agent {
                docker { image 'node:16.13.1-alpine' }
            }
            steps {
                sh 'node --version'
            }
        }
    }
}

使用Dockerfile

对于需要更定制的执行环境的项目,Pipeline还支持从源码库中的Dockerfile构建和运行容器。与之前使用“现成”容器的方法不同,使用代理 agent { dockerfile true }语法将从Dockerfile中构建新镜像,而不是从Docker Hub中拉取镜像。

在上面的示例的基础上增加一个自定义的Dockerfile

代码语言:javascript
复制
FROM node:16.13.1-alpine

RUN apk add -U subversion

通过将上述文件提交到源存储库的根目录,可以将Jenkins文件更改为基于此Dockerfile构建一个容器,然后使用该容器运行定义的步骤

代码语言:javascript
复制
pipeline {
    agent { dockerfile true }
    stages {
        stage('Test') {
            steps {
                sh 'node --version'
                sh 'svn --version'
            }
        }
    }
}

agent { dockerfile true } 语法支持许多其它选项,在 Pipeline 语法中 有更多关于这些选项的更详细介绍。

脚本化Pipeline运行“sidecar”容器的高级用法

在Pipeline中使用Docker是运行构建或一组测试可能依赖的服务的有效方法。与sidecar模式类似,Docker Pipeline可以“在后台”运行一个容器,同时在另一个容器中执行工作。利用这种sidecar方法,PIpeline可以为每次PIpeline运行准备一个“干净”的容器

备注:将本将属于应用程序的功能拆分成单独的进程,这个进程可以被理解为Sidecar

假设有一个集成测试套件,它依赖于本地MySQL数据库运行。使用Docker Pipeline插件为支持脚本化Pipeline实现的withRun方法,Jenkinsfile可以将MySQL作为一个sidecar运行:

代码语言:javascript
复制
node {
    checkout scm
    /*
     * In order to communicate with the MySQL server, this Pipeline explicitly
     * maps the port (3306) to a known port on the host machine.
     */
    docker.image('mysql:5').withRun('-e "MYSQL_ROOT_PASSWORD=my-secret-pw" -p 3306:3306') { c ->
        /* Wait until mysql service is up */
        sh 'while ! mysqladmin ping -h0.0.0.0 --silent; do sleep 1; done'
        /* Run some tests which require MySQL */
        sh 'make check'
    }
}

这个例子可以更进一步,同时使用两个容器。一个sidecar运行MySQL,另一个通过使用Docker容器链接提供 执行环境

代码语言:javascript
复制
node {
    checkout scm
    docker.image('mysql:5').withRun('-e "MYSQL_ROOT_PASSWORD=my-secret-pw"') { c ->
        docker.image('mysql:5').inside("--link ${c.id}:db") {
            /* Wait until mysql service is up */
            sh 'while ! mysqladmin ping -hdb --silent; do sleep 1; done'
        }
        docker.image('centos:7').inside("--link ${c.id}:db") {
            /*
             * Run some tests which require MySQL, and assume that it is
             * available on the host name db
             */
            sh 'make check'
        }
    }
}

上面的示例使用withRun暴露的对象,该对象通过id属性提供运行容器的ID。使用容器的ID,Pipeline 可以通过向inside()方法传递自定义Docker参数来创建链接。

id属性还可用于在管道退出之前检查正在运行的Docker容器中的日志:

代码语言:javascript
复制
sh "docker logs ${c.id}"

注意:withRun块内的shell步骤不是在容器内运行的,但它们可以使用本地TCP端口连接到容器

构建容器

为了创建Docker镜像,Docker Pipeline插件还提供了一个build()方法,用于在PIpeline运行期间根据源码库中的Dockerfile创建新镜像。

使用docker.build("my-image-name")语法的一个主要好处是脚本化Pipeline可以在后续Docker Pipeline调用中使用返回值,例如:

代码语言:javascript
复制
node {
    checkout scm

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

    customImage.inside {
        sh 'make test'
    }
}

返回值还可用于通过push()方法将Docker镜像发布到Docker Hub自定义注册中心, 例如:

代码语言:javascript
复制
node {
    checkout scm
    def customImage = docker.build("my-image:${env.BUILD_ID}")
    customImage.push()
}

镜像“tags”的一个常见用法是为最近有效的Docker镜像版本指定一个latest标签。push()方法接收一个可选的tag参数,允许Pipeline推送携带不同标签的自定义镜像,例如:

代码语言:javascript
复制
node {
    checkout scm
    def customImage = docker.build("my-image:${env.BUILD_ID}")
    customImage.push()

    customImage.push('latest')
}

默认情况下,build()方法根据当前目录中的 Dockerfile进行构建 。可以通过提供包含Dockerfile的目录路径作为 build()方法的第二个参数来覆盖这一点,例如:

代码语言:javascript
复制
node {
    checkout scm
    def testImage = docker.build("test-image", "./dockerfiles/test")  // 从./dockerfiles/test/Dockerfile构建test-image

    testImage.inside {
        sh 'make test'
    }
}

可以通过将其他参数添加到 build()方法的第二个参数并将其传递给docker构建。但是需要注意的是,以这种方式传递参数时,字符串中的最后一个值必须是Dockerfile的路径,并且该路径必须以用作构建上下文的文件夹结尾。

下例通过传递-f参参数覆盖默认的Dockerfile

代码语言:javascript
复制
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}
}

使用远程Docker服务

默认情况下,Docker Pipeline插件会与本地Docker守护进程通信,通常通过/var/run/Docker访问。

如果要选择非默认Docker服务器,例如使用 Docker Swarm,应使用withServer()方法。

通过将URI和在Jenkins中预先配置的Docker服务器证书身份验证的凭据ID(可选)传递给方法:

代码语言:javascript
复制
node {
    checkout scm

    docker.withServer('tcp://swarm.example.com:2376', 'swarm-certs') {
        docker.image('mysql:5').withRun('-p 3306:3306') {
            /* do things */
        }
    }
}

注意:

inside()build()无法直接与Docker Swarm服务器一起正常工作

为了让inside()工作,Docker服务器和Jenkins代理必须使用相同的文件系统,这样才能装载工作空间。

目前,Jenkins插件和Docker CLI都不会自动检测远程运行的服务器的文件系统;典型的症状是嵌套的sh命令出错,例如

代码语言:javascript
复制
cannot create /…@tmp/durable-…/pid: Directory nonexistent

当Jenkins检测到代理本身正在Docker容器中运行时,它会自动将--volumes from参数传递给inside容器,确保它可以与代理共享一个工作空间。

此外,Docker Swarm的一些版本不支持自定义注册中心。

使用自定义注册中心

默认情况下,Docker Pipeline假定了Docker Hub的默认Docker注册中心。为了使用自定义Docker注册中心,脚本化Pipeline的用户可以使用withRegistry()方法包装步骤,传递自定义注册中心 URL,例如:

代码语言:javascript
复制
node {
    checkout scm

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

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

对于需要身份验证的Docker注册中心,从Jenkins主页添加“用户名/密码”凭据项,并将凭据ID用作withRegistry()的第二个参数

代码语言:javascript
复制
node {
    checkout scm

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

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

        /* Push the container to the custom Registry */
        customImage.push()
    }
}

在容器内运行构建步骤

Jenkins项目通常要求在构建过程中提供特定的工具集或库。如果Jenkins中的许多项目都有相同的要求,并且代理很少,那么相应地预先配置这些代理并不困难。其他情况下,也可以将此类文件保存在项目源代码控制中。最后,对于一些工具,尤其是那些具有独立于平台的自包含下载的工具,比如Maven,可以使用Jenkins工具安装程序系统和Pipeline tool步骤来按需检索工具。然而,在许多情况下,这些技术不适用。

对于可以在Linux上运行的构建,Docker为这个问题提供了一个理想的解决方案。每个项目只需要选择一个包含它所需的所有工具和库的镜像(这可能是像maven这样的公开镜像,也可能是由这个或另一个Jenkins项目创建的)有两种方法可以在镜像中运行Jenkins构建步骤。一种需要在镜像中包含它所需的所有工具、运行环境,然后在镜像中运行整个构建,另一种借助插件inside()方法,实现在任意镜像中运行构建,和前者的区别在于后者可以不用提前在镜像中包含所需要工具、运行环境,在运行时提供即可。,例如:

1

代码语言:javascript
复制
docker.image('maven:3.3.3-jdk-8').inside {
  git '…your-sources…'
  sh 'mvn -B clean install'
}

以上是一个完整的Pipeline脚本,inside将:

  1. 自动获取代理和工作区(不需要额外的node块)
  2. 将请求的镜像拉取到Docker服务器(如果尚未缓存的话)
  3. 启动一个运行该镜像的容器
  4. 使用相同的文件路径,将Jenkins工作区作为“volume”装入容器中。
  5. 运行构建步骤。像sh这样的外部进程将被包装在docker exec中,以便在容器中运行。其他步骤(如测试报告)未经修改即可运行:它们仍然可以访问由构建步骤创建的工作区文件。
  6. 运行完上述代码块结束时,停止容器并释放其消耗的所有存储。
  7. Record the fact that the build used the specified image。这将解锁其他Jenkins插件中的功能:您可以使用镜像跟踪所有项目,或者将此项目配置为在更新的镜像推送到Docker注册表时自动触发。如果您使用Docker Traceability plugin插件,还可以查看Docker服务器上镜像的历史记录。

注意:如果你正在运行一个像Maven这样有一个大的下载缓存的工具,在其镜像中运行每次构建将意味着从网络下载大量数据,这通常是不可取的。避免这种情况的最简单方法是将缓存重定向到代理工作区,这样,如果在同一个代理上运行另一个构建,它将运行得更快。就Maven而言:

代码语言:javascript
复制
docker.image('maven:3.3.3-jdk-8').inside {
  git '…your-sources…'
  writeFile file: 'settings.xml', text: "<settings><localRepository>${pwd()}/.m2repo</localRepository></settings>"
  sh 'mvn -B -s settings.xml clean install'
}

(如果希望在代理上的其他位置使用缓存位置,则需要传递一个额外的--volume选项给inside,以便容器可以看到该路径)

其它解决方案是传递一个参数给inside以加载共享卷,比如 -v m2repo:/m2repo,并使用该路径作为 localRepository。要注意的是,Maven中默认的本地存储库管理对于并发构建来说并不是线程安全的,nstall:install 安装可能会跨构建甚至跨Job污染本地存储库。最安全的解决方案是使用仓库镜像作为缓存。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-02-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 配合Pipeline使用Docker
    • 定制执行环境
      • 工作空间同步
        • 为容器缓存数据
          • 使用多个容器
            • 使用Dockerfile
              • 脚本化Pipeline运行“sidecar”容器的高级用法
                • 构建容器
                  • 使用远程Docker服务
                    • 使用自定义注册中心
                      • 在容器内运行构建步骤
                      相关产品与服务
                      容器镜像服务
                      容器镜像服务(Tencent Container Registry,TCR)为您提供安全独享、高性能的容器镜像托管分发服务。您可同时在全球多个地域创建独享实例,以实现容器镜像的就近拉取,降低拉取时间,节约带宽成本。TCR 提供细颗粒度的权限管理及访问控制,保障您的数据安全。
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档