两万余字的系列干货长文,攻下持续集成与持续部署!
本文为第一篇,开始持续集成流水线的第一步工作——构建系统(Build System)的创建!看如何利用Docker创建容器化的构建系统,解决常见的三大挑战——依赖管理、管理环境依赖、复杂项目的漫长构建时间,最终创造一个可重复的、集中管理的、良好隔离的、并行化的构建系统。
介 绍
随着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来实现持续集成。
扩展构建系统的挑战
在分享最佳实践之前,让我们看看在维护构建系统中常出现的一些挑战。
首先,在扩展项目时你会面临的第一个问题就是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及其工具生态系统就可以帮助我们满足上述的需求。在本节中,我们将通过下面的步骤为应用程序创建容器化构建环境。
- 将你的构建环境容器化
- 用Docker将你的应用程序打包起来
- 使用Docker Compose创建构建环境
我们使用一个叫做go-messenger的实例应用程序来说明如何在构建流水线中使用Docker,后面章节也会用到它。你可以从Github中获取这个应用程序:
https://github.com/usmanismai...。
系统的主要数据流如下所示。该应用程序有两个组件:一个是用Golang便携的RESTful认证服务器,另一个是会话管理器,它接受来自客户端的长时运行TCP连接并在客户端之间路由消息。回到本文的目标,我们将重点介绍RESTful认证服务(go-auth)。这个子系统包含了一组无状态网络服务器以及一个数据库集群,用于存储用户信息。
将你的构建环境容器化
建立构建系统的第一步,是创建一个容器镜像,其中包含了构建项目所需的全部工具。我们镜像的Docker文件如下图所示。因为我们的应用程序是用Go语言编写的,所以使用的是官方的golang镜像,并且安装了govendor依赖管理工具。需要注意的是,如果你在自己的项目中使用的是Java语言,那么可以用Java基础镜像创建一个类似的“构建容器”,并安装Maven替代govendor。
然后我们添加了一个编译脚本,将构建和测试我们代码的所有步骤集中到了一块。下面所示的脚本使用了govendor restore下载依赖项,通过go fmt命令标准化格式,用go test命令执行测试,接着使用了go build来编译项目。
为确保可重复性,我们可以使用Docker容器以及一切需要的工具,将组件构建成一个单一的、版本化的容器镜像。该镜像可从Dockerhub上下载,也可以使用Dockerfile构建(docker build -t go-builder:1.8)。到这为止,所有的开发人员(以及构建环境的机器)都可以通过下面的命令,来使用容器构建任何的go项目:
上面的命令中我们运行了usman/go-builder镜像的1.8版本,并使用-v将我们的源代码安装到了容器中,使用-e指定了SOURCE_PATH环境变量。如果想要在我们的示例项目中测试go-builder,你可以使用下面的命令运行全部步骤,并在go-auth项目的根目录中创建一个名为go-auth的可执行文件。
将所有源从构建工具中隔离开来后,产生的一个有趣的副产物是,我们可以轻松地更换构建工具和配置。例如,在上面的命令中,我们使用了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容器。
使用Docker Compose创建构建环境
现在我们可以在集中管理的容器中重复构建项目了,该容器隔离了各种组件,我们还可以扩展构建管道来运行集成测试。这也充分展示了Docker在使用并行化时加速构建的能力。而测试不能并行化的一个主要原因是共享数据存储。对于集成测试来说尤为如此,因为我们通常不会去模拟外部数据库。我们的示例项目也有类似的问题,因为我们使用了MySQL数据库来存储用户。我们想编写一个测试,确保我们可以注册新用户。而第二次为同一用户进行注册时,我们期望会发生冲突错误。这让我们不得不对测试进行序列化,这样我们在测试完成后就可以清除注册用户,然后再开始新的测试。
要想设置隔离的、并行的构建,我们可以按如下的方式定义一个Docker Compose模板(docker-compose.yml)。我们定义了一个数据库服务,它使用MySQL官方镜像以及需要的环境变量。然后我们使用自己创建的容器,创建一个GoAuth服务来打包应用程序,并将其与数据库容器连接起来。需要注意的是,这里我们使用了GO_AUTH_VERSION变量替换。如果在环境中指定了该变量,那么compose将使用它作为go-auth镜像的标记,否则会使用默认值latest作为标记。
有了这个docker-compose模板,我们可以通过执行docker-compose up来运行应用程序环境。然后运行下面的curl命令来模拟我们的集成测试。第一次应该会返回200 OK,而第二次应该返回409 Conflict。如果你是在Linux上运行,则service_ip参数应该是localhost,而如果你使用的是OSX,那么参数应该是Docker虚拟机的IP。想要查找service_ip你可以运行:
最后,在运行完测试之后,我们可以运行docker-compose rm来清理整个应用程序环境。
如果想要运行多个独立版本的应用程序,我们需要更新docker-compose模板来,将服务database1和goauth1以相同的配置添加到其对应项中。唯一的变化是在Goauth1中,我们需要将9000:9000端口条目改变为9001:9000。这样应用程序公开的端口就不会发生冲突。完整的模板在这里。现在运行docker-compose时,可以并行运行两个集成测试。像这样的东西可以有效地用于为一个具有多个独立子组件的项目加速构建,例如,多模块的Maven项目。
总 结
在本文中,我们开始了构建持续集成流水线的第一步工作——构建系统(Build System)的创建。我们分析了【Build】这一环节的常见的三大挑战——依赖管理、管理环境依赖、复杂项目的漫长构建时间,以及如何用传统工具与方法解决这些问题。接着,我们分享了如何利用Docker创建容器化的构建系统以更轻松地解决那些传统挑战,包括如何将构建环境容器化、如何使用Docker打包应用程序、如何使用Docker Compose创建构建环境,最终创造一个可重复的、集中管理的、良好隔离的、并行化的构建系统。
在下一篇文章中,我们将分享如何创建一个持续集成的流水线,内容将包含分支模式以及如何使用Jenkins创建CI流水线,将涉及到构建应用、打包应用、执行集成测试等技术细节内容。
本系列文章计划分为四篇,共两万多字,后续文章将陆续在Rancher微信公众号发布,记得保持关注~