持续集成与持续部署宝典 Part 1:将构建环境容器化

阅读数:1 2020 年 4 月 15 日 23:04

持续集成与持续部署宝典Part 1:将构建环境容器化

介绍

随着 Docker 项目及其相关生态系统逐渐成熟,容器已经开始被更多企业用在了更大规模的项目中。因此,我们需要一套连贯的工作流程和流水线来简化大规模项目的部署。在本指南中,我们将从代码开发、持续集成、持续部署以及零停机更新几个方面进行介绍。在大型组织中,这已是相当标准的工作流;但在本系列文章中,我们会更着重于探讨 在容器时代,如何在基于 Docker 的环境中复制这些工作流 。另外,我们还将详细介绍如何利用 Docker 和 Rancher 自动化处理这些工作流。在本指南中,我们提供了每个步骤的详细示例,帮助你实现自己的 CI 系统。

我们希望你通过该指南,能够提取到其中的一些想法,利用诸如 Docker 和 Rancher 这类工具来创建属于你们企业的持续集成和持续部署流水线,并根据自己的实际情况和需求在这 CI/CD 流水线中也加入自定义的流程。

在我们开始之前,还有一些需要注意的事项:因为 Docker 和 Rancher 的版本更迭都非常快,可能会出现一些在不同版本的平台上 API 和实现不一致的情况。作为参考,我们在指南中的工作环境是:Golang 1.8,Docker 1.13.1+,Jenkins Version 2.32.2,docker-compose 1.11.1+ 以及 Rancher 1.4.1+。

第一部分:持续集成

那么踏出第一步,我们先从流水线的入口开始,即构建源代码。在任何项目开始时,构建 / 编译并不会是什么麻烦的问题,因为大多数语言和工具都有定义良好且记录详细的编译源代码过程。但是随着项目和团队在规模上的扩大以及依赖关系的增加,在确保代码质量的同时,如何为所有开发人员提供一致且稳定的构建,会逐渐成为一个更大的挑战。在本节中,我们将会介绍一些常见的挑战、最佳实践以及如何通过 Docker 来实现持续集成。

持续集成与持续部署宝典Part 1:将构建环境容器化

扩展构建系统的挑战

在分享最佳实践之前,让我们看看在维护构建系统中常出现的一些挑战。

持续集成与持续部署宝典Part 1:将构建环境容器化

首先,在扩展项目时你会面临的第一个问题就是 Dependency Management(依赖管理)。开发人员会从库中拉取代码并和源代码进行集成,如此一来,跟踪代码所使用的每个库的版本,确保项目所有部分都使用相同的版本,测试库版本的升级并把通过测试的更新 push 到你全部的项目中,这些过程都变得非常重要。

其次,管理环境依赖是一个和依赖管理相关但又有一些不同的问题。它包括 IDE 和 IDE 配置、工具版本(如 Maven 版本、Python 版本)和工具配置(如静态分析规则文件、代码格式化模版)。因为项目的不同部分可能相互间有需求冲突,环境依赖管理会变得非常棘手。和代码层面的依赖冲突不同,想要解决这些冲突往往是非常困难甚至是不可能的。比如,在最近的一个项目中,我们使用 fabric 进行自动化部署,使用 s3cmd 将工件上传到 Amazon S3。不幸的是,fabric 的最新版本需要 Python 2.7,而 s3cmd 需要使用 Python 2.6。修复程序需要我们切换到 s3cmd 的测试版本或者使用旧版的 fabric。

最后,对每个大型项目来说,它们主要面临的是构建时间问题。随着项目范围和复杂度增加,越来越多的语言添加了进来。同时,项目团队还需要为各种相互依赖的组件进行测试。例如,如果你有一个共享数据库,那么改变相同数据的测试是不能够同时执行的。此外,我们需要在测试执行之前设置预期状态,并能在完成后自行清理。如此一来,所有这一切可能需要几分钟到几个小时的时间进行构建,充分测试意味着会大大减慢开发速度,但如果跳过测试又有可能出现严重的问题。

