持续交付(CD)的核心原则之一是,测试需要在“类似生产”的环境中运行,以保证发布版本的稳定性。类似相同的环境可以减少“在我的机器上可以运行”之类的问题,并能轻松地在本地复现问题。我们可以避免由于平台差异引起的 bug,比如证书丢失或过期。
Docker 是实现这种对等性的最佳工具之一。它还允许我们将构建与 Jenkins 基础设施分离。比如,使用基于 Docker 的构建,我们无需担心预先安装在 Jenkins 代理上的 Java 版本。无论代理上安装了什么,构建都将使用我们指定的版本运行。尝试新的 JVM 版本如同变更 Docker 标签一样容易,可以让我们走地更快。
在本文中,我们将要学习如何使用 Docker 配置 Jenkins ,来确保构建环境与生产环境是相同的。我们还将学习如何通过在构建过程中共享缓存数据,来保持最小的构建时间。
首先,从预先安装了 Docker 的 Jenkins 代理开始构建。该代理通过签出源代码编译的方式获取,并在预先安装了 JDK 的 Docker 容器中运行测试。然后,富 JAR 将在生产环境中运行在一个 Docker 镜像,该镜像具有和构建时相同的 Java 版本。
不幸的是,我们需要考虑很多细节才能解决这个问题。下面我们将深入地讨论这些细节。
首先,需要预先安装一个带有 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 reuseNodetrue//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 -u11000: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)=11000and 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' reuseNodetrue//3 } } steps { sh'./gradlew build' } } } } }
Jenkinsfile
或者,我们也可以使用名为 volumes 的 Docker 作为 Gradle 缓存,但是 volume 文件夹由容器内的 root 拥有,如果我们使用非 root 用户运行构建,则会使设置变得更加复杂。
除了可以获得相同的构建和生产环境之外,还有其他一些好处:
我们需要付出一些努力才能确保构建环境和生产环境是相同的,并且花费的这些时间是值得的,因为它增强了我们对发布的信心。
使用 Docker 能够使用我们将 Jenkins 代理从构建环境中解耦出来,从而使我们能够精确地控制构建所需的 Java、Node 或 Python 版本,而不需要在代理上预先安装什么。通过使用公司中的标准化组件,可以减少长期维护。
原文链接:
https://engineering.klarna.com/ensuring-parity-between-build-and-production-environments-using-jenkins-and-docker-2695f758b549