Docker和Jenkins持续交付(全)绝不原创的飞龙

第一章《介绍持续交付》介绍了公司传统的软件交付方式,并解释了使用持续交付方法改进的理念。本章还讨论了引入该流程的先决条件,并介绍了本书将构建的系统。

第二章《介绍Docker》解释了容器化的概念和Docker工具的基础知识。本章还展示了如何使用Docker命令,将应用程序打包为Docker镜像,发布Docker容器的端口,并使用Docker卷。

第三章《配置Jenkins》介绍了如何安装、配置和扩展Jenkins。本章还展示了如何使用Docker简化Jenkins配置,并实现动态从节点供应。

第四章《持续集成管道》解释了流水线的概念,并介绍了Jenkinsfile语法。本章还展示了如何配置完整的持续集成管道。

第五章《自动验收测试》介绍了验收测试的概念和实施。本章还解释了工件存储库的含义,使用DockerCompose进行编排,以及编写面向BDD的验收测试的框架。

第六章,使用Ansible进行配置管理,介绍了配置管理的概念及其使用Ansible的实现。本章还展示了如何将Ansible与Docker和DockerCompose一起使用。

第七章,持续交付流水线,结合了前几章的所有知识,以构建完整的持续交付过程。本章还讨论了各种环境和非功能测试的方面。

第八章,使用DockerSwarm进行集群,解释了服务器集群的概念及其使用DockerSwarm的实现。本章还比较了替代的集群工具(Kubernetes和ApacheMesos),并解释了如何将集群用于动态Jenkins代理。

Docker需要64位Linux操作系统。本书中的所有示例都是使用Ubuntu16.04开发的,但任何其他具有3.10或更高内核版本的Linux系统都足够。

本书适用于希望改进其交付流程的开发人员和DevOps。无需先前知识即可理解本书。

在本书中,您将找到一些区分不同信息种类的文本样式。以下是一些这些样式的示例,以及它们的含义解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟URL、用户输入和Twitter句柄显示如下:dockerinfo

代码块设置如下:

FROMubuntu:16.04RUNapt-getupdate&&\apt-getinstall-ypython任何命令行输入或输出都以以下方式编写:

$dockerimagesREPOSITORYTAGIMAGEIDCREATEDSIZEubuntu_with_pythonlatestd6e85f39f5b7Aboutaminuteago202.6MBubuntu_with_git_and_jdklatest8464dc10abbb3minutesago610.9MB新术语和重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会在文本中显示为这样:"点击新项目"。

警告或重要说明以这样的框出现。

提示和技巧会显示在这样。

本章涵盖以下要点:

持续交付的最准确定义由JezHumble提出,如下所述:“持续交付是能够以可持续的方式将各种类型的变更,包括新功能、配置变更、错误修复和实验,安全快速地投入生产或交付给用户的能力。”该定义涵盖了关键点。

为了更好地展示要自动化的内容和方式,让我们从描述目前大多数软件系统使用的交付流程开始。

任何交付过程都始于客户定义的需求,并以在生产环境上发布结束。差异在于中间。传统上,它看起来如下发布周期图表所示:

传统交付过程在IT行业广泛使用,这可能不是你第一次读到这样的方法。尽管如此,它有许多缺点。让我们明确地看一下它们,以了解为什么我们需要努力追求更好的东西。

传统交付过程的最显著缺点包括以下内容:

为了能够持续交付,而不需要花费大量资金雇佣24/7工作的运维团队,我们需要自动化。简而言之,持续交付就是将传统交付流程的每个阶段转变为一系列脚本,称为自动化部署管道或持续交付管道。然后,如果不需要手动步骤,我们可以在每次代码更改后运行流程,因此持续向用户交付产品。

持续交付让我们摆脱了繁琐的发布周期,因此带来了以下好处:

不用说,我们可以通过消除所有交付阶段并直接在生产环境上进行开发来实现所有好处。然而,这会导致质量下降。实际上,引入持续交付的整个困难在于担心质量会随着消除手动步骤而下降。在本书中,我们将展示如何以安全的方式处理这个问题,并解释为什么与常见观念相反,持续交付的产品bug更少,更适应客户的需求。

雅虎很少部署,每次发布都带来了很多经过充分测试和准备的更改。Flickr以非常小的块工作,每个功能都被分成小的增量部分,并且每个部分都快速部署到生产环境。差异如下图所示:

故事中最重要的问题是-Flickr如何成为最可靠的系统?实际上,这个事实的原因已经在前面的部分提到过。如果一个发布是少量风险的话:

这就是为什么,即使发布本身是一项困难的活动,但频繁进行发布时要安全得多。

请记住,统计数据每天都在变得更好。然而,即使没有任何数字,想象一下每行代码您实现都安全地进入生产的世界。客户可以迅速做出反应并调整他们的需求,开发人员很高兴,因为他们不必解决那么多的错误,经理们很满意,因为他们总是知道当前的工作状态。毕竟,记住,唯一真正的进展度量是发布的软件。

我们已经知道持续交付流程是什么,以及为什么我们使用它。在这一部分,我们将描述如何实施它。

让我们首先强调传统交付流程中的每个阶段都很重要。否则,它根本不会被创建。没有人想在没有测试的情况下交付软件!UAT阶段的作用是检测错误,并确保开发人员创建的内容是客户想要的。运维团队也是如此——软件必须配置、部署到生产环境并进行监控。这是毋庸置疑的。那么,我们如何自动化这个过程,以便保留所有阶段?这就是自动化部署流水线的作用,它由以下图表中呈现的三个阶段组成:

自动化部署流水线是一系列脚本,每次提交到存储库的代码更改后都会执行。如果流程成功,最终会部署到生产环境。

每个步骤对应传统交付流程中的一个阶段,如下所示:

让我们深入了解每个阶段的责任和包括哪些步骤。

持续集成管道通常是起点。设置它很简单,因为一切都在开发团队内部完成,不需要与QA和运维团队达成协议。

自动验收测试阶段是与客户(和QA)一起编写的一套测试,旨在取代手动的UAT阶段。它作为一个质量门,决定产品是否准备发布。如果任何验收测试失败,那么管道执行将停止,不会运行进一步的步骤。它阻止了进入配置管理阶段,因此也阻止了发布。

自动化验收阶段的整个理念是将质量构建到产品中,而不是在后期进行验证。换句话说,当开发人员完成实现时,软件已经与验收测试一起交付,这些测试验证了软件是否符合客户的要求。这是对测试软件思维的一个重大转变。不再有一个人(或团队)批准发布,一切都取决于通过验收测试套件。这就是为什么创建这个阶段通常是持续交付过程中最困难的部分。它需要与客户的密切合作,并在过程的开始(而不是结束)创建测试。

在遗留系统的情况下,引入自动化验收测试尤其具有挑战性。我们在第九章高级持续交付中对这个主题进行了更详细的描述。

关于测试类型及其在持续交付过程中的位置通常存在很多混淆。也经常不清楚如何自动化每种类型,应该有多少覆盖范围,以及QA团队在整个开发过程中应该扮演什么角色。让我们使用敏捷测试矩阵和测试金字塔来澄清这一点。

这个分类回答了持续交付过程中最重要的问题之一:QA在过程中的角色是什么?

手动QA执行探索性测试,因此他们与系统一起玩耍,试图破坏它,提出问题,思考改进。自动化QA帮助进行非功能性和验收测试,例如,他们编写代码来支持负载测试。总的来说,QA在交付过程中并没有他们特别的位置,而是在开发团队中扮演着一个角色。

在自动化的持续交付过程中,不再有执行重复任务的手动QA的位置。

你可能会看到分类,想知道为什么你在那里看不到集成测试。BrianMarick在哪里,以及将它们放在持续交付管道的哪里?

为了解释清楚,我们首先需要提到,集成测试的含义取决于上下文。对于(微)服务架构,它们通常意味着与验收测试完全相同,因为服务很小,不需要除单元测试和验收测试之外的其他测试。如果构建了模块化应用程序,那么通过集成测试,我们通常指的是绑定多个模块(但不是整个应用程序)并一起测试它们的组件测试。在这种情况下,集成测试位于验收测试和单元测试之间。它们的编写方式与验收测试类似,但通常更加技术化,并且需要模拟不仅是外部服务,还有内部模块。集成测试与单元测试类似,代表了“代码”视角,而验收测试代表了“用户”视角。关于持续交付流水线,集成测试只是作为流程中的一个单独阶段实施。

前一节解释了过程中每种测试类型代表的含义,但没有提到我们应该开发多少测试。那么,在单元测试的情况下,代码覆盖率应该是多少呢?验收测试呢?

为了回答这些问题,迈克·科恩在他的书《敏捷成功:使用Scrum进行软件开发》中创建了所谓的测试金字塔。让我们看一下图表,以便更好地理解它。

在金字塔底部情况就不同了。单元测试便宜且快速,因此我们应该努力实现100%的代码覆盖率。它们由开发人员编写,并且为他们提供应该是任何成熟团队的标准程序。

我希望敏捷测试矩阵和测试金字塔澄清了验收测试的角色和重要性。

让我们转向持续交付流程的最后阶段,配置管理。

配置管理是解决手动在生产环境部署和配置应用程序带来的问题的解决方案。这种常见做法导致一个问题,即我们不再知道每个服务在哪里运行以及具有什么属性。配置管理工具(如Ansible、Chef或Puppet)能够在版本控制系统中存储配置文件,并跟踪在生产服务器上所做的每一次更改。

本书的其余部分致力于如何实施成功的持续交付流水线的技术细节。然而,该过程的成功不仅取决于本书中介绍的工具。在本节中,我们全面审视整个过程,并定义了三个领域的持续交付要求:

很久以前,当软件是由个人或微型团队编写时,开发、质量保证和运营之间没有明确的分离。一个人开发代码,测试它,然后将其投入生产。如果出了问题,同一个人调查问题,修复它,然后重新部署到生产环境。现在组织开发的方式逐渐改变,当系统变得更大,开发团队增长时。然后,工程师开始专门从事某个领域。这是完全有道理的,因为专业化会导致生产力的提升。然而,副作用是沟通开销。特别是如果开发人员、质量保证和运营在组织中处于不同的部门,坐在不同的建筑物中,或者外包到不同的国家。这种组织结构对持续交付流程不利。我们需要更好的东西,我们需要适应所谓的DevOps文化。

在某种意义上,DevOps文化意味着回归到根本。一个人或一个团队负责所有三个领域,如下图所示:

DevOps团队不一定只需要由开发人员组成。在许多正在转型的组织中,一个常见的情景是创建由四名开发人员、一个质量保证人员和一个运营人员组成的团队。然而,他们需要密切合作(坐在一起,一起开会,共同开发同一个产品)。

小型DevOps团队的文化影响软件架构。功能需求必须被很好地分离成(微)服务或模块,以便每个团队可以独立处理一个部分。

组织结构对软件架构的影响已经在1967年观察到,并被规定为康威定律:“任何设计系统(广义定义)的组织都将产生一个结构与组织沟通结构相同的设计。”

在持续交付采用过程中,客户(或产品负责人)的角色略有变化。传统上,客户参与定义需求,回答开发人员的问题,参加演示,并参与用户验收测试阶段,以确定构建的是否符合他们的意图。

在持续交付中,没有用户验收测试,客户在编写验收测试的过程中至关重要。对于一些已经以可测试的方式编写需求的客户来说,这并不是一个很大的转变。对于其他人来说,这意味着改变思维方式,使需求更加技术导向。

在敏捷环境中,一些团队甚至不接受没有验收测试的用户故事(需求)。即使这些技术可能听起来太严格,但通常会导致更好的开发生产力。

在本书的其余部分,我们将互换使用持续交付和持续部署这两个术语。

从技术方面来看,有一些要求需要牢记。我们将在整本书中讨论它们,所以在这里只是简单提一下而不详细讨论:

我们将在整本书中更多地讨论这些先决条件以及如何解决它们。记住这一点,让我们转到本章的最后一节,介绍我们计划在本书中构建的系统以及我们将用于此目的的工具。

我们介绍了持续交付过程的理念、好处和先决条件。在本节中,我们描述了将在整本书中使用的工具及其在完整系统中的位置。

如果你对持续交付过程的想法更感兴趣,那么可以看看杰兹·汉布尔和大卫·法利的一本优秀书籍,《持续交付:通过构建、测试和部署自动化实现可靠的软件发布》。

首先,具体的工具总是比理解其在流程中的作用更不重要。换句话说,任何工具都可以用另一个扮演相同角色的工具替换。例如,Jenkins可以用AtlassianBamboo替换,Chief可以用Ansible替换。这就是为什么每一章都以为什么需要这样的工具以及它在整个流程中的作用的一般描述开始。然后,具体的工具会与其替代品进行比较描述。这种形式给了你选择适合你环境的正确工具的灵活性。

另一种方法可能是在思想层面上描述持续交付过程;然而,我坚信用代码提取的确切示例,读者可以自行运行,会更好地理解这个概念。

有两种阅读本书的方式。第一种是阅读和理解持续交付流程的概念。第二种是创建自己的环境,并在阅读时执行所有脚本,以理解细节。

让我们快速看一下本书中将使用的工具。然而,在本节中,这只是对每种技术的简要介绍,随着本书的进行,会呈现更多细节。

Docker作为容器化运动的明确领导者,在近年来主导了软件行业。它允许将应用程序打包成与环境无关的镜像,因此将服务器视为资源的集群,而不是必须为每个应用程序配置的机器。Docker是本书的明确选择,因为它完全适合(微)服务世界和持续交付流程。

随着Docker一起出现的还有其他技术,如下所示:

Jenkins绝对是市场上最受欢迎的自动化服务器。它有助于创建持续集成和持续交付流水线,以及一般的任何其他自动化脚本序列。高度插件化,它有一个伟大的社区,不断通过新功能扩展它。更重要的是,它允许将流水线编写为代码,并支持分布式构建环境。

Ansible是一个自动化工具,可帮助进行软件供应、配置管理和应用部署。它的趋势比任何其他配置管理引擎都要快,很快就可以超过它的两个主要竞争对手:Chef和Puppet。它使用无代理架构,并与Docker无缝集成。

GitHub绝对是所有托管版本控制系统中的第一名。它提供了一个非常稳定的系统,一个出色的基于Web的用户界面,以及免费的公共存储库服务。话虽如此,任何源代码控制管理服务或工具都可以与持续交付一起使用,无论是在云端还是自托管,无论是基于Git、SVN、Mercurial还是其他任何工具。

多年来,Java一直是最受欢迎的编程语言。这就是为什么在本书中大多数代码示例都使用Java。与Java一起,大多数公司使用Spring框架进行开发,因此我们使用它来创建一个简单的Web服务,以解释一些概念。Gradle用作构建工具。它仍然比Maven不那么受欢迎,但发展速度更快。与往常一样,任何编程语言、框架或构建工具都可以替换,持续交付流程将保持不变,所以如果您的技术栈不同,也不用担心。

我们随意选择了Cucumber作为验收测试框架。其他类似的解决方案有Fitnesse和JBehave。对于数据库迁移,我们使用Flyway,但任何其他工具也可以,例如Liquibase。

您可以从两个角度看待本书的组织方式。

第一个角度是基于自动部署流水线的步骤。每一章都让您更接近完整的持续交付流程。如果您看一下章节的名称,其中一些甚至命名为流水线阶段的名称:

本书的内容还有第二个视角。每一章描述了环境的一个部分,这个环境又为持续交付流程做好了充分的准备。换句话说,本书逐步展示了如何逐步构建一个完整系统的技术。为了帮助您了解我们计划在整本书中构建的系统,现在让我们来看看每一章中系统将如何发展。

如果您目前不理解概念和术语,不用担心。我们将在相应的章节中从零开始解释一切。

在第二章中,介绍Docker,我们从系统的中心开始构建一个打包为Docker镜像的工作应用程序。本章的输出如下图所示:

一个docker化的应用程序(Web服务)作为一个容器在Docker主机上运行,并且可以像直接在主机上运行一样访问。这得益于端口转发(在Docker术语中称为端口发布)。

在第三章中,配置Jenkins,我们准备了Jenkins环境。多个代理(从)节点的支持使其能够处理大量并发负载。结果如下图所示:

Jenkins主节点接受构建请求,但执行是在一个Jenkins从节点(代理)机器上启动的。这种方法提供了Jenkins环境的水平扩展。

在第四章中,持续集成流水线,我们展示了如何创建持续交付流水线的第一阶段,即提交阶段。本章的输出是下图所示的系统:

该应用程序是使用SpringBoot框架编写的简单的JavaWeb服务。Gradle用作构建工具,GitHub用作源代码仓库。对GitHub的每次提交都会自动触发Jenkins构建,该构建使用Gradle编译Java代码,运行单元测试,并执行其他检查(代码覆盖率,静态代码分析等)。Jenkins构建完成后,会向开发人员发送通知。

在这一章之后,您将能够创建一个完整的持续集成流水线。

在第五章中,自动验收测试,我们最终合并了书名中的两种技术:Docker和Jenkins。结果如下图所示:

图中的附加元素与自动验收测试阶段有关:

在接下来的两章中,即第六章,使用Ansible进行配置管理和第七章,持续交付流水线,我们完成了持续交付流水线。输出是下图所示的环境:

Ansible负责环境,并使得同一应用程序可以部署到多台机器上。因此,我们将应用程序部署到暂存环境,运行验收测试套件,最后将应用程序发布到生产环境,通常是在多个实例上(在多个Docker主机上)。

在第八章中,使用DockerSwarm进行集群,我们用机器集群替换了每个环境中的单个主机。第九章,高级持续交付,此外还将数据库添加到了持续交付流程中。本书中创建的最终环境如下图所示:

暂存和生产环境配备有DockerSwarm集群,因此应用程序的多个实例在集群上运行。我们不再需要考虑我们的应用程序部署在哪台精确的机器上。我们只关心它们的实例数量。Jenkins从属也是在集群上运行。最后的改进是使用Flyway迁移自动管理数据库模式,这已经整合到交付流程中。

我希望你已经对我们在本书中计划构建的内容感到兴奋。我们将逐步进行,解释每一个细节和所有可能的选项,以帮助你理解程序和工具。阅读本书后,你将能够在你的项目中引入或改进持续交付流程。

在本章中,我们介绍了从想法开始的持续交付过程,讨论了先决条件,并介绍了本书其余部分使用的工具。本章的关键要点如下:

在下一章中,我们将介绍Docker,并介绍如何构建一个Docker化的应用程序。

我们将讨论现代持续交付过程应该如何看待,引入Docker,这种改变了IT行业和服务器使用方式的技术。

本章涵盖以下内容:

Docker是一个旨在通过软件容器帮助应用程序部署的开源项目。这句话来自官方Docker页面:

“Docker容器将软件包装在一个完整的文件系统中,其中包含运行所需的一切:代码、运行时、系统工具、系统库-任何可以安装在服务器上的东西。这保证软件无论在什么环境下都能始终运行相同。”

因此,Docker与虚拟化类似,允许将应用程序打包成可以在任何地方运行的镜像。

没有Docker,可以使用硬件虚拟化来实现隔离和其他好处,通常称为虚拟机。最流行的解决方案是VirtualBox、VMware和Parallels。虚拟机模拟计算机架构,并提供物理计算机的功能。如果每个应用程序都作为单独的虚拟机镜像交付和运行,我们可以实现应用程序的完全隔离。以下图展示了虚拟化的概念:

每个应用程序都作为一个单独的镜像启动,具有所有依赖项和一个客户操作系统。镜像由模拟物理计算机架构的hypervisor运行。这种部署方法得到许多工具(如Vagrant)的广泛支持,并专门用于开发和测试环境。然而,虚拟化有三个重大缺点:

容器化的概念提出了一个不同的解决方案:

每个应用程序都与其依赖项一起交付,但没有操作系统。应用程序直接与主机操作系统接口,因此没有额外的客户操作系统层。这导致更好的性能和没有资源浪费。此外,交付的Docker镜像大小明显更小。

请注意,在容器化的情况下,隔离发生在主机操作系统进程的级别。然而,这并不意味着容器共享它们的依赖关系。它们每个都有自己的正确版本的库,如果其中任何一个被更新,它对其他容器没有影响。为了实现这一点,Docker引擎为容器创建了一组Linux命名空间和控制组。这就是为什么Docker安全性基于Linux内核进程隔离。尽管这个解决方案已经足够成熟,但与虚拟机提供的完整操作系统级隔离相比,它可能被认为略微不够安全。

Docker容器化解决了传统软件交付中出现的许多问题。让我们仔细看看。

安装和运行软件是复杂的。您需要决定操作系统、资源、库、服务、权限、其他软件以及您的应用程序所依赖的一切。然后,您需要知道如何安装它。而且,可能会有一些冲突的依赖关系。那么你该怎么办?如果您的软件需要升级一个库,但其他软件不需要呢?在一些公司中,这些问题是通过拥有应用程序类别来解决的,每个类别由专用服务器提供服务,例如,一个用于具有Java7的Web服务的服务器,另一个用于具有Java8的批处理作业,依此类推。然而,这种解决方案在资源方面不够平衡,并且需要一支IT运维团队来照顾所有的生产和测试服务器。

环境复杂性的另一个问题是,通常需要专家来运行应用程序。一个不太懂技术的人可能会很难设置MySQL、ODBC或任何其他稍微复杂的工具。对于不作为特定操作系统二进制文件交付但需要源代码编译或任何其他特定环境配置的应用程序来说,这一点尤为真实。

保持工作区整洁。一个应用程序可能会改变另一个应用程序的行为。想象一下会发生什么。应用程序共享一个文件系统,因此如果应用程序A将某些内容写入错误的目录,应用程序B将读取不正确的数据。它们共享资源,因此如果应用程序A存在内存泄漏,它不仅会冻结自身,还会冻结应用程序B。它们共享网络接口,因此如果应用程序A和B都使用端口8080,其中一个将崩溃。隔离也涉及安全方面。运行有错误的应用程序或恶意软件可能会对其他应用程序造成损害。这就是为什么将每个应用程序保持在单独的沙盒中是一种更安全的方法,它限制了损害范围仅限于应用程序本身。

服务器通常会因为有大量运行的应用程序而变得混乱,而没有人知道这些应用程序是什么。你将如何检查服务器上运行的应用程序以及它们各自使用的依赖关系?它们可能依赖于库、其他应用程序或工具。如果没有详尽的文档,我们所能做的就是查看运行的进程并开始猜测。Docker通过将每个应用程序作为一个单独的容器来保持组织,这些容器可以列出、搜索和监视。

Docker将可移植性的概念提升了一个层次;如果Docker版本兼容,那么所提供的软件将在编程语言、操作系统或环境配置方面都能正确运行。因此,Docker可以用“不仅仅是代码,而是整个环境”来表达。

传统软件部署和基于Docker的部署之间的区别通常用小猫和牛的类比来表达。每个人都喜欢小猫。小猫是独一无二的。每只小猫都有自己的名字,需要特殊对待。小猫是用情感对待的。它们死了我们会哭。相反,牛只存在来满足我们的需求。即使牛的形式是单数,因为它只是一群一起对待的动物。没有命名,没有独特性。当然,它们是独一无二的(就像每个服务器都是独一无二的),但这是无关紧要的。

这就是为什么对Docker背后的理念最直接的解释是把你的服务器当作牛,而不是宠物。

Docker并不是市场上唯一的容器化系统。实际上,Docker的最初版本是基于开源的LXC(LinuxContainers)系统的,这是一个容器的替代平台。其他已知的解决方案包括FreeBSDJails、OpenVZ和SolarisContainers。然而,Docker因其简单性、良好的营销和创业方法而超越了所有其他系统。它适用于大多数操作系统,允许您在不到15分钟内做一些有用的事情,具有许多易于使用的功能,良好的教程,一个伟大的社区,可能是IT行业中最好的标志。

Docker的安装过程快速简单。目前,它在大多数Linux操作系统上得到支持,并提供了专门的二进制文件。Mac和Windows也有很好的本地应用支持。然而,重要的是要理解,Docker在内部基于Linux内核及其特定性,这就是为什么在Mac和Windows的情况下,它使用虚拟机(Mac的xhyve和Windows的Hyper-V)来运行Docker引擎环境。

Docker的要求针对每个操作系统都是特定的。

Mac:

Windows:

Linux:

如果您的机器不符合要求,那么解决方案是使用安装了Ubuntu操作系统的VirtualBox。尽管这种解决方法听起来有些复杂,但并不一定是最糟糕的方法,特别是考虑到在Mac和Windows的情况下Docker引擎环境本身就是虚拟化的。此外,Ubuntu是使用Docker的最受支持的系统之一。

本书中的所有示例都在Ubuntu16.04操作系统上进行了测试。

Dockers的安装过程非常简单,并且在其官方页面上有很好的描述。

在Ubuntu16.04的情况下,我执行了以下命令:

我们可以通过将他们添加到docker组来使其他用户使用Docker:

$sudousermod-aGdocker成功注销后,一切都设置好了。然而,通过最新的命令,我们需要采取一些预防措施,以免将Docker权限赋予不需要的用户,从而在Docker引擎中创建漏洞。这在服务器机器上安装时尤为重要。

DockerMachine工具有助于在Mac、Windows、公司网络、数据中心以及AWS或DigitalOcean等云提供商上安装和管理Docker引擎。

无论您选择了哪种安装方式(Mac、Windows、Ubuntu、Linux或其他),Docker都应该已经设置好并准备就绪。测试的最佳方法是运行dockerinfo命令。输出消息应该类似于以下内容:

$dockerinfoContainers:0Running:0Paused:0Stopped:0Images:0...在服务器上安装为了在网络上使用Docker,可以利用云平台提供商或在专用服务器上手动安装Docker。

在第一种情况下,Docker配置因平台而异,但在专门的教程中都有很好的描述。大多数云平台都可以通过用户友好的网络界面创建Docker主机,或者描述在其服务器上执行的确切命令。

在服务器上手动安装Docker与本地安装并没有太大区别。

还需要两个额外的步骤,包括设置Docker守护程序以侦听网络套接字和设置安全证书。

在Ubuntu的情况下,Docker守护程序由systemd配置,因此为了更改它的启动配置,我们需要修改/lib/systemd/system/docker.service文件中的一行:

Docker环境已经设置好,所以我们可以开始第一个示例。

在控制台中输入以下命令:

$dockerrunhello-worldUnabletofindimage'hello-world:latest'locallylatest:Pullingfromlibrary/hello-world78445dd45222:PullcompleteDigest:sha256:c5515758d4c5e1e838e9cd307f6c6a0d620b5e07e6f927b07d05f6d12a1ac8d7Status:Downloadednewerimageforhello-world:latestHellofromDocker!Thismessageshowsthatyourinstallationappearstobeworkingcorrectly....恭喜,您刚刚运行了您的第一个Docker容器。我希望您已经感受到Docker是多么简单。让我们逐步检查发生了什么:

预期的流程可以用以下图表表示:

让我们看一下在本节中所示的每个Docker组件。

官方Docker页面上说:

“DockerEngine是一个创建和管理Docker对象(如镜像和容器)的客户端-服务器应用程序。”

让我们搞清楚这意味着什么。

让我们看一下展示DockerEngine架构的图表:

DockerEngine由三个组件组成:

安装DockerEngine意味着安装所有组件,以便Docker守护程序作为服务在我们的计算机上运行。在hello-world示例中,我们使用Docker客户端与Docker守护程序交互;但是,我们也可以使用RESTAPI来做完全相同的事情。同样,在hello-world示例中,我们连接到本地Docker守护程序;但是,我们也可以使用相同的客户端与远程机器上运行的Docker守护程序交互。

要在远程机器上运行Docker容器,可以使用-H选项:docker-H:2375runhello-world

在Docker世界中,镜像是一个无状态的构建块。您可以将镜像想象为运行应用程序所需的所有文件的集合,以及运行它的方法。镜像是无状态的,因此可以通过网络发送它,将其存储在注册表中,命名它,对其进行版本控制,并将其保存为文件。镜像是分层的,这意味着可以在另一个镜像的基础上构建镜像。

容器是镜像的运行实例。如果我们想要多个相同应用的实例,我们可以从同一个镜像创建多个容器。由于容器是有状态的,我们可以与它们交互并更改它们的状态。

让我们来看一个容器和镜像层结构的例子:

底部始终是基础镜像。在大多数情况下,它代表一个操作系统,我们在现有的基础镜像上构建我们的镜像。从技术上讲,可以创建自己的基础镜像,但这很少需要。

在我们的例子中,ubuntu基础镜像提供了Ubuntu操作系统的所有功能。addgit镜像添加了Git工具包。然后,有一个添加了JDK环境的镜像。最后,在顶部,有一个从addJDK镜像创建的容器。这样的容器能够从GitHub仓库下载Java项目并将其编译为JAR文件。因此,我们可以使用这个容器来编译和运行Java项目,而无需在我们的操作系统上安装任何工具。

重要的是要注意,分层是一种非常聪明的机制,可以节省带宽和存储空间。想象一下,我们的应用程序也是基于ubuntu:

这次我们将使用Python解释器。在安装addpython镜像时,Docker守护程序将注意到ubuntu镜像已经安装,并且它需要做的只是添加一个非常小的python层。因此,ubuntu镜像是一个被重复使用的依赖项。如果我们想要在网络中部署我们的镜像,情况也是一样的。当我们部署Git和JDK应用程序时,我们需要发送整个ubuntu镜像。然而,随后部署python应用程序时,我们只需要发送一个小的addpython层。

许多应用程序以Docker镜像的形式提供,可以从互联网上下载。如果我们知道镜像名称,那么只需以与helloworld示例相同的方式运行它就足够了。我们如何在DockerHub上找到所需的应用程序镜像呢?

让我们以MongoDB为例。如果我们想在DockerHub上找到它,我们有两个选项:

在第二种情况下,我们可以执行以下操作:

$dockersearchmongoNAMEDESCRIPTIONSTARSOFFICIALAUTOMATEDmongoMongoDBdocumentdatabasesprovidehighav...2821[OK]mongo-expressWeb-basedMongoDBadmininterface,written...106[OK]mvertes/alpine-mongolightMongoDBcontainer39[OK]mongoclient/mongoclientOfficialdockerimageforMongoclient,fea...19[OK]...有很多有趣的选项。我们如何选择最佳镜像?通常,最吸引人的是没有任何前缀的镜像,因为这意味着它是一个官方的DockerHub镜像,因此应该是稳定和维护的。带有前缀的镜像是非官方的,通常作为开源项目进行维护。在我们的情况下,最好的选择似乎是mongo,因此为了运行MongoDB服务器,我们可以运行以下命令:

$dockerrunmongoUnabletofindimage'mongo:latest'locallylatest:Pullingfromlibrary/mongo5040bd298390:Pullcompleteef697e8d464e:Pullcomplete67d7bf010c40:Pullcompletebb0b4f23ca2d:Pullcomplete8efff42d23e5:Pullcomplete11dec5aa0089:Pullcompletee76feb0ad656:Pullcomplete5e1dcc6263a9:Pullcomplete2855a823db09:PullcompleteDigest:sha256:aff0c497cff4f116583b99b21775a8844a17bcf5c69f7f3f6028013bf0d6c00cStatus:Downloadednewerimageformongo:latest2017-01-28T14:33:59.383+0000ICONTROL[initandlisten]MongoDBstarting:pid=1port=27017dbpath=/data/db64-bithost=0f05d9df0dc2...就这样,MongoDB已经启动了。作为Docker容器运行应用程序是如此简单,因为我们不需要考虑任何依赖项;它们都与镜像一起提供。

在DockerHub服务上,你可以找到很多应用程序;它们存储了超过100,000个不同的镜像。

Docker可以被视为一个有用的工具来运行应用程序;然而,真正的力量在于构建自己的Docker镜像,将程序与环境一起打包。在本节中,我们将看到如何使用两种不同的方法来做到这一点,即Dockercommit命令和Dockerfile自动构建。

让我们从一个例子开始,使用Git和JDK工具包准备一个镜像。我们将使用Ubuntu16.04作为基础镜像。无需创建它;大多数基础镜像都可以在DockerHub注册表中找到:

$dockerrun-i-tubuntu:16.04/bin/bash我们拉取了ubuntu:16.04镜像,并将其作为容器运行,然后以交互方式(-i标志)调用了/bin/bash命令。您应该看到容器的终端。由于容器是有状态的和可写的,我们可以在其终端中做任何我们想做的事情。

root@dee2cb192c6c:/#apt-getupdateroot@dee2cb192c6c:/#apt-getinstall-ygitroot@dee2cb192c6c:/#whichgit/usr/bin/gitroot@dee2cb192c6c:/#exit$dockerdiffdee2cb192c6c该命令应打印出容器中所有更改的文件列表。

$dockercommitdee2cb192c6cubuntu_with_git我们刚刚创建了我们的第一个Docker镜像。让我们列出Docker主机上的所有镜像,看看镜像是否存在:

$dockerimagesREPOSITORYTAGIMAGEIDCREATEDSIZEubuntu_with_gitlatestf3d674114fe2Aboutaminuteago259.7MBubuntu16.04f49eec89601e7daysago129.5MBmongolatest0dffc7177b0610daysago402MBhello-worldlatest48b5124b27682weeksago1.84kB如预期的那样,我们看到了hello-world,mongo(之前安装的),ubuntu(从DockerHub拉取的基础镜像)和新构建的ubuntu_with_git。顺便说一句,我们可以观察到每个镜像的大小,它对应于我们在镜像上安装的内容。

现在,如果我们从镜像创建一个容器,它将安装Git工具:

$dockerrun-i-tubuntu_with_git/bin/bashroot@3b0d1ff457d4:/#whichgit/usr/bin/gitroot@3b0d1ff457d4:/#exit使用完全相同的方法,我们可以在ubuntu_with_git镜像的基础上构建ubuntu_with_git_and_jdk:

$dockerrun-i-tubuntu_with_git/bin/bashroot@6ee6401ed8b8:/#apt-getinstall-yopenjdk-8-jdkroot@6ee6401ed8b8:/#exit$dockercommit6ee6401ed8b8ubuntu_with_git_and_jdkDockerfile手动创建每个Docker镜像并使用commit命令可能会很费力,特别是在构建自动化和持续交付过程中。幸运的是,有一种内置语言可以指定构建Docker镜像时应执行的所有指令。

让我们从一个类似于Git和JDK的例子开始。这次,我们将准备ubuntu_with_python镜像。

FROMubuntu:16.04RUNapt-getupdate&&\apt-getinstall-ypython$dockerbuild-tubuntu_with_python.$dockerimagesREPOSITORYTAGIMAGEIDCREATEDSIZEubuntu_with_pythonlatestd6e85f39f5b7Aboutaminuteago202.6MBubuntu_with_git_and_jdklatest8464dc10abbb3minutesago610.9MBubuntu_with_gitlatestf3d674114fe29minutesago259.7MBubuntu16.04f49eec89601e7daysago129.5MBmongolatest0dffc7177b0610daysago402MBhello-worldlatest48b5124b27682weeksago1.84kB现在我们可以从镜像创建一个容器,并检查Python解释器是否存在,方式与执行dockercommit命令后的方式完全相同。请注意,即使ubuntu镜像是ubuntu_with_git和ubuntu_with_python的基础镜像,它也只列出一次。

在这个例子中,我们使用了前两个Dockerfile指令:

我们已经拥有构建完全可工作的应用程序作为Docker镜像所需的所有信息。例如,我们将逐步准备一个简单的Pythonhelloworld程序。无论我们使用什么环境或编程语言,这些步骤都是相同的。

创建一个新目录,在这个目录中,创建一个名为hello.py的文件,内容如下:

print"HelloWorldfromPython!"关闭文件。这是我们应用程序的源代码。

我们的环境将在Dockerfile中表示。我们需要定义以下指令:

在同一目录中,创建Dockerfile:

FROMubuntu:16.04MAINTAINERRafalLeszkoRUNapt-getupdate&&\apt-getinstall-ypythonCOPYhello.py.ENTRYPOINT["python","hello.py"]构建镜像现在,我们可以以与之前完全相同的方式构建镜像:

$dockerbuild-thello_world_python.运行应用程序我们通过运行容器来运行应用程序:

$dockerrunhello_world_python您应该看到友好的HelloWorldfromPython!消息。这个例子中最有趣的是,我们能够在没有在主机系统中安装Python解释器的情况下运行Python编写的应用程序。这是因为作为镜像打包的应用程序在内部具有所需的所有环境。

Python解释器的镜像已经存在于DockerHub服务中,因此在实际情况下,使用它就足够了。

我们已经运行了我们的第一个自制Docker应用程序。但是,如果应用程序的执行应该取决于一些条件呢?

例如,在生产服务器的情况下,我们希望将Hello打印到日志中,而不是控制台,或者我们可能希望在测试阶段和生产阶段有不同的依赖服务。一个解决方案是为每种情况准备一个单独的Dockerfile;然而,还有一个更好的方法,即环境变量。

让我们将我们的helloworld应用程序更改为打印HelloWorldfrom!。为了做到这一点,我们需要按照以下步骤进行:

importosprint"HelloWorldfrom%s!"%os.environ['NAME']$dockerbuild-thello_world_python_name.$dockerrun-eNAME=Rafalhello_world_python_nameHelloWorldfromRafal!ENVNAMERafal$dockerbuild-thello_world_python_name_default.$dockerrunhello_world_python_name_defaultHelloWorldfromRafal!当我们需要根据其用途拥有Docker容器的不同版本时,例如,为生产和测试服务器拥有单独的配置文件时,环境变量尤其有用。

如果环境变量在Dockerfile和标志中都有定义,那么命令标志优先。

到目前为止,我们运行的每个应用程序都应该做一些工作然后停止。例如,我们打印了HellofromDocker!然后退出。但是,有些应用程序应该持续运行,比如服务。要在后台运行容器,我们可以使用-d(--detach)选项。让我们尝试一下ubuntu镜像:

$dockerrun-d-tubuntu:16.04这个命令启动了Ubuntu容器,但没有将控制台附加到它上面。我们可以使用以下命令看到它正在运行:

$dockerpsCONTAINERIDIMAGECOMMANDSTATUSPORTSNAMES95f29bfbaadcubuntu:16.04"/bin/bash"Up5secondskickass_stonebraker这个命令打印出所有处于运行状态的容器。那么我们的旧容器呢,已经退出了?我们可以通过打印所有容器来找到它们:

$dockerps-aCONTAINERIDIMAGECOMMANDSTATUSPORTSNAMES95f29bfbaadcubuntu:16.04"/bin/bash"Up33secondskickass_stonebraker34080d914613hello_world_python_name_default"pythonhello.py"Exitedlonely_newton7ba49e8ee677hello_world_python_name"pythonhello.py"Exitedmad_turingdd5eb1ed81c3hello_world_python"pythonhello.py"Exitedthirsty_bardeen6ee6401ed8b8ubuntu_with_git"/bin/bash"Exitedgrave_nobel3b0d1ff457d4ubuntu_with_git"/bin/bash"Exiteddesperate_williamsdee2cb192c6cubuntu:16.04"/bin/bash"Exitedsmall_dubinsky0f05d9df0dc2mongo"/entrypoint.shmongo"Exitedtrusting_easley47ba1c0ba90ehello-world"/hello"Exitedtender_bell请注意,所有旧容器都处于退出状态。我们还没有观察到的状态有两种:暂停和重新启动。

所有状态及其之间的转换都在以下图表中显示:

暂停Docker容器非常罕见,从技术上讲,它是通过使用SIGSTOP信号冻结进程来实现的。重新启动是一个临时状态,当容器使用--restart选项运行以定义重新启动策略时(Docker守护程序能够在发生故障时自动重新启动容器)。

该图表还显示了用于将Docker容器状态从一个状态更改为另一个状态的Docker命令。

例如,我们可以停止正在运行的Ubuntu容器:

$dockerstop95f29bfbaadc$dockerpsCONTAINERIDIMAGECOMMANDCREATEDSTATUSPORTSNAMES我们一直使用dockerrun命令来创建和启动容器;但是,也可以只创建容器而不启动它。

如今,大多数应用程序不是独立运行的,而是需要通过网络与其他系统进行通信。如果我们想在Docker容器内运行网站、网络服务、数据库或缓存服务器,那么我们需要至少了解Docker网络的基础知识。

让我们从一个简单的例子开始,直接从DockerHub运行Tomcat服务器:

然而,在我们的情况下,Tomcat是在Docker容器内运行的。我们以与第一个HelloWorld示例相同的方式启动了它。我们可以看到它正在运行:

$dockerpsCONTAINERIDIMAGECOMMANDSTATUSPORTSNAMESd51ad8634factomcat"catalina.shrun"UpAboutaminute8080/tcpjovial_kare由于它是作为守护进程运行的(使用-d选项),我们无法立即在控制台中看到日志。然而,我们可以通过执行以下代码来访问它:

我们需要启动容器并指定端口映射,使用-p(--publish)标志:

-p,--publish:因此,让我们首先停止正在运行的容器并启动一个新的容器:

在大多数常见的Docker使用情况下,这样简单的端口映射命令就足够了。我们能够将(微)服务部署为Docker容器,并公开它们的端口以启用通信。然而,让我们深入了解一下发生在幕后的情况。

Docker允许使用-p::将指定的主机网络接口发布出去。

我们已经连接到容器内运行的应用程序。事实上,这种连接是双向的,因为如果你还记得我们之前的例子,我们是从内部执行apt-getinstall命令,并且包是从互联网下载的。这是如何可能的呢?

如果您检查您的机器上的网络接口,您会看到其中一个接口被称为docker0:

$ifconfigdocker0docker0Linkencap:EthernetHWaddr02:42:db:d0:47:dbinetaddr:172.17.0.1Bcast:0.0.0.0Mask:255.255.0.0...docker0接口是由Docker守护程序创建的,以便与Docker容器连接。现在,我们可以使用dockerinspect命令查看Docker容器内创建的接口:

"NetworkSettings":{"Bridge":"","Ports":{"8080/tcp":[{"HostIp":"0.0.0.0","HostPort":"8080"}]},"Gateway":"172.17.0.1","IPAddress":"172.17.0.2","IPPrefixLen":16,}为了过滤dockerinspect的响应,我们可以使用--format选项,例如,dockerinspect--format'{{.NetworkSettings.IPAddress}}'

请注意,默认情况下,容器受主机防火墙系统保护,并且不会从外部系统打开任何路由。我们可以通过使用--network标志并将其设置为以下内容来更改此默认行为:

不同的网络可以通过dockernetwork命令列出和管理:

$dockernetworklsNETWORKIDNAMEDRIVERSCOPEb3326cb44121bridgebridgelocal84136027df04hosthostlocal80c26af0351cnonenulllocal如果我们将none指定为网络,则将无法连接到容器,反之亦然;容器无法访问外部世界。host选项使容器网络接口与主机相同。它们共享相同的IP地址,因此容器上启动的所有内容在外部可见。最常用的选项是默认选项(bridge),因为它允许我们明确定义应发布哪些端口。它既安全又可访问。

EXPOSE8080这个Dockerfile指令表示应该从容器中公开端口8080。然而,正如我们已经看到的,这并不意味着端口会自动发布。EXPOSE指令只是通知用户应该发布哪些端口。

让我们尝试在不停止第一个Tomcat容器的情况下运行第二个Tomcat容器:

$dockerrun-d-p8080:8080tomcat0835c95538aeca79e0305b5f19a5f96cb00c5d1c50bed87584cfca8ec790f241docker:Errorresponsefromdaemon:driverfailedprogrammingexternalconnectivityonendpointdistracted_heyrovsky(1b1cee9896ed99b9b804e4c944a3d9544adf72f1ef3f9c9f37bc985e9c30f452):Bindfor0.0.0.0:8080failed:portisalreadyallocated.这种错误可能很常见。在这种情况下,我们要么自己负责端口的唯一性,要么让Docker使用publish命令的以下版本自动分配端口:

假设您想将数据库作为容器运行。您可以启动这样一个容器并输入数据。它存储在哪里?当您停止容器或删除它时会发生什么?您可以启动新的容器,但数据库将再次为空。除非这是您的测试环境,您不会期望这样的情况发生。

Docker卷是Docker主机的目录,挂载在容器内部。它允许容器像写入自己的文件系统一样写入主机的文件系统。该机制如下图所示:

Docker卷使容器的数据持久化和共享。卷还清楚地将处理与数据分开。

让我们从一个示例开始,并使用-v:选项指定卷并连接到容器:

$dockerrun-i-t-v~/docker_ubuntu:/host_directoryubuntu:16.04/bin/bash现在,我们可以在容器中的host_directory中创建一个空文件:

root@01bf73826624:/#touchhost_directory/file.txt让我们检查一下文件是否在Docker主机的文件系统中创建:

root@01bf73826624:/#exitexit$ls~/docker_ubuntu/file.txt我们可以看到文件系统被共享,数据因此得以永久保存。现在我们可以停止容器并运行一个新的容器,看到我们的文件仍然在那里:

$dockerstop01bf73826624$dockerrun-i-t-v~/docker_ubuntu:/host_directoryubuntu:16.04/bin/bashroot@a9e0df194f1f:/#lshost_directory/file.txtroot@a9e0df194f1f:/#exit不需要使用-v标志来指定卷,可以在Dockerfile中将卷指定为指令,例如:

VOLUME/host_directory在这种情况下,如果我们运行docker容器而没有-v标志,那么容器的/host_directory将被映射到主机的默认卷目录/var/lib/docker/vfs/。如果您将应用程序作为镜像交付,并且知道它因某种原因需要永久存储(例如存储应用程序日志),这是一个很好的解决方案。

如果卷在Dockerfile中和作为标志定义,那么命令标志优先。

Docker卷可能会更加复杂,特别是在数据库的情况下。然而,Docker卷的更复杂的用例超出了本书的范围。

到目前为止,当我们操作容器时,我们总是使用自动生成的名称。这种方法有一些优势,比如名称是唯一的(没有命名冲突)和自动的(不需要做任何事情)。然而,在许多情况下,最好为容器或镜像提供一个真正用户友好的名称。

命名容器有两个很好的理由:方便和自动化:

例如,我们希望有一些相互依赖的容器,并且有一个链接到另一个。因此,我们需要知道它们的名称。

要命名容器,我们使用--name参数:

$dockerrun-d--nametomcattomcat我们可以通过dockerps检查容器是否有有意义的名称。此外,作为结果,任何操作都可以使用容器的名称执行,例如:

$dockerlogstomcat请注意,当容器被命名时,它不会失去其身份。我们仍然可以像以前一样通过自动生成的哈希ID来寻址容器。

容器始终具有ID和名称。可以通过任何一个来寻址,它们两个都是唯一的。

图像可以被标记。我们在创建自己的图像时已经做过这个,例如,在构建hello-world_python图像的情况下:

$dockerbuild-thello-world_python.-t标志描述了图像的标签。如果我们没有使用它,那么图像将被构建而没有任何标签,结果我们将不得不通过其ID(哈希)来寻址它以运行容器。

图像可以有多个标签,并且它们应该遵循命名约定:

/:标签由以下部分组成:

我们将在第五章中介绍Docker注册表,自动验收测试。如果图像保存在官方DockerHub注册表上,那么我们可以跳过注册表地址。这就是为什么我们在没有任何前缀的情况下运行了tomcat图像。最后一个版本总是被标记为最新的,也可以被跳过,所以我们在没有任何后缀的情况下运行了tomcat图像。

图像通常有多个标签,例如,所有四个标签都是相同的图像:ubuntu:16.04,ubuntu:xenial-20170119,ubuntu:xenial和ubuntu:latest。

在本章中,我们创建了许多容器和图像。然而,这只是现实场景中的一小部分。即使容器此刻没有运行,它们也需要存储在Docker主机上。这很快就会导致存储空间超出并停止机器。我们如何解决这个问题呢?

首先,让我们看看存储在我们的机器上的容器。要打印所有容器(无论它们的状态如何),我们可以使用dockerps-a命令:

$dockerrm47ba1c0ba90e如果我们想要删除所有已停止的容器,我们可以使用以下命令:

$dockerrm$(dockerps--no-trunc-aq)-aq选项指定仅传递所有容器的ID(没有额外数据)。另外,--no-trunc要求Docker不要截断输出。

我们也可以采用不同的方法,并要求容器在停止时使用--rm标志自行删除,例如:

$dockerrun--rmhello-world在大多数实际场景中,我们不使用已停止的容器,它们只用于调试目的。

图像和容器一样重要。它们可能占用大量空间,特别是在持续交付过程中,每次构建都会产生一个新的Docker图像。这很快就会导致设备上没有空间的错误。要检查Docker容器中的所有图像,我们可以使用dockerimages命令:

$dockerimagesREPOSITORYTAGIMAGEIDCREATEDSIZEhello_world_python_name_defaultlatest9a056ca928412hoursago202.6MBhello_world_python_namelatest72c8c50ffa892hoursago202.6MBhello_world_pythonlatest3e1fa5c29b442hoursago202.6MBubuntu_with_pythonlatestd6e85f39f5b72hoursago202.6MBubuntu_with_git_and_jdklatest8464dc10abbb2hoursago610.9MBubuntu_with_gitlatestf3d674114fe23hoursago259.7MBtomcatlatestc822d296d2322daysago355.3MBubuntu16.04f49eec89601e7daysago129.5MBmongolatest0dffc7177b0611daysago402MBhello-worldlatest48b5124b27682weeksago1.84kB要删除图像,我们可以调用以下命令:

$dockerrmi48b5124b2768在图像的情况下,自动清理过程稍微复杂一些。图像没有状态,所以我们不能要求它们在不使用时自行删除。常见的策略是设置Cron清理作业,删除所有旧的和未使用的图像。我们可以使用以下命令来做到这一点:

$dockerrmi$(dockerimages-q)为了防止删除带有标签的图像(例如,不删除所有最新的图像),非常常见的是使用dangling参数:

$dockerrmi$(dockerimages-f"dangling=true"-q)如果我们有使用卷的容器,那么除了图像和容器之外,还值得考虑清理卷。最简单的方法是使用dockervolumels-qfdangling=true|xargs-rdockervolumerm命令。

通过执行以下help命令可以找到所有Docker命令:

$dockerhelp要查看任何特定Docker命令的所有选项,我们可以使用dockerhelp,例如:

在本章中,我们已经介绍了最有用的命令及其选项。作为一个快速提醒,让我们回顾一下:

在本章中,我们涵盖了大量的材料。为了记忆深刻,我们建议进行两个练习。

您可以使用dockersearch命令来查找CouchDB镜像。

在本章中,我们已经涵盖了足够构建镜像和运行应用程序作为容器的Docker基础知识。本章的关键要点如下:

在下一章中,我们将介绍Jenkins的配置以及Jenkins与Docker一起使用的方式。

我们已经看到如何配置和使用Docker。在本章中,我们将介绍Jenkins,它可以单独使用,也可以与Docker一起使用。我们将展示这两个工具的结合产生了令人惊讶的好结果:自动配置和灵活的可扩展性。

本章涵盖以下主题:

Jenkins是用Java编写的开源自动化服务器。凭借非常活跃的基于社区的支持和大量的插件,它是实施持续集成和持续交付流程的最流行工具。以前被称为Hudson,Oracle收购Hudson并决定将其开发为专有软件后更名为Jenkins。Jenkins仍然在MIT许可下,并因其简单性、灵活性和多功能性而备受推崇。

Jenkins优于其他持续集成工具,是最广泛使用的其类软件。这一切都是可能的,因为它的特性和能力。

让我们来看看Jenkins特性中最有趣的部分。

Jenkins安装过程快速简单。有不同的方法可以做到这一点,但由于我们已经熟悉Docker工具及其带来的好处,我们将从基于Docker的解决方案开始。这也是最简单、最可预测和最明智的方法。然而,让我们先提到安装要求。

最低系统要求相对较低:

然而,需要明白的是,要求严格取决于您打算如何使用Jenkins。如果Jenkins用于为整个团队提供持续集成服务器,即使是小团队,建议具有1GB以上的可用内存和50GB以上的可用磁盘空间。不用说,Jenkins还执行一些计算并在网络上传输大量数据,因此CPU和带宽至关重要。

为了了解在大公司的情况下可能需要的要求,Jenkins架构部分介绍了Netflix的例子。

让我们看看使用Docker安装Jenkins的逐步过程。

Jenkins镜像可在官方DockerHub注册表中找到,因此为了安装它,我们应该执行以下命令:

$dockerrun-p:8080-v:/var/jenkins_homejenkins:2.60.1我们需要指定第一个host_port参数——Jenkins在容器外可见的端口。第二个参数host_volume指定了Jenkins主目录映射的目录。它需要被指定为卷,并因此永久持久化,因为它包含了配置、管道构建和日志。

例如,让我们看看在Linux/Ubuntu上Docker主机的安装步骤会是什么样子。

完成这几个步骤后,Jenkins就可以使用了。基于Docker的安装有两个主要优点:

在本书的所有地方,我们使用的是版本2.60.1的Jenkins。

出于前面提到的原因,建议安装Docker。但是,如果这不是一个选择,或者有其他原因需要采取其他方式进行安装,那么安装过程同样简单。例如,在Ubuntu的情况下,只需运行:

无论您选择哪种安装方式,Jenkins的第一次启动都需要进行一些配置步骤。让我们一步一步地走过它们:

$dockerlogsjenkins...Jenkinsinitialsetupisrequired.Anadminuserhasbeencreatedandapasswordgenerated.Pleaseusethefollowingpasswordtoproceedtoinstallation:c50508effc6843a1a7b06f6491ed0ca6...安装完成后,您应该看到Jenkins仪表板:

我们已经准备好使用Jenkins并创建第一个管道。

整个IT世界的一切都始于HelloWorld的例子。

让我们遵循这个规则,看看创建第一个Jenkins管道的步骤:

pipeline{agentanystages{stage("Hello"){steps{echo'HelloWorld'}}}}我们应该在构建历史下看到#1。如果我们点击它,然后点击控制台输出,我们将看到管道构建的日志。

我们刚刚看到了第一个例子,成功的输出意味着Jenkins已经正确安装。现在,让我们转移到稍微更高级的Jenkins配置。

我们将在第四章中更详细地描述管道语法,持续集成管道。