解决方案和最佳实践

为了解决所有这些问题,就需要我们有一个支持下列需求的构建系统:

可重复性

我们必须能够在不同的开发机器和自动构建服务器上生成 / 创建出有同样依赖关系的、相似(或相同)的构建环境。

集中管理

我们必须能够控制所有开发人员的构建环境,并从中央代码仓库或服务器构建服务器。这包括了设置构建环境以及更新延时。

隔离

项目的各个子组件必须独立构建,而不是使用明确定义的共享依赖项。

并行化

我们必须能够为子组件提供并行化构建。

为了满足可重复性要求,我们必须使用集中式依赖管理。大多数现代语言和开发框架都支持自动依赖管理。Maven 广泛用于 Java 和其他几种语言,Python 使用 pip,Ruby 使用 Bundler。所有这些工具都有一个非常相似的样式,你可以 commit 索引文件(pom,xml, requirements.txt 或者 gemfile)到你的源码控制中。然后运行该工具,把依赖下载到构建机器上。我们可以在测试过它们后,集中管理索引文件,接着通过更新源码控制中的索引来进行更改。但是,管理环境依赖的问题依然存在,比如我们必须安装正确版本的 Maven、Python 和 Ruby。我们还需要确保这些工具由开发人员运行。Maven 能够自动检查依赖项更新,但对于 pip 和 Bundler,我们必须将构建命令包装在触发依赖项更新运行的脚本中。

对于设置依赖关系管理工具和脚本,大多数小型团队只使用文档,并把任务交给了开发人员。然而这种方法在大型团队中并不完全适用,特别是当依赖关系会随时间发生变化时。更复杂的是,根据构建机器的平台和操作系统不同,这些工具的安装命令都会发生变化。你可以使用编排工具(比如 Puppet 或 Chef)来管理依赖项的安装以及设置配置文件。Puppet 和 Cher 都允许在源代码控制中使用中央服务器或共享配置,来支持集中管理。这样一来,你就可以提前对配置更改进行测试,然后交给开发人员。但是,这些工具有一些缺点:首先,安装和配置 Puppet 或 Chef 会变得过于重要,而且它们的完整版本都不是免费的。另外,每一种工具都有自己的语言来定义任务,这就为 IT 团队和开发人员增加了另一项管理成本。还有一点是,编排工具不提供隔离,因此工具版本的冲突依旧是一个问题,而且执行并行化测试的问题也依然没有解决。

为了确保组件隔离并且缩短构建时间,我们可以使用自动虚拟化系统,比如 Vagrant。Vagrant 可以创建并运行虚拟机,这些虚拟机能够隔离各种组件的构建,而且能支持并行构建。当准备好集中管理时,Vagrant 配置文件可以提交到源码控制中,并且交给开发人员。另外。可以对虚拟机进行测试,将其部署到 Atlas,供所有开发人员下载。这样还是会有缺点,你需要进一步的配置来设置 Vagrant,而且在这个问题中,虚拟机是非常重要的解决方案。每个虚拟机运行一个完整的操作系统和网络堆栈,包含测试运行或者编译器。内存和磁盘资源需要提前分配给每一台虚拟机。

尽管存在一些警告和缺陷,但是使用依赖管理(Maven、pip、Bundler)、编排(Puppet、Chef)和虚拟化(Vagrant),我们可以构建一个稳定的、可测试的、集中管理的构建系统。并非所有的项目都需要有完整的工具堆栈;不过,任何长期运行的大型项目都需要这种层面的自动化。

利用 Docker 创建容器化的构建系统

Docker 出现之后,我们可以无需再花费过多时间和资源来支持上文我们提到的这些工具,Docker 及其工具生态系统就可以帮助我们满足上述的需求。在本节中,我们将通过下面的步骤为应用程序创建容器化构建环境。

