在流水线中使用Docker

许多组织使用 Docker 在机器之间统一构建和测试环境, 并为部署应用程序提供有效的机制。从流水线版本 2.5 或以上开始,流水线内置了与Jenkinsfile中的Docker进行交互的的支持。

虽然本节将介绍基础知识在Jenkinsfile中使用Docker的基础,但它不会涉及 Docker 的基本原理, 可以参考Docker入门指南

自定义执行环境

设计流水线的目的是更方便地使用Docker镜像作为单个Stage或整个流水线的执行环境。 这意味着用户可以定义流水线需要的工具,而无需手动配置代理。实际上,只需对 Jenkinsfile进行少量编辑,任何packaged in a Docker container的工具,都可轻松使用。

Jenkinsfile (Declarative Pipeline)

  1. pipeline {
  2. agent {
  3. docker { image 'node:7-alpine' }
  4. }
  5. stages {
  6. stage('Test') {
  7. steps {
  8. sh 'node --version'
  9. }
  10. }
  11. }
  12. }

Toggle Scripted Pipeline(Advanced)

Jenkinsfile (Scripted Pipeline)

  1. node {
  2. /* Requires the Docker Pipeline plugin to be installed */
  3. docker.image('node:7-alpine').inside {
  4. stage('Test') {
  5. sh 'node --version'
  6. }
  7. }
  8. }

当流水线执行时, Jenkins 将会自动地启动指定的容器并在其中执行指定的步骤:

  1. [Pipeline] stage
  2. [Pipeline] { (Test)
  3. [Pipeline] sh
  4. [guided-tour] Running shell script
  5. + node --version
  6. v7.4.0
  7. [Pipeline] }
  8. [Pipeline] // stage
  9. [Pipeline] }

容器的缓存数据

许多构建工具都会下载外部依赖并将它们缓存到本地以便于将来的使用。 由于容器最初是由 "干净的" 文件系统构建的, 这导致流水线速度变慢, 因为它们不会利用后续流水线运行的磁盘缓存。on-disk caches between subsequent Pipeline runs.

流水线支持向Docker中添加自定义的参数, 允许用户指定自定义的Docker Volumes装在, 这可以用于在流水线运行之间的agent上缓存数据。下面的示例将会在流水线运行期间使用maven container缓存 ~/.m2, 从而避免了在流水线的后续运行中重新下载依赖的需求。

Jenkinsfile (Declarative Pipeline)

  1. pipeline {
  2. agent {
  3. docker {
  4. image 'maven:3-alpine'
  5. args '-v $HOME/.m2:/root/.m2'
  6. }
  7. }
  8. stages {
  9. stage('Build') {
  10. steps {
  11. sh 'mvn -B'
  12. }
  13. }
  14. }
  15. }

Toggle Scripted Pipeline(Advanced)

Jenkinsfile (Scripted Pipeline)

  1. node {
  2. /* Requires the Docker Pipeline plugin to be installed */
  3. docker.image('maven:3-alpine').inside('-v $HOME/.m2:/root/.m2') {
  4. stage('Build') {
  5. sh 'mvn -B'
  6. }
  7. }
  8. }

使用多个容器

代码库依赖于多种不同的技术变得越来越容易。比如, 一个仓库既有基于Java的后端API 实现 and 有基于JavaScript的前端实现。 Docker和流水线的结合允许 Jenkinsfile 通过将 agent {} 指令和不同的阶段结合使用multiple 技术类型。

Jenkinsfile (Declarative Pipeline)

  1. pipeline {
  2. agent none
  3. stages {
  4. stage('Back-end') {
  5. agent {
  6. docker { image 'maven:3-alpine' }
  7. }
  8. steps {
  9. sh 'mvn --version'
  10. }
  11. }
  12. stage('Front-end') {
  13. agent {
  14. docker { image 'node:7-alpine' }
  15. }
  16. steps {
  17. sh 'node --version'
  18. }
  19. }
  20. }
  21. }

Toggle Scripted Pipeline(Advanced)