在常见情况下,也会有许多并发的管道。通常,整个团队,甚至整个组织,都使用同一个Jenkins实例。如何确保构建能够快速顺利地运行?

Jenkins很快就会变得过载。即使是一个小的(微)服务,构建也可能需要几分钟。这意味着一个频繁提交的团队很容易就能够使Jenkins实例崩溃。

因此,除非项目非常小,Jenkins不应该执行构建,而是将它们委托给从节点(代理)实例。准确地说,我们当前运行的Jenkins称为Jenkins主节点,它可以委托给Jenkins代理。

让我们看一下呈现主从交互的图表:

在分布式构建环境中,Jenkins主节点负责:

构建代理是一个负责构建开始后发生的一切的机器。

由于主节点和从节点的责任不同,它们有不同的环境要求:

代理也应尽可能通用。例如,如果我们有不同的项目:一个是Java,一个是Python,一个是Ruby,那么每个代理都可以构建任何这些项目将是完美的。在这种情况下,代理可以互换,有助于优化资源的使用。

如果代理不能足够通用以匹配所有项目,那么可以对代理和项目进行标记,以便给定的构建将在给定类型的代理上执行。

我们可以使用Jenkins从节点来平衡负载和扩展Jenkins基础架构。这个过程称为水平扩展。另一种可能性是只使用一个主节点并增加其机器的资源。这个过程称为垂直扩展。让我们更仔细地看看这两个概念。

垂直扩展意味着当主机负载增加时,会向主机的机器应用更多资源。因此,当我们的组织中出现新项目时,我们会购买更多的RAM,增加CPU核心,并扩展HDD驱动器。这可能听起来像是一个不可行的解决方案;然而,它经常被使用,甚至被知名组织使用。将单个Jenkins主设置在超高效的硬件上有一个非常强大的优势:维护。任何升级、脚本、安全设置、角色分配或插件安装都只需在一个地方完成。

水平扩展意味着当组织增长时,会启动更多的主实例。这需要将实例智能分配给团队,并且在极端情况下,每个团队都可以拥有自己的Jenkins主实例。在这种情况下,甚至可能不需要从属实例。

除了扩展方法,还有一个问题:如何测试Jenkins升级、新插件或流水线定义?Jenkins对整个公司至关重要。它保证了软件的质量,并且(在持续交付的情况下)部署到生产服务器。这就是为什么它需要高可用性,因此绝对不是为了测试的目的。这意味着应该始终存在两个相同的Jenkins基础架构实例:测试和生产。

测试环境应该尽可能与生产环境相似,因此也需要相似数量的附加代理。

我们已经知道应该有从属者,(可能是多个)主节点,以及一切都应该复制到测试和生产环境中。然而,完整的情况会是什么样子呢?

幸运的是,有很多公司发布了他们如何使用Jenkins以及他们创建了什么样的架构。很难衡量更多的公司是偏好垂直扩展还是水平扩展,但从只有一个主节点实例到每个团队都有一个主节点都有。范围很广。

他们有测试和生产主节点实例,每个实例都拥有一组从属者和额外的临时从属者。总共,它每天提供大约2000个构建。还要注意,他们的基础设施部分托管在AWS上,部分托管在他们自己的服务器上。

我们应该已经对Jenkins基础设施的外观有一个大致的想法,这取决于组织的类型。

现在让我们专注于设置代理的实际方面。

我们已经知道代理是什么,以及何时可以使用。但是,如何设置代理并让其与主节点通信呢?让我们从问题的第二部分开始,描述主节点和代理之间的通信协议。

为了让主节点和代理进行通信,必须建立双向连接。

有不同的选项可以启动它:

如果我们知道通信协议,让我们看看如何使用它们来设置代理。

在低级别上,代理始终使用上面描述的协议与Jenkins主服务器通信。然而,在更高级别上,我们可以以各种方式将从节点附加到主服务器。差异涉及两个方面:

这些差异导致了四种常见的代理配置策略:

让我们逐个检查每种解决方案。

我们从最简单的选项开始,即永久添加特定代理节点。可以完全通过JenkinsWeb界面完成。

在Jenkins主服务器上,当我们打开“管理Jenkins”,然后点击“管理节点”,我们可以查看所有已附加的代理。然后,通过点击“新建节点”,给它一个名称,并点击“确定”按钮,最终我们应该看到代理的设置页面:

让我们来看看我们需要填写的参数:

当代理正确设置后,可以将主节点离线,这样就不会在其上执行任何构建,它只会作为JenkinsUI和构建协调器。

正如前面提到的,这种解决方案的缺点是我们需要为不同的项目类型维护多个从属类型(标签)。这种情况如下图所示:

在我们的示例中,如果我们有三种类型的项目(java7,java8和ruby),那么我们需要维护三个分别带有标签的(集合)从属。这与我们在维护多个生产服务器类型时遇到的问题相同,如第二章引入Docker中所述。我们通过在生产服务器上安装DockerEngine来解决了这个问题。让我们尝试在Jenkins从属上做同样的事情。

这种解决方案的理念是永久添加通用从属。每个从属都配置相同(安装了DockerEngine),并且每个构建与Docker镜像一起定义,构建在其中运行。

配置是静态的,所以它的完成方式与我们为永久从属所做的完全相同。唯一的区别是我们需要在每台将用作从属的机器上安装Docker。然后,通常我们不需要标签,因为所有从属都可以是相同的。在从属配置完成后,我们在每个流水线脚本中定义Docker镜像。

pipeline{agent{docker{image'openjdk:8-jdk-alpine'}}...}当构建开始时,Jenkins从服务器会从Docker镜像openjdk:8-jdk-alpine启动一个容器,然后在该容器内执行所有流水线步骤。这样,我们始终知道执行环境,并且不必根据特定项目类型单独配置每个从服务器。

看着我们为永久代理所采取的相同场景,图表如下:

每个从服务器都是完全相同的,如果我们想构建一个依赖于Java8的项目,那么我们在流水线脚本中定义适当的Docker镜像(而不是指定从服务器标签)。

到目前为止,我们总是不得不在Jenkins主服务器中永久定义每个代理。这样的解决方案,即使在许多情况下都足够好,如果我们需要频繁扩展从服务器的数量,可能会成为负担。JenkinsSwarm允许您动态添加从服务器,而无需在Jenkins主服务器中对其进行配置。

使用JenkinsSwarm的第一步是在Jenkins中安装自组织Swarm插件模块插件。我们可以通过JenkinsWebUI在“管理Jenkins”和“管理插件”下进行。完成此步骤后,Jenkins主服务器准备好动态附加Jenkins从服务器。

第二步是在每台将充当Jenkins从服务器的机器上运行JenkinsSwarm从服务器应用程序。我们可以使用swarm-client.jar应用程序来完成。

要附加JenkinsSwarm从节点,只需运行以下命令:

$java-jarswarm-client.jar-master-username-password-namejenkins-swarm-slave-1在撰写本书时,存在一个client-slave.jar无法通过安全的HTTPS协议工作的未解决错误,因此需要在命令执行中添加-disableSslVerification选项。

成功执行后,我们应该注意到Jenkins主服务器上出现了一个新的从服务器,如屏幕截图所示:

现在,当我们运行构建时,它将在此代理上启动。

添加JenkinsSwarm代理的另一种可能性是使用从swarm-client.jar工具构建的Docker镜像。DockerHub上有一些可用的镜像。我们可以使用csanchez/jenkins-swarm-slave镜像。

JenkinsSwarm允许动态添加代理,但它没有说明是否使用特定的或基于Docker的从属,所以我们可以同时使用它。乍一看,JenkinsSwarm可能看起来并不是很有用。毕竟,我们将代理设置从主服务器移到了从属,但仍然需要手动完成。然而,正如我们将在第八章中看到的那样,使用DockerSwarm进行集群,JenkinsSwarm可以在服务器集群上动态扩展从属。

另一个选项是设置Jenkins在每次启动构建时动态创建一个新的代理。这种解决方案显然是最灵活的,因为从属的数量会动态调整到构建的数量。让我们看看如何以这种方式配置Jenkins。

我们需要首先安装Docker插件。与Jenkins插件一样,我们可以在“管理Jenkins”和“管理插件”中进行。安装插件后,我们可以开始以下配置步骤:

如果您计划在运行主服务器的相同Docker主机上使用它,则Docker守护程序需要在docker0网络接口上进行监听。您可以以与在服务器上安装部分中描述的类似方式进行操作。这与我们在维护多个生产服务器类型时遇到的问题相同,如第二章中所述,介绍Docker,通过更改/lib/systemd/system/docker.service文件中的一行为ExecStart=/usr/bin/dockerd-H0.0.0.0:2375-Hfd://

我们可以使用以下参数:

除了evarga/jenkins-slave之外,也可以构建和使用自己的从属镜像。当存在特定的环境要求时,例如安装了Python解释器时,这是必要的。在本书的所有示例中,我们使用了leszko/jenkins-docker-slave。

保存后,一切都设置好了。我们可以运行流水线来观察执行是否真的在Docker代理上进行,但首先让我们深入了解一下Docker代理的工作原理。

动态提供的Docker代理可以被视为标准代理机制的一层。它既不改变通信协议,也不改变代理的创建方式。那么,Jenkins会如何处理我们提供的Docker代理配置呢?

以下图表展示了我们配置的Docker主从架构:

让我们逐步描述Docker代理机制的使用方式:

将Jenkins主机作为Docker容器运行与将Jenkins代理作为Docker容器运行是独立的。两者都是合理的选择,但它们中的任何一个都可以单独工作。

这个解决方案在某种程度上类似于永久的Docker代理解决方案,因为最终我们是在Docker容器内运行构建。然而,不同之处在于从属节点的配置。在这里,整个从属都是docker化的,不仅仅是构建环境。因此,它具有以下两个巨大的优势:

无论选择了哪种代理配置,现在我们应该检查它是否正常工作。