持续集成与持续部署宝典Part 1:将构建环境容器化

  1. 将你的构建环境容器化
  2. 用 Docker 将你的应用程序打包起来
  3. 使用 Docker Compose 创建构建环境

我们使用一个叫做 go-messenger 的实例应用程序来说明如何在构建流水线中使用 Docker,后面章节也会用到它。你可以从 Github 中获取这个应用程序:

https://github.com/usmanismail/go-messenger/tree/golang-1.8

系统的主要数据流如下所示。该应用程序有两个组件:一个是用 Golang 便携的 RESTful 认证服务器,另一个是会话管理器,它接受来自客户端的长时运行 TCP 连接并在客户端之间路由消息。回到本文的目标,我们将重点介绍 RESTful 认证服务(go-auth)。这个子系统包含了一组无状态网络服务器以及一个数据库集群,用于存储用户信息。

持续集成与持续部署宝典Part 1:将构建环境容器化

将你的构建环境容器化

建立构建系统的第一步,是创建一个容器镜像,其中包含了构建项目所需的全部工具。我们镜像的 Docker 文件如下图所示。因为我们的应用程序是用 Go 语言编写的,所以使用的是官方的 golang 镜像,并且安装了 govendor 依赖管理工具。需要注意的是,如果你在自己的项目中使用的是 Java 语言,那么可以用 Java 基础镜像创建一个类似的“构建容器”,并安装 Maven 替代 govendor。

持续集成与持续部署宝典Part 1:将构建环境容器化

然后我们添加了一个编译脚本,将构建和测试我们代码的所有步骤集中到了一块。下面所示的脚本使用了 govendor restore 下载依赖项,通过 go fmt 命令标准化格式,用 go test 命令执行测试,接着使用了 go build 来编译项目。

持续集成与持续部署宝典Part 1:将构建环境容器化

为确保可重复性,我们可以使用 Docker 容器以及一切需要的工具,将组件构建成一个单一的、版本化的容器镜像。该镜像可从 Dockerhub 上下载,也可以使用 Dockerfile 构建(docker build -t go-builder:1.8)。到这为止,所有的开发人员(以及构建环境的机器)都可以通过下面的命令,来使用容器构建任何的 go 项目:

持续集成与持续部署宝典Part 1:将构建环境容器化

上面的命令中我们运行了 usman/go-builder 镜像的 1.8 版本,并使用 -v 将我们的源代码安装到了容器中,使用 -e 指定了 SOURCE_PATH 环境变量。如果想要在我们的示例项目中测试 go-builder,你可以使用下面的命令运行全部步骤,并在 go-auth 项目的根目录中创建一个名为 go-auth 的可执行文件。

持续集成与持续部署宝典Part 1:将构建环境容器化

将所有源从构建工具中隔离开来后,产生的一个有趣的副产物是,我们可以轻松地更换构建工具和配置。例如,在上面的命令中,我们使用了 golang 1.8。 把 go builder:1.8 改成 go builder:1.5,你就可以测试使用 golang 1.5 时对项目的影响。为了集中管理所有开发人员使用的镜像,我们可以将构建容器(builder container)的最新测试版本部署到一个固定版本(即 最新版本 ),并确保所有开发人员都使用了 go-builder:latest 构建源代码。同样地,如果我们项目中不同部分使用了不同版本的构建工具,我们可以使用不同的容器来构建他们,而无需担心在单个构建环境中管理多个语言版本的问题。例如,我们可以使用支持各种 python 版本的官方 python 镜像来减轻早期的 python 问题。

用 Docker 打包你的应用程序

