对于大多数项目来说,采纳持续集成实践是向高效率和高质量迈进的一大步。它保证那些创建大型复杂系统的团队具有高度的自信心和控制力。一旦代码提交引入了问题,持续集成就能为我们提供快速的反馈,从而确保我们作为一个团队所开发的软件是可以正常工作的。它主要关注于代码是否可以编译成功以及是否可通过单元测试和验收测试。但持续集成并不足以满足我们的需要。
持续集成的主要关注对象是开发团队。持续集成系统的输出通常作为手工测试流程和后续发布流程的输入。在软件的发布过程中,很多浪费来自于测试和运维环节。例如,我们常常看到:
当然,我们能找到很多种能很快得到收益的方法,来渐进改善软件交付过程,比如教开发人员如何才能写出可以随时在生产环境上运行的软件,在类生产环境中进行持续集成,以及组建跨功能团队等。然而,尽管这种实践肯定会让情况得到改善,但它们无法帮助你洞悉哪里是交付流程的瓶颈,以及如何进行优化。
解决方案就是采取一种更完整的端到端的方法来交付软件。在前面几章中,我们已经解决了配置管理以及自动化大量构建、部署、测试和发布流程的很多问题。现在,我们通常能通过一键式方式把软件的某个版本部署好,甚至可以将其一键式部署到生产环境中,这样就建立了一个非常有效的反馈环——由于很容易将应用程序部署到测试环境中,所以团队可以同时得到软件功能和部署流程两个方面的快速反馈。因为部署流程(无论是在开发机器上部署,还是为最后发布而进行的部署)是自动化的,所以可以频繁且有规律地运行并被测试,从而降低发布风险,也降低了向开发团队传递有关部署流程的知识时的风险。
从精益的角度来看,我们实现了一个“拉式系统”(pull system),即测试团队只要自己单击按钮,就能将某个特定的软件版本部署到测试环境中。运维人员也可以通过单击一下按钮就把软件部署到试运行环境和生产环境中。在整个发布流程中,开发人员能看到每个目标环境上部署了哪个版本,发现了哪些问题。管理人员也很容易就能看到一些关键的度量指标,比如周期时间(cycle time),吞吐量(throughput)以及代码质量等。整个交付过程中的所有人都因此具有两种能力,即他能使用任何他想使用的东西,也能看到整个发布流程,从而可以改善反馈循环,识别、优化并解决瓶颈。这样就形成了一个更加快速且更加安全的交付流程。
实现端到端的自动化构建、部署、测试和发布流程会带来一些连锁反应,还会带来一些意料之外的收益。通过在很多项目里使用这种技术,我们找到了这些项目中各种部署流水线系统之间的共同点。而且通过这种抽象总结出来的一些通用模式在我们尝试过的项目中都获得了成功。这种抽象使我们在项目开始就能很快建立一个相当成熟且能快速运行的构建、测试和部署系统。在交付项目里,这种端到端的部署流水线系统使我们获得了一定程度的自由和灵活性,而这在几年前是根本无法想象的。我们确信,这种方法能让我们以更高的质量和相当低的成本与风险来创建、测试、部署复杂系统。
这正是部署流水线的功用。
从某种抽象层次上讲,部署流水线是指软件从版本控制库到用户手中这一过程的自动化表现形式。对软件的每次变更都会经历一个复杂流程才能发布。这一流程包括构建软件,以及后续一系列不同阶段的测试与部署,而这些活动通常都需要多人或者多个团队之间的协作。部署流水线是对这一流程的建模,在持续集成和发布管理工具上,它体现为支持查看并控制整个流程,包括每次变更从被提交到版本控制库开始,直到通过各类测试和部署,再到发布给用户的过程。
因此,这个由部署流水线建模而成的流程(从代码提交到软件发布的这个流程)实际上就是“将客户或用户脑中的一个想法变成其手中真实可用的特性”这一过程的一部分,而整个流程(从概念到概念兑现)可以用一个价值流图来描述。关于创建新产品的一个抽象价值流图如图5-1所示。
这个价值流图讲述了一个故事。整个过程一共需要大约三个半月的时间,其中真正的工作时间只有大约两个半月,其余时间都是从概念到概念兑现整个流程中各阶段之间的等待时间。例如,在开发完成首次要发布的版本与测试开始之间有五天的等待时间。这个时间有可能是将应用部署到某个类生产环境上所需的时间。顺便说一句,图中故意没有表明该产品是否为迭代开发。如果是迭代开发流程,开发阶段本身就会包含几个迭代,而每个迭代都包括测试和演示。而且,从发现到发布这个过程也会被重复很多次。
本书中,我们仅讨论从开发到发布的价值流,也就是图5-1中的阴影部分。这部分价值流的一个关键不同点在于会有很多次构建通过这一流程走向最后的发布。要理解部署流水线以及代码变更在其上流动的方法,是把它看成一个序列图,如图5-2所示。
请注意,流水线的输入是版本控制中的某个具体版本。每次变更都会生成一次构建,这个构建像神话中的英雄一样,闯过一系列的测试,希望成为一个能到达生成环境中的发布版本。在这一系列的测试阶段中,每个阶段都从不同的角度评估这个构建版本,且和持续集成一样,它的起点是向版本控制库的每一次提交。
随着某个构建逐步通过每个测试阶段,我们对它的信心也在不断提高。当然,我们在每个阶段上花在环境方面的资源也在不断增加,即越往后的阶段,其环境与生产环境越相似,其目的就是在这个过程中尽早发现那些不满足发布条件的构建版本,并尽快将失败根源反馈给团队。一般来说,只要某个构建使无论是这一流程中的哪个阶段失败了,它都不会进入下一个阶段。这在图5-3中有所反映。
使用这种模式的话,有些非常重要的积极影响。
为了达到这种令人羡慕的状态,我们必须把那些用于证明某些版本满足业务要求的测试集合进行自动化。而且,我们还要把测试环境、试运行环境和生产环境上的部署过程自动化,这样可以避免那些手工密集型的易出错的步骤。对于很多系统来说,可能还需要其他形式的测试或者阶段,但对所有项目有一些阶段是共同具有的。
部署流水线就是由上述这些阶段,以及为软件交付流程建模所需的其他阶段组成,有时候也称为持续集成流水线、构建流水线、部署生产线或现行构建(living build)。无论把它叫做什么,从根本上讲,它就是一个自动化的软件交付流程。这并不是说该发布过程不需要人的参与,而是说在执行过程中那些易出错且复杂的步骤被变成可靠且可重复的自动化步骤。事实上,人工参与的活动反而有增加的趋势,因为在开发流程中所有阶段均可进行一键式部署这一事实,会促使测试人员、分析人员、开发人员以及(最重要的)用户更频繁地执行它。
最基本的部署流水线
图5-4中显示了一个典型的部署流水线,体现了这种方法的本质。当然,一个真正的流水线应该反映真实的软件交付流程。
这个流程的起点是开发人员向版本控制库提交代码。此时,持续集成管理系统对这次提交作出响应,触发该流水线的一个实例。
Nexus
和Artifactory
这样的工具可帮助管理这类过程产物。在提交阶段,你也许还会执行另外一些任务,比如为验收测试准备测试数据库。时新的持续集成服务器都支持通过构建网格并行执行这些任务。最后,一定要记住,我们所做的这一切都是为了尽快得到反馈。为了加速这个反馈循环,就必须能够看到每个环境中都部署了哪个版本,每个构建版本在流水线中处于哪个阶段。图5-5(产品Go的截屏)展示了这个实践是什么样子的。
你可能已经注意到,每次提交都列在了页面的一侧,并显示出了每次提交分别走到了流水线中的哪个阶段,以及相应的每个阶段是否成功了。要能够将某次代码提交、构建版本与其在部署流水线上通过了哪些阶段关联在一起,这一点是非常必要的。因为这样你就能立刻发现是哪次代码提交造成了本次验收测试的失败。
接下来,我们将讨论部署流水线中每个阶段的细节。在开始之前,为了能够获得该方法带来的好处,你需要遵循一些实践。
方便起见,我们将所有可执行代码的集合称作二进制包,例如Jar文件、.NET 程序集和.so文件。有时候代码根本不需要编译,那么这种情况下,二进制包就是指所有源文件的集合。
很多构建系统将版本控制库中的源代码作为多个步骤中最权威的源,不同上下文中会重复编译这个源,比如在提交时、做验收测试时或做容量测试时。而且,在每个不同的环境上部署时都要重新编译一次。但是,对于同一份源代码,每次都重新编译的话,会引入“编译结果不一致”的风险。在后续阶段里,其编译器的版本可能与提交阶段所用版本不一致。对于第三方库,你可能会不小心使用了本未打算使用的版本。甚至编译器的配置都会对应用程序的行为产生影响。我们曾遇到过由于上述原因导致在生产环境中出现问题的情景。
一种相关的反模式就是一直使用源代码,而不是二进制包。
这种反模式违反了两个重要原则。
假如重新创建二进制包,就会存在这样的风险,即从第一次创建二进制包到最后发布这两个时间点之间会引入某种变化,比如在不同阶段里,编译时所用的软件工具链有差异,此时这个即将发布的二进制包就不是我们曾经测试过的那个二进制包了。出于审计的目的,确保从二进制包的创建到发布之间不会因失误或恶意攻击而引入任何变化是非常关键的。如果是解释性语言的话,有些组织甚至要求只有资深人员才有权在某个特定的环境里进行编译、组装或打包,其他人不得插手。所以一旦创建了二进制包,在需要时最好是重用,而不是重新创建它们。
为了确保构建和部署流程被有效测试,在各种环境中使用相同流程对软件进行部署是非常必要的,这些环境即包括开发人员或分析人员的工作站,也包括测试环境和生产环境。显然,部署风险与部署频率成反比。部署频率最低的环境(生产环境)却是最重要的。因此,只有在很多环境中对部署过程测试过数百次以后,我们才能消除那些由于部署脚本错误而导致的问题。
只要把那些与特定环境相关的特定配置分开放置就行了。一种方法是使用属性文件保存配置信息,比如分别为每个环境保存一个属性文件,并将其放在版本控制库中。在部署时,通过本地服务器的主机名来查找正确的配置,而如果是在有多台服务器的环境中,可以将环境变量提供给部署脚本使用。当然还有一些其他方法提供部署时的配置信息,比如将其放在一个目录服务中(LDAP或ActiveDirectory),也可以将其放在数据库中,通过像ESCAPE这样的工具来访问它。
在同一个源(一个版本控制库、一个目录服务,或一个数据库)中找到所有环境中运行的所有应用程序的配置信息是完全可行的。
如果你所在的公司里,管理生产环境的团队与负责开发和测试的团队不是同一个团队,那么这两个团队就要在一起工作,确保自动化部署过程在所有环境中都是有效的(包括开发环境在内)。能够使用相同的脚本向开发环境和生产环境部署,是避免“它在我的机器上可以工作”病症的法宝
如果对于不同的环境,其部署脚本也不相同的话,你就无法知道某个测试过的脚本是否在上线部署时还能正常工作。相反,如果使用同一个脚本在所有的环境上进行部署,那么当在某个环境上部署失败时,就可以确定其原因一定来自以下三个方面:
那么到底是哪个原因呢?这是接下来的两个实践需要解决的问题。
当做应用程序部署时,你应该用一个自动化脚本做一下冒烟测试,用来确保应用程序已经正常启动并运行了。这个测试应该非常简单,比如只要启动应用程序,检查一下,能看到主页面,并在主页面上能看到正确的内容就行了。这个冒烟测试还应该检查一下应用程序所依赖的服务是否都已经启动,并且正常运行了,比如数据库、消息总线或外部服务等。
一旦有了单元测试之后,这种冒烟测试(部署测试)可能就是你要马上着手做的最重要测试了,甚至可以说是最最重要的测试。因为它可以让你对“应用程序可以运行起来”建立信心。如果应用程序不能运行,这个冒烟测试应该能够告诉你一些最基本的诊断提示,比如应用程序无法运行是否是因为其依赖的外部服务无法正常工作。
很多团队实际部署应用上线时可能遇到的另一个主要问题是,生产环境与他们的开发环境或测试环境有非常大的差异。为了对系统上线充满信心,你要尽可能在与生产环境相似的环境中进行测试和持续集成。
理想情况下,如果生产环境非常简单,或者有足够多的预算,我们完全可以建立与生产环境一模一样的环境,用于运行手工测试或自动化测试。另外,要想确保所有的环境都一样,需要有很多纪律保障良好的配置管理实践。你要确保:
你可以使用像磁盘镜像或虚拟化技术这类实践,以及Puppet、InstallShield这类工具和某个版本控制系统共同管理环境配置。我们将在第11章详细讨论这个问题。
在持续集成出现之前,很多项目都有一个各阶段的执行时间表,比如每小时构建一次,每天晚上运行一次验收测试,每个周末运行一次容量测试。部署流水线则使用了不同的方式:每次提交都要触发第一个阶段的执行,后续阶段在第一个阶段成功结束后,立即被触发。当然,假如某些阶段需要花较长的时间,而开发人员(尤其是在大型团队中)的提交又非常频繁,就很难做到这一点了。图5-6中就显示了这样做的问题。
在本例中,某人将代码提交到版本控制库,生成了版本1,并且触发了流水线的第一个阶段(构建及单元测试)。这一阶段通过之后,紧接着触发了第二个阶段——自动化验收测试。此时另一个人提交了另一个修改,在版本库中生成了版本2,流水线中的第一个阶段(构建及单元测试)被再次触发。然而,即便这次构建也通过了,它仍无法触发下一个自动化验收测试,因为有一个自动化验收测试正在运行。与此同时,又有两个新的版本被提交。可是持续集成系统不能同时构建它们两个,如果遵循这个原则,而开发人员继续以同样的速率提交代码的话,构建就会越来越落后于开发人员的开发速度。
另一种构建策略是,一旦代码构建和单元测试结束,持续集成系统就去检查版本库中是否有新的提交。如果有的话,就将最近还没有构建过的所有变更全部拿来进行构建,即对版本4进行构建。假设这次构建和单元测试失败了,那么构建系统是无法知道究竟是哪个版本(版本3还是版本4)引起的,但开发人员自己可以很容易发现问题在哪儿。有些持续集成系统可以让你执行某个特定版本的构建,而无需按顺序执行。假如持续集成服务器有这种功能的话,开发人员就可以运行一次对版本3的构建和单元测试,看版本3是否能够通过测试,这样就可以弄清楚到底是哪个版本引入了问题。无论哪种方法,开发人员会提交版本5,来修复这个构建。这样,当验收测试结束以后,持续集成系统的调度程序发现版本5已经通过了第一阶段的测试,就会直接触发针对版本5的验收测试。
这种聪明的调度方法对于实现部署流水线来说是非常关键的。一定要确保持续集成服务器支持这种调度方式(事实上,很多持续集成服务器都支持这种调度方式),而且要确保每次变更都能立即在流水线中传递,这样就不用按固定的时间表来执行不同的阶段了。
目前,这些策略只能用于那些完全自动化的阶段,比如包含自动化测试的阶段,而流水线中后续的那些为手工测试环境执行部署的阶段就要按需激活
就像我们在3.2节中所说的,为了达到本书所描述的目标(迅速、可重复且可靠的发布),对于团队来说,最重要的是要接受这样的思想:每次提交代码到版本控制系统中后,都能够构建成功并通过所有的测试。对于整个部署流水线来说,都适用这一要求。假如在某个环境上的某次部署失败了,整个团队就要对这次失败负责,应该停下手头的工作,把它修复后再做其他事情。
每次提交都生成部署流水线的一个新实例。如果提交阶段的测试通过了,这个版本就被视为一个候选发布版本。部署流水线中第一个阶段的目标就是消除那些不适合生产环境的构建,并尽早给团队一个信号——“应用程序出错了”。我们不想在那些明显有问题的版本上花时间和精力,所以当开发人员提交变更到版本控制系统后,我们希望尽快地评估一下这个最新版本。提交者要一直等到构建结果,然后才能做下一项工作。
在提交阶段,我们需要做以下几件事。这些任务通常作为一个工作集合运行在构建网格上(大多数持续集成服务器都提供类似功能),这样,提交阶段就能够在一个可接受的时间之内完成(最好在五分钟之内完成,最多不能超过十分钟)。一般来说,提交阶段包含以下步骤:
测试非功能特性(比如容量)可能比较困难,但仍旧可以通过一些分析工具,收集一些关于当前代码库的测试覆盖率、可维护性以及安全漏洞方面的信息。为这些度量项设定一个阈值,并像对待测试一样,一旦不满足阈值条件,就让提交阶段失败。比较有用的度量项包括:
如果前面这些任务都成功了,提交阶段的最后一步就是生成二进制包,用于后续阶段的部署。当然,只有这步也成功了,提交阶段才能算成功。把生成可执行代码作为成功的验收条件,是确保构建流程本身也能够被持续集成系统不断评估和检查的简单方法。
提交阶段最佳实践
在第3章中所描述的实践大多数都适用于提交阶段。开发人员需要一直等到部署流水线的提交阶段成功完成。如果它失败了,开发人员要么快速修复问题,要么将刚提交的代码回滚。在理想情况下(无限的处理能力和无限的网络带宽),我们希望开发人员能够一直等到所有测试(甚至是手工测试)全部通过,这样一旦出现问题,就可以马上修复。然而,这并不现实,因为部署流水线的后续阶段(自动化验收测试、容量测试和手工验收测试)都需要相对较长的时间。这也是规范测试流程的一个理由,因为当缺陷还比较容易修复时,尽快得到反馈是非常重要的,而不应花更大的代价得到全面的反馈。
全面的提交测试套件对于发现许多种错误来说,是非常优秀的试金石。然而,有很多类型的错误是它无法捕获的。在提交测试集合中,大部分是单元测试,而单元测试与底层的API是紧耦合的,以至于开发人员难免落入一个陷阱,即“用某种特殊方式来证明解决方案是正确的”,而不是断言它解决了某个具体问题。
每次提交后就立即运行提交测试的意义在于,它能为最新的一次构建或程序中可能存在的一些较小的代码问题提供及时反馈。然而,如果没有在类生产环境上执行验收测试,我们就根本不知道该应用程序是否符合了客户规范,也不知道它在现实世界中是否能够部署并运行。如果想在这些方面得到及时反馈的话,就必须在持续集成流程中引入更多测试并不断对系统各个方面进行演练。
这个自动化验收测试关卡是识别候选发布版本过程中第二个重要的里程碑。部署流水线只允许后续阶段(比如需要手工干预的手工部署阶段)获取那些已通过自动化验收测试的构建版本。我们当然可以不遵循这样的机制,但可能会导致大量时间和精力的消耗。如果能把这些时间和精力花在修复那些已被部署流水线发现的问题上,花在以受控的可重复方式进行的部署工作上,不是更好吗?在部署流水线的帮助下,我们更容易做正确的事情。
因此,如果一个候选版本不能满足所有的验收条件,就根本不会被交给用户
自动化验收测试最佳实践
实际上,就像整个团队负责流水线的每一个阶段一样,整个团队都是验收测试的所有者。如果验收测试失败了,整个团队都要停下来,马上修复它。
这一实践的一个重要推论是,开发人员必须能在自己的开发环境中运行自动化验收测试。这样,开发人员在发现验收测试失败后,就很容易在自己的机器上修复它,然后在本地再次运行验收测试来验证修复。对于这个实践来说,最常遇到的障碍是没有足够多的测试软件授权,应用程序的架构不允许将其部署到开发环境中,以至于无法运行验收测试。如果你的自动化验收测试策略是为长远打算的话,就应该尽早清除这类障碍。
验收测试阶段是整个遴选候选发布版本过程中的一个重要里程碑。一旦这个阶段结束了,这个候选版本就会受到开发人员之外更多人的广泛关注。
对于最简单的部署流水线来说,至少就系统的自动化测试而言,一个构建版本通过了验收测试就能够发布给用户了。如果某版本在验收测试阶段失败了,根据定义,它是不能发布的。
在迭代开发过程中,验收测试之后一定会有一些手工的探索性测试、易用性测试和演示。在此之前,开发人员可能已经向分析师和测试人员演示了应用程序的功能,但一定是在自动化测试通过之后。在这个过程中,测试人员所扮演的角色并不是回归测试该系统,而是首先通过手工证明验收条件已被满足,从而确保这些验收测试的确是验证了系统行为。
之后,测试人员会做一些机器不太擅长而人比较擅长的测试。他们做探索性测试、易用性测试,在不同平台上测试程序的界面是否正确,并着眼于一些不可控制的最坏情况进行测试。自动化验收测试使测试人员节省出更多的时间做那些高价值的活动,而不是测试脚本的人力执行器。
每个系统都有很多非功能需求。比如,几乎每个系统都有容量和安全性方面的要求,或者必须遵守服务水平协议等。通常应该用某些自动化测试衡量应用程序是否满足这些需求。如何能够做到这一点呢?请参见第9章。对于某些系统,并不需要连续不断地做非功能需求测试。根据我们的经验,如果需要的话,完全可以在部署流水线中创建一个阶段,用于运行这些自动化的非功能测试。
在定义部署流水线结构时,必须回答一个问题,即容量测试阶段的结果是可以作为一个门槛,还是需要由人来决定?对于高性能应用来说,可以在验收测试阶段通过之后,就运行容量测试,作为该版本整个自动化测试的输出结果。如果这个版本不能通过容量测试,就不能把它看成是可部署的版本。
然而,对于很多应用程序来说,判定“什么是可接受的”更加具有主观性。通常根据实际容量测试阶段的结果,由人来判定该版本是否可以作为候选版本来部署会更有意义
每次向生产环境发布时都有业务风险。一旦在发布时发生严重问题,可能最好的结果就是推迟部署有价值的新功能,而最糟糕的结果就是出了问题却没有合适的撤销计划,这可能导致关键业务无法运行,因为新版本中已经替换了原有版本的关键功能。
缓解这类风险非常简单,只要把这个发布环节视为部署流水线的一个自然结果就行。实际上,我们只需要:
我们的目标是实现一个完全自动化的发布过程。发布就应该简单到这种程度,即只要选择一个需要发布的版本,单击一下按钮就万事大吉了。撤销也应该同样简单。
对生产环境的控制权越小,遇到意外情况的可能性就越大。因此,无论何时发布软件系统,我们都希望有完全的控制权。然而,这里至少有两方面的约束。
管理生产环境的流程也应该用于测试环境,比如试运行环境、集成环境等。通过这种方式,就可以利用自动化变更管理系统来为手工测试环境创建一个完全一致的配置信息。根据容量测试的结果对配置进行不断的评估和调整,就会得到一个非常完美的配置。当满意后,就能在这种可预测且可靠的方式下,把这份配置放在每个需要这种配置的服务器上,也包括生产环境上的服务器。环境的所有方面都应该以这种方式来管理,包括中间件(如数据库、Web服务器、消息代理和应用服务器等)。每个配置都能够被调整,并把可选设置加到配置基线中。
通过自动化的环境准备和管理、最佳的配置管理实践以及虚拟化技术(如果适用的话),环境准备和维护的成本会显著降低。一旦环境配置被正确地管理起来了,就可以部署应用程序了。尽管很多实现细节更多地依赖于系统所使用的技术,但步骤基本上是相似的。这种方法与我们用来创建构建脚本和部署脚本,以及监控流程的方法相似。创建构建脚本与部署脚本的方法将在第6章中加以讨论。
传统上,人们对新版本的发布常常存在着恐惧心理,原因有两个。一是害怕引入问题,因为手工的软件发布过程很可能引入难以发现的人为错误,或者部署手册本身就隐藏着某个错误。二是担心由于发布过程中的一个问题或新版本的某个缺陷,使你原来承诺的发布失败。无论是哪种情况,你的唯一希望就是足够聪明且非常迅速地解决这个问题。
我们可以通过每天练习发布多次来证明自动化部署系统是可以工作的,这样就可以缓解第一种问题。对于第二个问题,可以准备一个撤销策略。最糟的情况也就是回滚到发布之前的状态,这样你就有足够的时间评估刚发现的问题,并找到一个合理的解决方案。
对于很简单的应用程序来说,这是可以做到的(忽略数据和配置信息的迁移),只要把每个版本都放在一个单独的目标中,再使用符号链接指向当前版本就行了。最复杂的情况就是在部署和撤销中涉及生产数据的迁移。
**撤销流程绝不应该与部署流程、增量部署流程或回滚流程有什么不同。然而,这些流程可能很少被测试,所以也就不可靠。**而且,这些流程也很少基于某个已知良好的版本基线,所以也就比较脆弱。因此,一定要让旧版本保持同步运行一段时间,或者在必要时完全重新部署某个已知良好的旧版本。
当一个候选发布版本能够部署到生产环境时,我们就确信:
这种“在成功的基础上构建”的方法, 完全符合我们常挂在嘴边的口头禅“尽快让这个流程或其任何环节失败”,这在任何层次都是有用的。
无论是从零创建新项目,还是想为已有的系统创建一个自动化的流水线,通常都应该使用增量方法来实现部署流水线。接下来,我们将描述如何从无到有,建立一个完整流水线的策略。一般来说,步骤是这样的:
正如本章开始所描述的,第一步就是画出从提交到发布整个过程的价值流图。如果项目已经建好并开始运行,你在半个小时内就能画完。然后和参与其中的每个人聊一下,记录下流程中的每个步骤,包括对经历时间(elapsed time)和增值时间(value-added time)的最佳估计值。如果是还没有启动的新项目,就要先设计一个合适的价值流,可以在同一组织中找个与你的项目相似的项目,思考它的价值流,也可以从最简单的价值流开始,即第一个阶段是提交阶段,用来构建应用程序并运行基本的度量和单元测试,第二个阶段用来运行验收测试,第三个阶段用来向类生产环境部署应用,以便用它来做演示。
一旦有了价值流图,就可以用持续集成和发布管理工具对流程建模了。如果所用工具不支持直接对价值流建模的话,可以使用“项目间依赖”来模拟它。首先,这些项目应该什么也不做,而只是作为可以被依次触发的占位符。如果是使用“最简单模型”,每当有人提交代码到版本控制系统时,就应该触发提交阶段。当提交阶段通过以后,验收测试阶段就应该被自动触发,并使用提交阶段刚刚创建的二进制包。为手工
测试或发布应用而向类生产环境部署二进制包的阶段,都应该会要求你具有通过单击按钮来选择到底部署哪个版本的能力,而这种能力通常都需要授权。
接下来,就让这些占位符真正做些事情。假如项目已经全面展开,那么把已有的构建、测试和部署脚本放进去就可以了。如果还没有的话,就先创建一个“从头到尾的轮廓”,即用最少的工作量将所有的关键元素准备就绪。首先是提交阶段。如果还没有开始写代码和单元测试的话,就写一个最简单的“Hello world”示例(如果是 Web 应用,写个 HTML 页 面 就 行 ),再写个单元测试,而这个测试只是“assert(true)”。其次,完成部署,比如在IIS上建立一个虚拟目标,将你的网页放进去。最后,进行验收测试。注意,要在完成部署阶段后再做验收测试。因为只有部署应用后才能做验收测试。对于Web应用,验收测试可以使用WebDriver或Sahi来验证网页中是否包括文字“Hello world”。
对于一个新项目,上述内容都应在开发工作正式开始之前完成,如果是迭代开发的话,这是迭代0(iteration zero)中的工作内容。另外,系统管理员或运维人员也应该参与到建立演示用的类生产环境和开发部署脚本的活动中。在下面的几节中,我们会更详细地讲述如何创建简单的可工作框架,并随着项目的进行而不断开发。
实现部署流水线的第一步是将构建和部署流程自动化。构建过程的输入是源代码,输出结果是二进制包。“二进制包”是我们故意含糊使用的一个词,因为由于所用开发技术的不同,构建过程的输出也不相同。在这里,二进制包的关键特征是“你能将它复制到一台新机器上(上面没有IDE等开发工具集),只要环境配置正确,且又有应用在该环境中所需的正确配置信息,它就可以启动并运行了,而不必依赖于在这台机器上安装的开发工具链的任何部分。
每当有人提交后,持续集成服务器就应执行构建——使用3.2节所列出的某个工具。持续集成服务器应该监视版本控制系统,每当发现有新提交的代码时,就签出或更新源代码,运行自动化构建流程,并将生成的二进制包放在文件系统的某个地方,使整个团队都能通过持续集成服务器的用户界面获取。
一旦持续构建流程建立并运行起来了,接下来就要做自动化部署了。首先,要找到能够部署应用程序的机器。对于刚启动的新项目,用持续集成服务器所在的机器也行。如果项目已比较成熟,可能就需要找几台专用机器了。这些环境可以称作试运行环境或者用户验收测试(UAT)环境(这在各组织中的叫法不同)。无论怎样,这个环境应该与生产环境相似——如第10章所述,而且它的准备和维护工作都要用全部自动化的流程完成——如第11章所述。
部署自动化的几种常见方法在第6章有详细描述。部署活动可能包含:(1) 为应用程序打包,而如果应用程序的不同组件需要部署在不同的机器上,就要分别打包;(2) 安装和配置过程应该实现自动化;(3) 写自动化部署测试脚本来验证部署是否成功了。部署流程的可靠性是非常重要的,因为它是自动化验收测试的前提条件。
一旦将部署流程自动化后,接下来就要向UAT环境做一键式部署了。配置一下持续集成服务器,使你能自由挑选应用版本,并做到通过单击按钮来触发一个流程,即获取作为构建输出的二进制包,运行部署脚本,再运行部署测试。在开发构建和部署系统的过程中,一定要确保遵循前面说过的那些原则,如只生成一次二进制包,将配置信息与二进制包分离,以便在不同环境的部署中可以使用相同的二进制包。这能确保配置管理有一个健全的基础。除非软件需要用户自行安装,否则发布流程应该与向测试环境部署的流程相同。即使有不同之处,也只能是环境配置信息不同而已。
开发部署流水线的下一步就是实现全面的提交阶段,也就是运行单元测试、进行代码分析,并对每次提交都运行那些挑选出来的验收测试和集成测试。运行单元测试应该不需要太复杂的步骤,因为根据单元测试的定义,它并不需要运行整个应用程序,只需要运行在一个xUnit风格的单元测试框架上。
因为单元测试并不需要访问文件系统或数据库(与之对应的是组件测试),所以运行速度应该很快。这也是构建应用程序之后就直接运行单元测试的原因。与此同时,还可以运行一些静态分析工具,得到一些有用的分析数据,比如代码风格、代码覆盖率、圈复杂度、耦合度等。
随着应用软件不断变得复杂,你就需要写更多的单元测试和组件测试了。这些测试也应该出现在提交阶段。一旦提交阶段运行超过五分钟,就应把它们分成几份,以便并行执行。为了做到这一点,就需要多台测试机器(或者一台更强大的机器,它要有足够大的内存和更多的CPU),以及一个支持多任务并行管理的持续集成服务器。
流水线的验收测试阶段可以重用向测试环境部署的脚本。唯一的不同之处就是在冒烟测试之后,就要启动验收测试框架,并在结束之后,为进行分析收集所有的测试结果报告。另外,最好也保存一下应用程序的运行日志文件。如果应用程序有图形用户界面的话,也可以在验收测试运行时使用一个像Vnc2swf这样的软件来进行屏幕录像,这对于诊断问题比较有用。
验收测试可分为两种类型:功能测试和非功能测试。在项目初期就开始非功能需求测试(比如测试容量和可扩展性等)是非常关键的,这样你就能得到一些数据,用来分析当前的应用程序是否满足这些非功能需求。关于安装和部署,我们可以使用与功能验收测试同样的方法。但是,测试内容有所不同(关于如何创建这些测试请参见第9章)。刚开始时,你完全可以把验收测试和性能测试放在同一个阶段里接连运行。之后,为了能很容易知道哪类测试失败了,你可以再将它们分开。一套好的自动化验收测试会帮助你追查随机问题和难以重现的问题,如竞争条件、死锁,以及资源争夺。这些问题在应用发布之后,就很难再被发现。
当然,在部署流水线中,提交测试阶段和验收阶段需要运行哪些测试取决于你的测试策略(参见第4章)。在项目初期,应该至少有每种测试的一到两个测试可以自动化运行,并把它们放到部署流水线中。这样,初步框架就建好了,今后随着项目的进展,就比较容易增加测试了。
我们发现,每个价值流图和流水线中几乎都有上面描述的步骤。通常这些是自动化的第一个目标。随着项目越来越复杂,价值流图也会演进。另外,对于流水线来说,还有两个常见的外延:组件和分支。大型应用程序最好由多个组件拼装而成。在这样的项目中,每个组件都应该有一个对应的“迷你流水线”,然后再用一个流水线把所有组件拼装在一起,并运行整个验收测试集(包括自动化的非功能测试),然后再部署到测试环境、试运行环境和生产环境中。第13章会详细讨论这部分内容,而分支管理会在第14章讨论。
当实现了部署流水线后,你会发现与相关人士的谈话以及效率的提高反过来又会对你的流程有影响。所以,一定要记住三件事。
反馈是所有软件交付流程的核心。改善反馈的最佳方法是缩短反馈周期,并让结果可视化。你应该持续度量,并把度量结果以一种让人无法回避的方式传播出去,比如使用张贴在墙上的海报或者用一个专门的计算机显示器以大号粗体字显示结果,这些设备就是信息辐射器。
根据精益思想,应该做整体优化,而不是局部优化。如果你花很多时间去解决某个瓶颈,而这个瓶颈在整个交付流程中并不是一个真正约束的话,整个交付流程并不会有什么根本性的变化。因此,应该对整个流程进行度量,从而判定这个交付流程作为一个整体是否存在问题
对于软件交付过程来说,最重要的全局度量指标就是周期时间(cycle time)。它指的是从决定要做某个特性开始,直到把这个特性交付给用户的这段时间。正如Mary Poppendieck所问的那样:“你所在的组织中,如果仅仅修改一行代码,需要多长时间才能把它部署到生产环境中?你们是否以一种可重复且可靠的方式做这类事情?”这个指标很难度量,因为它涉及软件交付过程中的很多环节(从分析到开发,直至发布)。然而,这个指标比其他任何度量项都更能反映软件交付过程的真实情况。
一旦知道了应用程序的周期时间,就能找到最佳办法来缩短它。你可以利用约束理论,按照下面的流程来做优化。
尽管周期时间是软件交付中最重要的度量项,但还有一些其他度量项可以对问题起到警报作用。这些度量项如下所示。
如何呈献这些度量项是值得斟酌的。上面这些报告会产生很多数据,而如何解析这些数据就是一门艺术。比如程序经理可能想在一个项目健康报告中以非常简单的红黄绿交通信号灯方式看到已分析的聚合数据,而不是看到一页又一页的报告。相比之下,一个团队中资深的软件工程师会希望看到更详细的情况,但也不会乐意查看多页的报告。
每个团队的持续集成服务器在每次提交后都应该能够产生这样的报告和可视化效果,并将报告保存起来,以便今后对照某一数据库中的这些数据,对每个团队进行追踪分析。这些结果数据应该发布到一个内部网站上,用不同页面分别显示一个特定项目的数据信息。最后,把它们聚合在一起,这样就可以在整个开发过程,甚至整个组织的所有项目中追踪监控这些数据。
部署流水线的目的是,让软件交付过程中的每个人都能够看到每个构建版本从提交到发布的整个过程。大家应该能够看到哪次修改破坏了应用程序,哪次修改可以作为候选发布版本进入到手工测试环节或发布环节。它应该能够支持人们执行到手工测试环境的一键式部署,并使大家能了解当前每个环境中运行的应用程序究竟是哪个版本,还能够支持一键式发布选定的某个版本,并清楚地标识出这一候选发布版本已成功通过整个流水线,并在类生产环境中经历了一连串的自动化测试和手工测试。
一旦有了部署流水线,发布流程中的低效环节就会显而易见。所有需要的信息都可从这个部署流水线上获取,比如一个候选发布版本需要多长时间能够通过各种手工测试阶段,从提交到发布的平均时间是多长,流程中每个阶段发现了多少缺陷。一旦掌握了这些信息,就可以优化软件的构建和发布流程了。
对于实现部署流水线这个复杂问题来说,没有万能钥匙一样的解决方案。关键还是在于创建一个记录系统,用来管理从提交到发布的任何变更,为你提供在流程中尽早发现问题所需要的信息。部署流水线可以帮助消除流程中的低效环节,这样可让反馈周期更短并更有效。这样做的途径有多种,比如添加更多的自动化验收测试,并行执行它们,或让测试环境与生产环境更相似,或者实现更好的配置管理流程等。
当然,部署流水线也依赖于一些基础设施,包括良好的配置管理,自动化的软件构建脚本和部署脚本,还要有自动化验收测试来验证软件会向用户交付价值。它还需要纪律性,比如确保只有通过了自动化构建、测试和部署的那些修改才能发布。我们会在第15章讨论这些前提条件和必要的纪律,其中有一个成熟度模型,用来评估持续集成、测试、数据管理等。