pipeline{agentanystages{stage("Hello"){steps{sleep300//5minutesecho'HelloWorld'}}}}点击“立即构建”并转到Jenkins主页后,我们应该看到构建是在代理上执行的。现在,如果我们多次点击构建,不同的代理应该执行不同的构建(如下截图所示):

为了防止作业在主节点上执行,记得将主节点设置为离线或在节点管理配置中将执行器数量设置为0。

通过观察代理执行我们的构建,我们确认它们已经正确配置。现在,让我们看看为什么以及如何创建我们自己的Jenkins镜像。

到目前为止,我们使用了从互联网上拉取的Jenkins镜像。我们使用jenkins作为主容器,evarga/jenkins-slave作为从容器。然而,我们可能希望构建自己的镜像以满足特定的构建环境要求。在本节中,我们将介绍如何做到这一点。

让我们从从容器镜像开始,因为它经常被定制。构建执行是在代理上执行的,因此需要调整代理的环境以适应我们想要构建的项目。例如,如果我们的项目是用Python编写的,可能需要Python解释器。同样的情况也适用于任何库、工具、测试框架或项目所需的任何内容。

构建和使用自定义镜像有三个步骤:

举个例子,让我们创建一个为Python项目提供服务的从节点。为了简单起见,我们可以基于evarga/jenkins-slave镜像构建它。让我们按照以下三个步骤来做:

FROMevarga/jenkins-slaveRUNapt-getupdate&&\apt-getinstall-ypython基础Docker镜像evarga/jenkins-slave适用于动态配置的Docker代理解决方案。对于永久性Docker代理,只需使用alpine、ubuntu或任何其他镜像即可,因为docker化的不是从节点,而只是构建执行环境。

$dockerbuild-tjenkins-slave-python.从节点的Dockerfile应该保存在源代码仓库中,并且可以由Jenkins自动执行构建。使用旧的Jenkins从节点构建新的Jenkins从节点镜像没有问题。

如果我们需要Jenkins构建两种不同类型的项目,例如一个基于Python,另一个基于Ruby,该怎么办?在这种情况下,我们可以准备一个足够通用以支持Python和Ruby的代理。然而,在Docker的情况下,建议创建第二个从节点镜像(通过类比创建jenkins-slave-ruby)。然后,在Jenkins配置中,我们需要创建两个Docker模板并相应地标记它们。

我们已经有一个自定义的从节点镜像。为什么我们还想要构建自己的主节点镜像呢?其中一个原因可能是我们根本不想使用从节点,而且由于执行将在主节点上进行,它的环境必须根据项目的需求进行调整。然而,这是非常罕见的情况。更常见的情况是,我们会想要配置主节点本身。

Jenkins使用XML文件进行配置,并提供基于Groovy的DSL语言来对其进行操作。这就是为什么我们可以将Groovy脚本添加到Dockerfile中,以操纵Jenkins配置。而且,如果需要比XML更多的更改,例如插件安装,还有特殊的脚本来帮助Jenkins配置。

例如,让我们创建一个已经安装了docker-plugin并将执行者数量设置为5的主镜像。为了做到这一点,我们需要:

让我们使用提到的三个步骤构建Jenkins主镜像。

FROMjenkinsCOPYexecutors.groovy/usr/share/jenkins/ref/init.groovy.d/executors.groovyRUN/usr/local/bin/install-plugins.shdocker-plugin$dockerbuild-tjenkins-master.创建图像后,组织中的每个团队都可以使用它来启动自己的Jenkins实例。

拥有自己的主从镜像可以为我们组织中的团队提供配置和构建环境。在接下来的部分,我们将看到Jenkins中还有哪些值得配置。

选择插件的数量实际上有很多。其中一些在初始配置过程中已经自动安装了。另一个(Docker插件)是在设置Docker代理时安装的。有用于云集成、源代码控制工具、代码覆盖等的插件。你也可以编写自己的插件,但最好先检查一下你需要的插件是否已经存在。

您应该如何处理Jenkins安全取决于您在组织中选择的Jenkins架构。如果您为每个小团队都有一个Jenkins主服务器,那么您可能根本不需要它(假设企业网络已设置防火墙)。然而,如果您为整个组织只有一个Jenkins主服务器实例,那么最好确保您已经很好地保护了它。

我们可以安装一个Jenkins插件(帮助我们设置定期备份),或者简单地设置一个cron作业将目录存档到一个安全的地方。为了减小大小,我们可以排除那些不感兴趣的子文件夹(这将取决于你的需求;然而,几乎可以肯定的是,你不需要复制:"war","cache","tools"和"workspace")。

有很多插件可以帮助备份过程;最常见的一个叫做备份插件。

在本章中,我们学到了很多关于Jenkins配置的知识。为了巩固这些知识,我们建议进行两个练习,准备Jenkins镜像并测试Jenkins环境。

sh"echo"puts'HelloWorldfromRuby'">hello.rb"

在本章中,我们已经介绍了Jenkins环境及其配置。所获得的知识足以建立完整基于Docker的Jenkins基础设施。本章的关键要点如下:

在下一章中,我们将专注于已经通过“helloworld”示例接触过的部分,即管道。我们将描述构建完整持续集成管道的思想和方法。

我们已经知道如何配置Jenkins。在本章中,您将看到如何有效地使用它,重点放在Jenkins核心的功能上,即管道。通过从头开始构建完整的持续集成过程,我们将描述现代团队导向的代码开发的所有方面。

管道是一系列自动化操作,通常代表软件交付和质量保证过程的一部分。它可以简单地被看作是一系列脚本,提供以下额外的好处:

管道的概念对于大多数持续集成工具来说是相似的,但命名可能有所不同。在本书中,我们遵循Jenkins的术语。

Jenkins管道由两种元素组成:阶段和步骤。以下图显示了它们的使用方式:

以下是基本的管道元素:

从技术上讲,可以创建并行步骤;然而,最好将其视为真正需要优化目的时的例外。

例如,让我们扩展HelloWorld管道,包含两个阶段:

pipeline{agentanystages{stage('FirstStage'){steps{echo'Step1\.HelloWorld'}}stage('SecondStage'){steps{echo'Step2\.SecondtimeHello'echo'Step3\.ThirdtimeHello'}}}}管道在环境方面没有特殊要求(任何从属代理),并在两个阶段内执行三个步骤。当我们点击“立即构建”时,我们应该看到可视化表示:

管道成功了,我们可以通过单击控制台查看步骤执行详细信息。如果任何步骤失败,处理将停止,不会运行更多的步骤。实际上,管道的整个目的是阻止所有进一步的步骤执行并可视化失败点。

我们已经讨论了管道元素,并已经使用了一些管道步骤,例如echo。在管道定义内部,我们还可以使用哪些其他操作?

让我们准备一个实验,在我们描述所有细节之前,阅读以下管道定义并尝试猜测它的作用:

pipeline{agentanytriggers{cron('*****')}options{timeout(time:5)}parameters{booleanParam(name:'DEBUG_BUILD',defaultValue:true,description:'Isitthedebugbuild')}stages{stage('Example'){environment{NAME='Rafal'}when{expression{returnparams.DEBUG_BUILD}}steps{echo"Hellofrom$NAME"script{defbrowsers=['chrome','firefox']for(inti=0;i

部分定义了流水线的结构,通常包含一个或多个指令或步骤。它们使用以下关键字进行定义:

指令表达了流水线或其部分的配置:

步骤是流水线最基本的部分。它们定义了要执行的操作,因此它们实际上告诉Jenkins要做什么。

请注意,流水线语法非常通用,从技术上讲,几乎可以用于任何自动化流程。这就是为什么应该将流水线视为一种结构化和可视化的方法。然而,最常见的用例是实现我们将在下一节中看到的持续集成服务器。

提交阶段的工作如下。开发人员将代码提交到存储库,持续集成服务器检测到更改,构建开始。最基本的提交流水线包含三个阶段:

让我们创建一个示例项目,看看如何实现提交流水线。

这是一个使用Git、Java、Gradle和SpringBoot等技术的项目的流水线示例。然而,相同的原则适用于任何其他技术。

从存储库检出代码始终是任何流水线中的第一个操作。为了看到这一点,我们需要有一个存储库。然后,我们将能够创建一个流水线。

在GitHub服务器上创建存储库只需几个步骤:

我们可以创建一个名为calculator的新流水线,并将代码放在一个名为Checkout的阶段的流水线脚本中:

请注意,Git工具包需要安装在执行构建的节点上。

当我们完成检出时,我们准备进行第二阶段。

为了编译一个项目,我们需要:

让我们使用Gradle构建的SpringBoot框架创建一个非常简单的Java项目。

SpringBoot是一个简化构建企业应用程序的Java框架。Gradle是一个基于ApacheMaven概念的构建自动化系统。

创建SpringBoot项目的最简单方法是执行以下步骤:

我们将使用Git工具执行commit和push操作:

让我们首先将存储库克隆到文件系统:

如果您愿意,您可以将项目导入到IntelliJ、Eclipse或您喜欢的IDE工具中。

结果,calculator目录应该有以下文件:

$ls-a...build.gradle.git.gitignoregradlegradlewgradlew.batREADME.mdsrc为了在本地执行Gradle操作,您需要安装JavaJDK(在Ubuntu中,您可以通过执行sudoapt-getinstall-ydefault-jdk来完成)。

我们可以使用以下代码在本地编译项目:

$./gradlewcompileJava在Maven的情况下,您可以运行./mvnwcompile。Gradle和Maven都编译src目录中的Java类。

现在,我们可以将其commit和push到GitHub存储库中:

$gitadd.$gitcommit-m"AddSpringBootskeleton"$gitpush-uoriginmaster运行gitpush命令后,您将被提示输入GitHub凭据(用户名和密码)。

代码已经在GitHub存储库中。如果您想检查它,可以转到GitHub页面并查看文件。

我们可以使用以下代码在管道中添加一个编译阶段:

stage("Compile"){steps{sh"./gradlewcompileJava"}}请注意,我们在本地和Jenkins管道中使用了完全相同的命令,这是一个非常好的迹象,因为本地开发过程与持续集成环境保持一致。运行构建后,您应该看到两个绿色的框。您还可以在控制台日志中检查项目是否已正确编译。

是时候添加最后一个阶段了,即单元测试,检查我们的代码是否符合预期。我们必须:

计算器的第一个版本将能够添加两个数字。让我们将业务逻辑作为一个类添加到src/main/java/com/leszko/calculator/Calculator.java文件中:

packagecom.leszko.calculator;importorg.springframework.stereotype.Service;@ServicepublicclassCalculator{intsum(inta,intb){returna+b;}}为了执行业务逻辑,我们还需要在单独的文件src/main/java/com/leszko/calculator/CalculatorController.java中添加网络服务控制器:

packagecom.leszko.calculator;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.bind.annotation.RestController;@RestControllerclassCalculatorController{@AutowiredprivateCalculatorcalculator;@RequestMapping("/sum")Stringsum(@RequestParam("a")Integera,@RequestParam("b")Integerb){returnString.valueOf(calculator.sum(a,b));}}这个类将业务逻辑公开为一个网络服务。我们可以运行应用程序并查看它的工作方式:

我们已经有了可工作的应用程序。我们如何确保逻辑按预期工作?我们已经尝试过一次,但为了不断了解,我们需要进行单元测试。在我们的情况下,这可能是微不足道的,甚至是不必要的;然而,在实际项目中,单元测试可以避免错误和系统故障。

让我们在文件src/test/java/com/leszko/calculator/CalculatorTest.java中创建一个单元测试:

packagecom.leszko.calculator;importorg.junit.Test;importstaticorg.junit.Assert.assertEquals;publicclassCalculatorTest{privateCalculatorcalculator=newCalculator();@TestpublicvoidtestSum(){assertEquals(5,calculator.sum(2,3));}}我们可以使用./gradlewtest命令在本地运行测试。然后,让我们commit代码并将其push到存储库中:

$gitadd.$gitcommit-m"Addsumlogic,controllerandunittest"$gitpush创建一个单元测试阶段现在,我们可以在管道中添加一个单元测试阶段:

stage("Unittest"){steps{sh"./gradlewtest"}}在Maven的情况下,我们需要使用./mvnwtest。

当我们再次构建流水线时,我们应该看到三个框,这意味着我们已经完成了持续集成流水线:

例如,如果您不需要代码编译,因为您的编程语言是解释性的(而不是编译的),那么您将不会有Compile阶段。您使用的工具也取决于环境。我们使用Gradle/Maven,因为我们构建了Java项目;然而,对于用Python编写的项目,您可以使用PyBuilder。这导致了一个想法,即流水线应该由编写代码的同一人员,即开发人员创建。此外,流水线定义应与代码一起放在存储库中。

这种方法带来了即时的好处,如下所示:

我们可以创建Jenkinsfile并将其推送到我们的GitHub存储库。它的内容几乎与我们编写的提交流水线相同。唯一的区别是,检出阶段变得多余,因为Jenkins必须首先检出代码(与Jenkinsfile一起),然后读取流水线结构(从Jenkinsfile)。这就是为什么Jenkins在读取Jenkinsfile之前需要知道存储库地址。

让我们在项目的根目录中创建一个名为Jenkinsfile的文件:

pipeline{agentanystages{stage("Compile"){steps{sh"./gradlewcompileJava"}}stage("Unittest"){steps{sh"./gradlewtest"}}}}我们现在可以commit添加的文件并push到GitHub存储库:

$gitadd.$gitcommit-m"AddsumJenkinsfile"$gitpush从Jenkinsfile运行流水线当Jenkinsfile在存储库中时,我们所要做的就是打开流水线配置,在Pipeline部分:

保存后,构建将始终从Jenkinsfile的当前版本运行到存储库中。

我们已成功创建了第一个完整的提交流水线。它可以被视为最小可行产品,并且实际上,在许多情况下,它作为持续集成流程是足够的。在接下来的章节中,我们将看到如何改进提交流水线以使其更好。

我们可以通过额外的步骤扩展经典的持续集成三个步骤。最常用的是代码覆盖和静态分析。让我们分别看看它们。

考虑以下情景:您有一个良好配置的持续集成流程;然而,项目中没有人编写单元测试。它通过了所有构建,但这并不意味着代码按预期工作。那么该怎么办?如何确保代码已经测试过了?

解决方案是添加代码覆盖工具,运行所有测试并验证代码的哪些部分已执行。然后,它创建一个报告显示未经测试的部分。此外,当未经测试的代码太多时,我们可以使构建失败。

有很多工具可用于执行测试覆盖分析;对于Java来说,最流行的是JaCoCo、Clover和Cobertura。

让我们使用JaCoCo并展示覆盖检查在实践中是如何工作的。为了做到这一点,我们需要执行以下步骤:

为了从Gradle运行JaCoCo,我们需要通过在插件部分添加以下行将jacoco插件添加到build.gradle文件中:

applyplugin:"jacoco"接下来,如果我们希望在代码覆盖率过低的情况下使Gradle失败,我们还可以将以下配置添加到build.gradle文件中:

jacocoTestCoverageVerification{violationRules{rule{limit{minimum=0.2}}}}此配置将最小代码覆盖率设置为20%。我们可以使用以下命令运行它:

$./gradlewtestjacocoTestCoverageVerification该命令检查代码覆盖率是否至少为20%。您可以尝试不同的最小值来查看构建失败的级别。我们还可以使用以下命令生成测试覆盖报告:

$./gradlewtestjacocoTestReport您还可以在build/reports/jacoco/test/html/index.html文件中查看完整的覆盖报告:

将代码覆盖率阶段添加到流水线中与之前的阶段一样简单:

stage("Codecoverage"){steps{sh"./gradlewjacocoTestReport"sh"./gradlewjacocoTestCoverageVerification"}}添加了这个阶段后,如果有人提交了未经充分测试的代码,构建将失败。

当覆盖率低且流水线失败时,查看代码覆盖率报告并找出尚未通过测试的部分将非常有用。我们可以在本地运行Gradle并生成覆盖率报告;然而,如果Jenkins为我们显示报告会更方便。

为了在Jenkins中发布代码覆盖率报告,我们需要以下阶段定义:

stage("Codecoverage"){steps{sh"./gradlewjacocoTestReport"publishHTML(target:[reportDir:'build/reports/jacoco/test/html',reportFiles:'index.html',reportName:"JaCoCoReport"])sh"./gradlewjacocoTestCoverageVerification"}}此阶段将生成的JaCoCo报告复制到Jenkins输出。当我们再次运行构建时,我们应该会看到代码覆盖率报告的链接(在左侧菜单下方的“立即构建”下)。

我们已经创建了代码覆盖率阶段,显示了未经测试且因此容易出现错误的代码。让我们看看还可以做些什么来提高代码质量。

您的代码可能运行得很好,但是代码本身的质量如何呢?我们如何确保它是可维护的并且以良好的风格编写的?

静态代码分析是一种自动检查代码而不实际执行的过程。在大多数情况下,它意味着对源代码检查一系列规则。这些规则可能适用于各种方面;例如,所有公共类都需要有Javadoc注释;一行的最大长度是120个字符,或者如果一个类定义了equals()方法,它也必须定义hashCode()方法。

对Java代码进行静态分析的最流行工具是Checkstyle、FindBugs和PMD。让我们看一个例子,并使用Checkstyle添加静态代码分析阶段。我们将分三步完成这个过程:

为了添加Checkstyle配置,我们需要定义代码检查的规则。我们可以通过指定config/checkstyle/checkstyle.xml文件来做到这一点:

我们还需要将checkstyle插件添加到build.gradle文件中:

applyplugin:'checkstyle'然后,我们可以运行以下代码来运行checkstyle:

$./gradlewcheckstyleMain在我们的项目中,这应该会导致失败,因为我们的公共类(Calculator.java,CalculatorApplication.java,CalculatorTest.java,CalculatorApplicationTests.java)都没有Javadoc注释。我们需要通过添加文档来修复它,例如,在src/main/java/com/leszko/calculator/CalculatorApplication.java文件中:

/*MainSpringApplication.*/@SpringBootApplicationpublicclassCalculatorApplication{publicstaticvoidmain(String[]args){SpringApplication.run(CalculatorApplication.class,args);}}现在,构建应该成功。

我们可以在流水线中添加一个“静态代码分析”阶段:

stage("Staticcodeanalysis"){steps{sh"./gradlewcheckstyleMain"}}现在,如果有人提交了一个没有Javadoc的公共类文件,构建将失败。

与JaCoCo非常相似,我们可以将Checkstyle报告添加到Jenkins中:

publishHTML(target:[reportDir:'build/reports/checkstyle/',reportFiles:'main.html',reportName:"CheckstyleReport"])它会生成一个指向Checkstyle报告的链接。

我们已经添加了静态代码分析阶段,可以帮助找到错误并在团队或组织内标准化代码风格。

SonarQube是最广泛使用的源代码质量管理工具。它支持多种编程语言,并且可以作为我们查看的代码覆盖率和静态代码分析步骤的替代品。实际上,它是一个单独的服务器,汇总了不同的代码分析框架,如Checkstyle、FindBugs和JaCoCo。它有自己的仪表板,并且与Jenkins集成良好。

与将代码质量步骤添加到流水线不同,我们可以安装SonarQube,在那里添加插件,并在流水线中添加一个“sonar”阶段。这种解决方案的优势在于,SonarQube提供了一个用户友好的Web界面来配置规则并显示代码漏洞。

到目前为止,我们一直通过点击“立即构建”按钮手动构建流水线。这样做虽然有效,但不太方便。所有团队成员都需要记住,在提交到存储库后,他们需要打开Jenkins并开始构建。流水线监控也是一样;到目前为止,我们手动打开Jenkins并检查构建状态。在本节中,我们将看到如何改进流程,使得流水线可以自动启动,并在完成后通知团队成员其状态。

自动启动构建的操作称为流水线触发器。在Jenkins中,有许多选择,但它们都归结为三种类型:

让我们来看看每一个。

外部触发器很容易理解。它意味着Jenkins在被通知者调用后开始构建,通知者可以是其他流水线构建、SCM系统(例如GitHub)或任何远程脚本。

下图展示了通信:

GitHub在推送到存储库后触发Jenkins并开始构建。

要以这种方式配置系统,我们需要以下设置步骤:

对于最流行的SCM提供商,通常都会提供专门的Jenkins插件。

还有一种更通用的方式可以通过对端点/job//buildtoken=进行REST调用来触发Jenkins。出于安全原因,它需要在Jenkins中设置token,然后在远程脚本中使用。

Jenkins必须可以从SCM服务器访问。换句话说,如果我们使用公共GitHub来触发Jenkins,那么我们的Jenkins服务器也必须是公共的。这也适用于通用解决方案;地址必须是可访问的。

轮询SCM触发器有点不太直观。下图展示了通信:

Jenkins定期调用GitHub并检查存储库是否有任何推送。然后,它开始构建。这可能听起来有些反直觉,但是至少有两种情况可以使用这种方法:

triggers{pollSCM('*****')}第一次手动运行流水线后,自动触发被设置。然后,它每分钟检查GitHub,对于新的提交,它会开始构建。为了测试它是否按预期工作,您可以提交并推送任何内容到GitHub存储库,然后查看构建是否开始。

我们使用神秘的*****作为pollSCM的参数。它指定Jenkins应该多久检查新的源更改,并以cron样式字符串格式表示。

计划触发意味着Jenkins定期运行构建,无论存储库是否有任何提交。

如下图所示,不需要与任何系统进行通信:

计划构建的实现与轮询SCM完全相同。唯一的区别是使用cron关键字而不是pollSCM。这种触发方法很少用于提交流水线,但适用于夜间构建(例如,在夜间执行的复杂集成测试)。

Jenkins提供了很多宣布其构建状态的方式。而且,与Jenkins中的所有内容一样,可以使用插件添加新的通知类型。

让我们逐一介绍最流行的类型,以便您选择适合您需求的类型。

电子邮件通知的配置非常简单;只需:

流水线配置可以如下:

post{always{mailto:'team@company.com',subject:"CompletedPipeline:${currentBuild.fullDisplayName}",body:"Yourbuildcompleted,pleasecheck:${env.BUILD_URL}"}}请注意,所有通知通常在流水线的post部分中调用,该部分在所有步骤之后执行,无论构建是否成功或失败。我们使用了always关键字;然而,还有不同的选项:

如果群聊(例如Slack或HipChat)是团队中的第一种沟通方式,那么考虑在那里添加自动构建通知是值得的。无论使用哪种工具,配置的过程始终是相同的:

让我们看一个Slack的样本流水线配置,在构建失败后发送通知:

post{failure{slackSendchannel:'#dragons-team',color:'danger',message:"Thepipeline${currentBuild.fullDisplayName}failed."}}团队空间随着敏捷文化的出现,人们认为最好让所有事情都发生在团队空间里。与其写电子邮件,不如一起见面;与其在线聊天,不如当面交谈;与其使用任务跟踪工具,不如使用白板。这个想法也适用于持续交付和Jenkins。目前,在团队空间安装大屏幕(也称为构建辐射器)非常普遍。因此,当你来到办公室时,你看到的第一件事就是流水线的当前状态。构建辐射器被认为是最有效的通知策略之一。它们确保每个人都知道构建失败,并且作为副作用,它们提升了团队精神并促进了面对面的沟通。

由于Jenkins可以通过插件进行扩展,其社区编写了许多不同的方式来通知构建状态。其中,您可以找到RSS订阅、短信通知、移动应用程序、桌面通知器等。

我们已经描述了持续集成管道应该是什么样子的一切。但是,它应该在什么时候运行?当然,它是在提交到存储库后触发的,但是提交到哪个分支?只提交到主干还是每个分支都提交?或者它应该在提交之前而不是之后运行,以便存储库始终保持健康?或者,怎么样采用没有分支的疯狂想法?

对于这些问题并没有单一的最佳答案。实际上,您使用持续集成过程的方式取决于团队的开发工作流程。因此,在我们继续之前,让我们描述一下可能的工作流程是什么。

开发工作流程是您的团队将代码放入存储库的方式。当然,这取决于许多因素,如源代码控制管理工具、项目特定性或团队规模。

因此,每个团队以稍微不同的方式开发代码。但是,我们可以将它们分类为三种类型:基于主干的工作流程、分支工作流程和分叉工作流程。

基于主干的工作流程是最简单的策略。其概述如下图所示:

有一个中央存储库,所有对项目的更改都有一个单一入口,称为主干或主要。团队的每个成员都克隆中央存储库,以拥有自己的本地副本。更改直接提交到中央存储库。

分支工作流,顾名思义,意味着代码被保存在许多不同的分支中。这个想法如下图所示:

分叉工作流在开源社区中非常受欢迎。其思想如下图所示:

每个开发人员都有自己的服务器端存储库。它们可能是官方存储库,也可能不是,但从技术上讲,每个存储库都是完全相同的。

分叉字面上意味着从其他存储库创建一个新存储库。开发人员将代码推送到自己的存储库,当他们想要集成代码时,他们会创建一个拉取请求到其他存储库。

分支工作流的主要优势在于集成不一定通过中央存储库。它还有助于所有权,因为它允许接受他人的拉取请求,而不给予他们写入权限。

在面向需求的商业项目中,团队通常只开发一个产品,因此有一个中央存储库,因此这个模型归结为分支工作流,具有良好的所有权分配,例如,只有项目负责人可以将拉取请求合并到中央存储库中。

我们描述了不同的开发工作流程,但它们如何影响持续集成配置呢?

每种开发工作流程都意味着不同的持续集成方法:

功能切换是一种替代维护多个源代码分支的技术,以便在功能完成并准备发布之前进行测试。它用于禁用用户的功能,但在测试时为开发人员启用。功能切换本质上是在条件语句中使用的变量。

功能切换的最简单实现是标志和if语句。使用功能切换进行开发,而不是使用功能分支开发,看起来如下:

if(feature_toggle){//dosomething}功能切换的好处在于所有开发都是在“主干”上进行的,这样可以实现真正的持续集成,并减轻合并代码的问题。

如果您决定以任何形式使用分支,长期功能分支或推荐的短期分支,那么在将其合并到主分支之前知道代码是否健康是很方便的。这种方法可以确保主代码库始终保持绿色,幸运的是,使用Jenkins可以很容易地实现这一点。

为了在我们的计算器项目中使用多分支,让我们按照以下步骤进行:

每分钟,此配置会检查是否有任何分支被添加(或删除),并创建(或删除)由Jenkinsfile定义的专用管道。

我们可以创建一个新的分支并看看它是如何工作的。让我们创建一个名为feature的新分支并将其push到存储库中:

$gitcheckout-bfeature$gitpushoriginfeature一会儿之后,您应该会看到一个新的分支管道被自动创建并运行:

现在,在将功能分支合并到主分支之前,我们可以检查它是否是绿色的。这种方法不应该破坏主构建。

在GitHub的情况下,有一种更好的方法,使用“GitHub组织文件夹”插件。它会自动为所有项目创建具有分支和拉取请求的管道。

一个非常类似的方法是为每个拉取请求构建一个管道,而不是为每个分支构建一个管道,这会产生相同的结果;主代码库始终保持健康。

这个想法有点过于简化,自动化工具很有用;然而,主要信息是,没有每个团队成员的参与,即使是最好的工具也无济于事。杰兹·汉布尔(JezHumble)在他的著作《持续交付》中提到了持续集成的先决条件,可以用以下几点重新表述:

你已经学到了如何配置持续集成过程。由于“熟能生巧”,我们建议进行以下练习:

在本章中,我们涵盖了持续集成管道的所有方面,这总是持续交付的第一步。本章的关键要点:

在下一章中,我们将专注于持续交付过程的下一个阶段,自动接受测试。它可以被认为是最重要的,而且在许多情况下,是最难实现的步骤。我们将探讨接受测试的概念,并使用Docker进行示例实现。

我们已经配置了持续交付过程的提交阶段,现在是时候解决验收测试阶段了,这通常是最具挑战性的部分。通过逐渐扩展流水线,我们将看到验收测试自动化的不同方面。

验收测试是为了确定业务需求或合同是否得到满足而进行的测试。它涉及对完整系统进行黑盒测试,从用户的角度来看,其积极的结果应意味着软件交付的验收。有时也称为用户验收测试(UAT)、最终用户测试或测试版测试,这是开发过程中软件满足真实世界受众的阶段。

许多项目依赖于由质量保证人员或用户执行的手动步骤来验证功能和非功能要求,但是,以编程可重复操作的方式运行它们要合理得多。

然而,自动验收测试可能被认为是困难的,因为它们具有特定的特点:

验收测试可能有多重含义;在本书中,我们将验收测试视为从用户角度进行的完整集成测试,不包括性能、负载和恢复等非功能性测试。

Docker注册表是用于存储Docker镜像的存储库。确切地说,它是一个无状态的服务器应用程序,允许在需要时发布(推送)和检索(拉取)镜像。我们已经在运行官方Docker镜像时看到了注册表的示例,比如jenkins。我们从DockerHub拉取了这些镜像,这是一个官方的基于云的Docker注册表。使用单独的服务器来存储、加载和搜索软件包是一个更一般的概念,称为软件存储库,甚至更一般的是构件存储库。让我们更仔细地看看这个想法。

虽然源代码管理存储源代码,但构件存储库专门用于存储软件二进制构件,例如编译后的库或组件,以后用于构建完整的应用程序。为什么我们需要使用单独的工具在单独的服务器上存储二进制文件?

最受欢迎的构件存储库是JFrogArtifactory和SonatypeNexus。

构件存储库在持续交付过程中扮演着特殊的角色,因为它保证了相同的二进制文件在所有流水线步骤中被使用。

让我们看一下下面的图,展示了它是如何工作的:

开发人员将更改推送到源代码存储库,这会触发流水线构建。作为提交阶段的最后一步,会创建一个二进制文件并存储在构件存储库中。之后,在交付过程的所有其他阶段中,都会拉取并使用相同的二进制文件。

构建的二进制文件通常被称为发布候选版本,将二进制文件移动到下一个阶段的过程称为提升。

根据编程语言和技术的不同,二进制格式可能会有所不同。

例如,在Java的情况下,通常会存储JAR文件,在Ruby的情况下会存储gem文件。我们使用Docker,因此我们将Docker镜像存储为构件,并且用于存储Docker镜像的工具称为Docker注册表。

一些团队同时维护两个存储库,一个是用于JAR文件的构件存储库,另一个是用于Docker镜像的Docker注册表。虽然在Docker引入的第一阶段可能会有用,但没有理由永远维护两者。

首先,我们需要安装一个Docker注册表。有许多选项可用,但其中两个比其他更常见,一个是基于云的DockerHub注册表,另一个是您自己的私有Docker注册表。让我们深入了解一下。

DockerHub是一个提供Docker注册表和其他功能的基于云的服务,例如构建镜像、测试它们以及直接从代码存储库中拉取代码。DockerHub是云托管的,因此实际上不需要任何安装过程。你需要做的就是创建一个DockerHub账户:

DockerHub绝对是开始使用的最简单选项,并且允许存储私有和公共图像。

DockerHub可能并不总是可接受的。对于企业来说,它并不免费,更重要的是,许多公司有政策不在其自己的网络之外存储其软件。在这种情况下,唯一的选择是安装私有Docker注册表。

Docker注册表安装过程快速简单,但是要使其在公共环境中安全可用,需要设置访问限制和域证书。这就是为什么我们将这一部分分为三个部分:

Docker注册表可用作Docker镜像。要启动它,我们可以运行以下命令:

$dockerrun-d-p5000:5000--restart=always--nameregistryregistry:2默认情况下,注册表数据存储为默认主机文件系统目录中的docker卷。要更改它,您可以添加-v:/var/lib/registry。另一种选择是使用卷容器。

该命令启动注册表并使其通过端口5000可访问。registry容器是从注册表镜像(版本2)启动的。--restart=always选项导致容器在关闭时自动重新启动。

考虑设置负载均衡器,并在用户数量较大的情况下启动几个Docker注册表容器。

如果注册表在本地主机上运行,则一切正常,不需要其他安装步骤。但是,在大多数情况下,我们希望为注册表设置专用服务器,以便图像广泛可用。在这种情况下,Docker需要使用SSL/TLS保护注册表。该过程与公共Web服务器配置非常相似,并且强烈建议使用CA(证书颁发机构)签名证书。如果获取CA签名的证书不是一个选项,那么我们可以自签名证书或使用--insecure-registry标志。

无论证书是由CA签名还是自签名,我们都可以将domain.crt和domain.key移动到certs目录并启动注册表。

$dockerrun-d-p5000:5000--restart=always--nameregistry-v`pwd`/certs:/certs-eREGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt-eREGISTRY_HTTP_TLS_KEY=/certs/domain.keyregistry:2在使用自签名证书的情况下,客户端必须明确信任该证书。为了做到这一点,他们可以将domain.crt文件复制到/etc/docker/certs.d/:5000/ca.crt。

不建议使用--insecure-registry标志,因为它根本不提供安全性。

除非我们在一个良好安全的私人网络中使用注册表,否则我们应该配置认证。

这样做的最简单方法是使用registry镜像中的htpasswd工具创建具有密码的用户:

$dockerrun-d-p5000:5000--restart=always--nameregistry-v`pwd`/auth:/auth-e"REGISTRY_AUTH=htpasswd"-e"REGISTRY_AUTH_HTPASSWD_REALM=RegistryRealm"-eREGISTRY_AUTH_HTPASSWD_PATH=/auth/passwords-v`pwd`/certs:/certs-eREGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt-eREGISTRY_HTTP_TLS_KEY=/certs/domain.keyregistry:2该命令除了设置证书外,还创建了仅限于auth/passwords文件中指定的用户的访问限制。

因此,在使用注册表之前,客户端需要指定用户名和密码。

在--insecure-registry标志的情况下,访问限制不起作用。

当涉及基于Docker的工件存储库时,DockerHub和私有注册表并不是唯一的选择。

其他选项如下:

当注册表配置好后,我们可以展示如何通过三个步骤与其一起工作:

让我们使用第二章中的示例,介绍Docker,并构建一个安装了Ubuntu和Python解释器的图像。在一个新目录中,我们需要创建一个Dockerfile:

FROMubuntu:16.04RUNapt-getupdate&&\apt-getinstall-ypython现在,我们可以构建图像:

$dockerbuild-tubuntu_with_python.推送图像为了推送创建的图像,我们需要根据命名约定对其进行标记:

/:"registry_address"可以是:

在大多数情况下,的形式是图像/应用程序版本。

让我们标记图像以使用DockerHub:

$dockertagubuntu_with_pythonleszko/ubuntu_with_python:1我们也可以在build命令中标记图像:"docker

build-tleszko/ubuntu_with_python:1.".

$dockerlogin--username--password可以使用dockerlogin命令而不带参数,并且Docker会交互式地要求用户名和密码。

现在,我们可以使用push命令将图像存储在注册表中:

为了演示注册表的工作原理,我们可以在本地删除图像并从注册表中检索它:

$dockerrmiubuntu_with_pythonleszko/ubuntu_with_python:1我们可以使用dockerimages命令看到图像已被删除。然后,让我们从注册表中检索图像:

$dockerpullleszko/ubuntu_with_python:1如果您使用免费的DockerHub帐户,您可能需要在拉取之前将ubuntu_with_python存储库更改为公共。

我们可以使用dockerimages命令确认图像已经恢复。

当我们配置了注册表并了解了它的工作原理后,我们可以看到如何在持续交付流水线中使用它并构建验收测试阶段。

我们已经理解了验收测试的概念,并知道如何配置Docker注册表,因此我们已经准备好在Jenkins流水线中进行第一次实现。

让我们看一下呈现我们将使用的过程的图表:

该过程如下:

为了简单起见,我们将在本地运行Docker容器(而不是在单独的暂存服务器上)。为了远程运行它,我们需要使用-H选项或配置DOCKER_HOST环境变量。我们将在下一章中介绍这部分内容。

让我们继续上一章开始的流水线,并添加三个更多的阶段:

请记住,您需要在Jenkins执行器(代理从属节点或主节点,在无从属节点配置的情况下)上安装Docker工具,以便它能够构建Docker镜像。

我们希望将计算器项目作为Docker容器运行,因此我们需要创建Dockerfile,并在Jenkinsfile中添加"Docker构建"阶段。

让我们在计算器项目的根目录中创建Dockerfile:

FROMfrolvlad/alpine-oraclejdk8:slimCOPYbuild/libs/calculator-0.0.1-SNAPSHOT.jarapp.jarENTRYPOINT["java","-jar","app.jar"]Gradle的默认构建目录是build/libs/,calculator-0.0.1-SNAPSHOT.jar是打包成一个JAR文件的完整应用程序。请注意,Gradle自动使用Maven风格的版本0.0.1-SNAPSHOT对应用程序进行了版本化。

Dockerfile使用包含JDK8的基础镜像(frolvlad/alpine-oraclejdk8:slim)。它还复制应用程序JAR(由Gradle创建)并运行它。让我们检查应用程序是否构建并运行:

我们可以停止容器,并将Dockerfile推送到GitHub存储库:

stage("Package"){steps{sh"./gradlewbuild"}}stage("Dockerbuild"){steps{sh"dockerbuild-tleszko/calculator."}}我们没有明确为镜像版本,但每个镜像都有一个唯一的哈希ID。我们将在下一章中介绍明确的版本控制。

请注意,我们在镜像标签中使用了Docker注册表名称。没有必要将镜像标记两次为“calculator”和leszko/calculator。

当我们提交并推送Jenkinsfile时,流水线构建应该会自动开始,我们应该看到所有的方框都是绿色的。这意味着Docker镜像已经成功构建。

当镜像准备好后,我们可以将其存储在注册表中。Dockerpush阶段非常简单。只需在Jenkinsfile中添加以下代码即可:

要执行验收测试,首先需要将应用程序部署到暂存环境,然后针对其运行验收测试套件。

让我们添加一个阶段来运行calculator容器:

stage("Deploytostaging"){steps{sh"dockerrun-d--rm-p8765:8080--namecalculatorleszko/calculator"}}运行此阶段后,calculator容器将作为守护程序运行,将其端口发布为8765,并在停止时自动删除。

验收测试通常需要运行一个专门的黑盒测试套件,检查系统的行为。我们将在“编写验收测试”部分进行介绍。目前,为了简单起见,让我们通过使用curl工具调用Web服务端点并使用test命令检查结果来执行验收测试。

在项目的根目录中,让我们创建acceptance_test.sh文件:

#!/bin/bashtest$(curllocalhost:8765/suma=1\&b=2)-eq3我们使用参数a=1和b=2调用sum端点,并期望收到3的响应。

然后,Acceptancetest阶段可以如下所示:

stage("Acceptancetest"){steps{sleep60sh"./acceptance_test.sh"}}由于dockerrun-d命令是异步的,我们需要使用sleep操作来确保服务已经在运行。

没有好的方法来检查服务是否已经在运行。睡眠的替代方法可能是一个脚本,每秒检查服务是否已经启动。

作为验收测试的最后一步,我们可以添加分段环境清理。这样做的最佳位置是在post部分,以确保即使失败也会执行:

没有依赖关系的生活是轻松的。然而,在现实生活中,几乎每个应用程序都链接到数据库、缓存、消息系统或另一个应用程序。在(微)服务架构的情况下,每个服务都需要一堆其他服务来完成其工作。单片架构并没有消除这个问题,一个应用程序通常至少有一些依赖,至少是数据库。

当涉及到自动化验收测试时,依赖问题不再仅仅是便利的问题,而是变成了必要性。虽然在单元测试期间,我们可以模拟依赖关系,但验收测试套件需要一个完整的环境。我们如何快速设置并以可重复的方式进行?幸运的是,DockerCompose是一个可以帮助的工具。

DockerCompose是一个用于定义、运行和管理多容器Docker应用程序的工具。服务在配置文件(YAML格式)中定义,并可以使用单个命令一起创建和运行。

DockerCompose使用标准的Docker机制来编排容器,并提供了一种方便的方式来指定整个环境。

DockerCompose具有许多功能,最有趣的是:

我们从安装过程开始介绍DockerCompose工具,然后介绍docker-compose.yml配置文件和docker-compose命令,最后介绍构建和扩展功能。

安装DockerCompose的最简单方法是使用pip软件包管理器:

$pipinstalldocker-compose要检查DockerCompose是否已安装,我们可以运行:

docker-compose.yml文件用于定义容器的配置、它们之间的关系和运行时属性。

换句话说,当Dockerfile指定如何创建单个Docker镜像时,docker-compose.yml指定了如何在Docker镜像之外设置整个环境。

让我们从一个例子开始,假设我们的计算器项目使用Redis服务器进行缓存。在这种情况下,我们需要一个包含两个容器calculator和redis的环境。在一个新目录中,让我们创建docker-compose.yml文件。

version:"3"services:calculator:image:calculator:latestports:-8080redis:image:redis:latest环境配置如下图所示:

让我们来看看这两个容器的定义:

如果我们希望通过不同的主机名来访问服务(例如,通过redis-cache而不是redis),那么我们可以使用链接关键字创建别名。

docker-compose命令读取定义文件并创建环境:

$docker-composeup-d该命令在后台启动了两个容器,calculator和redis(使用-d选项)。我们可以检查容器是否在运行:

$docker-composepsNameCommandStatePorts---------------------------------------------------------------------------project_calculator_1java-jarapp.jarUp0.0.0.0:8080->8080/tcpproject_redis_1docker-entrypoint.shredis...Up6379/tcp容器名称以项目名称project为前缀,该名称取自放置docker-compose.yml文件的目录的名称。我们可以使用-p选项手动指定项目名称。由于DockerCompose是在Docker之上运行的,我们也可以使用docker命令来确认容器是否在运行:

$dockerpsCONTAINERIDIMAGECOMMANDPORTS360518e46bd3calculator:latest"java-jarapp.jar"0.0.0.0:8080->8080/tcp2268b9f1e14bredis:latest"docker-entrypoint..."6379/tcp完成后,我们可以拆除环境:

$docker-composedown这个例子非常简单,但这个工具本身非常强大。通过简短的配置和一堆命令,我们可以控制所有服务的编排。在我们将DockerCompose用于验收测试之前,让我们看看另外两个DockerCompose的特性:构建镜像和扩展容器。

在前面的例子中,我们首先使用dockerbuild命令构建了calculator镜像,然后可以在docker-compose.yml中指定它。还有另一种方法让DockerCompose构建镜像。在这种情况下,我们需要在配置中指定build属性而不是image。

让我们把docker-compose.yml文件放在计算器项目的目录中。当Dockerfile和DockerCompose配置在同一个目录中时,前者可以如下所示:

version:"3"services:calculator:build:.ports:-8080redis:image:redis:latestdocker-composebuild命令构建镜像。我们还可以要求DockerCompose在运行容器之前构建镜像,使用docker-compose--buildup命令。

DockerCompose提供了自动创建多个相同容器实例的功能。我们可以在docker-compose.yml中指定replicas:参数,也可以使用docker-composescale命令。

例如,让我们再次运行环境并复制calculator容器:

$docker-composeup-d$docker-composescalecalculator=5我们可以检查正在运行的容器:

$docker-composepsNameCommandStatePorts---------------------------------------------------------------------------calculator_calculator_1java-jarapp.jarUp0.0.0.0:32777->8080/tcpcalculator_calculator_2java-jarapp.jarUp0.0.0.0:32778->8080/tcpcalculator_calculator_3java-jarapp.jarUp0.0.0.0:32779->8080/tcpcalculator_calculator_4java-jarapp.jarUp0.0.0.0:32781->8080/tcpcalculator_calculator_5java-jarapp.jarUp0.0.0.0:32780->8080/tcpcalculator_redis_1docker-entrypoint.shredis...Up6379/tcp五个calculator容器完全相同,除了容器ID、容器名称和发布端口号。

它们都使用相同的Redis容器实例。现在我们可以停止并删除所有容器:

$docker-composedown扩展容器是DockerCompose最令人印象深刻的功能之一。通过一个命令,我们可以扩展克隆实例的数量。DockerCompose负责清理不再使用的容器。

我们已经看到了DockerCompose工具最有趣的功能。

在接下来的部分,我们将重点介绍如何在自动验收测试的情境中使用它。

DockerCompose非常适合验收测试流程,因为它可以通过一个命令设置整个环境。更重要的是,在测试完成后,也可以通过一个命令清理环境。如果我们决定在生产环境中使用DockerCompose,那么另一个好处是验收测试使用的配置、工具和命令与发布的应用程序完全相同。

要了解如何在Jenkins验收测试阶段应用DockerCompose,让我们继续计算器项目示例,并将基于Redis的缓存添加到应用程序中。然后,我们将看到两种不同的方法来运行验收测试:先Jenkins方法和先Docker方法。

DockerCompose提供了容器之间的依赖关系;换句话说,它将一个容器链接到另一个容器。从技术上讲,这意味着容器共享相同的网络,并且一个容器可以从另一个容器中看到。为了继续我们的示例,我们需要在代码中添加这个依赖关系,我们将在几个步骤中完成。

在build.gradle文件中,在dependencies部分添加以下配置:

compile"org.springframework.data:spring-data-redis:1.8.0.RELEASE"compile"redis.clients:jedis:2.9.0"它添加了负责与Redis通信的Java库。

添加一个新文件src/main/java/com/leszko/calculator/CacheConfig.java:

当缓存配置好后,我们最终可以将缓存添加到我们的网络服务中。为了做到这一点,我们需要更改src/main/java/com/leszko/calculator/Calculator.java文件如下:

packagecom.leszko.calculator;importorg.springframework.cache.annotation.Cacheable;importorg.springframework.stereotype.Service;/**Calculatorlogic*/@ServicepublicclassCalculator{@Cacheable("sum")publicintsum(inta,intb){returna+b;}}从现在开始,求和计算将被缓存在Redis中,当我们调用calculator网络服务的/sum端点时,它将首先尝试从缓存中检索结果。

假设我们的docker-compose.yml在计算器项目的目录中,我们现在可以启动容器了:

$./gradlewcleanbuild$docker-composeup--build-d我们可以检查计算器服务发布的端口:

$docker-composeportcalculator80800.0.0.0:32783如果我们在localhost:32783/suma=1&b=2上打开浏览器,计算器服务应该回复3,同时访问redis服务并将缓存值存储在那里。为了查看缓存值是否真的存储在Redis中,我们可以访问redis容器并查看Redis数据库内部:

$docker-composeexecredisredis-cli127.0.0.1:6379>keys*1)"\xac\xed\x00\x05sr\x00/org.springframework.cache.interceptor.SimpleKeyL\nW\x03km\x93\xd8\x02\x00\x02I\x00\bhashCode\x00\x06paramst\x00\x13[Ljava/lang/Object;xp\x00\x00\x03\xe2ur\x00\x13[Ljava.lang.Object;\x90\xceX\x9f\x10s)l\x02\x00\x00xp\x00\x00\x00\x02sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x01sq\x00~\x00\x05\x00\x00\x00\x02"2)"sum~keys"docker-composeexec命令在redis容器内执行了redis-cli(Redis客户端以浏览其数据库内容)命令。然后,我们可以运行keys*来打印Redis中存储的所有内容。

您可以通过计算器应用程序进行更多操作,并使用不同的值在浏览器中打开,以查看Redis服务内容增加。之后,重要的是使用docker-composedown命令拆除环境。

在接下来的章节中,我们将看到多容器项目的两种验收测试方法。显然,在Jenkins上采取任何行动之前,我们需要提交并推送所有更改的文件(包括docker-compose.yml)到GitHub。

请注意,对于进一步的步骤,Jenkins执行器上必须安装DockerCompose。

第一种方法是以与单容器应用程序相同的方式执行验收测试。唯一的区别是现在我们有两个容器正在运行,如下图所示:

从用户角度来看,redis容器是不可见的,因此单容器和多容器验收测试之间唯一的区别是我们使用docker-composeup命令而不是dockerrun。

其他Docker命令也可以用它们的DockerCompose等效命令替换:docker-composebuild替换dockerbuild,docker-composepush替换dockerpush。然而,如果我们只构建一个镜像,那么保留Docker命令也是可以的。

让我们改变部署到暂存阶段来使用DockerCompose:

stage("Deploytostaging"){steps{sh"docker-composeup-d"}}我们必须以完全相同的方式改变清理:

post{always{sh"docker-composedown"}}改变验收测试阶段为了使用docker-composescale,我们没有指定我们的web服务将发布在哪个端口号下。如果我们这样做了,那么扩展过程将失败,因为所有克隆将尝试在相同的端口号下发布。相反,我们让Docker选择端口。因此,我们需要改变acceptance_test.sh脚本,首先找出端口号是多少,然后使用正确的端口号运行curl。

#!/bin/bashCALCULATOR_PORT=$(docker-composeportcalculator8080|cut-d:-f2)test$(curllocalhost:$CALCULATOR_PORT/suma=1\&b=2)-eq3让我们找出我们是如何找到端口号的:

我们可以将更改推送到GitHub并观察Jenkins的结果。这个想法和单容器应用程序的想法是一样的,设置环境,运行验收测试套件,然后拆除环境。尽管这种验收测试方法很好并且运行良好,让我们看看另一种解决方案。

在Docker-first方法中,我们创建了一个额外的test容器,它从Docker主机内部执行测试,如下图所示:

这种方法在检索端口号方面简化了验收测试脚本,并且可以在没有Jenkins的情况下轻松运行。它也更符合Docker的风格。

缺点是我们需要为测试目的创建一个单独的Dockerfile和DockerCompose配置。

我们将首先为验收测试创建一个单独的Dockerfile。让我们在计算器项目中创建一个新目录acceptance和一个Dockerfile。

FROMubuntu:trustyRUNapt-getupdate&&\apt-getinstall-yqcurlCOPYtest.sh.CMD["bash","test.sh"]它创建一个运行验收测试的镜像。

在同一个目录下,让我们创建docker-compose-acceptance.yml来提供测试编排:

version:"3"services:test:build:./acceptance它创建一个新的容器,链接到被测试的容器:calculator。而且,内部始终是8080,这就消除了端口查找的麻烦部分。

最后缺失的部分是测试脚本。在同一目录下,让我们创建代表验收测试的test.sh文件:

#!/bin/bashsleep60test$(curlcalculator:8080/suma=1\&b=2)-eq3它与之前的验收测试脚本非常相似,唯一的区别是我们可以通过calculator主机名来访问计算器服务,端口号始终是8080。此外,在这种情况下,我们在脚本内等待,而不是在Jenkinsfile中等待。

我们可以使用根项目目录下的DockerCompose命令在本地运行测试:

$docker-compose-fdocker-compose.yml-facceptance/docker-compose-acceptance.yml-pacceptanceup-d--build该命令使用两个DockerCompose配置来运行acceptance项目。其中一个启动的容器应该被称为acceptance_test_1,并对其结果感兴趣。我们可以使用以下命令检查其日志:

$dockerlogsacceptance_test_1%Total%Received%XferdAverageSpeedTime1001100100100:00:01日志显示curl命令已成功调用。如果我们想要检查测试是成功还是失败,可以检查容器的退出代码:

$dockerwaitacceptance_test_100退出代码表示测试成功。除了0之外的任何代码都意味着测试失败。测试完成后,我们应该像往常一样清理环境:

$docker-compose-fdocker-compose.yml-facceptance/docker-compose-acceptance.yml-pacceptancedown更改验收测试阶段最后一步,我们可以将验收测试执行添加到流水线中。让我们用一个新的验收测试阶段替换Jenkinsfile中的最后三个阶段:

stage("Acceptancetest"){steps{sh"docker-compose-fdocker-compose.yml-facceptance/docker-compose-acceptance.ymlbuildtest"sh"docker-compose-fdocker-compose.yml-facceptance/docker-compose-acceptance.yml-pacceptanceup-d"sh'test$(dockerwaitacceptance_test_1)-eq0'}}这一次,我们首先构建test服务。不需要构建calculator镜像;它已经在之前的阶段完成了。最后,我们应该清理环境:

post{always{sh"docker-compose-fdocker-compose.yml-facceptance/docker-compose-acceptance.yml-pacceptancedown"}}在Jenkinsfile中添加了这个之后,我们就完成了第二种方法。我们可以通过将所有更改推送到GitHub来测试这一点。

总之,让我们比较两种解决方案。第一种方法是从用户角度进行真正的黑盒测试,Jenkins扮演用户的角色。优点是它非常接近于在生产中将要做的事情;最后,我们将通过其Docker主机访问容器。第二种方法是从另一个容器的内部测试应用程序。这种解决方案在某种程度上更加优雅,可以以简单的方式在本地运行;但是,它需要创建更多的文件,并且不像在生产中将来要做的那样通过其Docker主机调用应用程序。

在下一节中,我们将远离Docker和Jenkins,更仔细地研究编写验收测试的过程。

验收测试是为用户编写的,应该让用户能够理解。这就是为什么编写它们的方法取决于客户是谁。

例如,想象一个纯粹的技术人员。如果你编写了一个优化数据库存储的Web服务,而你的系统只被其他系统使用,并且只被其他开发人员读取,那么你的测试可以以与单元测试相同的方式表达。通常情况下,测试是好的,如果开发人员和用户都能理解。

在现实生活中,大多数软件都是为了提供特定的业务价值而编写的,而这个业务价值是由非开发人员定义的。因此,我们需要一种共同的语言来合作。一方面,业务了解需要什么,但不知道如何做;另一方面,开发团队知道如何做,但不知道需要什么。幸运的是,有许多框架可以帮助连接这两个世界,例如Cucumber、FitNesse、JBehave、Capybara等等。它们彼此之间有所不同,每一个都可能成为一本单独的书的主题;然而,编写验收测试的一般思想是相同的,并且可以用以下图表来表示:

验收标准由用户(或其代表产品所有者)与开发人员的帮助下编写。它们通常以以下场景的形式编写:

GivenIhavetwonumbers:1and2WhenthecalculatorsumsthemThenIreceive3asaresult开发人员编写称为fixtures或步骤定义的测试实现,将人性化的DSL规范与编程语言集成在一起。因此,我们有了一个可以很好集成到持续交付管道中的自动化测试。

不用说,编写验收测试是一个持续的敏捷过程,而不是瀑布式过程。这需要开发人员和业务方的不断协作,以改进和维护测试规范。

对于具有用户界面的应用程序,直接通过界面执行验收测试可能很诱人(例如,通过记录Selenium脚本);然而,如果没有正确执行,这种方法可能导致测试速度慢且与界面层紧密耦合的问题。

让我们看看实践中编写验收测试的样子,以及如何将它们绑定到持续交付管道中。

让我们使用黄瓜框架为计算器项目创建一个验收测试。如前所述,我们将分三步完成这个过程:

让我们将业务规范放在src/test/resources/feature/calculator.feature中:

Feature:CalculatorScenario:SumtwonumbersGivenIhavetwonumbers:1and2WhenthecalculatorsumsthemThenIreceive3asaresult这个文件应该由用户在开发人员的帮助下创建。请注意,它是以非技术人员可以理解的方式编写的。

下一步是创建Java绑定,以便特性规范可以被执行。为了做到这一点,我们创建一个新文件src/test/java/acceptance/StepDefinitions.java:

要运行自动化测试,我们需要进行一些配置:

testCompile("info.cukes:cucumber-java:1.2.4")testCompile("info.cukes:cucumber-junit:1.2.4")taskacceptanceTest(type:Test){include'**/acceptance/**'systemPropertiesSystem.getProperties()}test{exclude'**/acceptance/**'}这将测试分为单元测试(使用./gradlewtest运行)和验收测试(使用./gradlewacceptanceTest运行)。

packageacceptance;importcucumber.api.CucumberOptions;importcucumber.api.junit.Cucumber;importorg.junit.runner.RunWith;/**AcceptanceTest*/@RunWith(Cucumber.class)@CucumberOptions(features="classpath:feature")publicclassAcceptanceTest{}这是验收测试套件的入口点。

在进行此配置之后,如果服务器正在本地主机上运行,我们可以通过执行以下代码来测试它:

最后一个问题是,在软件开发生命周期的哪个阶段应准备验收测试?或者换句话说,我们应该在编写代码之前还是之后创建验收测试?

从技术上讲,结果是一样的;代码既有单元测试,也有验收测试覆盖。然而,考虑先编写测试的想法是很诱人的。TDD(测试驱动开发)的理念可以很好地适用于验收测试。如果在编写代码之前编写单元测试,结果代码会更清洁、结构更好。类似地,如果在系统功能之前编写验收测试,结果功能将更符合客户的需求。这个过程,通常称为验收测试驱动开发,如下图所示:

用户与开发人员以人性化的DSL格式编写验收标准规范。开发人员编写固定装置,测试失败。然后,使用TDD方法进行内部功能开发。功能完成后,验收测试应该通过,这表明功能已完成。

一个非常好的做法是将Cucumber功能规范附加到问题跟踪工具(例如JIRA)中的请求票据上,以便功能总是与其验收测试一起请求。一些开发团队采取了更激进的方法,拒绝在没有准备验收测试的情况下开始开发过程。毕竟,这是有道理的,你怎么能开发客户无法测试的东西呢?

在本章中,我们涵盖了很多新材料,为了更好地理解,我们建议做练习,并创建自己的验收测试项目:

验收标准以以下Cucumber功能的形式交付:

Scenario:StorebookinthelibraryGiven:Book"TheLordoftheRings"by"J.R.R.Tolkien"withISBNnumber"0395974682"When:IstorethebookinlibraryThen:IamabletoretrievethebookbytheISBNnumber摘要在本章中,您学会了如何构建完整和功能齐全的验收测试阶段,这是持续交付过程的重要组成部分。本章的关键要点:

在下一章中,我们将介绍完成持续交付流水线所需的缺失阶段。

我们已经涵盖了持续交付过程的两个最关键的阶段:提交阶段和自动接受测试。在本章中,我们将专注于配置管理,将虚拟容器化环境与真实服务器基础设施连接起来。

举个例子,我们可以想象一个使用Redis服务器的计算器Web服务。让我们看一下展示配置管理工具如何工作的图表。

配置管理工具读取配置文件并相应地准备环境(安装依赖工具和库,将应用程序部署到多个实例)。

在前面的例子中,基础设施配置指定了计算器服务应该在服务器1和服务器2上部署两个实例,并且Redis服务应该安装在服务器3上。计算器应用程序配置指定了Redis服务器的端口和地址,以便服务之间可以通信。

配置可能因环境类型(QA、staging、production)的不同而有所不同,例如,服务器地址可能不同。

现代配置管理解决方案应该是什么样的?让我们来看看最重要的因素:

在创建配置时以及在选择正确的配置管理工具之前,重要的是要牢记这些要点。

最流行的配置管理工具是Ansible、Puppet和Chef。它们每个都是一个不错的选择;它们都是开源产品,有免费的基本版本和付费的企业版本。它们之间最重要的区别是:

无代理的特性是一个重要的优势,因为它意味着不需要在服务器上安装任何东西。此外,Ansible正在迅速上升,这就是为什么选择它作为本书的原因。然而,其他工具也可以成功地用于持续交付过程。

Ansible是一个开源的、无代理的自动化引擎,用于软件供应、配置管理和应用部署。它于2012年首次发布,其基本版本对个人和商业用途都是免费的。企业版称为AnsibleTower,提供GUI管理和仪表板、RESTAPI、基于角色的访问控制等更多功能。

我们介绍了安装过程以及如何单独使用它以及与Docker一起使用的描述。

Ansible使用SSH协议进行通信,对其管理的机器没有特殊要求。也没有中央主服务器,因此只需在任何地方安装Ansible客户端工具,就可以用它来管理整个基础架构。

被管理的机器的唯一要求是安装Python工具和SSH服务器。然而,这些工具几乎总是默认情况下在任何服务器上都可用。

安装说明因操作系统而异。在Ubuntu的情况下,只需运行以下命令即可:

安装过程完成后,我们可以执行Ansible命令来检查是否一切都安装成功。

$ansible--versionansible2.3.2.0configfile=/etc/ansible/ansible.cfgconfiguredmodulesearchpath=Defaultw/ooverrides基于Docker的Ansible客户端还可以将Ansible用作Docker容器。我们可以通过运行以下命令来实现:

$dockerrunwilliamyeh/ansible:ubuntu14.04ansible-playbook2.3.2.0configfile=/etc/ansible/ansible.cfgconfiguredmodulesearchpath=Defaultw/ooverridesAnsibleDocker镜像不再得到官方支持,因此唯一的解决方案是使用社区驱动的版本。您可以在DockerHub页面上阅读更多关于其用法的信息。

为了使用Ansible,首先需要定义清单,代表可用资源。然后,我们将能够执行单个命令或使用Ansibleplaybook定义一组任务。

清单是由Ansible管理的所有服务器的列表。每台服务器只需要安装Python解释器和SSH服务器。默认情况下,Ansible假定使用SSH密钥进行身份验证;但是,也可以通过在Ansible命令中添加--ask-pass选项来使用用户名和密码进行身份验证。

SSH密钥可以使用ssh-keygen工具生成,并通常存储在~/.ssh目录中。

清单是在/etc/ansible/hosts文件中定义的,它具有以下结构:

清单文件中可能有0个或多个组。例如,让我们在一个服务器组中定义两台机器。

[webservers]192.168.0.241192.168.0.242我们还可以创建带有服务器别名的配置,并指定远程用户:

我们可以运行的最简单的命令是对所有服务器进行ping测试。

$ansibleall-mpingweb1|SUCCESS=>{"changed":false,"ping":"pong"}web2|SUCCESS=>{"changed":false,"ping":"pong"}我们使用了-m选项,允许指定应在远程主机上执行的模块。结果是成功的,这意味着服务器是可达的,并且身份验证已正确配置。

请注意,我们使用了all,以便可以处理所有服务器,但我们也可以通过组名webservers或单个主机别名来调用它们。作为第二个例子,让我们只在其中一个服务器上执行一个shell命令。

$ansibleweb1-a"/bin/echohello"web1|SUCCESS|rc=0>>hello-a选项指定传递给Ansible模块的参数。在这种情况下,我们没有指定模块,因此参数将作为shellUnix命令执行。结果是成功的,并且打印了hello。

如果ansible命令第一次连接服务器(或服务器重新安装),那么我们会收到密钥确认消息(当主机不在known_hosts中时的SSH消息)。由于这可能会中断自动化脚本,我们可以通过取消注释/etc/ansible/ansible.cfg文件中的host_key_checking=False或设置环境变量ANSIBLE_HOST_KEY_CHECKING=False来禁用提示消息。

在其简单形式中,Ansible临时命令的语法如下:

ansible-m-a临时命令的目的是在不必重复时快速执行某些操作。例如,我们可能想要检查服务器是否存活,或者在圣诞假期关闭所有机器。这种机制可以被视为在一组机器上执行命令,并由模块提供的附加语法简化。然而,Ansible自动化的真正力量在于playbooks。

Ansibleplaybook是一个配置文件,描述了服务器应该如何配置。它提供了一种定义一系列任务的方式,这些任务应该在每台机器上执行。Playbook使用YAML配置语言表示,这使得它易于阅读和理解。让我们从一个示例playbook开始,然后看看我们如何使用它。

一个playbook由一个或多个plays组成。每个play包含一个主机组名称,要执行的任务以及配置细节(例如,远程用户名或访问权限)。一个示例playbook可能如下所示:

----hosts:web1become:yesbecome_method:sudotasks:-name:ensureapacheisatthelatestversionapt:name=apache2state=latest-name:ensureapacheisrunningservice:name=apache2state=startedenabled=yes此配置包含一个play,其中:

例如,我们可以在清单中定义两组服务器:database和webservers。然后,在playbook中,我们可以指定应该在所有托管数据库的机器上执行的任务,以及应该在所有web服务器上执行的一些不同的任务。通过使用一个命令,我们可以设置整个环境。

当定义了playbook.yml时,我们可以使用ansible-playbook命令来执行它。

$ansible-playbookplaybook.ymlPLAY[web1]*************************************************************TASK[setup]************************************************************ok:[web1]TASK[ensureapacheisatthelatestversion]***************************changed:[web1]TASK[ensureapacheisrunning]*****************************************ok:[web1]PLAYRECAP**************************************************************web1:ok=3changed=1unreachable=0failed=0如果服务器需要输入sudo命令的密码,那么我们需要在ansible-playbook命令中添加--ask-sudo-pass选项。也可以通过设置额外变量-eansible_become_pass=来传递sudo密码(如果需要)。

已执行playbook配置,因此安装并启动了apache2工具。请注意,如果任务在服务器上做了一些改变,它会被标记为changed。相反,如果没有改变,它会被标记为ok。

可以使用-f选项并行运行任务。

我们可以再次执行命令。

$ansible-playbookplaybook.ymlPLAY[web1]*************************************************************TASK[setup]************************************************************ok:[web1]TASK[ensureapacheisatthelatestversion]***************************ok:[web1]TASK[ensureapacheisrunning]*****************************************ok:[web1]PLAYRECAP**************************************************************web1:ok=3changed=0unreachable=0failed=0请注意输出略有不同。这次命令没有在服务器上做任何改变。这是因为每个Ansible模块都设计为幂等的。换句话说,按顺序多次执行相同的模块应该与仅执行一次相同。

实现幂等性的最简单方法是始终首先检查任务是否尚未执行,并且仅在尚未执行时执行它。幂等性是一个强大的特性,我们应该始终以这种方式编写我们的Ansible任务。

如果所有任务都是幂等的,那么我们可以随意执行它们。在这种情况下,我们可以将playbook视为远程机器期望状态的描述。然后,ansible-playbook命令负责将机器(或一组机器)带入该状态。

某些操作应仅在某些其他任务更改时执行。例如,假设您将配置文件复制到远程机器,并且只有在配置文件更改时才应重新启动Apache服务器。如何处理这种情况?

例如,假设您将配置文件复制到远程机器,并且只有在配置文件更改时才应重新启动Apache服务器。如何处理这种情况?

Ansible提供了一种基于事件的机制来通知变化。为了使用它,我们需要知道两个关键字:

让我们看一个例子,我们如何将配置复制到服务器并且仅在配置更改时重新启动Apache。

tasks:-name:copyconfigurationcopy:src:foo.confdest:/etc/foo.confnotify:-restartapachehandlers:-name:restartapacheservice:name:apache2state:restarted现在,我们可以创建foo.conf文件并运行ansible-playbook命令。

$touchfoo.conf$ansible-playbookplaybook.yml...TASK[copyconfiguration]**********************************************changed:[web1]RUNNINGHANDLER[restartapache]***************************************changed:[web1]PLAYRECAP*************************************************************web1:ok=5changed=2unreachable=0failed=0处理程序始终在play结束时执行,只执行一次,即使由多个任务触发。

Ansible复制了文件并重新启动了Apache服务器。重要的是要理解,如果我们再次运行命令,将不会发生任何事情。但是,如果我们更改foo.conf文件的内容,然后运行ansible-playbook命令,文件将再次被复制(并且Apache服务器将被重新启动)。

$echo"something">foo.conf$ansible-playbookplaybook.yml...TASK[copyconfiguration]***********************************************changed:[web1]RUNNINGHANDLER[restartapache]****************************************changed:[web1]PLAYRECAP**************************************************************web1:ok=5changed=2unreachable=0failed=0我们使用了copy模块,它足够智能,可以检测文件是否已更改,然后在这种情况下在服务器上进行更改。

虽然Ansible自动化使多个主机的事物变得相同和可重复,但不可避免地,服务器可能需要一些差异。例如,考虑应用程序端口号。它可能因机器而异。幸运的是,Ansible提供了变量,这是一个处理服务器差异的良好机制。让我们创建一个新的playbook并定义一个变量。

例如,考虑应用程序端口号。它可能因机器而异。幸运的是,Ansible提供了变量,这是一个处理服务器差异的良好机制。让我们创建一个新的playbook并定义一个变量。

debug模块在执行时打印消息。如果我们运行ansible-playbook命令,就可以看到变量的使用情况。

除了用户定义的变量,还有预定义的自动变量。例如,hostvars变量存储了有关清单中所有主机信息的映射。使用Jinja2语法,我们可以迭代并打印清单中所有主机的IP地址。

----hosts:web1tasks:-name:printIPaddressdebug:msg:"{%forhostingroups['all']%}{{hostvars[host]['ansible_host']}}{%endfor%}"然后,我们可以执行ansible-playbook命令。

$ansible-playbookplaybook.yml...TASK[printIPaddress]************************************************ok:[web1]=>{"msg":"192.168.0.241192.168.0.242"}请注意,使用Jinja2语言,我们可以在Ansible剧本文件中指定流程控制操作。

我们可以使用Ansible剧本在远程服务器上安装任何工具。想象一下,我们想要一个带有MySQL的服务器。我们可以轻松地准备一个类似于带有apache2包的playbook。然而,如果你想一想,带有MySQL的服务器是一个相当常见的情况,肯定有人已经为此准备了一个playbook,所以也许我们可以重用它?这就是Ansible角色和AnsibleGalaxy的用武之地。

Ansible角色是一个精心构建的剧本部分,准备包含在剧本中。角色是独立的单元,始终具有以下目录结构:

...-name:EnsureMySQLPythonlibrariesareinstalled.apt:"name=python-mysqldbstate=installed"-name:EnsureMySQLpackagesareinstalled.apt:"name={{item}}state=installed"with_items:"{{mysql_packages}}"register:deb_mysql_install_packages...这只是在tasks/main.yml文件中定义的任务之一。其他任务负责MySQL配置。

with_items关键字用于在所有项目上创建循环。when关键字意味着任务仅在特定条件下执行。

如果我们使用这个角色,那么为了在服务器上安装MySQL,只需创建以下playbook.yml:

----hosts:allbecome:yesbecome_method:sudoroles:-role:geerlingguy.mysqlbecome:yes这样的配置使用geerlingguy.mysql角色将MySQL数据库安装到所有服务器上。

要从AnsibleGalaxy安装角色,我们可以使用ansible-galaxy命令。

$ansible-galaxyinstallusername.role_name此命令会自动下载角色。在MySQL示例中,我们可以通过执行以下命令下载角色:

$ansible-galaxyinstallgeerlingguy.mysql该命令下载mysql角色,可以在playbook文件中后续使用。

我们已经介绍了Ansible的最基本功能。现在,让我们暂时忘记Docker,使用Ansible配置完整的部署步骤。我们将在一个服务器上运行计算器服务,而在第二个服务器上运行Redis服务。

我们可以在新的playbook中指定一个play。让我们创建playbook.yml文件,内容如下:

----hosts:web1become:yesbecome_method:sudotasks:-name:installRedisapt:name:redis-serverstate:present-name:startRedisservice:name:redis-serverstate:started-name:copyRedisconfigurationcopy:src:redis.confdest:/etc/redis/redis.confnotify:restartRedishandlers:-name:restartRedisservice:name:redis-serverstate:restarted该配置在一个名为web1的服务器上执行。它安装redis-server包,复制Redis配置,并启动Redis。请注意,每次更改redis.conf文件的内容并重新运行ansible-playbook命令时,配置都会更新到服务器上,并且Redis服务会重新启动。

我们还需要创建redis.conf文件,内容如下:

daemonizeyespidfile/var/run/redis/redis-server.pidport6379bind0.0.0.0此配置将Redis作为守护程序运行,并将其暴露给端口号为6379的所有网络接口。现在让我们定义第二个play,用于设置计算器服务。

我们分三步准备计算器Web服务:

首先,我们需要使构建的JAR文件可执行,以便它可以作为Unix服务轻松在服务器上运行。为了做到这一点,只需将以下代码添加到build.gradle文件中:

bootRepackage{executable=true}更改Redis主机地址以前,我们已将Redis主机地址硬编码为redis,所以现在我们应该在src/main/java/com/leszko/calculator/CacheConfig.java文件中将其更改为192.168.0.241。

在实际项目中,应用程序属性通常保存在属性文件中。例如,对于SpringBoot框架,有一个名为application.properties或application.yml的文件。

最后,我们可以将部署配置作为playbook.yml文件中的新play添加。

-hosts:web2become:yesbecome_method:sudotasks:-name:ensureJavaRuntimeEnvironmentisinstalledapt:name:default-jrestate:present-name:createdirectoryforCalculatorfile:path:/var/calculatorstate:directory-name:configureCalculatorasaservicefile:path:/etc/init.d/calculatorstate:linkforce:yessrc:/var/calculator/calculator.jar-name:copyCalculatorcopy:src:build/libs/calculator-0.0.1-SNAPSHOT.jardest:/var/calculator/calculator.jarmode:a+xnotify:-restartCalculatorhandlers:-name:restartCalculatorservice:name:calculatorenabled:yesstate:restarted让我们走一遍我们定义的步骤:

与往常一样,我们可以使用ansible-playbook命令执行playbook。在此之前,我们需要使用Gradle构建计算器项目。

请注意,我们通过执行一个命令配置了整个环境。而且,如果我们需要扩展服务,只需将新服务器添加到清单中并重新运行ansible-playbook命令即可。

我们已经展示了如何使用Ansible进行环境配置和应用程序部署。下一步是将Ansible与Docker一起使用。

正如您可能已经注意到的,Ansible和Docker解决了类似的软件部署问题:

如果我们比较这些工具,那么Docker做了更多,因为它提供了隔离、可移植性和某种安全性。我们甚至可以想象在没有任何其他配置管理工具的情况下使用Docker。那么,我们为什么还需要Ansible呢?

Ansible可能看起来多余;然而,它为交付过程带来了额外的好处:

我们可以将Ansible视为负责基础设施的工具,而将Docker视为负责环境配置的工具。概述如下图所示:

Ansible与Docker集成得很顺利,因为它提供了一组专门用于Docker的模块。如果我们为基于Docker的部署创建一个Ansibleplaybook,那么第一个任务需要确保Docker引擎已安装在每台机器上。然后,它应该使用Docker运行一个容器,或者使用DockerCompose运行一组交互式容器。

我们可以使用Ansibleplaybook中的以下任务来安装Docker引擎。

此配置安装Docker引擎,使admin用户能够使用Docker,并安装了DockerCompose及其依赖工具。

安装Docker后,我们可以添加一个任务,该任务将运行一个Docker容器。

使用docker_container模块来运行Docker容器,它看起来与我们为DockerCompose配置所呈现的非常相似。让我们将其添加到playbook.yml文件中。

现在我们可以执行playbook来观察Docker是否已安装并且Redis容器已启动。请注意,这是一种非常方便的使用Docker的方式,因为我们不需要在每台机器上手动安装Docker引擎。

Ansibleplaybook与DockerCompose配置非常相似。它们甚至共享相同的YAML文件格式。而且,可以直接从Ansible使用docker-compose.yml。我们将展示如何做到这一点,但首先让我们定义docker-compose.yml文件。

version:"2"services:calculator:image:leszko/calculator:latestports:-8080redis:image:redis:latest这几乎与我们在上一章中定义的内容相同。这一次,我们直接从DockerHub注册表获取计算器镜像,并且不在docker-compose.yml中构建它,因为我们希望构建一次镜像,将其推送到注册表,然后在每个部署步骤(在每个环境中)重复使用它,以确保相同的镜像部署在每台Docker主机上。当我们有了docker-compose.yml,我们就准备好向playbook.yml添加新任务了。

-name:copydocker-compose.ymlcopy:src:./docker-compose.ymldest:./docker-compose.yml-name:rundocker-composedocker_service:project_src:.state:present我们首先将docker-compose.yml文件复制到服务器,然后执行docker-compose。结果,Ansible创建了两个容器:计算器和Redis。

我们已经看到了Ansible的最重要特性。在接下来的章节中,我们会稍微介绍一下基础设施和应用程序版本控制。在本章结束时,我们将介绍如何使用Ansible来完成持续交付流程。

在本章中,我们已经介绍了Ansible的基础知识以及与Docker一起使用它的方式。作为练习,我们提出以下任务:

我们已经介绍了配置管理过程及其与Docker的关系。本章的关键要点如下:

在下一章中,我们将结束持续交付过程并完成最终的Jenkins流水线。

我们已经涵盖了持续交付过程中最关键的部分:提交阶段、构件存储库、自动验收测试和配置管理。

到目前为止,我们总是使用一个Docker主机来处理一切,并将其视为无尽资源的虚拟化,我们可以在其中部署一切。显然,Docker主机实际上可以是一组机器,我们将在接下来的章节中展示如何使用DockerSwarm创建它。然而,即使Docker主机在资源方面是无限的,我们仍然需要考虑底层基础设施,至少有两个原因:

考虑到这些事实,在本节中,我们将讨论不同类型的环境,在持续交付过程中的作用以及基础设施安全方面。

有四种最常见的环境类型:生产、暂存、QA(测试)和开发。让我们讨论每种环境及其基础设施。

生产环境是最终用户使用的环境。它存在于每家公司中,当然,它是最重要的环境。

让我们看看下面的图表,看看大多数生产环境是如何组织的:

用户通过负载均衡器访问服务,负载均衡器选择确切的机器。如果应用程序在多个物理位置发布,那么(首先)设备通常是基于DNS的地理负载均衡器。在每个位置,我们都有一个服务器集群。如果我们使用Docker,那么这个服务器集群可以隐藏在一个或多个Docker主机后面(这些主机在内部由使用DockerSwarm的许多机器组成)。

暂存环境是发布候选版本部署的地方,以便在上线之前进行最终测试。理想情况下,这个环境应该是生产环境的镜像。

让我们看看以下内容,以了解在交付过程的背景下,这样的环境应该是什么样子的:

请注意,暂存环境是生产的精确克隆。如果应用程序在多个位置部署,那么暂存环境也应该有多个位置。

在持续交付过程中,所有自动接受功能和非功能测试都针对这个环境运行。虽然大多数功能测试通常不需要相同的类似生产的基础设施,但在非功能(尤其是性能)测试的情况下,这是必须的。

为了节省成本,暂存基础设施与生产环境不同(通常包含较少的机器)并不罕见。然而,这种方法可能导致许多生产问题。MichaelT.Nygard在他的著作ReleaseIt!中举了一个真实场景的例子,其中暂存环境使用的机器比生产环境少。

故事是这样的:在某家公司,系统一直很稳定,直到某个代码更改导致生产环境变得极其缓慢,尽管所有压力测试都通过了。这是怎么可能的?事实上,有一个同步点,每个服务器都要与其他服务器通信。在暂存环境中,只有一个服务器,所以实际上没有阻塞。然而,在生产环境中,有许多服务器,导致服务器相互等待。这个例子只是冰山一角,如果暂存环境与生产环境不同,许多生产问题可能无法通过验收测试来测试。

QA环境(也称为测试环境)旨在供QA团队进行探索性测试,以及依赖我们服务的外部应用程序进行集成测试。QA环境的用例和基础设施如下图所示:

虽然暂存环境不需要稳定(在持续交付的情况下,它在每次提交到存储库的代码更改后都会更改),但QA实例需要提供一定的稳定性,并公开与生产环境相同(或向后兼容)的API。与暂存环境相反,基础设施可以与生产环境不同,因为其目的不是确保发布候选版本正常工作。

一个非常常见的情况是为了QA实例的目的分配较少的机器(例如,只来自一个位置)。

部署到QA环境通常是在一个单独的流水线中进行的,这样它就可以独立于自动发布流程。这种方法很方便,因为QA实例的生命周期与生产环境不同(例如,QA团队可能希望对从主干分支出来的实验性代码进行测试)。

开发环境可以作为所有开发人员共享的服务器创建,或者每个开发人员可以拥有自己的开发环境。这里呈现了一个简单的图表:

开发环境始终包含代码的最新版本。它用于实现开发人员之间的集成,并且可以像QA环境一样对待,但是由开发人员而不是QA使用。

其他环境通常对于持续交付并不重要。如果我们希望在每次提交时部署到QA或开发环境,那么我们可以为此创建单独的流水线(小心不要混淆主要发布流水线)。在许多情况下,部署到QA环境是手动触发的,因为它可能与生产环境有不同的生命周期。

所有环境都需要得到很好的保护。这是明显的。更明显的是,最重要的要求是保持生产环境的安全,因为我们的业务取决于它,任何安全漏洞的后果在那里可能是最严重的。

在持续交付过程中,从属必须能够访问服务器,以便它可以部署应用程序。

提供从属机器与服务器凭据的不同方法:

每种解决方案都有一些优点和缺点。在使用任何一种解决方案时,我们都必须格外小心,因为当一个从属系统可以访问生产环境时,任何人入侵从属系统就等于入侵生产环境。

最危险的解决方案是将SSH私钥放入Jenkins从属系统镜像中,因为镜像存储的所有地方(Docker注册表或带有Jenkins的Docker主机)都需要得到很好的保护。

在上一章中,我们学到了很多关于功能需求和自动化验收测试。然而,对于非功能性需求,我们应该怎么办呢?甚至更具挑战性的是,如果没有需求怎么办?在持续交付过程中,我们应该完全跳过它们吗?让我们在本节中回答这些问题。

软件的非功能性方面总是重要的,因为它们可能对系统的运行造成重大风险。

例如,许多应用程序失败,是因为它们无法承受用户数量突然增加的负载。在《可用性工程》一书中,JakobNielsen写道,1.0秒是用户思维流程保持不间断的极限。想象一下,我们的系统在负载增加的情况下开始超过这个极限。用户可能会因为性能问题而停止使用服务。考虑到这一点,非功能性测试与功能性测试一样重要。

长话短说,我们应该始终为非功能性测试采取以下步骤:

无论非功能性测试的类型如何,其思想总是相同的。然而,方法可能略有不同。让我们来看看不同的测试类型以及它们带来的挑战。

性能测试有不同的定义。在许多地方,它们意味着包括负载、压力和可伸缩性测试。有时它们也被描述为白盒测试。在本书中,我们将性能测试定义为衡量系统延迟的最基本的黑盒测试形式。

可扩展性测试应该是自动化的,并且应该提供图表,展示机器数量和并发用户数量之间的关系。这些数据有助于确定系统的限制以及增加更多机器不会有所帮助的点。

可扩展性测试,类似于压力测试,很难放入连续交付流程中,而应该保持独立。

安全测试应该作为连续交付的一个流水线阶段包括在内。它们可以使用与验收测试相同的框架编写,也可以使用专门的安全测试框架,例如BDD安全。

安全也应始终成为解释性测试过程的一部分,测试人员和安全专家会发现安全漏洞并添加新的测试场景。

恢复测试是一种确定系统在因软件或硬件故障而崩溃后能够多快恢复的技术。最好的情况是,即使系统的一部分服务停止,系统也不会完全崩溃。一些公司甚至会故意进行生产故障,以检查他们是否能够在灾难中生存。最著名的例子是Netflix和他们的混沌猴工具,该工具会随机终止生产环境的随机实例。这种方法迫使工程师编写能够使系统对故障具有弹性的代码。

恢复测试显然不是连续交付过程的一部分,而是定期事件,用于检查整体健康状况。

非功能方面给软件开发和交付带来了新的挑战:

解决非功能方面的最佳方法是采取以下步骤:

正如所述,有许多类型的非功能性测试,它们给交付过程带来了额外的挑战。然而,为了系统的稳定性,这些测试绝不能被简单地跳过。技术实现因测试类型而异,但在大多数情况下,它们可以以类似的方式实现功能验收测试,并应该针对暂存环境运行。

如果您对非功能性测试、系统属性和系统稳定性感兴趣,请阅读MichaelT.Nygard的书《发布它!》。

到目前为止,在每次Jenkins构建期间,我们都创建了一个新的Docker镜像,将其推送到Docker注册表,并在整个过程中使用最新版本。然而,这种解决方案至少有三个缺点:

管理Docker镜像版本与持续交付过程的推荐方式是什么?在本节中,我们将看到不同的版本控制策略,并学习在Jenkins流水线中创建版本的不同方法。

有不同的应用版本控制方式。

让我们讨论这些最流行的解决方案,这些解决方案可以与持续交付过程一起应用(每次提交都创建一个新版本)。

所有解决方案都可以与持续交付流程一起使用。语义化版本控制要求从构建执行向存储库提交,以便在源代码存储库中增加版本。

Maven(和其他构建工具)推广了版本快照,为未发布的版本添加了后缀SNAPSHOT,但仅用于开发过程。由于持续交付意味着发布每个更改,因此没有快照。

正如前面所述,使用软件版本控制时有不同的可能性,每种可能性都可以在Jenkins中实现。

在我们使用Docker镜像的每个地方,我们需要添加标签后缀:${BUILD_TIMESTAMP}。

例如,Docker构建阶段应该是这样的:

请注意,在显式标记图像后,它不再隐式标记为最新版本。

版本控制完成后,我们终于准备好完成持续交付流程。

在讨论了Ansible、环境、非功能测试和版本控制的所有方面后,我们准备扩展Jenkins流水线并完成一个简单但完整的持续交付流程。

我们将分几步来完成:

在最简单的形式中,我们可以有两个环境:暂存和生产,每个环境都有一个Docker主机。在现实生活中,如果我们希望在不同位置拥有服务器或具有不同要求,可能需要为每个环境添加更多的主机组。

让我们创建两个Ansible清单文件。从暂存开始,我们可以定义inventory/staging文件。假设暂存地址是192.168.0.241,它将具有以下内容:

[webservers]web1ansible_host=192.168.0.241ansible_user=admin类比而言,如果生产IP地址是192.168.0.242,那么inventory/production应该如下所示:

[webservers]web2ansible_host=192.168.0.242ansible_user=admin只为每个环境拥有一个机器可能看起来过于简化了;然而,使用DockerSwarm(我们稍后在本书中展示),一组主机可以隐藏在一个Docker主机后面。

有了定义的清单,我们可以更改验收测试以使用暂存环境。

根据我们的需求,我们可以通过在本地Docker主机上运行应用程序(就像我们在上一章中所做的那样)或者使用远程暂存环境来测试应用程序。前一种解决方案更接近于生产中发生的情况,因此可以被认为是更好的解决方案。这与上一章的方法1:首先使用Jenkins验收测试部分非常接近。唯一的区别是现在我们将应用程序部署到远程Docker主机上。

为了做到这一点,我们可以使用带有-H参数的docker(或docker-compose命令),该参数指定了远程Docker主机地址。这将是一个很好的解决方案,如果您不打算使用Ansible或任何其他配置管理工具,那么这就是前进的方式。然而,出于本章已经提到的原因,使用Ansible是有益的。在这种情况下,我们可以在持续交付管道中使用ansible-playbook命令。

stage("Deploytostaging"){steps{sh"ansible-playbookplaybook.yml-iinventory/staging"}}如果playbook.yml和docker-compose.yml看起来与使用Docker的Ansible部分中的内容相同,那么将足以将应用程序与依赖项部署到暂存环境中。

“验收测试”阶段与上一章完全相同。唯一的调整可能是暂存环境的主机名(或其负载均衡器)。还可以添加用于对运行在暂存环境上的应用程序进行性能测试或其他非功能测试的阶段。

在所有测试通过后,是时候发布应用程序了。

生产环境应尽可能接近暂存环境。发布的Jenkins步骤也应与将应用程序部署到暂存环境的阶段非常相似。

在最简单的情况下,唯一的区别是清单文件和应用程序配置(例如,在SpringBoot应用程序的情况下,我们将设置不同的Spring配置文件,这将导致使用不同的属性文件)。在我们的情况下,没有应用程序属性,所以唯一的区别是清单文件。

stage("Release"){steps{sh"ansible-playbookplaybook.yml-iinventory/production"}}实际上,如果我们想要实现零停机部署,发布步骤可能会更加复杂。关于这个主题的更多内容将在接下来的章节中介绍。

发布完成后,我们可能认为一切都已完成;然而,还有一个缺失的阶段,即冒烟测试。

冒烟测试是验收测试的一个非常小的子集,其唯一目的是检查发布过程是否成功完成。否则,我们可能会出现这样的情况:应用程序完全正常,但发布过程中出现问题,因此我们可能最终得到一个无法工作的生产环境。

冒烟测试通常与验收测试以相同的方式定义。因此,管道中的“冒烟测试”阶段应该如下所示:

stage("Smoketest"){steps{sleep60sh"./smoke_test.sh"}}设置完成后,连续交付构建应该自动运行,并且应用程序应该发布到生产环境。通过这一步,我们已经完成了连续交付管道的最简单但完全有效的形式。

总之,在最近的章节中,我们创建了相当多的阶段,这导致了一个完整的连续交付管道,可以成功地应用于许多项目。

接下来我们看到计算器项目的完整Jenkins文件:

在本章中,我们涵盖了持续交付管道的许多新方面;为了更好地理解这个概念,我们建议您进行以下练习:

在本章中,我们完成了持续交付管道,最终发布了应用程序。以下是本章的要点:

在下一章中,我们将介绍DockerSwarm工具,该工具可帮助我们创建Docker主机集群。

我们已经涵盖了持续交付流水线的所有基本方面。在本章中,我们将看到如何将Docker环境从单个Docker主机更改为一组机器,并如何与Jenkins一起使用它。

到目前为止,我们已经分别与每台机器进行了交互。即使我们使用Ansible在多台服务器上重复相同的操作,我们也必须明确指定应在哪台主机上部署给定服务。然而,在大多数情况下,如果服务器共享相同的物理位置,我们并不关心服务部署在哪台特定的机器上。我们所需要的只是让它可访问并在许多实例中复制。我们如何配置一组机器以便它们共同工作,以至于添加新的机器不需要额外的设置?这就是集群的作用。

在本节中,您将介绍服务器集群的概念和DockerSwarm工具包。

服务器集群是一组连接的计算机,它们以一种可以类似于单个系统的方式一起工作。服务器通常通过本地网络连接,连接速度足够快,以确保服务分布的影响很小。下图展示了一个简单的服务器集群:

用户通过称为管理器的主机访问集群,其界面应类似于常规Docker主机。在集群内,有多个工作节点接收任务,执行它们,并通知管理器它们的当前状态。管理器负责编排过程,包括任务分派、服务发现、负载平衡和工作节点故障检测。

管理者也可以执行任务,这是DockerSwarm的默认配置。然而,对于大型集群,管理者应该配置为仅用于管理目的。

自Docker1.12以来,DockerSwarm已经作为swarm模式被原生集成到DockerEngine中。在旧版本中,需要在每个主机上运行swarm容器以提供集群功能。

关于术语,在swarm模式下,运行的镜像称为服务,而不是在单个Docker主机上运行的容器。一个服务运行指定数量的任务。任务是swarm的原子调度单元,保存有关容器和应在容器内运行的命令的信息。副本是在节点上运行的每个容器。副本的数量是给定服务的所有容器的预期数量。

让我们看一下展示术语和DockerSwarm集群过程的图像:

我们首先指定一个服务,Docker镜像和副本的数量。管理者会自动将任务分配给工作节点。显然,每个复制的容器都是从相同的Docker镜像运行的。在所呈现的流程的上下文中,DockerSwarm可以被视为DockerEngine机制的一层,负责容器编排。

在上面的示例图像中,我们有三个任务,每个任务都在单独的Docker主机上运行。然而,也可能所有容器都在同一个Docker主机上启动。一切取决于分配任务给工作节点的管理节点使用的调度策略。我们将在后面的单独章节中展示如何配置该策略。

DockerSwarm提供了许多有趣的功能。让我们来看看最重要的几个:

让我们看看这在实践中是什么样子。

DockerEngine默认包含了Swarm模式,因此不需要额外的安装过程。由于DockerSwarm是一个本地的Docker集群系统,管理集群节点是通过docker命令完成的,因此非常简单和直观。让我们首先创建一个管理节点和两个工作节点。然后,我们将从Docker镜像运行和扩展一个服务。

为了设置一个Swarm,我们需要初始化管理节点。我们可以在一个即将成为管理节点的机器上使用以下命令来做到这一点:

$dockerswarminitSwarminitialized:currentnode(qfqzhk2bumhd2h0ckntrysm8l)isnowamanager.Toaddaworkertothisswarm,runthefollowingcommand:dockerswarmjoin\--tokenSWMTKN-1-253vezc1pqqgb93c5huc9g3n0hj4p7xik1ziz5c4rsdo3f7iw2-df098e2jpe8uvwe2ohhhcxd6w\192.168.0.143:2377Toaddamanagertothisswarm,run'dockerswarmjoin-tokenmanager'andfollowtheinstructions.一个非常常见的做法是使用--advertise-addr参数,因为如果管理机器有多个潜在的网络接口,那么dockerswarminit可能会失败。

在我们的情况下,管理机器的IP地址是192.168.0.143,显然,它必须能够从工作节点(反之亦然)访问。请注意,在控制台上打印了要在工作机器上执行的命令。还要注意,已生成了一个特殊的令牌。从现在开始,它将被用来连接机器到集群,并且必须保密。

我们可以使用dockernode命令来检查Swarm是否已创建:

$dockernodelsIDHOSTNAMESTATUSAVAILABILITYMANAGERSTATUSqfqzhk2bumhd2h0ckntrysm8l*ubuntu-managerReadyActiveLeader当管理器正常运行时,我们准备将工作节点添加到Swarm中。

$dockerswarmjoin\--tokenSWMTKN-1-253vezc1pqqgb93c5huc9g3n0hj4p7xik1ziz5c4rsdo3f7iw2-df098e2jpe8uvwe2ohhhcxd6w\192.168.0.143:2377Thisnodejoinedaswarmasaworker.我们可以使用dockernodels命令来检查节点是否已添加到Swarm中。假设我们已经添加了两个节点机器,输出应该如下所示:

$dockernodelsIDHOSTNAMESTATUSAVAILABILITYMANAGERSTATUScr7vin5xzu0331fvxkdxla22nubuntu-worker2ReadyActivemd4wx15t87nn0c3pyv24kewtzubuntu-worker1ReadyActiveqfqzhk2bumhd2h0ckntrysm8l*ubuntu-managerReadyActiveLeader在这一点上,我们有一个由三个Docker主机组成的集群,ubuntu-manager,ubuntu-worker1和ubuntu-worker2。让我们看看如何在这个集群上运行一个服务。

为了在集群上运行一个镜像,我们不使用dockerrun,而是使用专门为Swarm设计的dockerservice命令(在管理节点上执行)。让我们启动一个单独的tomcat应用并给它命名为tomcat:

$dockerservicecreate--replicas1--nametomcattomcat该命令创建了服务,因此发送了一个任务来在一个节点上启动一个容器。让我们列出正在运行的服务:

$dockerservicelsIDNAMEMODEREPLICASIMAGEx65aeojumj05tomcatreplicated1/1tomcat:latest日志确认了tomcat服务正在运行,并且有一个副本(一个Docker容器正在运行)。我们甚至可以更仔细地检查服务:

$dockerservicepstomcatIDNAMEIMAGENODEDESIREDSTATECURRENTSTATEkjy1udwcnwmitomcat.1tomcat:latestubuntu-managerRunningRunningaboutaminuteago如果您对服务的详细信息感兴趣,可以使用dockerserviceinspect命令。

从控制台输出中,我们可以看到容器正在管理节点(ubuntu-manager)上运行。它也可以在任何其他节点上启动;管理器会自动使用调度策略算法选择工作节点。我们可以使用众所周知的dockerps命令来确认容器正在运行:

$dockerpsCONTAINERIDIMAGECOMMANDCREATEDSTATUSPORTSNAMES6718d0bcba98tomcat@sha256:88483873b279aaea5ced002c98dde04555584b66de29797a4476d5e94874e6de"catalina.shrun"AboutaminuteagoUpAboutaminute8080/tcptomcat.1.kjy1udwcnwmiosiw2qn71nt1r如果我们不希望任务在管理节点上执行,可以使用--constraintnode.role==worker选项来限制服务。另一种可能性是完全禁用管理节点执行任务,使用dockernodeupdate--availabilitydrain

当服务运行时,我们可以扩展或缩小它,以便它在许多副本中运行:

$dockerservicescaletomcat=5tomcatscaledto5我们可以检查服务是否已扩展:

$dockerservicepstomcatIDNAMEIMAGENODEDESIREDSTATECURRENTSTATEkjy1udwcnwmitomcat.1tomcat:latestubuntu-managerRunningRunning2minutesago536p5zc3kaxztomcat.2tomcat:latestubuntu-worker2RunningPreparing18secondsagonpt6ui1g9bdptomcat.3tomcat:latestubuntu-managerRunningRunning18secondsagozo2kger1rmqctomcat.4tomcat:latestubuntu-worker1RunningPreparing18secondsago1fb24nf94488tomcat.5tomcat:latestubuntu-worker2RunningPreparing18secondsago请注意,这次有两个容器在manager节点上运行,一个在ubuntu-worker1节点上,另一个在ubuntu-worker2节点上。我们可以通过在每台机器上执行dockerps来检查它们是否真的在运行。

如果我们想要删除服务,只需执行以下命令即可:

Docker服务,类似于容器,具有端口转发机制。我们可以通过添加-p:参数来使用它。启动服务可能如下所示:

默认情况下,使用内部DockerSwarm负载平衡。因此,只需将所有请求发送到管理机器,它将负责在节点之间进行分发。另一种选择是配置外部负载均衡器(例如HAProxy或Traefik)。

我们已经讨论了DockerSwarm的基本用法。现在让我们深入了解更具挑战性的功能。

DockerSwarm提供了许多在持续交付过程中有用的有趣功能。在本节中,我们将介绍最重要的功能。

滚动更新是一种自动替换服务副本的方法,一次替换一个副本,以确保一些副本始终在工作。DockerSwarm默认使用滚动更新,并且可以通过两个参数进行控制:

DockerSwarm滚动更新过程如下:

让我们来看一个例子,将Tomcat应用程序从版本8更改为版本9。假设我们有tomcat:8服务,有五个副本:

$dockerservicecreate--replicas5--nametomcat--update-delay10stomcat:8我们可以使用dockerservicepstomcat命令检查所有副本是否正在运行。另一个有用的命令是dockerserviceinspect命令,可以帮助检查服务:

现在,我们可以将服务更新为tomcat:9镜像:

$dockerserviceupdate--imagetomcat:9tomcat让我们看看发生了什么:

$dockerservicepstomcatIDNAMEIMAGENODEDESIREDSTATECURRENTSTATE4dvh6ytn4lsqtomcat.1tomcat:8ubuntu-managerRunningRunning4minutesago2mop96j5q4ajtomcat.2tomcat:8ubuntu-managerRunningRunning4minutesagoowurmusr1c48tomcat.3tomcat:9ubuntu-managerRunningPreparing13secondsagor9drfjpizuxf\_tomcat.3tomcat:8ubuntu-managerShutdownShutdown12secondsago0725ha5d8p4vtomcat.4tomcat:8ubuntu-managerRunningRunning4minutesagowl25m2vrqgc4tomcat.5tomcat:8ubuntu-managerRunningRunning4minutesago请注意,tomcat:8的第一个副本已关闭,第一个tomcat:9已经在运行。如果我们继续检查dockerservicepstomcat命令的输出,我们会注意到每隔10秒,另一个副本处于关闭状态,新的副本启动。如果我们还监视dockerinspect命令,我们会看到值UpdateStatus:State将更改为updating,然后在更新完成后更改为completed。

滚动更新是一个非常强大的功能,允许零停机部署,并且应该始终在持续交付过程中使用。

当我们需要停止工作节点进行维护,或者我们只是想将其从集群中移除时,我们可以使用Swarm排水节点功能。排水节点意味着要求管理器将所有任务移出给定节点,并排除它不接收新任务。结果,所有副本只在活动节点上运行,排水节点处于空闲状态。

让我们看看这在实践中是如何工作的。假设我们有三个集群节点和一个具有五个副本的Tomcat服务:

$dockernodelsIDHOSTNAMESTATUSAVAILABILITYMANAGERSTATUS4mrrmibdrpa3yethhmy13mwzqubuntu-worker2ReadyActivekzgm7erw73tu2rjjninxdb4wp*ubuntu-managerReadyActiveLeaderyllusy42jp08w8fmze43rmqqsubuntu-worker1ReadyActive$dockerservicecreate--replicas5--nametomcattomcat让我们检查一下副本正在哪些节点上运行:

$dockerservicepstomcatIDNAMEIMAGENODEDESIREDSTATECURRENTSTATEzrnawwpupuqltomcat.1tomcat:latestubuntu-managerRunningRunning17minutesagox6rqhyn7mrottomcat.2tomcat:latestubuntu-worker1RunningRunning16minutesagorspgxcfv3is2tomcat.3tomcat:latestubuntu-worker2RunningRunning5weeksagocf00k61vo7xhtomcat.4tomcat:latestubuntu-managerRunningRunning17minutesagootjo08e06qbxtomcat.5tomcat:latestubuntu-worker2RunningRunning5weeksago有两个副本正在ubuntu-worker2节点上运行。让我们排水该节点:

$dockernodeupdate--availabilitydrainubuntu-worker2节点被设置为drain可用性,因此所有副本应该移出该节点:

$dockerservicepstomcatIDNAMEIMAGENODEDESIREDSTATECURRENTSTATEzrnawwpupuqltomcat.1tomcat:latestubuntu-managerRunningRunning18minutesagox6rqhyn7mrottomcat.2tomcat:latestubuntu-worker1RunningRunning17minutesagoqrptjztd777itomcat.3tomcat:latestubuntu-worker1RunningRunninglessthanasecondagorspgxcfv3is2\_tomcat.3tomcat:latestubuntu-worker2ShutdownShutdownlessthanasecondagocf00k61vo7xhtomcat.4tomcat:latestubuntu-managerRunningRunning18minutesagok4c14tyo7leqtomcat.5tomcat:latestubuntu-worker1RunningRunninglessthanasecondagootjo08e06qbx\_tomcat.5tomcat:latestubuntu-worker2ShutdownShutdownlessthanasecondago我们可以看到新任务在ubuntu-worker1节点上启动,并且旧副本已关闭。我们可以检查节点的状态:

$dockernodelsIDHOSTNAMESTATUSAVAILABILITYMANAGERSTATUS4mrrmibdrpa3yethhmy13mwzqubuntu-worker2ReadyDrainkzgm7erw73tu2rjjninxdb4wp*ubuntu-managerReadyActiveLeaderyllusy42jp08w8fmze43rmqqsubuntu-worker1ReadyActive如预期的那样,ubuntu-worker2节点可用(状态为Ready),但其可用性设置为排水,这意味着它不托管任何任务。如果我们想要将节点恢复,可以将其可用性检查为active:

$dockernodeupdate--availabilityactiveubuntu-worker2一个非常常见的做法是排水管理节点,结果是它不会接收任何任务,只做管理工作。

排水节点的另一种方法是从工作节点执行dockerswarmleave命令。然而,这种方法有两个缺点:

拥有单个管理节点是有风险的,因为当管理节点宕机时,整个集群也会宕机。在业务关键系统的情况下,这种情况显然是不可接受的。在本节中,我们将介绍如何管理多个主节点。

为了将新的管理节点添加到系统中,我们需要首先在(当前单一的)管理节点上执行以下命令:

$dockerswarmjoin-tokenmanagerToaddamanagertothisswarm,runthefollowingcommand:dockerswarmjoin\--tokenSWMTKN-1-5blnptt38eh9d3s8lk8po3069vbjmz7k7r3falkm20y9v9hefx-a4v5olovq9mnvy7v8ppp63r23\192.168.0.143:2377输出显示了令牌和需要在即将成为管理节点的机器上执行的整个命令。执行完毕后,我们应该看到添加了一个新的管理节点。

另一种添加管理节点的选项是使用dockernodepromote命令将其从工作节点角色提升为管理节点。为了将其重新转换为工作节点角色,我们可以使用dockernodedemote命令。

假设我们已经添加了两个额外的管理节点;我们应该看到以下输出:

$dockernodelsIDHOSTNAMESTATUSAVAILABILITYMANAGERSTATUS4mrrmibdrpa3yethhmy13mwzqubuntu-manager2ReadyActivekzgm7erw73tu2rjjninxdb4wp*ubuntu-managerReadyActiveLeaderpkt4sjjsbxx4ly1lwetieuj2nubuntu-manager1ReadyActiveReachable请注意,新的管理节点的管理状态设置为可达(或留空),而旧的管理节点是领导者。其原因是始终有一个主节点负责所有Swarm管理和编排决策。领导者是使用Raft共识算法从管理节点中选举出来的,当它宕机时,会选举出一个新的领导者。

假设我们关闭了ubuntu-manager机器;让我们看看新领导者是如何选举的:

$dockernodelsIDHOSTNAMESTATUSAVAILABILITYMANAGERSTATUS4mrrmibdrpa3yethhmy13mwzqubuntu-manager2ReadyActiveReachablekzgm7erw73tu2rjjninxdb4wpubuntu-managerReadyActiveUnreachablepkt4sjjsbxx4ly1lwetieuj2n*ubuntu-manager1ReadyActiveLeader请注意,即使其中一个管理节点宕机,Swarm也可以正常工作。

Raft算法本身对管理者的数量有限制。分布式决策必须得到大多数节点的批准,称为法定人数。这一事实意味着建议使用奇数个管理者。

要理解为什么,让我们看看如果我们有两个管理者会发生什么。在这种情况下,法定人数是两个,因此如果任何一个管理者宕机,那么就不可能达到法定人数,因此也无法选举领导者。结果,失去一台机器会使整个集群失效。我们增加了一个管理者,但整个集群变得不太容错。在三个管理者的情况下情况会有所不同。然后,法定人数仍然是两个,因此失去一个管理者不会停止整个集群。这是一个事实,即使从技术上讲并不是被禁止的,但只有奇数个管理者是有意义的。

到目前为止,我们已经了解到管理者会自动将工作节点分配给任务。在本节中,我们将深入探讨自动分配的含义。我们介绍DockerSwarm调度策略以及根据我们的需求进行配置的方法。

DockerSwarm使用两个标准来选择合适的工作节点:

标签分为两类,node.labels和engine.labels。第一类是由运营团队添加的;第二类是由DockerEngine收集的,例如操作系统或硬件特定信息。

例如,如果我们想在具体节点ubuntu-worker1上运行Tomcat服务,那么我们需要使用以下命令:

$dockerservicecreate--constraint'node.hostname==ubuntu-worker1'tomcat我们还可以向节点添加自定义标签:

$dockernodeupdate--label-addsegment=AAubuntu-worker1上述命令添加了一个标签node.labels.segment,其值为AA。然后,在运行服务时我们可以使用它:

$dockerservicecreate--constraint'node.labels.segment==AA'tomcat这个命令只在标记有给定段AA的节点上运行tomcat副本。

标签和约束使我们能够配置服务副本将在哪些节点上运行。尽管这种方法在许多情况下是有效的,但不应该过度使用,因为最好让副本分布在多个节点上,并让DockerSwarm负责正确的调度过程。

我们已经描述了如何使用DockerSwarm来部署一个服务,该服务又从给定的Docker镜像中运行多个容器。另一方面,还有DockerCompose,它提供了一种定义容器之间依赖关系并实现容器扩展的方法,但所有操作都在一个Docker主机内完成。我们如何将这两种技术合并起来,以便我们可以指定docker-compose.yml文件,并自动将容器分布在集群上?幸运的是,有DockerStack。

DockerStack是在Swarm集群上运行多个关联容器的方法。为了更好地理解它如何将DockerCompose与DockerSwarm连接起来,让我们看一下下面的图:

DockerSwarm编排哪个容器在哪台物理机上运行。然而,容器之间没有任何依赖关系,因此为了它们进行通信,我们需要手动链接它们。相反,DockerCompose提供了容器之间的链接。在前面图中的例子中,一个Docker镜像(部署为三个复制的容器)依赖于另一个Docker镜像(部署为一个容器)。然而,所有容器都运行在同一个Docker主机上,因此水平扩展受限于一台机器的资源。DockerStack连接了这两种技术,并允许使用docker-compose.yml文件在一组Docker主机上运行链接容器的完整环境。

举个例子,让我们使用依赖于redis镜像的calculator镜像。让我们将这个过程分为四个步骤:

我们已经在前面的章节中定义了docker-compose.yml文件,它看起来类似于以下内容:

version:"3"services:calculator:deploy:replicas:3image:leszko/calculator:latestports:-"8881:8080"redis:deploy:replicas:1image:redis:latest请注意,所有镜像在运行dockerstack命令之前必须推送到注册表,以便它们可以从所有节点访问。因此,不可能在docker-compose.yml中构建镜像。

使用所提供的docker-compose.yml配置,我们将运行三个calculator容器和一个redis容器。计算器服务的端点将在端口8881上公开。

让我们使用dockerstack命令来运行服务,这将在集群上启动容器:

$dockerstackdeploy--compose-filedocker-compose.ymlappCreatingnetworkapp_defaultCreatingserviceapp_redisCreatingserviceapp_calculatorDocker计划简化语法,以便不需要stack这个词,例如,dockerdeploy--compose-filedocker-compose.ymlapp。在撰写本文时,这仅在实验版本中可用。

服务已经启动。我们可以使用dockerservicels命令来检查它们是否正在运行:

$dockerservicelsIDNAMEMODEREPLICASIMAGE5jbdzt9wolorapp_calculatorreplicated3/3leszko/calculator:latestzrr4pkh3n13fapp_redisreplicated1/1redis:latest我们甚至可以更仔细地查看服务,并检查它们部署在哪些Docker主机上:

$dockerservicepsapp_calculatorIDNAMEIMAGENODEDESIREDSTATECURRENTSTATEjx0ipdxwdilmapp_calculator.1leszko/calculator:latestubuntu-managerRunningRunning57secondsagopsweuemtb2wfapp_calculator.2leszko/calculator:latestubuntu-worker1RunningRunningaboutaminuteagoiuas0dmi7abnapp_calculator.3leszko/calculator:latestubuntu-worker2RunningRunning57secondsago$dockerservicepsapp_redisIDNAMEIMAGENODEDESIREDSTATECURRENTSTATE8sg1ybbggx3lapp_redis.1redis:latestubuntu-managerRunningRunningaboutaminuteago我们可以看到,ubuntu-manager机器上启动了一个calculator容器和一个redis容器。另外两个calculator容器分别在ubuntu-worker1和ubuntu-worker2机器上运行。

当我们完成了stack,我们可以使用方便的dockerstackrm命令来删除所有内容:

$dockerstackrmappRemovingserviceapp_calculatorRemovingserviceapp_redisRemovingnetworkapp_default使用DockerStack允许在DockerSwarm集群上运行DockerCompose规范。请注意,我们使用了确切的docker-compose.yml格式,这是一个很大的好处,因为对于Swarm,不需要指定任何额外的内容。

这两种技术的合并使我们能够在Docker上部署应用程序的真正力量,因为我们不需要考虑单独的机器。我们只需要指定我们的(微)服务如何相互依赖,用docker-compose.yml格式表达出来,然后让Docker来处理其他一切。物理机器可以简单地被视为一组资源。

DockerSwarm不是唯一用于集群Docker容器的系统。尽管它是开箱即用的系统,但可能有一些有效的理由安装第三方集群管理器。让我们来看一下最受欢迎的替代方案。

Kubernetes是一个由谷歌最初设计的开源集群管理系统。尽管它不是Docker原生的,但集成非常顺畅,而且有许多额外的工具可以帮助这个过程;例如,kompose可以将docker-compose.yml文件转换成Kubernetes配置文件。

让我们来看一下Kubernetes的简化架构:

Kubernetes和DockerSwarm类似,它也有主节点和工作节点。此外,它引入了pod的概念,表示一组一起部署和调度的容器。大多数pod都有几个容器组成一个服务。Pod根据不断变化的需求动态构建和移除。

Kubernetes相对较年轻。它的开发始于2014年;然而,它基于谷歌的经验,这是它成为市场上最受欢迎的集群管理系统之一的原因之一。越来越多的组织迁移到Kubernetes,如eBay、Wikipedia和Pearson。

ApacheMesos是一个在2009年由加州大学伯克利分校发起的开源调度和集群系统,早在Docker出现之前就开始了。它提供了一个在CPU、磁盘空间和内存上的抽象层。Mesos的一个巨大优势是它支持任何Linux应用程序,不一定是(Docker)容器。这就是为什么可以创建一个由数千台机器组成的集群,并且用于Docker容器和其他程序,例如基于Hadoop的计算。

让我们来看一下展示Mesos架构的图:

ApacheMesos,类似于其他集群系统,具有主从架构。它使用安装在每个节点上的节点代理进行通信,并提供两种类型的调度器,Chronos-用于cron风格的重复任务和Marathon-提供RESTAPI来编排服务和容器。

与其他集群系统相比,ApacheMesos非常成熟,并且已经被许多组织采用,如Twitter、Uber和CERN。

Kubernetes、DockerSwarm和Mesos都是集群管理系统的不错选择。它们都是免费且开源的,并且它们都提供重要的集群管理功能,如负载均衡、服务发现、分布式存储、故障恢复、监控、秘钥管理和滚动更新。它们在持续交付过程中也可以使用,没有太大的区别。这是因为,在Docker化的基础设施中,它们都解决了同样的问题,即Docker容器的集群化。然而,显然,这些系统并不完全相同。让我们看一下表格,展示它们之间的区别:

无论选择哪个系统,您都可以将其用于暂存/生产环境,也可以用于扩展Jenkins代理。让我们看看如何做到这一点。

服务器集群的明显用例是暂存和生产环境。在使用时,只需连接物理机即可增加环境的容量。然而,在持续交付的背景下,我们可能还希望通过在集群上运行Jenkins代理(从属)节点来改进Jenkins基础设施。在本节中,我们将看两种不同的方法来实现这个目标。

我们在《配置Jenkins》的第三章中看到了动态从属配置。使用DockerSwarm,这个想法保持完全一样。当构建开始时,Jenkins主服务器会从Jenkins从属Docker镜像中运行一个容器,并在容器内执行Jenkinsfile脚本。然而,DockerSwarm使解决方案更加强大,因为我们不再局限于单个Docker主机,而是可以提供真正的水平扩展。向集群添加新的Docker主机有效地扩展了Jenkins基础设施的容量。

无论从属是如何配置的,我们总是通过安装适当的插件并在ManageJenkins|ConfigureSystem的Cloud部分中添加条目来配置它们。

如果我们不想使用动态从属配置,那么集群化Jenkins从属的另一个解决方案是使用JenkinsSwarm。我们在《配置Jenkins》的第三章中描述了如何使用它。在这里,我们为DockerSwarm添加描述。

首先,让我们看看如何使用从swarm-client.jar工具构建的Docker镜像来运行JenkinsSwarm从属。DockerHub上有一些可用的镜像;我们可以使用csanchez/jenkins-swarm-slave镜像:

$dockerruncsanchez/jenkins-swarm-slave:1.16-master-username-password-namejenkins-swarm-slave-2该命令执行应该与第三章中介绍的具有完全相同的效果,配置Jenkins;它动态地向Jenkins主节点添加一个从节点。

然后,为了充分利用JenkinsSwarm,我们可以在DockerSwarm集群上运行从节点容器:

$dockerservicecreate--replicas5--namejenkins-swarm-slavecsanchez/jenkins-swarm-slave-master-disableSslVerification-username-password-namejenkins-swarm-slave上述命令在集群上启动了五个从节点,并将它们附加到了Jenkins主节点。请注意,通过执行dockerservicescale命令,可以非常简单地通过水平扩展Jenkins。

动态从节点配置和JenkinsSwarm都可以在集群上运行,从而产生以下图表中呈现的架构:

Jenkins从节点在集群上运行,因此非常容易进行水平扩展和缩减。如果我们需要更多的Jenkins资源,我们就扩展Jenkins从节点。如果我们需要更多的集群资源,我们就向集群添加更多的物理机器。

这两种解决方案之间的区别在于,动态从节点配置会在每次构建之前自动向集群添加一个Jenkins从节点。这种方法的好处是,我们甚至不需要考虑此刻应该运行多少Jenkins从节点,因为数量会自动适应流水线构建的数量。这就是为什么在大多数情况下,动态从节点配置是首选。然而,JenkinsSwarm也具有一些显著的优点:

集群化Jenkins从属节点带来了许多好处,这就是现代Jenkins架构应该看起来的样子。这样,我们可以为持续交付过程提供动态的水平扩展基础设施。

在本章中,我们详细介绍了DockerSwarm和集群化过程。为了增强这方面的知识,我们建议进行以下练习:

在本章中,我们看了一下Docker环境的集群化方法,这些方法可以实现完整的分段/生产/Jenkins环境的设置。以下是本章的要点:

在下一章中,我们将描述持续交付过程的更高级方面,并介绍构建流水线的最佳实践

在上一章中,我们介绍了服务器集群的工作原理以及如何与Docker和Jenkins一起使用。在本章中,我们将看到一系列不同方面的内容,这些内容在持续交付过程中非常重要,但尚未被描述。

在本节中,我将解释如何解决这些挑战,以便持续交付过程尽可能安全。

NoSQL数据库通常没有任何限制模式,因此简化了持续交付过程,因为不需要额外的模式更新步骤。这是一个巨大的好处;然而,这并不一定意味着使用NoSQL数据库编写应用程序更简单,因为我们在源代码中需要更多的努力来进行数据验证。

关系数据库具有静态模式。如果我们想要更改它,例如向表中添加新列,我们需要编写并执行SQLDDL(数据定义语言)脚本。为每个更改手动执行这个操作需要大量的工作,并且会导致易出错的解决方案,运维团队必须保持代码和数据库结构同步。一个更好的解决方案是以增量方式自动更新模式。这样的解决方案称为数据库迁移。

数据库模式迁移是对关系数据库结构进行增量更改的过程。让我们看一下以下图表,以更好地理解它:

迁移脚本应该存储在版本控制系统中,通常与源代码存储在同一个仓库中。

迁移工具及其使用的策略可以分为两类:

市场上有许多数据库迁移工具,其中最流行的是Flyway、Liquibase和RailMigrations(来自RubyonRails框架)。为了了解这些工具的工作原理,我们将以Flyway工具为例进行介绍。

还有一些商业解决方案专门针对特定的数据库,例如Redgate(用于SQLServer)和OptimDatabaseAdministrator(用于DB2)。

让我们使用Flyway为计算器Web服务创建数据库模式。数据库将存储在服务上执行的所有操作的历史记录:第一个参数、第二个参数和结果。

我们展示如何在三个步骤中使用SQL数据库和Flyway。

为了将Flyway与Gradle一起使用,我们需要将以下内容添加到build.gradle文件中:

对于其他SQL数据库(例如MySQL),配置将非常相似。唯一的区别在于Gradle依赖项和JDBC连接。

应用此配置后,我们应该能够通过执行以下命令来运行Flyway工具:

$./gradlewflywayMigrate-i该命令在文件/tmp/calculator.mv.db中创建了数据库。显然,由于我们还没有定义任何内容,它没有模式。

Flyway可以作为命令行工具、通过JavaAPI或作为流行构建工具Gradle、Maven和Ant的插件来使用。

下一步是定义SQL文件,将计算表添加到数据库模式中。让我们创建src/main/resources/db/migration/V1__Create_calculation_table.sql文件,内容如下:

createtableCALCULATION(IDintnotnullauto_increment,Avarchar(100),Bvarchar(100),RESULTvarchar(100),primarykey(ID));请注意迁移文件的命名约定,__.sql。SQL文件创建了一个具有四列ID、A、B、RESULT的表。ID列是表的自动递增主键。现在,我们准备运行Flyway命令来应用迁移:

$./gradlewflywayMigrate-i…Successfullyapplied1migrationtoschema"PUBLIC"(executiontime00:00.028s).:flywayMigrate(Thread[DaemonworkerThread2,5,main])completed.Took1.114secs.该命令自动检测到迁移文件并在数据库上执行了它。

迁移文件应始终保存在版本控制系统中,通常与源代码一起。

我们执行了第一个迁移,因此数据库已准备就绪。为了查看完整的示例,我们还应该调整我们的项目,以便它可以访问数据库。

首先,让我们配置Gradle依赖项以使用SpringBoot项目中的H2数据库。我们可以通过将以下行添加到build.gradle文件中来实现这一点:

dependencies{compile("org.springframework.boot:spring-boot-starter-data-jpa")compile("com.h2database:h2")}下一步是在src/main/resources/application.properties文件中设置数据库位置和启动行为:

spring.datasource.url=jdbc:h2:file:/tmp/calculator;DB_CLOSE_ON_EXIT=FALSEspring.jpa.hibernate.ddl-auto=validate第二行意味着SpringBoot不会尝试从源代码模型自动生成数据库模式。相反,它只会验证数据库模式是否与Java模型一致。

现在,让我们在新的src/main/java/com/leszko/calculator/Calculation.java文件中为计算创建JavaORM实体模型:

packagecom.leszko.calculator;importjavax.persistence.Entity;importjavax.persistence.GeneratedValue;importjavax.persistence.GenerationType;importjavax.persistence.Id;@EntitypublicclassCalculation{@Id@GeneratedValue(strategy=GenerationType.AUTO)privateIntegerid;privateStringa;privateStringb;privateStringresult;protectedCalculation(){}publicCalculation(Stringa,Stringb,Stringresult){this.a=a;this.b=b;this.result=result;}}实体类在Java代码中表示数据库映射。一个表被表示为一个类,每一列被表示为一个字段。下一步是创建用于加载和存储Calculation实体的存储库。

让我们创建src/main/java/com/leszko/calculator/CalculationRepository.java文件:

packagecom.leszko.calculator;importorg.springframework.data.repository.CrudRepository;publicinterfaceCalculationRepositoryextendsCrudRepository{}最后,我们可以使用Calculation和CalculationRepository类来存储计算历史。让我们将以下代码添加到src/main/java/com/leszko/calculator/CalculatorController.java文件中:

...classCalculatorController{...@AutowiredprivateCalculationRepositorycalculationRepository;@RequestMapping("/sum")Stringsum(@RequestParam("a")Integera,@RequestParam("b")Integerb){Stringresult=String.valueOf(calculator.sum(a,b));calculationRepository.save(newCalculation(a.toString(),b.toString(),result));returnresult;}}现在,当我们启动服务并执行/sum端点时,每个求和操作都会记录到数据库中。

如果您想浏览数据库内容,那么您可以将spring.h2.console.enabled=true添加到application.properties文件中,然后通过/h2-console端点浏览数据库。

我们解释了数据库模式迁移的工作原理以及如何在使用Gradle构建的Spring项目中使用它。现在,让我们看看它如何在持续交付过程中集成。

在持续交付管道中使用数据库更新的第一种方法可能是在迁移命令执行中添加一个阶段。这个简单的解决方案对许多情况都能正确工作;然而,它有两个重大缺点:

这导致我们需要解决的两个约束:

我们将针对两种不同情况解决这些约束:向后兼容的更新和非向后兼容的更新。

向后兼容的更改更简单。让我们看一下以下图表,看看它们是如何工作的:

假设模式迁移“数据库v10”是向后兼容的。如果我们需要回滚“服务v1.2.8”版本,那么我们部署“服务v1.2.7”,并且不需要对数据库做任何操作(数据库迁移是不可逆的,所以我们保留“数据库v11”)。由于模式更新是向后兼容的,“服务v.1.2.7”可以完美地与“数据库v11”配合使用。如果我们需要回滚到“服务v1.2.6”,等等,也是一样的。现在,假设“数据库v10”和所有其他迁移都是向后兼容的,那么我们可以回滚到任何服务版本,一切都会正常工作。

让我们看一个向后兼容更改的例子。我们将创建一个模式更新,向计算表添加一个created_at列。迁移文件src/main/resources/db/migration/V2__Add_created_at_column.sql如下所示:

altertableCALCULATIONaddCREATED_ATtimestamp;除了迁移脚本,计算器服务还需要在Calculation类中添加一个新字段:

...privateTimestampcreatedAt;...我们还需要调整它的构造函数,然后在CalculatorController类中使用它:

calculationRepository.save(newCalculation(a.toString(),b.toString(),result,Timestamp.from(Instant.now())));运行服务后,计算历史记录将与created_at列一起存储。请注意,这个更改是向后兼容的,因为即使我们恢复Java代码并保留数据库中的created_at列,一切都会正常工作(恢复的代码根本不涉及新列)。

不向后兼容的更改要困难得多。看看前面的图,如果数据库更改v11是不向后兼容的,那么将无法将服务回滚到1.2.7版本。在这种情况下,我们如何处理不向后兼容的数据库迁移,以便回滚和零停机部署是可能的呢?

为了更好地说明这一点,让我们看一下以下图片:

让我们考虑一个删除列的例子。一个提议的方法包括两个步骤:

删除列是一个非常简单的例子。让我们看一个更困难的情景,并在我们的计算器服务中重命名结果列。我们将在几个步骤中介绍如何做到这一点:

假设我们需要将result列重命名为sum。第一步是添加一个将是重复的新列。我们必须创建一个src/main/resources/db/migration/V3__Add_sum_column.sql迁移文件:

altertableCALCULATIONaddSUMvarchar(100);因此,在执行迁移后,我们有两列:result和sum。

下一步是在源代码模型中重命名列,并将两个数据库列用于设置和获取操作。我们可以在Calculation类中进行更改:

publicclassCalculation{...privateStringsum;...publicCalculation(Stringa,Stringb,Stringsum,TimestampcreatedAt){this.a=a;this.b=b;this.sum=sum;this.result=sum;this.createdAt=createdAt;}publicStringgetSum(){returnsum!=nullsum:result;}}为了100%准确,在getSum()方法中,我们应该比较类似最后修改列日期的内容(不一定总是首先使用新列)。

从现在开始,每当我们向数据库添加一行时,相同的值将被写入result和sum列。在读取sum时,我们首先检查它是否存在于新列中,如果不存在,则从旧列中读取。

可以通过使用数据库触发器来实现相同的结果,触发器会自动将相同的值写入两列。

到目前为止,我们所做的所有更改都是向后兼容的,因此我们可以随时回滚服务,到任何我们想要的版本。

updateCALCULATIONsetCALCULATION.sum=CALCULATION.resultwhereCALCULATION.sumisnull;我们仍然没有回滚的限制;然而,如果我们需要部署在第2步之前的版本,那么这个数据库迁移需要重复执行。

publicclassCalculation{...privateStringsum;...publicCalculation(Stringa,Stringb,Stringsum,TimestampcreatedAt){this.a=a;this.b=b;this.sum=sum;this.createdAt=createdAt;}publicStringgetSum(){returnsum;}}在此操作之后,我们不再在代码中使用result列。请注意,此操作仅向后兼容到第2步。如果我们需要回滚到第1步,那么我们可能会丢失此步骤之后存储的数据。

最后一步是从数据库中删除旧列。这个迁移应该在回滚期之后执行,当我们确定在第4步之前不需要回滚时。

让我们添加最终的迁移,V5__Drop_result_column.sql:

altertableCALCULATIONdropcolumnRESULT;在这一步之后,我们终于完成了列重命名的过程。请注意,我们所做的一切只是稍微复杂了操作,以便将其延长。这减少了向后不兼容的数据库更改的风险,并允许零停机部署。

到目前为止,在所有的图表中,我们都提到数据库迁移是与服务发布一起运行的。换句话说,每个提交(意味着每个发布)都包括数据库更改和代码更改。然而,推荐的方法是明确分离存储库的提交是数据库更新还是代码更改。这种方法在下图中呈现:

数据库服务变更分离的好处是,我们可以免费获得向后兼容性检查。想象一下,更改v11和v1.2.7涉及到一个逻辑更改,例如,向数据库添加一个新列。然后,我们首先提交数据库v11,这样持续交付流水线中的测试就会检查数据库v11是否与服务v.1.2.6正常工作。换句话说,它们检查数据库更新v11是否向后兼容。然后,我们提交v1.2.7的更改,这样流水线就会检查数据库v11是否与服务v1.2.7正常工作。

数据库-代码分离并不意味着我们必须有两个单独的Jenkins流水线。流水线可以始终同时执行,但我们应该将其作为一个良好的实践,即提交要么是数据库更新,要么是代码更改。

总之,数据库架构的更改不应该手动完成。相反,我们应该始终使用迁移工具自动化它们,作为持续交付流水线的一部分执行。我们还应该避免非向后兼容的数据库更新,确保这一点的最佳方法是将数据库和代码更改分别提交到存储库中。

在许多系统中,我们可以发现数据库成为了多个服务之间共享的中心点。在这种情况下,对数据库的任何更新都变得更加具有挑战性,因为我们需要在所有服务之间进行协调。

例如,想象一下我们开发了一个在线商店,我们有一个包含以下列的Customers表:名字,姓氏,用户名,密码,电子邮件和折扣。有三个服务对客户数据感兴趣:

让我们看一下下面的图片,展示了这种情况:

它们依赖于相同的数据库架构。这种方法至少存在两个问题:

基于之前提到的原因,每个服务应该有自己的数据库,并且服务应该通过它们的API进行通信。根据我们的例子,我们可以应用以下的重构:

重构后的版本如下图所示:

这种方法与微服务架构的原则一致,应该始终被应用。通过API的通信比直接访问数据库更加灵活。

在单体系统的情况下,数据库通常是集成点。由于这种方法会引起很多问题,被认为是一种反模式。

我们已经介绍了保持数据库架构一致的数据库迁移,这是一个副作用。这是因为如果我们在开发机器上、在暂存环境中或者在生产环境中运行相同的迁移脚本,我们总是会得到相同的架构结果。然而,表内的数据值是不同的。我们如何准备测试数据,以有效地测试我们的系统?这就是本节的主题。

这个问题的答案取决于测试的类型,对于单元测试、集成/验收测试和性能测试是不同的。让我们分别来看每种情况。

在单元测试的情况下,我们不使用真实的数据库。我们要么在持久化机制的层面(仓库、数据访问对象)模拟测试数据,要么用内存数据库(例如H2数据库)伪造真实的数据库。由于单元测试是由开发人员创建的,确切的数据值通常是由开发人员虚构的,而且并不重要。

集成和验收测试通常使用测试/暂存数据库,该数据库应尽可能与生产环境相似。许多公司采取的一种方法是将生产数据快照到暂存,以确保两者完全相同。然而,出于以下原因,这种方法被视为反模式:

基于上述原因,首选的方法是通过与客户或业务分析师一起手动准备测试数据,选择生产数据的子集。当生产数据库增长时,值得重新审视其内容,看是否有任何应该添加的合理情况。

将数据添加到暂存数据库的最佳方法是使用服务的公共API。这种方法与通常是黑盒的验收测试一致。而且,使用API可以保证数据本身的一致性,并通过限制直接数据库操作简化数据库重构。

性能测试的测试数据通常类似于验收测试。一个重要的区别是数据量。为了正确测试性能,我们需要提供足够数量的输入数据,至少与生产环境(在高峰时段)的数据量一样大。为此,我们可以创建数据生成器,通常在验收和性能测试之间共享。

我们已经知道启动项目并使用Jenkins和Docker设置持续交付管道所需的一切。本节旨在通过一些推荐的Jenkins管道实践来扩展这些知识。

在整本书中,我们总是按顺序执行流水线,一步一步地进行。这种方法使得很容易理清构建的状态和结果。如果首先是验收测试阶段,然后是发布阶段,这意味着在验收测试成功之前,发布永远不会发生。顺序流水线易于理解,通常不会引起任何意外。这就是为什么解决任何问题的第一种方法是按顺序进行。

让我们看看实际操作中是什么样子。如果我们想要并行运行两个步骤,Jenkinsfile脚本应该如下所示:

pipeline{agentanystages{stage('Stage1'){steps{parallel(one:{echo"parallelstep1"},two:{echo"parallelstep2"})}}stage('Stage2'){steps{echo"runafterbothparallelstepsarecompleted"}}}}在阶段1中,使用parallel关键字,我们执行两个并行步骤,one和two。请注意,只有在两个并行步骤都完成后,才会执行阶段2。这就是为什么这样的解决方案非常安全地并行运行测试;我们始终可以确保只有在所有并行测试都已通过后,才会运行部署阶段。

前面的描述涉及到并行步骤级别。另一个解决方案是使用并行阶段,因此在单独的代理机器上运行每个阶段。选择使用哪种类型的并行通常取决于两个因素:

作为一般建议,单元测试可以并行运行,但性能测试通常最好在单独的机器上运行。

当Jenkinsfile脚本变得越来越复杂时,我们可能希望在相似的管道之间重用其部分。

例如,我们可能希望为不同的环境(开发、QA、生产)拥有单独但相似的管道。在微服务领域的另一个常见例子是,每个服务都有一个非常相似的Jenkinsfile。那么,我们如何编写Jenkinsfile脚本,以便不重复编写相同的代码?为此有两种好的模式,参数化构建和共享库。让我们逐一描述它们。

我们已经在第四章中提到,持续集成管道,管道可以有输入参数。我们可以使用它们来为相同的管道代码提供不同的用例。例如,让我们创建一个带有环境类型参数的管道:

pipeline{agentanyparameters{string(name:'Environment',defaultValue:'dev',description:'Whichenvironment(dev,qa,prod)')}stages{stage('Environmentcheck'){steps{echo"Currentenvironment:${params.Environment}"}}}}构建需要一个输入参数,环境。然后,在这一步中,我们所做的就是打印参数。我们还可以添加一个条件,以执行不同环境的不同代码。

有了这个配置,当我们开始构建时,我们将看到一个输入参数的提示,如下所示:

参数化构建可以帮助重用管道代码,适用于只有少许不同的情况。然而,不应该过度使用这个功能,因为太多的条件会使Jenkinsfile难以理解。

重用管道的另一个解决方案是将其部分提取到共享库中。

共享库是作为单独的源代码控制项目存储的Groovy代码。此代码可以稍后用作许多Jenkinsfile脚本的管道步骤。为了明确起见,让我们看一个例子。共享库技术始终需要三个步骤:

我们首先创建一个新的Git项目,在其中放置共享库代码。每个Jenkins步骤都表示为位于vars目录中的Groovy文件。

让我们创建一个sayHello步骤,它接受name参数并回显一个简单的消息。这应该存储在vars/sayHello.groovy文件中:

/*Helloworldstep.*/defcall(Stringname){echo"Hello$name!"}共享库步骤的可读性描述可以存储在*.txt文件中。在我们的例子中,我们可以添加带有步骤文档的vars/sayHello.txt文件。

当库代码完成时,我们需要将其推送到存储库,例如,作为一个新的GitHub项目。

下一步是在Jenkins中注册共享库。我们打开“管理Jenkins|配置系统”,找到全局管道库部分。在那里,我们可以添加一个选择的名称的库,如下所示:

我们指定了库注册的名称和库存储库地址。请注意,库的最新版本将在管道构建期间自动下载。

最后,我们可以在Jenkinsfile脚本中使用共享库。

让我们看一个例子:

pipeline{agentanystages{stage("Hellostage"){steps{sayHello'Rafal'}}}}如果在Jenkins配置中没有选中“隐式加载”,那么我们需要在Jenkinsfile脚本的开头添加"@Library('example')_"。

正如您所看到的,我们可以使用Groovy代码作为管道步骤sayHello。显然,在管道构建完成后,在控制台输出中,我们应该看到HelloRafal!。

共享库不限于一个步骤。实际上,借助Groovy语言的强大功能,它们甚至可以作为整个Jenkins管道的模板。

在持续交付的背景下,有两个可能发生失败的时刻:

顺便说一句,如果流水线顺利完成但出现了生产bug,那么这意味着我们的测试还不够好。因此,在回滚之后的第一件事是扩展单元/验收测试套件,以涵盖相应的场景。

最常见的持续交付流程是一个完全自动化的流水线,从检出代码开始,以发布到生产结束。

以下图表显示了这是如何工作的:

在本书中,我们已经介绍了经典的持续交付管道。如果回滚应该使用完全相同的流程,那么我们需要做的就是从存储库中恢复最新的代码更改。结果,管道会自动构建、测试,最后发布正确的版本。

存储库回滚和紧急修复不应跳过管道中的测试阶段。否则,我们可能会因为其他问题导致发布仍然无法正常工作,使得调试变得更加困难。

一般来说,持续交付管道应该是完全自动化的,由对存储库的提交触发,并在发布后结束。然而,有时我们无法避免出现手动步骤。最常见的例子是发布批准,这意味着流程是完全自动化的,但有一个手动步骤来批准新发布。另一个常见的例子是手动测试。其中一些可能是因为我们在传统系统上操作;另一些可能是因为某些测试根本无法自动化。无论原因是什么,有时除了添加手动步骤别无选择。

Jenkins语法提供了一个关键字input用于手动步骤:

stage("Releaseapproval"){steps{input"Doyouapprovetherelease"}}管道将在“输入”步骤上停止执行,并等待手动批准。

请记住,手动步骤很快就会成为交付过程中的瓶颈,这就是为什么它们应该始终被视为次于完全自动化的解决方案的原因。

在上一节中,我们讨论了用于加快构建执行(并行步骤)、帮助代码重用(共享库)、限制生产错误风险(回滚)和处理手动批准(手动步骤)的Jenkins流水线模式。本节介绍了下一组模式,这次与发布过程有关。它们旨在减少将生产环境更新到新软件版本的风险。

我们已经在《使用DockerSwarm进行集群化》的第八章中描述了一个发布模式,即滚动更新。在这里,我们介绍另外两种:蓝绿部署和金丝雀发布。

在图中,当前可访问的环境是蓝色的。如果我们想进行新的发布,那么我们将所有内容部署到绿色环境,并在发布过程结束时将负载均衡器切换到绿色环境。结果,用户突然开始使用新版本。下次我们想进行发布时,我们对蓝色环境进行更改,并在最后将负载均衡器切换到蓝色。每次都是这样进行,从一个环境切换到另一个环境。

蓝绿部署技术在两个假设条件下能够正常工作:环境隔离和无编排发布。

这种解决方案带来了两个重要的好处:

有技术和工具可以克服这些挑战,因此蓝绿部署模式在IT行业中被高度推荐和广泛使用。

金丝雀发布是一种减少引入新软件版本风险的技术。与蓝绿部署类似,它使用两个相同的环境,如下图所示:

与蓝绿部署技术类似,发布过程始于在当前未使用的环境中部署新版本。然而,相似之处到此为止。负载均衡器不是切换到新环境,而是仅将选定的用户组链接到新环境。其余所有用户仍然使用旧版本。这样,一些用户可以测试新版本,如果出现错误,只有一小部分用户受到影响。测试期结束后,所有用户都切换到新版本。

这种方法有一些很大的好处:

金丝雀发布与蓝绿部署具有相同的缺点。额外的挑战是我们同时运行两个生产系统。尽管如此,金丝雀发布是大多数公司用来帮助发布和测试的一种优秀技术。

到目前为止,我们所描述的一切都顺利适用于全新项目,为这些项目设置持续交付流水线相对简单。

然而,遗留系统要困难得多,因为它们通常依赖于手动测试和手动部署步骤。在本节中,我们将逐步介绍将持续交付应用于遗留系统的推荐方案。

作为第零步,我建议阅读MichaelFeathers的一本优秀书籍WorkingEffectivelywithLegacyCode。他关于如何处理测试、重构和添加新功能的想法清楚地解决了如何为传统系统自动化交付流程的大部分问题。

应用持续交付流程的方式在很大程度上取决于当前项目的自动化、使用的技术、硬件基础设施和当前发布流程。通常,它可以分为三个步骤:

让我们详细看一下。

第一步包括自动化部署过程。好消息是,在我所使用的大多数传统系统中,已经存在一些自动化,例如以shell脚本的形式。

无论如何,自动化部署的活动包括以下内容:

在前面的步骤之后,我们可以将所有内容放入部署流水线,并将其用作手动UAT(用户验收测试)周期后的自动化阶段。

从流程的角度来看,已经值得开始更频繁地发布。例如,如果发布是每年一次,尝试将其改为每季度一次,然后每月一次。对这一因素的推动将最终导致更快的自动化交付采用。

下一步,通常更加困难,是为系统准备自动化测试。这需要与QA团队沟通,以了解他们目前如何测试软件,以便我们可以将所有内容移入自动验收测试套件。这个阶段需要两个步骤:

最终目标是拥有一个自动化验收测试套件,它将取代开发周期中的整个UAT阶段。然而,我们可以从一个理智的测试开始,它将简要检查系统是否从回归的角度正确。

当我们至少拥有了基本的回归测试套件时,我们就可以开始添加新功能并重构旧代码。最好一步一步地进行,因为一次性重构通常会导致混乱,从而导致生产故障(与任何特定更改都没有明显关系)。

这个阶段通常包括以下活动:

在这个阶段,值得阅读MartinFowler的一本优秀书籍,重构:改善现有代码的设计。

在触及旧代码时,最好遵循先添加通过的单元测试的规则,然后再更改代码。通过这种方法,我们可以依赖自动化来检查我们不会意外改变业务逻辑。

在本节中,我们讨论了如何应对遗留系统以及它们带来的挑战。如果您正在将项目和组织转变为持续交付方法,那么您可能希望查看持续交付成熟度模型,该模型旨在为采用自动交付的过程提供结构。

在本章中,我们已经涵盖了持续交付过程的各个方面。由于熟能生巧,我们建议进行以下练习:

本章混合了以前未涉及的各种持续交付方面。本章的主要要点如下:

感谢阅读本书。我希望您已经准备好将持续交付方法引入您的IT项目中。作为本书的最后一部分,我提出了前10个持续交付实践清单。祝您愉快!

团队内部拥有整个流程,从接收需求到监控生产。正如有人曾经说过:在开发者的机器上运行的程序是赚不到钱的。这就是为什么有一个小的DevOps团队完全拥有产品是很重要的。实际上,这就是DevOps的真正含义-从开始到结束的开发和运营:

自动化一切,从业务需求(以验收测试的形式)到部署过程。手动描述,带有指导步骤的wiki页面,它们很快就会过时,并导致部落知识,使流程变得缓慢,繁琐和不可靠。这反过来又导致需要发布排练,并使每次部署都变得独特。不要走上这条路!一般来说,如果你做某件事第二次,就自动化它:

对一切进行版本控制:软件源代码,构建脚本,自动化测试,配置管理文件,持续交付管道,监控脚本,二进制文件和文档。简直就是一切。让你的工作基于任务,每个任务都会导致对存储库的提交,无论是与需求收集,架构设计,配置还是软件开发有关。任务从敏捷看板开始,最终进入存储库。这样,你就可以保持一个真实的历史和更改原因的单一真相:

使用面向业务的语言进行验收测试,以改善双方的沟通和对需求的共同理解。与产品负责人密切合作,创建埃里克·埃文所说的“普遍语言”,即业务和技术之间的共同方言。误解是大多数项目失败的根本原因:

准备好回滚;迟早你会需要这样做。记住,你不需要更多的质量保证人员,你需要更快的回滚。如果在生产环境出现问题,你想做的第一件事就是确保安全,并回到上一个可用版本:

为交付过程和工作系统构建可追溯性。没有比没有日志消息的失败更糟糕的了。监控请求数量、延迟、生产服务器的负载、持续交付管道的状态,以及您能想到的任何能帮助您分析当前软件的东西。要主动!在某个时候,您将需要检查统计数据和日志:

经常集成,实际上,一直都在!正如有人所说:“持续性比你想象的更频繁”。没有什么比解决合并冲突更令人沮丧的了。持续集成更多关乎团队实践而非工具。每天至少将代码集成到一个代码库中几次。忘掉持续存在的特性分支和大量的本地更改。基于主干的开发和功能切换才是胜利之道!

经常发布,最好是在每次提交到存储库后。俗话说,“如果痛苦,就更频繁地做。”每天发布使过程变得可预测和平静。远离陷入罕见的发布习惯。那只会变得更糟,最终你将以每年一次的频率发布,需要三个月的准备期!

THE END
1.Android调用系统邮箱关闭mob64ca12dea1dc的技术博客在Android 开发中,有时我们需要访问设备的系统邮箱来发送邮件。但在某些情况下,您可能希望在发送完邮件后自动关闭邮箱应用。虽然 Android 不允许你直接控制所有应用,但我们仍然可以通过实现一些技巧间接做到这一点。本文将详细介绍如何在 Android 中调用系统邮箱并进行相应的操作。 https://blog.51cto.com/u_16213357/12870070
2.及时司机关闭通知推送的详细步骤与方法解析!在及时司机这款游戏中,推送通知常常会影响玩家的体验,尤其是当玩家在专心游戏时,突然收到各种系统或活动的推送提醒。因此,了解如何关掉这些通知推送,成为许多玩家提升游戏体验的一项重要技能。本文将为你详细介绍如何在及时司机中关闭通知推送的步骤,帮助你创造一个更专注的游戏环境。 为什么要关闭通知推送? 许多玩家在http://m.wgqm.net/wgqm04/14886f70c.html
3.如何关闭烦人的Mac通知,关闭不必要的消息推送有妙招!当我们在使用Mac电脑专注做一件事情的时候,总是会被一些消息推送通知所打扰,这时候,我们就希望关闭这些烦人的Mac通知。因此,定期关闭通知有助于我们在Mac上工作时更加专注,不会被最新的新闻,八卦或应用更新分心。 那么如何关闭烦人的Mac通知就成我们关注的问题,今天macdown小编就给大家带来如何关闭烦人的Mac通知的方http://baijiahao.baidu.com/s?id=1648798712239112713&wfr=spider&for=pc
4.HolidayhomeHumbleXXVI哈姆堡(2024最新房价)至少需要1条点评才能计算评分。如果您预订并点评了住宿,您将帮助Holiday home Humble XXVI达成这一目标。 整套全由你承包 108 平方米 面积 厨房 花园 洗衣机 免费无线网络连接 露台 私人浴室 禁烟客房 暖气 Holiday home Humble XXVI位于哈姆堡,提供带免费WiFi的花园景客房,配备花园。 这家度假屋配有露台、4间卧室https://www.booking.com/hotel/dk/holiday-home-humble-xxvi.zh-cn.html
5.免费游戏兑换说明–HumbleBundle成功登录后,您将被重定向到Humble Bundle帐户设置页面,能够看到您的Steam帐户已成功关联。 当Steam帐户已链接到另一个Humble时捆绑帐户,将显示此错误消息无法验证您的Steam帐户。您将需要访问您的拙劣的捆绑帐户设置页面并取消链接该帐户,如上面的屏幕截图所示。如果您不知道您的Steam帐户链接到的电子邮件,请与谦虚的忍https://support.humblebundle.com/hc/zh-cn/articles/115000997168-%E5%85%8D%E8%B4%B9%E6%B8%B8%E6%88%8F%E5%85%91%E6%8D%A2%E8%AF%B4%E6%98%8E
6.HumbleBundle低价游戏网站介绍和购买相关教程Humble Bundle领取免费游戏 因为写稿的时候正好遇到了 HB 免费赠送游戏,机会难得,就顺带也说一说了。 以本次的《超级房车赛2》(GRID2)为例。HB 免费赠送的游戏一般都会显示在主页上,点击即可进入详情页面 点击“GET THE GAME”,或者直接拉到页面底部,然后就会看到有一块内容,显示订阅Humble Bundle的新闻邮件就能得https://xyuxf.com/archives/1545
7.“暗”的搜索结果–Mac玩儿法在拍摄界面、RAW 格式模式关闭,需要你每次拍摄时手动拍摄,毕竟我们日常使用不是经常处于拍摄状态,而仅仅是拍照。 样张参考 对ProRAW 图片进行处理有许多工具可以选择,毕竟常用有 Lightroom、Darkroom,因为这两款有移动app版本,可以直接在 iPhone、iPad 上进行处理创作。 夜间模式jpeg直出 vs ProRAW 后期处理,明显后者https://www.waerfa.com/search/%E6%9A%97/feed/rss2/
8.所有36人一锅端被裁?游戏发行商HumbleGames声明:重组中,将有消息称独立游戏发行商 Humble Games 已经裁掉了所有员工,且会关闭工作室。官方随后发布声明,承认公司正在重组,但否认关闭传闻,称公司将继续运营。 被曝裁员 Nicola Kwan 在其 LinkedIn 页面发帖,IT之家翻译如下:“今天上午 9 点,Humble Games 的 36 名员工被告知,我们将被解雇,公司即将关闭。” https://m.jrj.com.cn/madapter/finance/2024/07/24134641760147.shtml
9.翻译小组讨论。开发者体验的下一步是什么?在我们与Suhail Patel、Hannah Paine、Crystal Hirschorn、CharlesHumble和Jamie Dobson的专家小组讨论中,看看DevEx为什么以及如何在资源配置、招聘、保留和创新中至关重要。 提到的关键资源 :《如何衡量任何东西:寻找商业中的无形资产 》 ,作者是Douglas W. Hubbard https://blog.csdn.net/community_717/article/details/128261218
10.HumbleBundle免费游戏和月度游戏优惠包领取和购买教程教程之后网站就会发送一封邮件到你的邮箱,你点击给出的链接,直接就会跳转到他们的网站,这样你的帐号就注册完成! 接下来就是免费领取游戏,小编以最近这两天送的《德波尼亚:完整旅程》为案例为大家讲解,如何领取!(猎游人会为大家推送Humble Bundle每次送的游戏) https://www.lieyouren.cn/shaidan/193.html
11.在非常不确定的时期内解决问题的六种心态2. Tolerate ambiguity—and stay humble! 当我们想到问题解决者时,我们中的许多人都倾向于描绘一个冷静而有才华的工程师。我们可以想象一个策划者知道她在做什么并且有目的地解决问题。然而,现实是,大多数好的问题解决方法都需要反复试验。它更像是橄榄球的表面随机性,而不是线性编程的精度。我们形成假设,研究数据https://maimai.cn/article/detail?fid=1532896009&efid=II9JyqmoXB5hILGeFyTo6w
12.SiteMapPushbullet 子弹推送 - 快速在手机电脑间互相双向传送消息/图片的工具 (跨设备复制粘贴) 不用花钱!谷歌官方 Android SDK 模拟器让你在电脑上运行安卓系统 SkyDrive - 微软免费网盘重大更新!推出跨平台网络同步客户端,开启个人云存储时代! 最值得一看的几条简单的谷歌 Google 搜索技巧,瞬间提升你的网络搜索能力! 寻找https://www.iplaysoft.com/sitemap.html
13.Quicker作者服务动作单HumbleCoder 2023-07-17 20:12 8 63 <10 运行最后的动作 支持黑名单 H-D-G 2023-07-25 11:34 7 60 <10 OperationEditor 自动补全operation H-D-G 2023-08-09 20:05 7 21 <10 工具菜单 Cea 2023-11-19 16:34 7 44 <10 推送服务链接生成 表单填写参数,便捷生成推送服务(长连接功能)的http://www.getquicker.net/Share/ActionLists/List?id=0febbf46-cf84-4daa-fca2-08da3d2bde29
14.28圈游戏官网平台28圈游戏官网平台V9.8.33通常包括用户名、密码、电子邮件地址、手机号码等。请务必认真仔细填写 步骤5:设置安全选项通过点击28圈苹果手机下载链接:一般需要您填写账户密码保护,为您的账户添加更安全的保护措施。 步骤6:阅读并同意条款在注册过程中,28圈苹果手机下载链接 步骤7:完成注册http://wap.rpegdf.com/
15.词根词缀大全② 表示 “邮件, 邮政 ” postage邮资( post+age钱; 状态) postoffice邮局( post+ofpostcard明信片(post+card 卡片) postal邮政的( post+al) 64.pre-表示“…. 前的, 预先 ” preschool 学龄 前的(pre+prehistory 史前 (pre+history历史prefix 前缀(pre+fix 固定→固定在前面→前缀) preposition 前置词 ,https://www.360doc.cn/document/259842_1058108387.html
16.HumbleBundle官方的微博HumbleBundle官方,欢迎来到Humble Bundle 官方微博,我们将不定期推送Humble Bundle相关游戏及特惠第一手消息。HumbleBundle官方的微博主页、个人资料、相册。新浪微博,随时随地分享身边的新鲜事儿。https://weibo.com/p/1005057480422713/home
17.HumbleKendrickLamarPiano相似游戏下载预约Humble - Kendrick Lamar - Piano 0 人评论 已有超过7679人预约,上线后免费推送 下载豌豆荚预约 相似应用,小编亲测可用 闪耀暖暖 1.91GB 查看 黑猫奇闻社 1.24GB 查看 奥比岛:梦想国度 2.99GB 查看 恋与制作人 1.9GB 查看 Always wanted to play your favorite song on piano but couldn't get around https://m.wandoujia.com/apps/8044925
18.基于live555框架开发的ros2功能包,作为rtsp服务端,推送H264/Hros_rtsp_server是基于live555框架开发的ros2功能包,作为rtsp服务端,用于通过rtsp推送H264/H265编码视频,客户端可通过url来拉取视频。 支持的平台和系统 平台:X86, ARM 系统:Ubuntu 20.04 & ROS2 Foxy, Ubuntu 22.04 & ROS2 Humble 安装依赖 安装ros2开发工具 https://github.com/zhukao/ros_rtsp_server