Jenkinsfile (Scripted Pipeline)

  1. node {
  2. /* Requires the Docker Pipeline plugin to be installed */
  3. stage('Back-end') {
  4. docker.image('maven:3-alpine').inside {
  5. sh 'mvn --version'
  6. }
  7. }
  8. stage('Front-end') {
  9. docker.image('node:7-alpine').inside {
  10. sh 'node --version'
  11. }
  12. }
  13. }

使用Dockerfile

对于更需要自定义执行环境的项目, 流水线还支持从源仓库的Dockerfile 中构建和运行容器。 与使用"现成" 容器的 previous approach 不同的是, 使用 agent { dockerfile true } 语法从 Dockerfile 中构建一个新的镜像而不是从Docker Hub中拉取一个。

重复使用上面的示例, 使用一个更加自定义的 Dockerfile:

Dockerfile

  1. FROM node:7-alpine
  2. RUN apk add -U subversion

通过提交它到源仓库的根目录下, 可以更改 Jenkinsfile 文件,来构建一个基于该 Dockerfile 文件的容器然后使用该容器运行已定义的步骤:

Jenkinsfile (Declarative Pipeline)

  1. pipeline {
  2. agent { dockerfile true }
  3. stages {
  4. stage('Test') {
  5. steps {
  6. sh 'node --version'
  7. sh 'svn --version'
  8. }
  9. }
  10. }
  11. }

agent { dockerfile true } 语法支持大量的其它选项,这些选项的更详细的描述请参考流水线语法 部分。

Using a Dockerfile with Jenkins Pipeline

指定Docker标签

的了agent 都能够运行基于Docker的流水线。对于有macOS, Windows, 或其他代理的Jenkins环境, 不能运行Docker守护进程, 这个默认设置可能会有问题。流水线在 Manage Jenkins 页面和 文件夹级别提供一个了全局选项,用来指定运行基于Docker的流水线的代理 (通过标签)。

Configuring the Pipeline Docker Label

脚本化流水线的高级用法

运行 "sidecar" 容器

在流水线中使用Docker可能是运行构建或一组测试的所依赖的服务的有效方法。类似于sidecar模式, Docker 流水线可以"在后台"运行一个容器 , 而在另外一个容器中工作。 利用这种sidecar 方式, 流水线可以为每个流水线运行提供一个"干净的" 容器。

考虑一个假设的集成测试套件,它依赖于本地 MySQL数据库来运行。使用 withRun 方法, 在Docker Pipeline 插件中实现对脚本化流水线的支持,Jenkinsfile 文件可以运行 MySQL作为sidecar :

  1. node {
  2. checkout scm
  3. /*
  4. * In order to communicate with the MySQL server, this Pipeline explicitly
  5. * maps the port (`3306`) to a known port on the host machine.
  6. */
  7. docker.image('mysql:5').withRun('-e "MYSQL_ROOT_PASSWORD=my-secret-pw" -p 3306:3306') { c ->
  8. /* Wait until mysql service is up */
  9. sh 'while ! mysqladmin ping -h0.0.0.0 --silent; do sleep 1; done'
  10. /* Run some tests which require MySQL */
  11. sh 'make check'
  12. }
  13. }

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

  1. node {
  2. checkout scm
  3. docker.image('mysql:5').withRun('-e "MYSQL_ROOT_PASSWORD=my-secret-pw"') { c ->
  4. docker.image('mysql:5').inside("--link ${c.id}:db") {
  5. /* Wait until mysql service is up */
  6. sh 'while ! mysqladmin ping -hdb --silent; do sleep 1; done'
  7. }
  8. docker.image('centos:7').inside("--link ${c.id}:db") {
  9. /*
  10. * Run some tests which require MySQL, and assume that it is
  11. * available on the host name `db`
  12. */
  13. sh 'make check'
  14. }
  15. }
  16. }

上面的示例使用 withRun公开的项目, 它通过id 属性具有可用的运行容器的ID。使用该容器的ID, 流水线通过自定义 Docker 参数生成一个到inside()方法的链。

The id property can also be useful for inspecting logs from a running Dockercontainer before the Pipeline exits:

  1. sh "docker logs ${c.id}"

构建容器

为了构建 Docker 镜像,Docker 流水线插件也提供了一个 build() 方法用于在流水线运行期间从存储库的Dockerfile 中创建一个新的镜像。