如果你想将可执行文件打包到自己的容器中,那么需要先添加一个 dockerfile 文件,包含下面显示的内容,接着运行“docker build -t go-auth”。在 dockerfile 中,我们将最后一步的二进制输出添加到一个新容器中,并将 9000 端口公开给应用程序以便接受传入的连接。我们还指定了运行二进制文件的入口点,该入口点使用了给定的参数。由于 Go 的二进制文件是自包含(self-contained)的,因此我们使用了原版的 Ubuntu 镜像。不过如果你的项目需要运行时(run time)依赖项,那么也可以将它们打包到容器中。例如,如果你准备生成一个 war 文件,你可以使用 tomcat 容器。

持续集成与持续部署宝典Part 1:将构建环境容器化

使用 Docker Compose 创建构建环境

现在我们可以在集中管理的容器中重复构建项目了,该容器隔离了各种组件,我们还可以扩展构建管道来运行集成测试。这也充分展示了 Docker 在使用并行化时加速构建的能力。而测试不能并行化的一个主要原因是共享数据存储。对于集成测试来说尤为如此,因为我们通常不会去模拟外部数据库。我们的示例项目也有类似的问题,因为我们使用了 MySQL 数据库来存储用户。我们想编写一个测试,确保我们可以注册新用户。而第二次为同一用户进行注册时,我们期望会发生冲突错误。这让我们不得不对测试进行序列化,这样我们在测试完成后就可以清除注册用户,然后再开始新的测试。

要想设置隔离的、并行的构建,我们可以按如下的方式定义一个 Docker Compose 模板(docker-compose.yml)。我们定义了一个数据库服务,它使用 MySQL 官方镜像以及需要的环境变量。然后我们使用自己创建的容器,创建一个 GoAuth 服务来打包应用程序,并将其与数据库容器连接起来。需要注意的是,这里我们使用了 GO_AUTH_VERSION 变量替换。如果在环境中指定了该变量,那么 compose 将使用它作为 go-auth 镜像的标记,否则会使用默认值 latest 作为标记。

持续集成与持续部署宝典Part 1:将构建环境容器化

有了这个 docker-compose 模板,我们可以通过执行 docker-compose up 来运行应用程序环境。然后运行下面的 curl 命令来模拟我们的集成测试。第一次应该会返回 200 OK,而第二次应该返回 409 Conflict。如果你是在 Linux 上运行,则 service_ip 参数应该是 localhost,而如果你使用的是 OSX,那么参数应该是 Docker 虚拟机的 IP。想要查找 service_ip 你可以运行:

持续集成与持续部署宝典Part 1:将构建环境容器化

最后,在运行完测试之后,我们可以运行 docker-compose rm 来清理整个应用程序环境。

如果想要运行多个独立版本的应用程序,我们需要更新 docker-compose 模板来,将服务 database1 和 goauth1 以相同的配置添加到其对应项中。唯一的变化是在 Goauth1 中,我们需要将 9000:9000 端口条目改变为 9001:9000。这样应用程序公开的端口就不会发生冲突。完整的模板在这里。现在运行 docker-compose 时,可以并行运行两个集成测试。像这样的东西可以有效地用于为一个具有多个独立子组件的项目加速构建,例如,多模块的 Maven 项目。

持续集成与持续部署宝典Part 1:将构建环境容器化

总结

在本文中,我们开始了构建持续集成流水线的第一步工作——构建系统(Build System)的创建。我们分析了【Build】这一环节的常见的三大挑战—— 依赖管理、管理环境依赖、复杂项目的漫长构建时间,以及如何用传统工具与方法解决这些问题。 接着,我们分享了如何利用 Docker 创建容器化的构建系统以更轻松地解决那些传统挑战,包括如何将构建环境容器化、如何使用 Docker 打包应用程序、如何使用 Docker Compose 创建构建环境,最终创造一个可重复的、集中管理的、良好隔离的、并行化的构建系统。

在下一篇文章中,我们将分享 如何创建一个持续集成的流水线 ,内容将包含分支模式以及如何使用 Jenkins 创建 CI 流水线,将涉及到构建应用、打包应用、执行集成测试等技术细节内容。

评论

发布