既然我们已经探讨了在单个主机上启动一组容器的工具,现在我们需要看看如何在大规模的生产环境中进行这样的操作。在本章中,我们的目标是向您展示如何根据我们自己的经验将容器用于生产。您可能需要根据您的应用程序和环境进行许多定制,但这将为您提供一个坚实的起点,以帮助您理解Docker的实际运用哲学。
进入生产环境
将一个应用程序从构建和配置的阶段带到在生产系统上运行的阶段,是从零开始进入生产的最具风险的步骤之一。传统上,这一过程非常复杂,但通过航运集装箱模式,得到了极大的简化。如果您能想象在有航运集装箱之前,将货物装上船穿越海洋是什么样子,那么您就能体会到大多数传统部署系统的样子。在那个旧的航运模式中,各种随机大小的箱子、木箱、桶和各种其他包装都是手工装载到船上的。然后必须由人手动卸载,他们能够判断哪些零件需要先卸载,以免整个堆像竖摞积木一样崩塌。
集装箱改变了这一切:现在我们有了一个标准化的、尺寸已知的箱子。这些集装箱可以按照逻辑顺序装载和卸载,并且整组物品可以在预期时间一起到达。航运业已经建立了高效管理集装箱的机制。Docker的部署模型非常类似。所有Linux容器都支持相同的外部接口,工具只是将它们放在应该放置的服务器上,不用考虑其中的内容。
在新的模型中,当我们有一个正在运行的应用程序构建时,我们不需要编写太多定制的工具来启动部署。如果我们只想将其发送到一台服务器,docker命令行工具将大部分工作处理掉。如果我们想将其发送到更多的服务器,那么我们将不得不从更广泛的容器生态系统中寻找一些更高级的工具。在任何情况下,您的应用程序都需要知道一些事情,并且在将容器化的应用程序投入生产之前,您需要考虑一些问题。
在使用Docker将应用程序带入生产环境时,您将遵循以下进展:
- 在开发环境中本地构建和测试Docker镜像。
- 从持续集成(CI)或构建系统中构建用于测试和部署的官方镜像。
- 将镜像推送到注册表。
- 部署Docker镜像到服务器,然后配置和启动容器。
随着您的工作流程的发展,最终会将所有这些步骤合并为一个流畅的工作流程:
- 编排镜像的构建、测试和存储,以及将容器部署到生产服务器。
但事情并不仅限于此。在最基本的层面上,生产过程必须涵盖三个方面:
- 它必须是一个可重复的过程。每次调用它时,它都需要执行相同的操作。理想情况下,它将为您的所有应用程序执行相同的操作。
- 它需要处理配置。您必须能够在特定环境中定义应用程序的配置,然后保证在每次部署时都会使用该配置。
- 它必须提供一个可执行的可执行文件,可以启动。
为了实现这一点,有几个方面需要考虑。我们将尝试通过提供一个框架来帮助您考虑应用程序在其环境中的情况。
Docker在生产环境中的作用
我们已经介绍了Docker带来的许多功能,并讨论了一些通用的生产策略。在我们更深入地探讨生产容器之前,让我们看看Docker如何适应传统和更现代的生产环境。如果您正在从更传统的系统迁移到Docker,您可以选择将哪些部分委托给Docker、部署工具,或者委托给更大的平台,例如Kubernetes或基于云的容器系统,或者您甚至可能决定保留在传统基础设施上。我们已经成功地将多个系统从传统部署转换为容器化系统,有许多不同的解决方案。但是理解所需的组件以及现代和传统变体的构成将帮助您做出正确的选择。
在图9-1中,我们描述了生产系统中需要考虑的几个问题,以及解决这些问题的现代组件,以及它们在更传统的环境中可能替代的系统。我们将这些问题分为Docker本身解决的问题和我们所称之为“平台”的问题。平台是一个系统,通常围绕着一组服务器,并为Linux容器管理提供一个共同的接口。这可能是一个像Kubernetes或Docker Swarm这样的统一系统,也可能由不同的组件组成,这些组件结合在一起形成一个平台。在向完全容器化的系统与调度器转换时,平台可能一次性处理多个事务。因此,让我们看看这些问题如何相互关联。
在图9-1中,您可以看到应用程序位于堆栈的顶部。它依赖于生产系统中下面的所有问题。在某些情况下,您的环境可能会专门提到这些问题,而在其他情况下,它们可能由您不一定认为会填补这些问题的内容来解决。但是您的生产应用程序无论如何都将在某种程度上依赖于其中的大部分问题,并且需要在您的生产环境中加以解决。如果您想从现有环境过渡到基于Linux容器的环境,您需要考虑您今天是如何提供这些问题的,以及在新系统中如何解决这些问题。
我们将从熟悉的领域开始,然后从底部到顶部进行分析。这个熟悉的领域就是您的应用程序。您的应用程序位于顶部!其他所有内容都是为了向您的应用程序提供功能。毕竟,应用程序提供业务价值,其他所有内容都是为了实现这一目标,以便在规模和可靠性方面进行操作,并在各个应用程序之间标准化其工作方式。尽管底部各项事务的顺序是有意的,但并不是每个层次的功能都提供给上面的层次。它们都向应用程序本身提供该功能。
由于Linux容器和Docker可以实现许多这些功能,将您的系统容器化将使许多这些选择更加容易。随着我们接近堆栈的平台部分,我们将有更多需要考虑的事情,但是了解位于其下面的一切将使这一切变得更加可管理。
让我们从应用程序作业控制开始。
作业控制
作业控制是现代部署的基本要求。这是绘图中关注点的蓝色区块的一部分。实际上,您无法在没有作业控制的情况下拥有任何类型的系统。这是我们通常更传统地留给操作系统或Linux init系统(例如systemd、System V init、runit、BSD rc脚本等)的一部分。我们向操作系统告知我们要运行的进程,然后配置在重新启动它、重新加载其配置和管理应用程序的生命周期时应该采取的行为。
当我们想要启动或停止应用程序时,我们依赖这些系统来处理。在某些情况下,我们还依赖它们通过在应用程序失败时重新启动它来更稳健地运行应用程序。不同的应用程序需要不同的作业控制。在传统的Linux系统中,您可能会使用cron来定时启动和停止作业。如果应用程序崩溃,systemd可能会负责重新启动它。但是,系统如何执行这些操作取决于具体的系统,而且有许多不同的实现需要处理,这并不理想。
如果我们正在转向航运集装箱模型,我们希望能够从外部以更或多或少相同的方式处理所有作业。我们可能需要有关作业的更多元数据,以使它们执行正确的操作,但我们不想查看容器内部。Docker引擎在作业控制方面提供了一组强大的基本操作,例如docker container start、docker container stop、docker container run和docker container kill,这些操作与应用程序生命周期中的大多数关键步骤相对应。围绕Docker容器构建的所有平台,包括Kubernetes,在生命周期行为方面也都遵循这些规则。我们将这放在关注点堆栈的底部,因为从根本上说,这是Docker为您的应用程序提供的最低抽象。即使我们没有使用Docker的任何其他部分,这也是一个巨大的优势,因为对于所有应用程序和运行Docker容器的所有平台来说,这都是相同的。
资源限制
位于作业控制之上的是资源限制。在Linux系统中,如果需要,可以直接使用Linux控制组(cgroups)来管理资源限制,一些生产环境确实已经这样做。但更传统的做法是依赖于诸如ulimit以及不同的应用程序运行时环境(如Java、Ruby或Python虚拟机)的不同设置。在云系统中,早期的一个成功之处是,我们可以启动单独的虚拟服务器来限制单个业务应用程序周围的资源。这是一个很好的创新:不再有吵闹的邻居应用程序。然而与容器相比,这是一种相当粗略的控制。
使用Linux容器,您可以通过cgroups轻松地为容器应用一系列广泛的资源控制。您可以决定在生产中运行时是否限制应用程序对内存、磁盘空间或I/O等资源的访问。然而,我们强烈建议您在熟悉您的应用程序需求后,花时间来做这个决定。如果不这样做,您将无法充分利用容器化应用程序的一个核心功能:在同一台机器上运行多个应用程序,基本上不会发生干扰。正如我们所讨论的,Docker可以免费为您提供这个功能,这是使容器有价值的核心部分。您可以在第5章中查看Docker用于管理这些资源的具体参数。
网络
有关Docker网络的详细信息在第11章中有很多,因此我们在这里不会过多涉及,但是您的容器化系统需要管理连接网络上的应用程序。Docker为网络提供了丰富的配置选项。您应该在生产环境中选择一种机制并在容器之间进行标准化。尝试混合使用这些机制并不是一条容易获得成功的道路。如果您正在运行像Kubernetes这样的平台,那么其中一些决策将由系统自动完成。但好的一面是,通常来说,网络的构建方式的复杂性超出了容器中应用程序的关注。请考虑Docker或更大的平台会为您提供这些功能,只要您遵循以下几条规则,您的应用程序就可以在容器中的本地机器上以及在生产环境中以相同的方式工作:
- 依赖于Docker或您的平台动态映射端口,并告诉应用程序它们映射到哪里。这通常以环境变量的形式提供给应用程序。
- 避免使用像FTP或RTSP这样为返回流量映射随机端口的协议。在容器化平台中,这种情况很难支持。
- 依赖于Docker或生产运行时为容器提供的DNS。
如果遵循这些规则,通常情况下,您的应用程序可以相当独立于部署位置。大多数生产环境都可以让您定义实际的配置,并在运行时应用它们。Docker Compose、Docker Swarm模式、Kubernetes以及像ECS这样的云提供商运行时都会为您完成这些工作。
配置
所有应用程序都需要以某种方式访问其配置。一个应用程序有两个层次的配置。最低层次是它期望其周围的Linux环境如何配置。容器通过提供Dockerfile来处理这个问题,我们可以使用它来重复构建相同的环境。在更传统的系统中,我们可能会使用诸如Chef、Puppet或Ansible等配置管理系统来完成这个任务。在容器化的世界中,您仍然可以使用这些系统,但通常不会使用它们来提供应用程序的依赖项。这项工作属于Docker和Dockerfile。即使Dockerfile的内容对不同的应用程序是不同的,机制和工具都是相同的,这是一个巨大的优势。
下一个层次的配置是直接应用于应用程序的配置。我们之前详细讨论过这个问题。Docker的本地机制是使用环境变量,这适用于所有现代平台。一些系统,特别是一些使您更容易依赖于更传统配置文件的系统。特别是Kubernetes使得依赖于文件相对容易,但是如果您真正想要一个便携式的、面向容器的应用程序,我们建议不要这么做。我们发现这可能会显著影响应用程序的可观察性,并且会阻止您依赖于这个绊脚石。有关环境变量背后的原因,在第13章中有更多的讨论。
打包和交付
在我们的讨论中,我们将打包和交付放在一起。这是一个容器化系统相对于传统系统具有重大优势的领域。在这里,我们不必过多地想象与航运集装箱模型的相似之处:我们有一个一致的包装,即容器镜像,以及将它们传送到各个地方的标准化方式——Docker的镜像仓库和镜像获取、推送设施。在更传统的系统中,我们会构建手工制作的部署工具,其中一些工具我们希望在应用程序之间进行标准化。但是,如果我们需要具有多语言环境,那么这可能会带来麻烦。在您的容器化环境中,您需要考虑如何将应用程序打包到镜像中以及如何存储这些镜像。
后者的最简单路径是订阅托管的商业镜像仓库。如果您的公司接受这一点,那么您应该考虑这一点。包括亚马逊在内的几个云提供商都提供了您可以在环境内部部署的镜像托管服务,这是另一个不错的选择。当然,您也可以构建和维护一个内部私有仓库,就像我们在“运行私有仓库”中讨论的那样。有广泛的提供商生态系统可供选择,您应该调查您的选项。
日志
日志记录位于您可以依靠Docker在容器化环境中提供的关注点和平台需要管理的关注点的边界上。这是因为,正如我们在第6章中详细介绍的那样,Docker可以收集来自容器的所有日志并将其传送到某个地方。但默认情况下,这个地方甚至不是离开本地系统。这对于有限规模的环境可能很不错,如果本地主机存储对您来说足够好,您可以在那里停止考虑它。但是,您的平台将负责处理许多系统上许多应用程序的日志,因此您可能希望将这些日志集中到一个可以显著提高可见性并简化故障排除的系统中。在设计此系统时,请参考第6章以获取有关日志记录的更多详细信息。一些系统,如Kubernetes,对日志的收集有一定的意见。但从应用程序的角度来看,您只需要确保它将日志发送到标准输出(stdout)或标准错误(stderr),然后让Docker或平台处理其余的事情。
监控
在Docker或Linux容器普遍的情况下,系统中的第一部分并没有被整理得井然有序,但Docker为整个系统带来的标准化仍然改进了这部分。如第6章所讨论的那样,以标准化的方式进行应用程序健康检查意味着监控应用程序健康的过程变得更加简化。在许多系统中,平台本身会处理监控,并且调度器将动态关闭不健康的容器,可能将工作负载迁移到不同的服务器上,或者在同一系统上重新启动工作负载。在旧系统中,容器通常由现有的系统(如Nagios、Zabbix或其他传统监控系统)进行监控。正如我们在第6章中展示的那样,还有一些更新的选择,包括像Prometheus这样的系统。应用性能监控(APM)供应商,如New Relic、Datadog或Honeycomb,都对Linux容器及其所包含的应用程序提供了一流的支持。因此,如果您的应用程序已经受到其中一个供应商的监控,您可能不需要做太多改变。
在旧系统中,通常是工程师被呼叫并回应问题,并做出如何处理失败应用程序的决策。在动态系统中,这项工作通常会转移到平台内部的更自动化的流程中。在过渡期内,您的系统可能会在转向自动化系统的同时,同时存在这两者,即只有在平台真正无法干预时,才会呼叫工程师。无论如何,人类仍然需要成为最后的防线。但是,当事情出错时,容器化系统要容易处理得多,因为这些机制在应用程序之间是标准化的。
调度
您如何决定哪些服务在哪些服务器上运行?容器很容易移动,因为Docker提供了如此出色的机制来实现这一点。这为更好的资源利用、更好的可靠性、自愈服务和动态扩展打开了许多可能性。但是某些事情必须做出这些决策。
在旧的系统中,通常使用专用服务器来运行每个服务。您通常会在部署脚本中配置一系列服务器,每次部署时都会将新应用程序发送到同一组服务器。每个服务器一个服务的模型推动了私有数据中心中早期虚拟化的发展。云系统通过将服务器切分成通用虚拟服务器,鼓励了每个服务器一个服务的模型。像AWS这样的系统中的自动扩展部分处理了这种动态行为的一部分。但是,如果您转向使用容器,在同一台虚拟服务器上可能会运行许多服务,服务器级别上的扩展和动态行为将无法帮助您。
分布式调度
分布式调度器利用Docker让您可以像处理单个计算机一样思考整个服务器网络。这里的想法是您定义一些关于如何运行应用程序的策略,然后让系统决定在哪里运行它以及要运行多少个实例。如果某个服务器或应用程序出现问题,您可以让调度器在任何可用的健康资源上启动它,只要这些资源符合应用程序的要求。这更符合Docker公司创始人Solomon Hykes最初对Docker的愿景:一种可以在任何地方运行应用程序而无需担心如何到达那里的方式。通常,在这种模型中,零停机部署是通过蓝绿部署风格完成的,您会在旧一代应用程序的旁边启动新一代应用程序,然后逐渐将工作从旧堆栈迁移到新堆栈中。
使用现在由Kelsey Hightower广为人知的比喻,调度器就像是为您玩俄罗斯方块,动态地将服务放置在服务器上以实现最佳匹配。
虽然Kubernetes不是第一个实现这一功能的平台(最早的平台是Mesos和Cloud Foundry等),但是如今它是基于容器的调度器中的无可争议的领导者。Kubernetes于2014年由谷歌发布,它汲取了谷歌从其内部Borg系统中学到的经验,并将其带到了开源社区。它从一开始就建立在Docker和Linux容器之上,不仅支持Docker的containerd,还支持其他一些容器运行时,这些运行时都使用Docker容器。Kubernetes是一个庞大的系统,有许多不同的商业和基于云的Kubernetes发行版。Cloud Native Computing Foundation提供认证,以确保每个发行版符合更广泛的Kubernetes社区的某些标准。这个领域在迅速变化,虽然Kubernetes非常强大,但它是一个不断演化的目标,很难跟上。如果您要从头开始构建一个全新的系统,您可能会强烈考虑使用Kubernetes。如果没有其他经验,如果您在云上运行,您的提供商的实现可能是最容易遵循的路径。尽管我们鼓励您在任何复杂系统中考虑使用它,但Kubernetes并不是唯一的选择。
Docker Swarm模式于2015年由Docker公司发布,从头开始构建为一种Docker本地系统。如果您正在寻找一个非常简单的编排工具,它完全在Docker平台内部,并且由单个供应商支持,那么Docker Swarm模式可能是一个吸引人的选择。然而,Docker Swarm模式在市场上并没有得到广泛的应用,而且由于Docker将Kubernetes集成到其工具中,这可能不再是一个明确的路径。
编排
当我们谈论调度器时,通常不仅讨论它们将作业匹配到资源的能力,还讨论它们的编排能力。通过这一点,我们指的是在整个系统中指挥和组织应用程序和部署的能力。您的调度器可以在运行时为您移动作业,也可以让您在每个服务器上运行特定任务。在旧系统中,这通常是由特定的编排工具处理的。
在大多数现代容器系统中,包括调度在内的所有编排任务都由核心集群软件处理,无论是Kubernetes、Swarm、云提供商的定制容器管理系统还是其他什么系统。
在所有平台提供的功能中,调度无疑是最强大的。它在将应用程序移到容器中时也对应用程序产生了最大的影响。许多传统应用程序并未设计为在其底层进行服务发现和资源分配的情况下运行,并需要进行大量的更改,以便在真正动态的环境中运行良好。因此,您迁移到容器化系统可能不一定会首先涵盖迁移到调度平台。通常,最佳的生产容器路径在于在传统系统内运行应用程序的同时对其进行容器化,然后再进一步转向更具动态性的调度系统。这可能意味着最初将您的应用程序作为容器运行在当前部署的同一台服务器上,然后一旦运行良好,您可以引入一个调度器。
服务发现
您可以将服务发现视为应用程序在网络上找到其所有其他所需服务和资源的机制。很少有应用程序不依赖于任何其他内容。无状态的静态网站可能是唯一不需要任何服务发现的系统。几乎所有其他系统都需要了解周围系统的一些信息,并需要一种发现这些信息的方式。大多数情况下,这涉及到不止一个系统,但它们通常紧密耦合。
您可能不会以这种方式考虑它们,但在传统系统中,负载均衡器是服务发现的主要手段之一。负载均衡器用于可靠性和扩展性,但它们还会跟踪与特定服务相关联的所有终端点。这有时是手动配置的,有时更具动态性,但其他系统查找服务的终端点的方式是使用已知的负载均衡器地址或名称。这是一种服务发现的形式,在旧系统中,负载均衡器是实现这一目标的常见方法。它们在现代环境中也经常用于此,即使它们看起来与传统的负载均衡器不太相似。旧系统中的其他服务发现方法包括静态数据库配置或应用程序配置文件。
正如您在图9-1中所看到的那样,Docker在您的环境中并没有解决服务发现的问题,除非使用Docker Swarm模式。对于绝大多数系统,服务发现留给了平台来处理。这意味着这是您在更具动态性的系统中需要解决的首要问题之一。容器本质上很容易移动,这可能会打破围绕更静态部署的应用程序构建的传统系统。每个平台都以不同的方式处理这个问题,您需要了解对您的系统来说哪种方式最合适。
您可能熟悉以下一些服务发现机制的示例:
- 具有众所周知地址的负载均衡器
- 轮询 DNS
- DNS SRV 记录
- 动态 DNS 系统
- 多播 DNS
- 具有众所周知地址的覆盖网络
- Gossip 协议
- 苹果的 Bonjour 协议
- Apache ZooKeeper
- HashiCorp 的 Consul
- etcd
这是一个很大的列表,还有许多其他选项。其中一些系统还可以执行更多任务,这可能会让问题变得复杂。在您尝试理解这个概念时,可能更接近您的是Docker Compose在第8章中使用的链接机制。此机制依赖于由dockerd服务器提供的DNS系统,允许Docker Compose中的一个服务引用另一个对等服务的名称并返回正确的容器IP地址。Kubernetes在其最简单的形式中也有类似的系统,它使用注入的环境变量。但这些是现代系统中最简单的发现形式。
通常情况下,您会发现这些系统的接口依赖于对服务的众所周知的名称和/或端口。您可以调用service-a.example.com来访问一个众所周知的名称上的服务A。或者您可以调用services.example.com:service-a-port来访问同一个众所周知名称和端口上的服务。现代环境通常以不同的方式处理这个问题。通常在新系统中,这个过程会由系统进行管理,并且非常无缝。对于新应用程序,通常很容易从平台调用更传统的系统,但有时反过来就不那么容易。通常,最好的初始系统(虽然不一定是长期的)是一个您提供动态配置的负载均衡器,这些负载均衡器对于旧环境中的系统来说非常容易访问。如果您使用该平台,Kubernetes提供了Ingress路由的形式,可能是要考虑的路径之一。
其中一些示例包括:
- Kubernetes的Ingress控制器,包括Traefik或Contour等
- Linkerd服务网格
- 使用Lyft的Envoy代理的独立Sidecar服务发现
- Istio服务网格和Lyft的Envoy
如果您正在运行混合的现代和传统系统,则将流量引入较新的容器化系统通常是更难解决的问题,也是您应该首先考虑的问题。
生产环境总结
许多人将会从使用简单的Docker编排工具开始。然而,随着容器数量和部署频率的增加,分布式调度器的吸引力将迅速显现出来。诸如Kubernetes之类的工具可以让您将单个服务器和整个数据中心抽象为大型资源池,以运行基于容器的任务。
在部署领域,无疑还有许多其他值得关注的项目。但在撰写本文时,这些是最常被引用的,并且拥有最多的公开可用信息。这是一个快速发展的领域,因此值得四处寻找,看看有什么新的工具正在推出。
无论如何,您应该首先建立一个Linux容器基础设施,然后再考虑外部工具。Docker内置的工具可能已经足够好用。我们建议使用最轻量级的工具来完成任务,但具备灵活性是一个很好的状态,而且Linux容器正在得到越来越强大的工具支持。
Docker与DevOps流水线
所以一旦我们考虑并实现了所有这些功能,我们的生产环境应该是坚固的。但是,我们如何知道它是否有效?Docker的一个关键承诺是能够在与生产环境中完全相同的操作环境中测试您的应用程序及其所有依赖项。它不能保证您已经正确测试了外部依赖项(如数据库),也不提供任何神奇的测试框架,但它可以确保您的库和其他代码依赖项都在一起进行了测试。更改基础依赖关系是出现问题的一个关键环节,即使对于具有强大测试纪律的组织也是如此。有了Docker,您可以构建映像,在开发环境中运行它,然后在将其送往生产服务器之前,在持续集成流水线中测试相同的映像。
测试您的容器化应用程序并不比测试应用程序本身更复杂,只要您的测试环境设计用于管理Linux容器工作负载。接下来,让我们来看一个如何做到这一点的示例。
简要概述
让我们为一个虚构的公司绘制一个示例的生产环境。我们将试图描述一个类似于许多公司的环境,其中加入了Docker以进行说明。 我们虚构的公司环境拥有一组运行Docker守护程序的生产服务器,以及部署在那里的各种应用程序。有多个构建和测试工作节点与管道协调服务器相连。我们将暂时忽略部署,并在虚构的应用程序经过测试并准备好上线时再讨论它。 图9-2展示了测试容器化应用程序的常见工作流程,包括以下步骤:
- 通过某种外部方式触发构建 - 例如,来自源代码仓库的Webhook调用或开发人员的手动触发。
- 构建服务器启动容器镜像构建。
- 图像在本地服务器上创建。
- 图像被打上构建或版本号,或提交哈希值的标签。
- 基于新构建的图像配置一个新容器,用于运行测试套件。
- 测试套件针对容器运行,并将结果由构建服务器捕获。
- 构建被标记为通过或失败。
- 通过的构建被传输到镜像注册表或其他存储机制。
您会注意到,这与常见的应用程序测试模式并没有太大不同。至少,您需要有一个可以启动测试套件的作业。我们在这里添加的步骤只是先创建一个容器镜像,然后在容器内调用测试套件。
让我们看一下这是如何适用于我们在虚构公司部署的应用程序的。我们刚刚更新了应用程序,并将最新的代码推送到了Git仓库。我们有一个提交后钩子,在每次提交时触发构建,因此该作业在构建服务器上启动,该服务器也正在运行dockerd守护进程。构建服务器上的作业将任务分配给测试工作节点。该工作节点没有运行dockerd,但安装了docker命令行工具。因此,我们针对远程dockerd守护进程运行docker图像构建,生成新的图像在远程Docker服务器上。
图像构建完成后,我们的测试作业将基于我们的新生产图像创建并运行一个新的容器。我们的图像被配置为在生产中运行应用程序,但是我们需要为测试运行不同的命令。没问题!Docker允许我们通过在docker container run命令的末尾提供命令来实现这一点。在生产环境中,我们想象中的容器将启动supervisor,然后启动nginx实例和一些位于其后的Ruby unicorn web服务器实例。但是对于测试,我们不需要nginx,也不需要运行我们的Web应用程序。相反,我们的构建作业以如下方式调用容器:
$ docker container run -e ENVIRONMENT=testing -e API_KEY=12345 \
-it awesome_app:version1 /opt/awesome_app/test.sh
我们调用了docker container run,但我们在这里还做了一些额外的事情。我们将一些环境变量传递到容器中:ENVIRONMENT和API_KEY。这些可以是新的环境变量,也可以是对Docker已经为我们导出的环境变量的覆盖。我们还要求使用特定的标签 - 在这个例子中是version1。这将确保我们在正确的镜像之上构建,即使同时有另一个构建正在运行。然后,我们重写了容器的配置,该配置是根据Dockerfile中的CMD行设置的。相反,我们调用了我们的测试脚本,即/opt/awesome_app/test.sh。虽然在这个示例中没有必要,但你应该注意,在某些情况下,你需要覆盖Dockerfile的ENTRYPOINT(--entrypoint)以运行与该容器的默认命令不同的内容。
这里需要强调的一个关键点是,docker container run 的退出状态将与在容器中调用的命令的退出状态一致。这意味着我们只需要查看退出状态就可以看到测试是否成功。如果你的测试套件设计得当,这可能就是你所需要的。如果你需要运行多个步骤,或者不能依赖退出代码,处理这种情况的一种方法是将测试运行的所有输出捕获到一个文件中,然后在输出中查找状态消息。我们虚构的构建系统就是这样做的。我们将测试套件的输出写入文件,并且我们的test.sh在最后一行使用echo输出Result: SUCCESS! 或 Result: FAILURE! 来表示测试是否通过。如果你需要依赖这种机制,请确保查找一些在正常测试套件输出中不会偶然出现的输出字符串。如果我们需要查找"success",例如,我们应该将其限制为仅查看文件的最后一行,并且还要确保整行与我们通常期望的确切输出匹配。在这种情况下,我们仅查看文件的最后一行并找到了我们的成功字符串,所以我们将构建标记为通过。
还有一个与容器相关的步骤。我们希望将通过的构建推送到我们的镜像仓库。镜像仓库是构建和部署之间的交换点。它还允许我们与同行和可能基于该镜像构建的其他构建共享镜像。但是现在,我们只需要将其视为放置和标记成功构建的地方。我们的构建脚本现在将执行docker image tag来为镜像添加正确的构建标签(可能包括latest),然后执行docker image push来将构建推送到镜像仓库。
就是这样!正如你所看到的,与测试普通应用程序相比,这并不复杂。我们利用了Docker的客户端/服务器模型,将测试从主要的测试服务器调用到另一台服务器上,然后将我们的测试封装到一个合并的shell脚本中以生成我们的输出状态。总体而言,这与大多数其他现代构建系统方法非常相似。
最关键的要点是,我们虚构的公司系统确保他们只部署那些测试套件在相同的Linux发行版、相同的库和相同的构建设置下通过的应用程序。这个容器可能会被测试与任何外部依赖(如数据库或缓存)一起运行,而无需模拟它们。虽然这并不能保证成功,但它让我们更接近这一目标,远比那些没有基于容器技术构建的生产部署系统常常遇到的依赖关系问题。
外部依赖
但是那些我们忽略的外部依赖怎么办?像数据库、Memcached或Redis实例这样的东西,我们需要在容器中运行我们的测试。如果我们的虚构公司的应用程序需要数据库、Memcached或Redis实例来运行,我们需要解决这个外部依赖以获得干净的测试环境。通过一些工作,您可以使用像Docker Compose这样的工具来实现,我们在第8章中对其进行了详细描述。在Docker Compose中,我们的构建作业可以在容器之间表达一些依赖关系,然后Compose会无缝连接它们。
能够在类似应用程序将要运行的环境中测试应用程序是一个巨大的优势。Compose使这一点变得相当容易设置。您仍然需要依赖于您自己的语言测试框架进行测试,但环境很容易进行编排。
总结
现在我们已经概述了容器化应用程序如何与外部环境交互,以及在这些各个领域中的界限在哪里,我们准备探讨如何构建Docker集群,以支持许多现代技术运营的全球、始终可用和按需性质。