在流水线中使用 Docker
许多组织使用 Docker 来统一跨机器的构建和测试环境,并为部署应用程序提供有效的机制。从 Pipeline 2.5 及更高版本开始,流水线内置了与 Jenkinsfile
中的 Docker 进行交互的支持。
Docker 的基本原理, 可以参考 Docker Getting Started Guide
自定义执行环境
设计流水线的目的是更方便地使用 Docker 镜像作为单个 Stage 或整个流水线的执行环境。这意味着用户可以定义流水线需要的工具,而无需手动配置代理。实际上,只需对 Jenkinsfile
进行少量编辑,任何 packaged in a Docker container 的工具,都可轻松使用。
// Jenkinsfile (Declarative Pipeline)
pipeline {
agent {
docker { image 'node:16.13.1-alpine' }
}
stages {
stage('Test') {
steps {
sh 'node --version'
}
}
}
}
// Jenkinsfile (Scripted Pipeline)
node {
/* Requires the Docker Pipeline plugin to be installed */
docker.image('node:16.13.1-alpine').inside {
stage('Test') {
sh 'node --version'
}
}
}
当 Pipeline 执行时,Jenkins 会自动启动指定的容器并执行其中定义的步骤
工作区同步
简短:如果保持工作空间与其他阶段同步很重要,请使用 reuseNode true. 否则,dockerized stage 可以在任何其他代理或同一代理上运行,但可以在临时工作区中运行。
默认情况下,对于容器化阶段,Jenkins 会:
- 选择任何代理,
- 创建新的空工作区,
- 将管道代码克隆到其中,
- 将此新工作区安装到容器中。
如果您有多个 Jenkins 代理,您的容器化阶段可以在其中任何一个上启动。
当 reuseNode 设置为 true:不会创建新的工作空间,并且当前代理的当前工作空间将被挂载到容器中,并且容器将在同一节点启动,因此将同步整个数据。
// Jenkinsfile (Declarative Pipeline)
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 支持添加传递给 Docker 的自定义参数,允许用户指定要挂载的自定义 Docker Volumes,可用于在 Pipeline 运行之间缓存代理上的数据。以下示例将使用 maven container 在 Pipeline 运行之间进行 ~/.m2
缓存,从而避免为 Pipeline 的后续运行重新下载依赖项。
// Jenkinsfile (Declarative Pipeline)
pipeline {
agent {
docker {
image 'maven:3.8.1-adoptopenjdk-11'
args '-v $HOME/.m2:/root/.m2'
}
}
stages {
stage('Build') {
steps {
sh 'mvn -B'
}
}
}
}
// Jenkinsfile (Scripted Pipeline)
node {
/* Requires the Docker Pipeline plugin to be installed */
docker.image('maven:3-alpine').inside('-v $HOME/.m2:/root/.m2') {
stage('Build') {
sh 'mvn -B'
}
}
}
使用多个容器
代码库依赖多种不同的技术变得越来越普遍。例如:存储库可能同时具有基于 Java 的后端 API 实现和基于 JavaScript 的前端实现。结合 Docker 和 Pipeline 允许 Jenkinsfile
通过将 agent {}
指令与不同阶段结合使用多种类型的技术。
// Jenkinsfile (Declarative Pipeline)
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'
}
}
}
}
// Jenkinsfile (Scripted Pipeline)
node {
/* Requires the Docker Pipeline plugin to be installed */
stage('Back-end') {
docker.image('maven:3-alpine').inside {
sh 'mvn --version'
}
}
stage('Front-end') {
docker.image('node:7-alpine').inside {
sh 'node --version'
}
}
}
使用 Dockerfile
对于需要更多自定义执行环境的项目,Pipeline 还支持从 Dockerfile
源存储库中构建和运行容器。与之前使用 “现成” 容器的方法相比,使用该 agent { dockerfile true }
语法将从 a 构建一个新镜像,Dockerfile
而不是从 Docker Hub 拉取一个镜像。
重新使用上面的示例,更自定义 Dockerfile:
# Dockerfile
FROM node:16.13.1-alpine
RUN apk add -U subversion
通过将其提交到源存储库的根目录,Jenkinsfile
可以将其更改为基于此 Dockerfile
构建一个容器,然后使用该容器运行定义的步骤:
// Jenkinsfile (Declarative Pipeline)
pipeline {
agent { dockerfile true }
stages {
stage('Test') {
steps {
sh 'node --version'
sh 'svn --version'
}
}
}
}
指定 Docker 标签
默认情况下,Pipeline 假定任何已配置的代理都能够运行基于 Docker 的 Pipelines。对于具有 macOS、Windows 或其他代理且无法运行 Docker 守护程序的 Jenkins 环境,此默认设置可能有问题。Pipeline 在 Manage Jenkins 页面和文件夹级别提供了一个全局选项,用于指定哪些代理(通过 Label)用于运行基于 Docker 的管道。
mac OS 用户的路径设置
默认情况下,该 /usr/local/bin
目录不包含在 macOS PATH
中用于 Docker 映像。/usr/local/bin
如果需要从 Jenkins 中调用可执行文件,则 PATH
需要将其扩展为包含 /usr/local/bin
。在文件 “/usr/local/Cellar/jenkins-lts/XXX/homebrew.mxcl.jenkins-lts.plist” 中添加一个路径节点,如下所示:
// Contents of homebrew.mxcl.jenkins-lts.plist
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key
<string><!-- insert revised path here --></string>
</dict>
修改 PATH string
后的目录应该是一个冒号分隔的目录列表,格式与 PATH
环境变量相同,应该包括:
- /usr/local/bin
- /usr/bin
- /bin
- /usr/sbin
- /sbin
- /Applications/Docker.app/Contents/Resources/bin/
- /Users/XXX/Library/Group\ Containers/group.com.docker/Applications/Docker.app/Contents/Resources/bin(其中
XXX
替换为您的用户名)
现在使用 “brew services restart jenkins-lts” 重新启动 jenkins
脚本管道的高级用法
运行 “sidecar” 容器
在 Pipeline 中使用 Docker 可能是运行构建或一组测试可能依赖的服务的有效方式。与 sidecar 模式类似,Docker Pipeline 可以 “在后台” 运行一个容器,同时在另一个容器中执行工作。利用这种 sidecar 方法,Pipeline 可以为每个 Pipeline 运行提供一个 “干净的” 容器。
考虑一个假设的集成测试套件,它依赖于运行的本地 MySQL 数据库。使用在 Docker Pipeline 插件对 Scripted Pipeline 的支持中 withRun
实现的方法,可以将 MySQL 作为 sidecar 运行 Jenkinsfile:
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'
}
}
这个例子可以更进一步,同时使用两个容器。一个运行 MySQL 的 “sidecar”,另一个提供执行环境,通过使用 Docker 容器链接。
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。inside()
方法使用容器的 ID,管道可以通过将自定义 Docker 参数传递给方法来创建链接。
该 id 属性还可用于在管道退出之前检查正在运行的 Docker 容器中的日志:
sh "docker logs ${c.id}"
构建容器
为了创建 Docker 镜像,Docker Pipeline 插件还提供了一种在 Pipeline 运行期间从存储库中 Dockerfile
创建新镜像 build()
的方法。
使用 docker.build("my-image-name")
该语法的一个主要好处是脚本化管道可以将返回值用于后续 Docker 管道调用,例如:
node {
checkout scm
def customImage = docker.build("my-image:${env.BUILD_ID}")
customImage.inside {
sh 'make test'
}
}
返回值也可用于通过 push()
该方法将 Docker 镜像发布到 Docker Hub
或 custom Registry
,例如:
node {
checkout scm
def customImage = docker.build("my-image:${env.BUILD_ID}")
customImage.push()
}
镜像 “标签” 的一种常见用法是为 Docker 镜像的最新验证版本指定 latest
标签。该 push()
方法接受一个可选 tag
参数,允许管道使用不同的标签推送 customImage
,例如:
node {
checkout scm
def customImage = docker.build("my-image:${env.BUILD_ID}")
customImage.push()
customImage.push('latest')
}
build()
该方法默认在当前目录中构建 Dockerfile
。这可以通过提供包含一个 Dockerfile
文件作为 build()
方法的第二个参数的目录路径来覆盖,例如:
node {
checkout scm
def testImage = docker.build("test-image", "./dockerfiles/test")
testImage.inside {
sh 'make test'
}
}
- 从在
./dockerfiles/test/Dockerfile
中发现的Dockerfile
中构建test-image
。
可以通过将其他参数添加到 build()
方法的第二个参数来将其他参数传递给 docker build
。以这种方式传递参数时,该字符串中的最后一个值必须是 docker 文件的路径,并且应该以用作构建上下文的文件夹结尾
此示例通过传递 -f
标志覆盖 Dockerfile
默认值:
node {
checkout scm
def dockerfile = 'Dockerfile.test'
def customImage = docker.build("my-image:${env.BUILD_ID}",
"-f ${dockerfile} ./dockerfiles")
}
my-image:${env.BUILD_ID}
从位于./dockerfiles/Dockerfile.test
的 Dockerfile 构建。
使用远程 Docker 服务器
默认情况下,Docker Pipeline 插件将与本地 Docker 守护进程通信,通常通过 /var/run/docker.sock
访问。
要选择非默认 Docker 服务器,例如:使用 Docker Swarm
,应使用该 withServer()
方法。
通过将 URI 和可选的 Jenkins 中预先配置的 Docker Server Certificate Authentication
的凭据 ID 传递给具有以下功能的方法:
node {
checkout scm
docker.withServer('tcp://swarm.example.com:2376', 'swarm-certs') {
docker.image('mysql:5').withRun('-p 3306:3306') {
/* do things */
}
}
}
使用自定义注册表
默认情况下,Docker Pipeline 集成了 Docker Hub 的默认 Docker Registry 。
为了使用自定义的 Docker Registry,Scripted Pipeline 的用户可以使用该 withRegistry()
方法包装步骤,传入自定义的 Registry URL,例如:
node {
checkout scm
docker.withRegistry('https://registry.example.com') {
docker.image('my-custom-image').inside {
sh 'make test'
}
}
}
对于需要身份验证的 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}")
/* Push the container to the custom Registry */
customImage.push()
}
}