使用 Jenkins 和 Docker 确保构建环境和生产环境之间的对等

阅读数:13598 2019 年 8 月 26 日 10:21

使用Jenkins和Docker确保构建环境和生产环境之间的对等

持续交付(CD)的核心原则之一是,测试需要在“类似生产”的环境中运行,以保证发布版本的稳定性。类似相同的环境可以减少“在我的机器上可以运行”之类的问题,并能轻松地在本地复现问题。我们可以避免由于平台差异引起的 bug,比如证书丢失或过期。

Docker 是实现这种对等性的最佳工具之一。它还允许我们将构建与 Jenkins 基础设施分离。比如,使用基于 Docker 的构建,我们无需担心预先安装在 Jenkins 代理上的 Java 版本。无论代理上安装了什么,构建都将使用我们指定的版本运行。尝试新的 JVM 版本如同变更 Docker 标签一样容易,可以让我们走地更快。

在本文中,我们将要学习如何使用 Docker 配置 Jenkins ,来确保构建环境与生产环境是相同的。我们还将学习如何通过在构建过程中共享缓存数据,来保持最小的构建时间。

Java 构建的简单概述

首先,从预先安装了 Docker 的 Jenkins 代理开始构建。该代理通过签出源代码编译的方式获取,并在预先安装了 JDK 的 Docker 容器中运行测试。然后,富 JAR 将在生产环境中运行在一个 Docker 镜像,该镜像具有和构建时相同的 Java 版本。

不幸的是,我们需要考虑很多细节才能解决这个问题。下面我们将深入地讨论这些细节。

使用 Docker 构建的详细指南

首先,需要预先安装一个带有 Docker 二进制文件的 Jenkins 代理。因为这是唯一的要求,所以这个代理可以在整个公司内共享,没有必要为每个团队都构建自己的代理。在本例中,klarna-jenkins-agent 是附带了 Docker 的通用代理。它还有一个 ID 为 11000 的 Jenkins 用户,该用户属于 ID 为 12000 的 Docker 组。

如果你想先看下结果,请阅读本文末尾部分,以了解此处讨论的完整 Jenkinsfile。

我们在 pipeline 的根目录处配置代理,以确保所有的 Docker 镜像都能在签出源代码的同一台机器上运行。

复制代码
// Jenkinsfile
pipeline {
agent { label ‘klarna-jenkins-agent’ }
stages { ... }
}

我们使用 amazonlinux-java-jdk:11 镜像来运行构建。考虑到安全性,镜像安装了 JDK 并使用非 root 帐户来运行。该步骤应该在同一代理(1,3)上运行,以便可以访问项目的源代码。

或者,如果集成测试需要 Docker,则可以安装 docker.sock(2)。

复制代码
stage ('Compile & Test') {
agent {
docker {
image 'docker-registry.klarna/amazonlinux-java-jdk:11'
label 'klarna-jenkins-agent' // 1
args '-v /var/run/docker.sock:/var/run/docker.sock -u 11000:12000' // 2
reuseNode true // 3
}
}
steps {
sh './gradlew build'
}
}

如果项目需要,也可以在自定义的 Docker 镜像中运行构建。该过程,仅需指定要使用的 Dockerfile 即可,而无需指定镜像:

复制代码
agent {
docker {
filename 'MyDockerfile.build'
...
}
}

镜像将会缓存在代理上,只有当 Dockerfile 的内容发生变更时,镜像才会重新生成。

提高性能

现在我们已经运行了一个基本的构建,我们可以通过在运行之间共享 Gradle 缓存来提高性能。

我们为 Gradle 缓存准备了一个“home”。在将它挂载之前,必须先创建一个.gradle_home 文件夹(4),才能确保 Jenkins 用户可以对其进行写入。否则,该文件夹将由 Docker 创建,只有 root 帐户才有权访问它。

复制代码
pipeline {
environment {
GRADLE_USER_HOME = "/home/jenkins/.gradle_home"
}
stages {
stage (‘Setup’) {
steps { sh 'mkdir /home/jenkins/.gradle_home || true' // 4
}
}
}

最后一步,我们修改 docker args 部分,以挂载新创建的文件夹。完整的 args 参数如下所示:

复制代码
args '-v /var/run/docker.sock:/var/run/docker.sock -u 11000:12000 -v /home/jenkins/.gradle_home:/home/jenkins/.gradle_home'

Jenkinsfile 的最终版本如下:

复制代码
pipeline {
agent { label 'klarna-jenkins-agent' }
stages {
environment {
GRADLE_USER_HOME = "/home/jenkins/.gradle_home"
}
stages {
stage ('Setup') {
steps { sh 'mkdir /home/jenkins/.gradle_home || true' // 4
}
stage ('Compile & Test') {
agent {
docker {
image 'docker-registry.klarna/amazonlinux-java-jdk:11'
// alternatively, build a docker image by specifying a Dockerfile
// filename 'MyDockerfile.build'
label 'klarna-jenkins-agent' // 1
// assuming uid(jenkins)=11000 and gid(docker)=12000
args '-v /var/run/docker.sock:/var/run/docker.sock -u 11000:12000 -v /home/jenkins/.gradle_home:/home/jenkins/.gradle_home'
reuseNode true // 3
}
}
steps {
sh './gradlew build'
}
}
}
}
}

Jenkinsfile

或者,我们也可以使用名为 volumes 的 Docker 作为 Gradle 缓存,但是 volume 文件夹由容器内的 root 拥有,如果我们使用非 root 用户运行构建,则会使设置变得更加复杂。

好处

除了可以获得相同的构建和生产环境之外,还有其他一些好处:

  1. 不需要为每个团队或项目维护一个定制的 Jenkins 代理。只需一个 Docker 通用代理就足够了。
  2. 不受 Jenkins 代理上的 Java 版本限制。
  3. 如果项目在 pipeline 的特定步骤中需要一些自定义的软件,我们可以为它创建一个 Dockerfile,而无需创建自定义的代理,并受那些预先安装在代理上的软件的约束。如果以后可能会切换到其他构建工具上,我们也有很多选择。
  4. 升级到下一个 JDK 版本很容易。只需将 Jenkinsfile 中的标签 11 替换为 12 即可。我们甚至可以将构建参数化来验证兼容性。

结论

我们需要付出一些努力才能确保构建环境和生产环境是相同的,并且花费的这些时间是值得的,因为它增强了我们对发布的信心。

使用 Docker 能够使用我们将 Jenkins 代理从构建环境中解耦出来,从而使我们能够精确地控制构建所需的 Java、Node 或 Python 版本,而不需要在代理上预先安装什么。通过使用公司中的标准化组件,可以减少长期维护。

原文链接:
https://engineering.klarna.com/ensuring-parity-between-build-and-production-environments-using-jenkins-and-docker-2695f758b549

评论

发布