使用语法 docker.build("my-image-name") 的主要好处是,脚本化的流水线能够使用后续 Docker流水线调用的返回值, 比如:

  1. node {
  2. checkout scm
  3. def customImage = docker.build("my-image:${env.BUILD_ID}")
  4. customImage.inside {
  5. sh 'make test'
  6. }
  7. }

该返回值也可以用于通过 push() 方法将Docker 镜像发布到Docker Hub,或 custom Registry,比如:

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

镜像 "tags"的一个常见用法是 为最近的, 验证过的, Docker镜像的版本,指定 latest 标签。 push() 方法接受可选的 tag 参数, 允许流水线使用不同的标签 push customImage , 比如:

  1. node {
  2. checkout scm
  3. def customImage = docker.build("my-image:${env.BUILD_ID}")
  4. customImage.push()
  5. customImage.push('latest')
  6. }

在默认情况下, build() 方法在当前目录构建一个 Dockerfile。提供一个包含 Dockerfile文件的目录路径作为build() 方法的第二个参数 就可以覆盖该方法, 比如:

  1. node {
  2. checkout scm
  3. def testImage = docker.build("test-image", "./dockerfiles/test") (1)
  4. testImage.inside {
  5. sh 'make test'
  6. }
  7. }
1从在 ./dockerfiles/test/Dockerfile中发现的Dockerfile中构建test-image

通过添加其他参数到 build() 方法的第二个参数中,传递它们到docker build。当使用这种方法传递参数时, 该字符串的最后一个值必须是Docker文件的路径。

该示例通过传递 -f标志覆盖了默认的Dockerfile :

  1. node {
  2. checkout scm
  3. def dockerfile = 'Dockerfile.test'
  4. def customImage = docker.build("my-image:${env.BUILD_ID}", "-f ${dockerfile} ./dockerfiles") (1)
  5. }
1从在./dockerfiles/Dockerfile.test发现的Dockerfile构建 my-image:${env.BUILD_ID}

使用远程 Docker 服务器

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

要选择一个非默认的Docker 服务器, 比如Docker 集群,应使用withServer() 方法。

通过传递一个URI, 在Jenkins中预先配置的 Docker Server Certificate Authentication的证书ID, 如下:

  1. node {
  2. checkout scm
  3. docker.withServer('tcp://swarm.example.com:2376', 'swarm-certs') {
  4. docker.image('mysql:5').withRun('-p 3306:3306') {
  5. /* do things */
  6. }
  7. }
  8. }
inside()build() 不能正确的在Docker集群服务器中工作。对于inside() 工作, Docker 服务器和Jenkins 代理必须使用相同的文件系统,这样才能安装工作区。目前,Jenkins 插件和Docker CLI 都不会自动的检查服务器远程运行的情况; 典型的症状是嵌套的sh命令的错误,比如
  1. cannot create /…@tmp/durable-…/pid: Directory nonexistent
当 Jenkins 检查到代理本身在 Docker容器中运行时, 它会自动地传递 —volumes-from 参数到inside 容器,确保它能够和代理共享工作区。另外,Docker集群的一些版本不支持自定义注册。

使用自定义注册表

默认情况下, Docker 流水线 集成了 Docker Hub默认的 Docker注册表。.

为了使用自定义Docker 注册吧, 脚本化流水线的用户能够使用 withRegistry() 方法完成步骤,传入自定义注册表的URL, 比如:

  1. node {
  2. checkout scm
  3. docker.withRegistry('https://registry.example.com') {
  4. docker.image('my-custom-image').inside {
  5. sh 'make test'
  6. }
  7. }
  8. }

对于需要身份验证的Docker 注册表, 从Jenkins 主页添加一个 "Username/Password"证书项, 并使用证书ID 作为 withRegistry()的第二个参数:

  1. node {
  2. checkout scm
  3. docker.withRegistry('https://registry.example.com', 'credentials-id') {
  4. def customImage = docker.build("my-image:${env.BUILD_ID}")
  5. /* Push the container to the custom Registry */
  6. customImage.push()
  7. }
  8. }