基础设施即代码-模式和实践-全-

基础设施即代码、模式和实践(全)

原文:Infrastructure as Code, Patterns and Practices

译者:飞龙

协议:CC BY-NC-SA 4.0

目录

前言

我第一次参观数据中心时,被入口处的视网膜扫描仪、闪烁的灯光、冷却系统和五颜六色的电线深深吸引。由于我的背景是电气工程,我能够欣赏到管理硬件的复杂性。当一家公司聘请我来管理一个私有云平台时,我遇到了令人困惑的云计算概念。我不再插线并构建服务器,而是盯着数千台服务器的用户界面中的进度条,并编写糟糕的脚本来部署它们。

在那时,我意识到我需要学习更多。我想自动化更多的基础设施,并编写其他团队成员可以使用的可持续代码。我的学习之旅反映了云计算和 DevOps 哲学的发展。我们需要学习如何改变和扩展我们的基础设施,以跟上业务创新并避免影响关键系统!随着公共云使得按需获取基础设施资源变得更加容易,我们几乎可以将我们的基础设施视为我们软件的延伸。

我通过成为一名通才经历了坎坷的学习之旅。我评估了公共云迁移的成本,与高级 Java 开发者配对(这个挑战让我流泪),将设计模式和软件开发理论应用于代码,尝试了敏捷方法,并向质量保证和安全专业人士提出了许多问题。当我吸收了不同的观点和技术经验后,我试图作为顾问和开源基础设施工具的开发者倡导者,帮助其他人在他们的学习之旅上。

我决定写这本书,因为足够多的系统管理员、安全专业人士和软件开发者表示,他们想学习基础设施即代码(IaC),并需要一个组织编写 IaC 的模式和实践的资源。这本书反映了我早期关于 IaC 以及应用特定模式和实践的考虑和挑战的所有愿望,不受工具和技术的影响。

我从未预料到这本书会有如此多的细节。每当我发布一个章节时,我都会收到来自某人的关于我忘记的事情或建议将某个主题扩展为一个章节的笔记。许多章节涵盖了有整本书(甚至纪录片)专门讨论的主题,但在这本书中只进行了概括性的、高层次的处理。我专注于你必须知道的最重要的事情,以便将主题应用于 IaC。

您可能会看看这本书中的示例,问:“为什么不使用这个其他工具?”我在平衡高级理论和实际示例方面遇到了困难。代码列表引发了我的审稿人和编辑们的热烈讨论,他们中的许多人建议在不同的语言、工具和平台上进行扩展或替换!我尽力找到语言、工具和平台的组合来展示这些模式。在写作时,您会发现代码列表是用 Python 编写的,由 HashiCorp Terraform 部署,并在 Google Cloud Platform (GCP)上运行。每个代码列表都附带了一个高级描述,其中包含了模式和练习,您可以根据需要应用,无论语言、工具或平台。

我希望您阅读这本书,并找到一两个有助于您编写更干净的 IaC、在团队中协作 IaC 以及在公司范围内扩展和保障 IaC 的模式。请不要期望使用每一个模式和练习,或者一次性应用所有这些。您可能会感到不知所措!当您在 IaC 中遇到挑战时,我希望您能回到这本书,参考更多模式。

致谢

写一本书需要社区的支持,而帮助我的这个社区是杰出的。

感谢我的伴侣,Adam,他帮助我抽出时间(以及大量的咖啡)来专注于这本书的编写。还要感谢我的家人,他们鼓励我追求我对基础设施的兴趣。即使你们不理解我试图理清的技术概念,你们也提供了鼓励和支持的言语,并倾听我的想法。

我非常感谢 Manning 出版社的编辑们——Chris Philips、Mike Shepard、Tricia Louvar 和 Frances Lefkowitz——他们的耐心、鼓励、指导和建议。感谢你们在草稿非常粗糙的情况下,始终保持一致的反馈和承诺。我还想感谢这本书的生产和推广团队。

感谢阅读我的手稿并抽出时间和精力提供反馈的审稿人:Cosimo Attanasi, David Krief, Deniz Vehbi, Domingo Salazar, Ernesto Cárdenas Cangahuala, George Haines, Gualtiero Testa, Jeffrey Chu, Jeremy Bryan, Joel Clermont, John Guthrie, Lucian Maly, Michael Bright, Ognyan Dimitrov, Peter Schott, Ravi Tamiri, Sean T. Booker, Stanford S. Guillory, Steffen Weitkamp, Steven Oxley, Sylvain Martel, 和 Zorodzayi Mukuya,你们的建议帮助这本书变得更加出色。

感谢 HashiCorp 社区团队,感谢你们提供意见、审查概念并鼓励我写作。你们在技术挑战和偶尔的冒名顶替综合症中激励我继续写作。特别感谢我的同事和技术校对员 Taylor Dolezal,他勇敢地检查了我的代码,并在多个版本中阅读了这本书。

向我合作过的系统管理员、测试人员、产品经理、基础设施工程师、业务分析师、软件开发人员和安全工程师,您可能会在这本书中认出一些我们共同探讨、讨论或辩论的模式和最佳实践。感谢您在基础设施即代码方面教会我一些东西。您是使这一切成为可能的社会群体的一部分。

关于本书

我编写了《基础设施即代码,模式和最佳实践》一书,旨在帮助您编写不会影响关键业务系统的 IaC。本书侧重于您作为个人、团队或公司可以在整个基础设施系统中应用的模式和最佳实践。它侧重于可以应用于您的 IaC 的高级模式和最佳实践,同时提供具体的示例来展示实现方法。

谁应该阅读这本书?

本书面向任何开始使用云基础设施和 IaC 并希望将其扩展到团队或公司的人(软件开发者、安全工程师、质量保证工程师或基础设施工程师)。您将已经编写了一些 IaC,手动运行它,并在公共云上创建了资源。

然而,您现在面临的是在团队或公司内促进 IaC 协作的挑战。您必须减轻多个团队成员和其他团队进行基础设施更改和请求安全、合规性或功能更新时的摩擦。虽然许多资源在特定工具的背景下介绍 IaC,但本书将您可以应用于各种基础设施用例、工具和系统的模式和最佳实践进行了概括,这些工具和系统将随着时间的推移而发展。

本书是如何组织的:路线图

我将本书组织为三个部分,共 13 章。

第一部分介绍了基础设施即代码(IaC)以及作为个人如何编写它。

  • 第一章定义了 IaC 及其优势和原则。本章解释了本书包含 Python 示例,由 HashiCorp Terraform 运行,并部署到谷歌云平台(GCP)。我还讨论了您在 IaC 之旅中可能会遇到的工具和用例。

  • 第二章深入探讨了不可变性的原则以及如何将现有的基础设施资源迁移到 IaC。它还涵盖了编写干净 IaC 的实践。

  • 第三章提供了一些将基础设施资源划分为模块的模式的建议。每个模式都包含一个示例和用例列表。

  • 第四章涵盖了如何管理基础设施资源和模块之间的依赖关系,并通过依赖注入和一些常见模式将它们解耦。

第二部分描述了如何作为团队编写和协作 IaC。

  • 第五章组织了在不同存储库结构中表达 IaC 以及与团队共享时的实践和考虑因素。

  • 第六章提供了基础设施测试策略。它描述了每种测试类型以及如何为 IaC 编写它们。

  • 第七章将持续交付应用于 IaC。它涵盖了分支模型的高级视图以及您的团队如何使用它们来更改基础设施。

  • 第八章提供了构建安全且符合规定的 IaC 的技术,包括测试和标记。

第三部分涵盖了如何在公司内部管理 IaC。

  • 第九章将不可变性应用于基础设施更改,包括蓝绿部署的示例。

  • 第十章重构了大量 IaC,以提高其可维护性并减轻对单个代码库中失败更改的冲击范围。

  • 第十一章描述了撤销 IaC 和将更改向前推进到系统中的方法。

  • 第十二章讨论了使用 IaC 来管理云计算成本。它包括 IaC 成本估算的示例。

  • 第十三章通过实践来管理和更新 IaC 工具,从而完成本书。

您会发现本书中许多概念相互关联,如果您之前没有练习过 IaC,按顺序阅读章节可能会有所帮助。否则,您可以选择最适合您在 IaC 实践中面临的挑战的部分。

在阅读特定概念的各个章节之前,您可能首先想阅读第一章或附录 A,以了解如何阅读和运行示例。附录 A 提供了有关示例中相关的库、工具和平台的额外详细信息,而附录 B 提供了练习题的答案。

关于代码

由于基础设施配置的冗长,本书中的一些代码列表为了清晰起见,并没有包含整个基础设施定义。第二章到第十二章包含了代码列表,作为其概念的示例。

现有的代码列表使用了 Python 3.9、HashiCorp Terraform 1.0 和 Google Cloud Platform 的组合。附录中包含了有关如何运行示例及其工具和库的更多信息。我对 GitHub 上工具的较小版本更新源代码。

本书包含许多源代码示例,既有编号列表,也有与普通文本混排。在这两种情况下,源代码都使用固定宽度字体如这样来格式化,以将其与普通文本区分开来。有时代码也会加粗以突出显示与章节中先前步骤相比有所更改的代码,例如当新功能添加到现有代码行时。

在许多情况下,原始源代码已被重新格式化;我们添加了换行符并重新调整了缩进,以适应书籍中的可用页面空间。在极少数情况下,即使这样也不够,列表中还包括了行续接标记(➥)。此外,当代码在文本中描述时,源代码中的注释通常已从列表中删除。代码注释伴随任何列表,突出显示重要概念。您可以从本书的 liveBook(在线)版本中获取可执行的代码片段,网址为 livebook.manning.com/book/essential-infrastructure-as-code。完整的源代码可以从 Manning 网站 www.manning.com/books/infrastructure-as-code-patterns-and-practices 和 GitHub github.com/joatmon08/manning-book 下载。

liveBook 讨论论坛

购买 基础设施即代码:模式和最佳实践 包括免费访问 liveBook,Manning 的在线阅读平台。使用 liveBook 的独家讨论功能,您可以在全球范围内或针对特定部分或段落附加评论。为自己做笔记、提出和回答技术问题以及从作者和其他用户那里获得帮助都非常简单。要访问论坛,请访问 livebook.manning.com/book/infrastructure-as-code-patterns-and-practices/discussion。您还可以在 livebook.manning.com/discussion 了解更多关于 Manning 论坛和行为准则的信息。

Manning 对读者的承诺是提供一个场所,让读者之间以及读者与作者之间可以进行有意义的对话。这不是对作者参与特定数量承诺的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们建议您尝试向作者提出一些挑战性的问题,以免她的兴趣转移!论坛和以前讨论的存档将从出版社网站提供,直到书籍有售。

关于云服务提供商

在决定用于示例的云服务提供商时,我遇到了挑战。虽然亚马逊网络服务(AWS)或微软 Azure(Azure)可能在出版时更为流行,但它们需要创建许多资源。例如,它们的网络需要网络、子网、路由表、网关和安全组,你才能使用它们。相反,我决定使用谷歌云平台(GCP)作为主要的云服务提供商,以简化您需要创建的资源数量。

虽然我在示例中使用 GCP,但概念、流程和指南旨在保持中立,您应该能够将它们适应到其他云服务提供商。对于更喜欢使用 AWS 或 Azure 的读者,每个示例都包含了这两个平台上的等效服务信息。此外,一些示例还包括代码仓库中的等效内容。

在第一章中,您可以了解更多关于我选择使用 GCP 的原因,以及如何将示例适应 Azure 和 AWS。附录 A 中包含了在 GCP 上设置和运行示例的说明,以及针对 Azure 和 AWS 用户的提示。

其他在线资源

请参考针对您特定 IaC 工具或基础设施提供商的在线资源。其中许多提供了他们在工具中实现的实践和模式示例。

关于作者

Rosemary Wang 作者照片

玫瑰玛丽·王致力于弥合基础设施、安全和应用开发之间的技术和文化障碍。作为贡献者、公众演讲者、作家和开源基础设施工具的倡导者,她对解决棘手问题充满热情。当她不在白板上绘制时,玫瑰玛丽在她的笔记本电脑上调试各种基础设施系统,同时给她的室内植物浇水。

关于封面插图

《基础设施即代码:模式和实战》的封面图题为“Bayadere”,意为“印度的一位女性印度舞者”,取自雅克·格拉塞·德·圣索沃尔于 1797 年出版的作品集。每一幅插图都是手工精心绘制和着色的。

在那些日子里,人们通过他们的服饰很容易就能识别出他们住在哪里,以及他们的职业或社会地位。曼宁通过基于几个世纪前丰富多样的地区文化的封面设计,庆祝计算机行业的创新和主动性,这些文化通过如这一系列图片这样的收藏品被重新带回生活。

第一部分. 初步步骤

什么是基础设施即代码(IaC),你该如何着手编写它?第一部分回答了这些问题,并介绍了你可以应用于编写 IaC 的实践和模式。在第一章中,你将了解 IaC 是如何工作的,它解决了哪些问题,以及这本书将如何帮助你开始使用它。第二章讨论了如何编写干净的代码,以及如何在现有系统中定义 IaC 的指南。

第三章和第四章深入探讨了声明基础设施组(称为模块)及其依赖关系的模式。你将学习基础设施模块的模式,以及如何解耦模块依赖以支持变更并最小化影响范围。这些章节还提供了针对你的范围和情况的适当模式选择的指南。

1 介绍基础设施即代码

本章涵盖

  • 定义基础设施

  • 定义基础设施即代码

  • 理解基础设施即代码的重要性

如果你刚开始与公共云提供商或数据中心基础设施合作,你可能会觉得所有需要学习的内容让你感到不知所措。你不知道需要知道什么来完成你的工作!在数据中心基础设施概念、新的公共云服务、容器编排器、编程语言和软件开发之间,你需要研究很多东西。

除了学习尽可能多的知识外,你还必须跟上公司的创新和增长需求。构建支持所有这些的系统具有挑战性。你需要一种方式来支持更复杂的系统,最小化你的维护工作,并避免对你的应用程序使用者的服务中断。

你需要了解什么才能与云计算或数据中心基础设施合作?你如何在一个团队和组织中扩展你的系统?这两个问题的答案都涉及基础设施即代码IaC),这是一种以编码方式自动化基础设施变更的过程,以实现可扩展性、可靠性和安全性。

每个人都可以使用 IaC,从系统管理员、站点可靠性工程师、DevOps 工程师、安全工程师和软件开发人员,到质量保证工程师。无论你是否刚刚完成了你的第一个 IaC 教程,或者已经通过了公共云认证(恭喜!),你都可以将 IaC 应用于更大的系统和团队,以简化、持续和扩展你的基础设施。

这本书通过将软件开发实践和模式应用于基础设施管理,提供了一种实用的基础设施即代码(IaC)方法。书中介绍了测试、持续交付、重构和设计模式等实践,并从基础设施的角度进行阐述。你将找到各种实践和模式,以帮助你在任何自动化、工具、平台或技术下管理你的基础设施。

我将这本书分为三个部分(图 1.1)。第一部分涵盖了你可以应用于编写 IaC 的实践,第二部分描述了你的团队在协作时使用的模式和实践。第三部分涵盖了在组织内扩展 IaC 的一些方法。

图 1.1 在本书中,你将学习到你的实践、团队和组织之间的交集,以及它们如何帮助你扩展系统和支持关键任务应用。

本书中的许多模式和实践都与这三个兴趣点相交。单独编写好的 IaC 可以帮助你更好地在团队和组织中共享和扩展它。良好的 IaC 有助于解决与协作相关的 IaC 问题,特别是随着越来越多的人采用它。

第一部分从定义基础设施和解释常见的 IaC 设计模式开始。这些主题涉及帮助你在团队中扩展 IaC 的基础概念。你可能已经熟悉本部分的一些材料,因此请回顾这些章节,为更高级的概念建立基础。

在第二部分和第三部分,你将学习到扩展系统和支持关键任务应用程序基础设施所需的模式和最佳实践。这些实践从你扩展到你的团队和组织,从为应用程序创建一个指标警报到在一个拥有 50,000 人的组织中实施网络变更。许多术语和概念在这些部分中相互依赖,因此你可能发现按顺序阅读章节会有所帮助。

1.1 什么是基础设施?

在我深入探讨基础设施即代码(IaC)之前,让我们先从基础设施的定义开始。当我开始在数据中心工作时,文献通常将基础设施定义为提供网络、存储或计算能力的硬件或设备。图 1.2 展示了应用程序如何在服务器上运行(计算)、通过交换机连接(网络),并在磁盘上维护数据(存储)。

图 1.2 数据中心对基础设施的定义包括用于运行应用程序的网络、计算和存储资源。

这三个类别与我们管理的数据中心中的物理设备相匹配。我们通过扫描我们的 ID 进入大楼,插入设备,输入命令,并希望一切仍然正常工作。随着云计算的出现,我们继续使用这些类别来讨论特定设备的虚拟化。

然而,数据中心对基础设施的定义并不完全适用于今天的服务和产品。想象一下,另一个团队要求你帮助他们将应用程序交付给用户的生产环境。你通过一个清单,包括以下设置:

  • 足够的服务器

  • 用户网络连接

  • 用于存储应用程序数据的数据库

完成这个清单意味着团队可以在生产中运行这个应用程序吗?不一定。你不知道你是否设置了足够的服务器或适当的访问权限来登录应用程序。你还需要知道网络延迟是否会影响应用程序的数据库连接。

在这个狭义的基础设施定义中,你省略了一些对于生产准备至关重要的任务,包括以下内容:

  • 监控应用程序指标

  • 导出指标以进行业务报告

  • 为操作应用程序的团队设置警报

  • 为服务器和数据库添加健康检查

  • 支持用户身份验证

  • 记录和汇总应用程序事件

  • 在密钥管理器中存储和轮换数据库密码

您需要这些待办事项来交付一个可靠且安全的应用程序到生产环境中。您可能会将它们视为运营要求,但它们仍然需要基础设施资源。

除了与运营相关的基础设施之外,公共云提供商抽象化了基础网络、计算和存储的管理,并提供平台即服务(PaaS)产品,例如从存储桶到事件流平台如托管 Apache Kafka 等对象存储。提供商甚至提供函数即服务(FaaS)或容器即服务,这是计算资源的额外抽象。软件即服务(SaaS)市场的日益增长,如托管的应用性能监控软件,可能也需要支持生产中的应用程序,也可能被视为基础设施。

由于有如此多的服务,我们无法仅用计算、网络和存储类别来描述基础设施。我们需要在我们的应用程序交付中包括运营基础设施、PaaS 或 SaaS 产品。图 1.3 调整了基础设施模型,包括额外的服务产品,如 SaaS 和 PaaS,帮助我们交付应用程序。

由于日益增长的复杂性、不同的运营模式以及数据中心管理的用户抽象化,我们不能将基础设施的定义限制在硬件或与计算、网络或存储相关的物理设备上。

定义基础设施是指将应用程序交付或部署到生产环境的软件、平台或硬件。

这里是一个您可能遇到的非详尽的基础设施列表:

  • 服务器

  • 工作负载编排平台(例如,Kubernetes,HashiCorp Nomad)

  • 网络交换机

  • 负载均衡器

  • 数据库

  • 对象存储

  • 缓存

  • 队列

  • 事件流平台

  • 监控平台

  • 数据管道系统

  • 支付平台

扩展基础设施的定义为跨团队提供了管理各种目的资源的通用语言。例如,一个管理组织持续集成(CI)框架的团队可能使用来自持续集成 SaaS 或公共云的计算资源。另一个团队在此基础上构建,因此使其成为关键基础设施。

图片

图 1.3 应用程序的基础设施可能包括公共云上的队列、运行应用程序的容器、用于额外处理的无服务器函数,甚至用于检查系统健康的监控服务。

1.2 什么是基础设施即代码?

在解释基础设施即代码之前,我们必须理解手动基础设施配置。在本节中,我概述了基础设施和手动配置的问题。然后我定义了基础设施即代码

1.2.1 基础设施的手动配置

作为网络团队的一员,我学会了通过复制粘贴文本文档中的命令来更改网络交换机。我曾在粘贴时将shutdown误写成no shutdown,关闭了网络接口!我迅速将其重新打开,希望没有人注意到并且没有影响任何事情。然而,一周后,我发现它关闭了与一个关键应用程序的连接,并影响了几个客户请求。

回顾过去,我在手动复制粘贴命令和基础设施配置方面遇到了一些问题。首先,我不知道我的变更会影响哪些资源(也称为爆炸半径)。我不知道哪些网络或应用程序使用了该接口。

定义:爆炸半径指的是一个失败的变更对系统造成的影响。较大的爆炸半径通常会影响更多的组件或最关键的组件。

其次,网络交换机接受了我的命令,而没有测试其效果或检查其意图。最后,没有人知道是什么影响了应用程序处理客户请求,他们花了整整一周时间才确定根本原因是我的误复制命令。

以编码的方式编写基础设施如何帮助我发现误复制的网络交换机命令?我可以将我的配置和自动化存储在源代码管理中,以记录命令。为了防止未来出现错误,我创建了一个虚拟交换机和一个测试,该测试运行我的脚本并检查接口的健康状况。

测试通过后,我将变更推进到生产环境,因为测试会检查正确的命令。如果我应用了错误的命令,我可以搜索基础设施配置来确定哪些应用程序运行在受影响的网络上。你可以参考第六章了解测试实践,第十一章了解变更回滚。

除了配置错误的风险外,由于手动配置基础设施,我的开发进度有时会放缓。有一次,我花了近两个月的时间来测试我的应用程序与数据库的兼容性。在这两个月里,我的团队提交了超过 10 张与创建数据库、配置新的路由以将应用程序连接到数据库以及打开防火墙规则以允许我的应用程序相关的工单。平台团队在公共云中手动配置了一切。由于安全考虑,开发团队没有直接访问权限。

换句话说,随着系统和团队的增长,手动配置基础设施通常无法扩展。手动变更*增加了系统的变更失败率,减缓了开发进度**,并将系统的攻击面暴露于潜在的安全漏洞之中。你总会诱惑着将一些值更新到控制台。然而,这些变更会累积。

下一个人对系统进行更改时,可能会引入系统故障,因为更改尚未经过审计或整理。例如,在开发过程中更新防火墙以允许某些流量,可能会无意中使系统容易受到攻击。

1.2.2 基础设施即代码

如果不是手动更改,你应该做什么来更改基础设施?你可以将软件开发生命周期应用于基础设施资源和配置,形式为 IaC。然而,基础设施开发生命周期不仅限于配置文件和脚本。

基础设施需要扩展、管理故障、支持快速软件开发,并保护应用程序。基础设施开发生命周期涉及更具体的模式和流程来支持协作、部署和测试。在图 1.4 中,简化的工作流程通过使用配置或脚本更改基础设施,并将它们提交到版本控制。提交会自动启动一个工作流程来部署和测试对基础设施的更改。

图片

图 1.4 基础设施开发生命周期包括编写代码作为文档、将其提交到版本控制、以自动方式将其应用于基础设施,并对其进行测试。

为什么你应该记住开发生命周期?你可以将其用作管理更改和验证它们不会影响你系统的一般模式。生命周期捕捉到基础设施即代码,以编码方式自动化基础设施更改,并应用 DevOps 实践,如版本控制和持续交付。

定义 基础设施即代码 (IaC) 将 DevOps 实践应用于以编码方式自动化基础设施更改,以实现可扩展性、弹性和安全性。

我经常发现 IaC 被引用为 DevOps 的必要实践。它确实解决了 CAMS 模型(文化、自动化、测量和共享)中的自动化部分。图 1.5 将 IaC 定位为 DevOps 模型中自动化实践和哲学的一部分。代码作为文档、版本控制、软件开发模式和持续交付的实践与之前讨论的开发生命周期工作流程相一致。

图片

图 1.5 IaC 应用版本控制、软件开发模式、持续集成和代码作为文档到基础设施。

为什么在 DevOps 模型中将 IaC 作为自动化的一部分进行关注?你的组织不必采用 DevOps 就可以使用 IaC。它的好处可以提高 DevOps 的采用率和指标,但仍然适用于任何基础设施配置。你仍然可以使用 IaC 实践来改进基础设施更改的过程,而不会影响生产。

注意:您将在本书中看到一些 DevOps 实践,但我并不专注于其理论或原则。我推荐 Nicole Forsgren 等人所著的《加速》(IT Revolution Press,2018 年)以获得对 DevOps 的更高层次理解。您还可以阅读 Gene Kim 等人所著的《凤凰项目》(IT Revolution Press,2013 年),该书描述了一个采用 DevOps 的组织所进行的虚构转型。

本书涵盖了一些将基础设施编码化的方法,以消除扩展的摩擦,同时保持为应用程序用户维护基础设施的可靠性和安全性,无论您使用数据中心还是云。诸如配置文件版本控制、CI 管道和测试等软件开发实践可以帮助扩展和演进基础设施的变化,同时减轻停机时间并构建安全的配置。

1.2.3 什么是非基础设施即代码?

如果您在文档中键入一些配置,您是否在进行 IaC?您可能会认为 IaC 包括在变更票中添加配置指令。您可能会认为构建队列的教程或配置服务器的 shell 脚本算作 IaC。如果这些例子可以用来做到以下事项,那么它们可以是IaC 的形式:

  • 可靠且准确地重现所表达的基础设施

  • 将配置还原到特定版本或时间点

  • 传达并评估变更对资源的爆炸半径

然而,配置或脚本通常过时、未版本化或意图不明确。您甚至可能发现自己难以理解和更改用 IaC 工具编写的配置。该工具促进了 IaC 工作流程,但并不一定促进允许系统在减少运营责任和降低变更失败率的同时增长的做法和途径。您需要一套原则来识别 IaC。

1.3 基础设施即代码的原则

正如我提到的,并非所有与基础设施相关的代码或配置都能进行扩展或减轻停机时间。在本书中,我强调了 IaC 原则如何应用于某些代码列表或实践。您甚至可以使用这些原则来评估您的 IaC。

当其他人可能对这个原则列表进行增减时,我记住四个最重要的原则,即通过记忆法“RICE”。这代表的是可重现性、幂等性、可组合性和可扩展性。我在以下章节中定义并应用每个原则。

1.3.1 可重现性

想象一下,有人要求您创建一个包含队列和服务器的发展环境。您与您的同事共享一组配置文件。他们用这些文件在不到一个小时的时间内为自己重新创建了一个新环境。图 1.6 显示了您如何共享配置并使您的同事能够重现一个新环境。您发现了可重现性的力量,这是 IaC 的第一个原则!

为什么 IaC 应符合这个可重复性原则?能够复制和重用基础设施配置可以节省您和您的团队在创建新环境或基础设施资源时的初始工程时间。您不必重新发明轮子来创建新环境或基础设施资源。

图片

图 1.6 手动更改会在版本控制和实际状态之间引入漂移,影响可重复性,因此您应该更新版本控制中的更改。

定义 可重复性原则 意味着您可以使用相同的配置来重现环境或基础设施资源。

然而,您会发现遵循可重复性原则比复制粘贴配置要复杂得多。为了展示这种细微差别,想象您需要将网络地址空间从 /16 减少到 /24。您确实有表达网络的 IaC。然而,您决定选择登录云提供商并在文本框中输入 /24 的简单途径。

在您登录云提供商之前,您会反思您的更改工作流程是否遵循可重复性。您会问自己以下问题:

  • 同事是否会知道您已更新了网络?

  • 如果您运行您的配置,网络地址空间会返回到 /16 吗?

  • 如果您使用版本控制中的配置创建新环境,它将具有 /24 的地址空间吗?

您对每个问题的回答都是否定。您无法保证您能成功重现手动更改。

如果您在云提供商的控制台中输入 /24,则您的网络已从 IaC(图 1.7)中表达的理想状态发生 漂移。为了符合可重复性,您决定更新版本控制配置为 /24 并应用自动化。

图片

图 1.7 手动更改会在版本控制和实际状态之间引入漂移,影响可重复性,因此您应该更新版本控制中的更改。

这个场景展示了符合可重复性的挑战。您需要最小化预期和实际基础设施配置之间的一致性,也称为 配置漂移

定义 配置漂移 是基础设施配置从期望配置到实际配置的偏差。

作为一种实践,您可以通过将配置文件放在版本控制中并尽可能保持版本控制更新来确保可重复性原则。维护可重复性原则有助于您更好地协作 并且 管理与生产环境类似的测试环境。

在第六章中,你将了解更多关于基础设施测试环境的内容,这些环境受益于可重复性。你还将将可重复性应用于测试和升级基础设施的实践和模式,从创建与生产环境镜像的测试基础设施到部署新的基础设施以替换旧系统(蓝绿部署)。

1.3.2 幂等性

一些基础设施即代码(IaC)将可重复性作为一个原则,这意味着运行相同的自动化操作并产生一致的结果。我认为 IaC 需要更严格的要求。运行自动化操作应该导致基础设施资源达到相同的最终状态。毕竟,我编写自动化脚本的主要目标就是能够多次运行自动化操作并获得相同的结果。

让我们考虑为什么 IaC 需要更严格的要求。想象一下,你编写了一个网络脚本,首先配置一个接口,然后重启。第一次运行脚本时,交换机配置了接口并重启。你将这个脚本保存为版本 1。

几个月后,你的同事要求你在交换机上再次运行这个脚本。你运行了脚本,交换机重启了。然而,重启断开了某些关键应用程序!你已经配置了网络接口。为什么还需要重启交换机?

如果你已经配置了网络接口,你找到了一种防止交换机重启的方法。在图 1.8 中,你创建了脚本的版本 2 并添加了一个条件if语句。该语句在重启交换机之前检查你是否已经配置了接口。当你再次运行版本 2 的脚本时,你不会断开应用程序。

条件语句符合幂等性原则。幂等性确保你可以重复自动化操作而不会影响基础设施,除非你更改了配置或发生了漂移。如果一个基础设施配置或脚本具有幂等性,你可以多次重新运行自动化操作,而不会影响资源的状态或可操作性。

图片

图 1.8 在脚本版本 1 中,每次运行脚本时都会重启交换机。在版本 2 中,你在重启交换机之前检查网络接口是否已配置。这保留了网络的正常工作状态。

定义 幂等性原则 确保你可以在基础设施上反复运行自动化操作,而不会影响其最终状态或产生任何副作用。你应该只在更新自动化操作中的基础设施属性时影响基础设施。

为什么你应该在你的 IaC 中坚持幂等性,比如在你的网络脚本中?在示例中,你希望避免重启网络交换机以保持网络的正常运行。你已经配置了网络接口;为什么还要重新配置?你应该只有在接口不存在或发生变化时才需要配置接口。

没有幂等性,你的自动化可能会意外中断。例如,你可能重复执行一个脚本,创建一组新的服务器,使它们的数量翻倍。更严重的是,你可能自动化了一个数据库更新,结果却删除了一个关键的数据库!

你可以通过检查脚本的重复性和配置来确保幂等性原则。作为一种一般做法,在运行自动化之前,包括一系列条件语句来检查配置是否与预期的一致。条件语句有助于在需要时应用更改,并避免可能影响基础设施操作性的副作用。

使用幂等性设计自动化可以降低风险,因为它鼓励包含逻辑以保留系统的最终预期状态。如果自动化失败一次并导致系统中断,组织不再希望再次自动化,因为其感知的风险。随着你学习如何在第十一章中安全地推进更改并在部署前预览自动化更改,幂等性将成为一个指导原则。

1.3.3 可组合性

你希望混合和匹配任何一组基础设施组件,无论工具或配置如何。你还需要在不影响整个系统的情况下更新单个配置。这两个要求都促进了模块化和解耦基础设施依赖,你将在第三章和第四章中了解更多关于这方面的内容。

例如,想象你为在 hello-world.com 上访问的应用程序创建基础设施。以下是你需要一个安全且生产就绪配置的最小资源:

  • 服务器

  • 负载均衡器

  • 为服务器创建的私有网络

  • 为负载均衡器创建的公共网络

  • 允许从私有网络流出的流量路由规则

  • 允许公共流量到达负载均衡器的路由规则

  • 允许从负载均衡器到服务器的流量路由规则

  • 为 hello-world.com 创建域名

你可以从头开始编写这个配置。然而,如果你发现预先构建的模块,这些模块可以组合你用来构建系统的基础设施组件,你会怎么办?现在你有多个模块可以创建以下内容:

  • 网络(私有和公共网络、路由流量从私有网络的网关、允许流量从私有网络流出的路由规则)

  • 服务器

  • 负载均衡器(域名、允许流量从负载均衡器到服务器的路由规则)

在图 1.9 中,你选择了网络、服务器和负载均衡器模块来构建你的生产环境。后来,你意识到你需要一个高级负载均衡器。你用高级负载均衡器替换了标准负载均衡器,以便可以处理更多的流量。服务器和网络继续运行,不会影响用户。

你的队友甚至可以在不影响负载均衡器、服务器或网络的情况下将数据库添加到环境中。你可以以各种组合分组和选择基础设施资源,这符合可组合性原则。

定义 组合性 确保您能够组装任何基础设施资源的组合,并更新每一个资源而不会影响其他资源。

图 1.9 您使用基础设施的构建块构建生产环境,这样您可以轻松地添加新的资源,如高级负载均衡器。

您的配置越具有组合性,创建新系统就越容易,且所需努力更少。想象一下用构建块构建您的基础设施即代码(IaC)。您希望能够在不破坏整个系统的情况下更新或演进资源子集!如果您不考虑 IaC 的组合性,您可能会因为复杂基础设施系统中的未知依赖而导致变更失败。

组合性带来的自助服务优势可以帮助您的组织进行扩展,并赋予团队安全地与基础设施系统交互的能力。第三章和第四章探讨了可以帮助您接近更模块化基础设施构建并提高组合性的某些模式。

1.3.4 可进化性

您希望考虑到系统的规模和增长,但不要过早和不必要地优化配置。大部分基础设施配置会随着时间的推移而改变,包括其架构。

作为实际例子,您可能最初将基础设施资源命名为 example。后来,您需要将资源名称更改为 production。您开始变更的过程是查找并替换数百个标签、名称、依赖资源等。查找和替换的过程需要大量的努力。

当您应用变更时,您注意到您忘记更改了一些字段,并且您的新基础设施变更失败了。为了确保名称、标签和其他元数据的未来进化,您创建了一个名称变量,并且配置引用了这个变量。在图 1.10 中,您更新了全局 NAME 变量,并且变更在整个系统中传播。

图 1.10 而不是查找并替换所有名称实例,您可以设置一个顶层变量,用于所有资源的名称。

这个例子似乎过于简单。为什么更改名称很重要?以 可进化性 为原则构建的 IaC 最小化了更改系统所需的努力(时间和成本)以及变更失败的风险。

定义 可进化性原则 确保您可以在最小化努力和失败风险的情况下更改基础设施资源,以适应系统规模或增长。

系统进化包括除微小变更之外的变化,例如名称变更。在基础设施架构中可能涉及更颠覆性的变化,比如用 Amazon Elastic Map Reduce (EMR) 替换 Google Cloud Bigtable。需要替换的应用程序已经使用 Apache HBase 进行了未来化,Apache HBase 是一个支持两种提供方式的开源分布式数据库,只需数据库端点即可。

我们通过输出数据库端点以检索应用程序,并在幕后创建配置以更新两种服务来在 IaC 中考虑这种演进。在测试了亚马逊网络服务(AWS)数据库后,我们输出其端点供应用程序使用。

注意,本书并未全面涵盖演进架构背后的理论。如果你想要了解更多,我强烈推荐 Neal Ford 等人所著的《构建演进式架构》(O’Reilly,2017)。这本书讨论了如何构建你的基础设施架构以适应变化。

你可能会发现自己难以演进你的系统,因为你没有使用允许其变化的模式和最佳实践。有用的 IaC 专注于促进未来演进的技巧。本书中的许多章节都展示了有助于保持可演进性和最小化对关键系统变更影响的模式。

1.3.5 应用原则

可重复性、幂等性、组合性和可演进性在定义上似乎很具体。然而,它们都帮助约束你的基础设施架构并定义了许多 IaC 工具的行为。你的 IaC 必须与所有四个原则一致才能扩展、协作和改变你的公司。图 1.11 总结了这四个重要原则及其定义。

图 1.11 IaC 应该是可重复的、幂等的、可组合的和可演进的。你可以问自己一系列问题来确定你的 IaC 是否符合所有四个原则。

在编写 IaC 时,请思考你是否符合所有四个原则。这些原则帮助你更轻松地编写和分享 IaC,并理想情况下最小化对系统变更的影响。缺失的原则可能会阻碍基础设施资源的更新或增加潜在失败的影响范围。

在练习基础设施即代码(IaC)时,请思考你的配置或工具是否与最佳实践相符合。例如,对你的工具提出以下问题:

  • 工具是否允许你重新创建整个环境?

  • 当你重新运行工具以强制配置时会发生什么?

  • 你能否混合和匹配各种配置片段来创建一组新的基础设施组件?

  • 工具是否提供帮助你在不影响其他系统的情况下演进基础设施资源的功能?

本书使用这些原则来回答这些问题,并为你提供考虑弹性和可扩展性的技能来测试、升级和部署基础设施。

练习 1.1

选择你组织中的一个基础设施脚本或配置。评估它是否遵循 IaC 的原则。它是否促进可重复性、使用幂等性、帮助组合性和简化可演进性?

请参阅附录 B 以获取练习的答案。

1.4 为什么使用基础设施即代码?

IaC 通常被认为是一种 DevOps 实践。然而,你不必在整个组织中应用 DevOps 就可以使用它。你仍然希望以减少变更失败率和平均恢复时间(MTTR)的方式来管理你的基础设施,这样你就可以在周末睡懒觉,或者作为开发者有更多时间编写代码。即使你认为你不需要它,也有几个原因要使用 IaC。

1.4.1 变更管理

当你将变更应用到某些基础设施上,却意识到有人报告说它破坏了某些东西时,你可能会感到一种沉没感。组织试图通过变更管理来防止这种情况,这是一系列步骤和审查,以确保你的变更不会影响生产。这个过程通常包括一个变更审查委员会来审查变更,或者变更窗口来封锁执行变更的时间。

定义变更管理概述了你在公司中采取的一系列步骤和审查,以在生产中实施变更并防止其失败。

然而,没有任何变更是没有风险的。通过模块化你的基础设施(第三章)和向前滚动变更(第十一章)来限制影响范围,应用 IaC 实践可以减轻变更的风险。

在一个令人遗憾的例子中,我忽视了使用基础设施即代码(IaC)来减轻变更风险的直觉。我不得不在服务器上推出一个新的二进制文件,这要求它们重新启动一系列依赖服务。我编写了一个脚本,让我的队友检查它,并让变更审查委员会签字批准运行它。在周末应用并验证变更后,我周一来到办公室,看到几条消息告诉我支持对账应用程序的服务器在夜间崩溃。我的队友追踪到我的脚本中的依赖与较旧操作系统的不兼容性。

回顾过去,IaC 本可以减轻变更的风险。当我将 RICE 原则应用到变更上时,我意识到我忘记了以下内容:

  • 可重复性——我没有在模拟各种服务器的各种测试实例上重复我的脚本。

  • 幂等性——我没有在运行命令之前检查操作系统的逻辑。

  • 可组合性——我没有将变更的影响范围限制在少量不太关键的服务器上。

  • 可进化性——我没有更新服务器以使用较新的操作系统并减少基础设施中的差异。

减少差异允许基础设施进化和风险缓解,因为实际的配置与你在自动化过程中期望的配置相匹配,使变更更加直接和可靠地应用。我们将在第七章讨论如何将 IaC 融入你的变更管理流程。

1.4.2 时间投资回报

IaC 和时间投资可能难以证明其合理性,特别是如果你的设备或硬件没有合适的自动化接口。除了缺乏易于自动化的接口外,自动化一年或十年才做一次的任务可能也难以证明花费时间进行自动化是合理的。虽然 IaC 可能需要额外的时间来实现,但它从长远来看降低了执行变更所需的时间。这究竟是如何工作的呢?

想象一下,你需要更新 10 台服务器上的相同软件包。你以前没有使用 IaC 来做这件事。你会手动登录,更新软件包,验证一切是否正常工作,修复错误,然后转到下一台。平均来说,你会在更新服务器上花费 10 个小时。

图 1.12 显示,没有 IaC 的变更在时间上具有恒定的努力水平。如果你有额外的变更,你可能需要花费几个小时来修复或更新系统。一个失败的变更可能意味着你需要在几天内花费更多努力来修复系统。

你决定为这些服务器(图 1.12 中的实线)投入时间来构建 IaC。你减少了服务器的配置漂移,这大约需要 40 个小时。在你进行初步时间投资之后,每次进行更改时,你只需花费不到 5 分钟来更新所有服务器。

为什么需要理解基础设施即代码(IaC)中时间和努力之间的关系呢?预防有助于减少修复工作的努力。如果没有 IaC,你可能会发现自己花费数周时间来修复主要系统故障。你通常会在那几周内尝试逆向工程手动更改,撤销特定更改,或者在最糟糕的情况下,从头开始构建一个新的系统。

图片

图 1.12 在经历初期的高自动化努力后,IaC 随时间推移所需的工作量减少。没有 IaC,执行变更所需的时间可能高度可变。

必须在编写 IaC 上进行初步投资,即使这看起来成本很高。随着时间的推移,这项投资会帮助你,因为你将花费更少的时间调试失败的配置或恢复损坏的系统。如果你的系统有一天完全失败,你可以通过运行你的 IaC 轻松地重新创建它。

自动化和测试鼓励可预测性,并限制失败变更的影响范围。它们降低了失败系统的变更失败率和平均修复时间(MTTR)。随着你的基础设施系统发展和扩展,你可以使用本书中涵盖的详细测试实践来提高你系统中变更的失败率,并减轻未来变更的负担。

1.4.3 知识共享

IaC 传达基础设施架构和配置,这有助于减少人为错误并提高可靠性。一位工程师曾告诉我:“我们不需要为三级被动数据中心(用于备份)的网络交换机做 IaC。反正都是我在配置东西,而且我们只需要做一次这个更改,之后再也不用碰它了。”

工程师在配置开关后不久就离开了组织。后来,我的团队需要将三级被动数据中心转换为活动数据中心以满足合规性要求。在恐慌中,我们匆忙逆向工程开关上的配置。我们花了大约两个月的时间来弄清楚网络连接,重新配置其配置,并使用 IaC 进行管理。

即使一项任务看起来特别晦涩,或者配置基础设施的团队只有一个人,投入时间和精力以“代码即文档”的方式处理基础设施配置,可以帮助适应变化,尤其是在基础设施系统和团队规模扩大时。你会发现,当有人报告故障或向新团队成员教授服务器配置时,你会花更多时间记住如何配置那个晦涩的开关。

将任务“编写为代码”,也称为“代码即文档”,传达了基础设施和系统架构的预期状态。

定义:“代码即文档”确保代码在无需额外参考文档的情况下传达软件或系统的意图。

对于不熟悉该系统的某人来说,应检查基础设施配置并理解其意图。你不能期望所有代码从实用角度来说都能作为文档。然而,代码应反映你大部分的基础设施架构和系统期望。

1.4.4 安全性

在 IaC 中审计和检查不安全的配置可以突出显示开发过程中的早期安全关注。您听到的是“左移安全”。如果在流程中较早地纳入安全检查,当系统配置在生产环境中运行时,您会发现更少的漏洞。您将在第八章中了解更多关于安全模式和惯例的知识。

例如,您可能暂时增加对对象存储的访问权限,以便任何人在开发中都可以读写。您将其推送到生产环境。然而,存储中的一些对象允许每个人都可以读写。虽然这看起来像是一个简单的错误,但如果存储包含客户数据,配置的影响将是严重的。

注意:要了解更多不安全的配置示例,您可以在新闻中搜索一个配置错误的对象存储,该存储泄露了驾驶执照信息,甚至是一个默认密码泄露了数百万张消费者信用卡的数据库。一些安全漏洞涉及合法漏洞,但许多涉及不安全的配置。那些一开始就防止这些误配置的组织通常可以快速检查配置、审计访问控制、评估影响范围,并修复漏洞。

IaC 通过在单个配置中表达来简化访问控制。使用 IaC,你可以测试配置以确保对象存储不允许公开访问。此外,你可以包括生产检查以验证你的策略只允许对特定对象的读取访问。甚至数据中心中的安全策略,如防火墙规则,也可以用 IaC 表达并审计,以确保其规则只允许来自已知来源的入站连接。

如果你遇到安全漏洞,IaC 允许你检查配置,快速审计访问控制,评估影响范围,并修复漏洞。你可以使用相同的 IaC 实践来做出各种更改。你将在本书中找到一些实践,以帮助根据 IaC 原则审计和确保你的基础设施的安全。

1.5 工具

IaC 工具因应用于各种资源而差异很大。大多数工具属于以下三种用例之一,所有这些用例都解决非常不同的功能,并且在行为上差异很大,包括以下内容:

  • 配置部署

  • 配置管理

  • 镜像构建

在本书中,我主要关注用于配置的工具体,这些工具部署和管理一组基础设施资源。我还包括一些配置管理和镜像构建的侧边栏和示例,以突出方法上的差异。

1.5.1 本书中的示例

在本书中,我面临了一个挑战,即构建与工具或平台无关的具体示例。作为模式和惯例的书籍,我需要找到一种方法,在通用编程语言中表达概念,而不需要重写工具的逻辑。

Python 和 Terraform

图 1.13 概述了代码列表和示例的工作流程。更多技术实现信息,请参阅附录 A。我使用 Python 编写代码列表,以创建 HashiCorp Terraform 消费的 JavaScript 对象表示法 (JSON) 文件,这是一个具有跨公共云和其他基础设施提供商各种集成的配置工具。

图片

图 1.13 本书示例使用 Python 创建 Terraform 可以消费的 JSON 文件。

当你使用 python run.py 运行 Python 脚本时,代码会创建一个扩展名为 *.tf.json 的 JSON 文件。该 JSON 文件使用 Terraform 特定的语法。然后你可以进入包含 *.tf.json 文件的目录,并运行 terraform initterraform apply 来创建资源。虽然 Python 代码看起来增加了不必要的抽象,但它确保我可以提供与平台和工具无关的具体示例。

我认识到这个工作流程的复杂性似乎没有意义。然而,它有两个目的。Python 文件提供了编程语言的通用模式和惯例的实现。JSON 配置允许你使用工具而不是我编写抽象来运行和创建资源。

注意:你可以在本书的代码库中找到完整的代码示例:github.com/joatmon08/manning-book

你不需要深入了解 Python 或 Terraform 就能理解代码示例。如果你想运行示例并创建资源,我建议你回顾一下 Terraform 或 Python 的入门教程,以了解语法和命令。

注意:你可以在 Terraform 和 Python 的众多资源中找到。查看 Scott Winkler 的《Terraform in Action》(Manning,2021 年),Naomi R. Cedar 的《The Quick Python Book》(Manning,2018 年),或 Reuven M. Lerner 的《Python Workout》(Manning,2020 年)。

Google Cloud Platform

虽然 AWS 或 Microsoft Azure 可能在出版时更为流行,但我决定使用 Google Cloud Platform(GCP)作为主要云提供商,主要有三个原因。首先,GCP 需要更少的资源来实现类似的架构。这减少了示例的冗长性,并专注于模式和途径,而不是配置。

其次,GCP 的提供的服务使用更直接的命名和通用的基础设施术语。如果你在数据中心工作,你仍然能够识别出服务创建的内容。例如,Google Cloud SQL 创建 SQL 数据库。

如果你运行示例,你将在 GCP 中使用以下资源:

  • 网络管理(网络、负载均衡器和防火墙)

  • 计算

  • 管理队列(Pub/Sub)

  • 存储(Cloud Storage)

  • 身份和访问管理(IAM)

  • Kubernetes 提供的服务(Kubernetes Engine 和 Cloud Run)

  • 数据库(Cloud SQL)

不需要了解每个服务的细节。我使用它们来演示基础设施资源之间的依赖关系管理。我避免了使用每个云平台都不同的专用服务,例如机器学习。

AWS 和 Azure 等效

每个示例都包括与等效的 AWS 和 Azure 服务提供的侧边栏,以进一步巩固特定的模式和技巧。为了更新为你选择的云提供商的示例,你可能需要进行一些语言替换并更改一些依赖项。例如,你可以使用内置网关创建 GCP 网络,而在 AWS 网络中你必须明确构建它们。

书中的代码库中有一些示例具有 AWS 等效版本(github.com/joatmon08/manning-book)。你还可以在附录 A 中找到有关设置 AWS 或 Azure 以与示例一起运行的更多信息。

第三个原因是我选择在 GCP 中编写示例涉及成本。GCP 提供免费使用层。如果你使用 GCP 创建新账户,你将获得高达$300 的免费试用(截至出版时)。如果你已有账户,你可以使用免费使用层中的服务。我会在出版时注明任何不符合免费使用层的所需资源。

使用 Google Cloud Platform

有关 GCP 免费计划的更多信息,请参阅其计划网页 cloud.google.com/free

我建议创建一个单独的 GCP 项目来运行所有示例。单独的项目将隔离您的资源。当您完成这本书后,您可以删除项目及其资源。请参阅创建 GCP 项目的教程 mng.bz/e7QG

1.5.2 配置

配置 工具为特定提供者创建和管理一组基础设施资源,无论是公共云、数据中心还是托管监控解决方案。提供者指的是负责提供基础设施资源的数据中心、IaaS、PaaS 或 SaaS。

定义 A 配置工具为公共云、数据中心或托管监控解决方案创建和管理一组基础设施资源。

一些配置工具仅与特定提供者合作,而其他工具则与多个提供者集成(表 1.1)。

表 1.1 配置工具和提供者示例

工具 提供者
AWS CloudFormation 亚马逊网络服务
Google Cloud Deployment Manager Google Cloud Platform
Azure Resource Manager 微软 Azure
Bicep 微软 Azure
HashiCorp Terraform 各种(完整列表,请参阅 www.terraform.io/docs/providers/index.xhtml)
Pulumi 各种(完整列表,请参阅 www.pulumi.com/docs/intro/cloud-providers/)
AWS Cloud Development Kit 亚马逊网络服务
Kubernetes 清单 Kubernetes(容器编排器)

大多数配置工具可以预览系统更改,并表达基础设施资源之间的依赖关系,这被称为干运行

定义 A 干运行在您将更改应用到资源之前分析并输出预期的基础设施更改。

例如,您可以表达网络和服务器之间的依赖关系。如果您更改网络,配置工具将显示服务器也可能更改。

1.5.3 配置管理

配置 管理工具确保服务器和计算机系统运行在所需的状态。大多数配置管理工具在设备配置方面表现卓越,例如服务器安装和维护。

定义 A 配置管理工具配置一组服务器或资源上的软件包和属性。

例如,如果您数据中心有 10,000 台服务器,您如何确保它们都运行您安全团队批准的特定版本的软件包?手动登录 10,000 台服务器并手动输入命令来审查是不切实际的。如果您使用配置管理工具配置服务器,您只需运行一个命令即可审查所有 10,000 台服务器并执行软件包的更新。

解决此问题空间的一些配置管理工具的非详尽列表包括以下内容:

  • Chef

  • Puppet

  • Red Hat Ansible

  • Salt

  • CFEngine

虽然这本书侧重于供应工具和管理多提供者系统,但我将讨论一些与基础设施测试、更新基础设施和安全相关的配置管理实践。配置管理可以帮助调整你的服务器和网络基础设施。

注意:我推荐你选择配置管理工具的书籍和教程以获取更多信息。它们将提供针对其设计方法的更详细指南。

为了增加混乱,你可能注意到一些配置管理工具提供了与数据中心和云提供商的集成。因此,你可能会考虑将你的配置工具作为供应工具使用。虽然这是可能的,但这种方法可能不是最佳选择,因为供应工具通常针对基础设施资源之间的依赖关系具有不同的设计方法。我将在下一章探讨这个细微差别。

1.5.4 形象构建

当你创建服务器时,你必须指定一个带有操作系统的机器镜像。形象构建工具创建用于应用程序运行时的镜像,无论是容器还是服务器。

定义:形象构建工具用于为应用程序运行时构建机器镜像,例如容器或服务器。

大多数形象构建工具允许你指定运行环境和构建目标。表 1.2 概述了一些工具、它们支持的运行环境以及它们的构建目标和平台。

表 1.2 形象构建工具和提供者的示例

工具 运行环境 构建目标
HashiCorp Packer 容器和服务器 各种(完整列表请参阅www.packer.io/docs/builders
Docker 容器 容器注册库
EC2 Image Builder 服务器 亚马逊网络服务
Azure VM Image Builder 服务器 微软 Azure

我在这本书中不包括关于形象构建的详细讨论。然而,第 6 至 8 章中测试、交付和合规性的模式确实有关于形象构建的边栏。在下一章中,你将了解不可变性,这是一个对形象构建者方法至关重要的范例。

图 1.14 展示了形象构建、配置管理和供应工具是如何协同工作的。部署新的服务器配置的过程通常从配置管理工具开始,正如你开始构建基础并测试你的服务器配置是否正确一样。

在确定你想要的服务器配置后,你使用形象构建工具来保存服务器的镜像及其版本和运行时。最后,你的供应工具引用形象构建器的快照来创建具有你想要配置的新生产服务器。

图片

图 1.14 每种 IaC 工具都对服务器基础设施资源的生命周期做出贡献,从配置到镜像捕获和部署。

此工作流程代表了使用基础设施即代码(IaC)工具管理和部署服务器的理想端到端方法。然而,正如你将学到的,基础设施可能很复杂,这个工作流程可能并不适用于每个用例。各种基础设施系统和它们的依赖关系使得配置变得复杂,这也是我选择将此作为本书主要关注点的原因,因此示例使用了配置工具。

摘要

  • 基础设施可以是软件、平台或硬件,它们将应用程序交付或部署到生产环境中。

  • 基础设施即代码(Infrastructure as Code,简称 IaC)是一种 DevOps 实践,旨在通过自动化基础设施来实现可靠性、可扩展性和安全性。

  • IaC 的原则包括可重复性、幂等性、可组合性和可进化性。

  • 通过遵循 IaC 的原则,你可以改善变更管理流程,长期降低修复失败系统所花费的时间,更好地共享知识和上下文,并将安全性融入你的基础设施。

  • IaC 工具包括配置、配置管理和镜像构建工具。

2 编写基础设施即代码

本章涵盖

  • 当前基础设施状态如何影响基础设施的可重复性

  • 检测和修复由于可变更改导致的基础设施漂移

  • 实施编写可重复基础设施即代码的最佳实践

假设你已经为 hello-world 应用程序创建了一个开发环境。你有机地构建它,根据需要添加新组件。最终,你需要复制配置以供生产使用,人们可以公开访问。你还需要将生产扩展到三个地理区域以实现高可用性。

要做到这一点,你必须为生产环境创建和更新防火墙、负载均衡器、服务器和数据库,这些都在新的网络中。图 2.1 显示了开发环境中的防火墙、负载均衡器、服务器和数据库以及你需要在生产中复制的组件。

该图还概述了开发和生产之间的差异。生产配置需要三台服务器以实现高可用性,扩展的防火墙规则以允许所有 HTTP 流量,以及更严格的防火墙规则以供服务器连接到数据库。在审查所有差异后,你可能会对最佳和最简单的方法来做出这些更改有很多疑问。

图片

图 2.1 当你基于开发创建生产环境时,你必须回答许多关于新基础设施配置的问题,并逆向工程开发环境的功能。

例如,你可能会想知道,为什么开发环境中缺乏基础设施即代码会影响你创建生产环境的能力。第一个原因是你不能轻松地复制基础设施资源。你必须逆向工程一周的手动配置!使用 IaC,你可以复制并粘贴一些配置,并对其进行修改以适应生产环境。

其次,你无法轻松地将基础设施资源与新资源组合。你需要一个用于生产的服务器池而不是单个服务器。如果你构建了一个基础设施模块,你可以使用这个构建块来创建多个服务器,而无需从头开始更新配置。

最后,你无法轻松地根据其特定要求进化生产环境。生产环境需要一些不同的基础设施资源,如安全的操作系统和更大的数据库。你将不得不手动调整配置,这些配置你在开发环境中从未运行过。

你可以通过两种方式解决这些挑战并提高可重复性、可组合性和可进化性。首先,你需要一种将手动配置的基础设施迁移到基础设施即代码(IaC)的方法。其次,你需要编写干净的 IaC 以促进可重复性和可进化性。

本章的第一部分概述了编写 IaC 和将现有基础设施迁移到代码的基本概念。本章的第二部分将代码卫生实践应用于基础设施。这些实践的组合将帮助您编写可重复的 IaC,并为未来系统的组合和演变奠定基础。

2.1 表达基础设施更改

我在第一章中提到,IaC 自动化更改。事实证明,随着时间的推移复制和自动化许多更改需要付出努力。例如,如果您想在 GCP 上配置和管理服务器,您通常会随着时间的推移进行以下更改:

  1. 通过使用控制台、终端或代码在 GCP 中创建服务器。

  2. 在 GCP 中读取服务器以检查您是否已使用正确的规格创建了服务器——例如,操作系统为 Ubuntu 18.04。

  3. 使用公开可访问的网络地址更新 GCP 中的服务器以便登录。

  4. 由于您不再需要它,请删除 GCP 中的服务器。

为了进行更复杂的更新或在另一个环境中复制服务器,您需要采取以下步骤:

  1. 创建服务器。

  2. 使用 read 命令检查其是否存在。

  3. 如果需要登录,请更新它。

  4. 如果您不再需要服务器,请删除它。

无论您自动化哪种资源,您都可以始终将您的更改分解为创建、读取、更新和删除(CRUD)。您创建一个基础设施资源,搜索其元数据,更新其属性,并在不再需要时删除它。

注意:您通常不会有明确表示“读取服务器”的更改记录。记录通常意味着读取步骤以验证资源是否已创建或更新。

CRUD 允许您按照特定顺序逐步自动化您的基础设施。这种被称为 命令式风格 的方法描述了如何配置基础设施。您可以将它想象成一本操作手册。

定义 IaC 的 命令式风格 描述了如何逐步配置基础设施资源。

虽然看起来直观,但命令式风格在您对系统进行更多更改时无法扩展。我曾经不得不基于开发环境创建一个新的数据库环境。我开始重建过去两年中提交给开发环境的 200 个更改请求。每个更改请求都变成了一系列创建、更新和删除资源的步骤。我花了整整一个月半的时间才完成了一个仍然与现有的开发环境不匹配的环境!

而不是费力地重新创建每个步骤,我希望能够仅根据开发环境的运行状态描述新的数据库环境,并让工具找出如何达到该状态。在大多数基础设施即代码(IaC)中,您会发现以声明式风格复制环境和进行更改更容易。声明式风格 描述了基础设施资源的期望最终状态。工具决定配置基础设施资源所需的步骤。

定义 IaC 的 声明式风格 描述了基础设施资源的期望最终状态。自动化和工具决定如何实现最终状态,而不需要你的知识。

使用 IaC 的此过程需要几个步骤。首先,你需要搜索库存源以获取有关数据库服务器的信息。接下来,你获取数据库 IP 地址。最后,你根据收集到的信息编写配置。

你的配置在版本控制中成为基础设施的 真相源。你声明新的数据库环境的期望状态,而不是描述可能不会产生相同结果的步骤集。

定义基础设施的 真相源 一致且唯一地结构化关于你的基础设施系统状态的信息。

你在基础设施的真相源上进行所有更改。然而,即使在理想情况下(例如在第七章中提到的 GitOps),随着时间的推移,你可能会有些配置漂移来自手动更改。如果你使用声明式风格并创建真相源,你可以使用不可变性来更改基础设施并降低失败的风险。

练习 2.1

以下配置基础设施是使用命令式还是声明式风格?

if __name__ == "__main__":
   update_packages()
   read_ssh_keys()
   update_users()
   if enable_secure_configuration:
       update_ip_tables()

请参阅附录 B 以获取练习的答案。

2.2 理解不可变性

你如何防止配置漂移并快速重建你的基础设施?这始于改变你对改变的看法。想象你用 Python 版本 2 创建了一个服务器。你可以更新你的脚本以登录服务器并升级 Python 而无需重启服务器。你可以将服务器视为 可变基础设施,因为你更新服务器而不重启它。

定义 可变基础设施 意味着你可以在不重新创建或重启的情况下更新基础设施资源。

然而,将服务器视为可变基础设施会引发一个问题。服务器上的其他包不与 Python 3 兼容。与其更新每个其他包并破坏服务器,你还可以更改你的更新脚本以创建一个 服务器,该服务器具有 Python 版本 3 和兼容的依赖项。然后你可以删除具有 Python 2 的旧服务器。图 2.2 展示了如何做到这一点。

图片

图 2.2 你通过登录和更新 Python 包版本来可变地处理服务器。相比之下,你通过用升级到 Python 3 的新服务器替换旧服务器来不可变地处理服务器。

你新的脚本将服务器视为 不可变基础设施,在其中你可以用更改替换现有的基础设施。你不会更新现有的基础设施。不可变性意味着在你创建资源后,你不会更改其配置。

定义 不可变 基础设施 意味着你必须为任何基础设施配置的改变创建一个新的资源。你不会在创建资源后对其进行修改。

为什么以两种不同的方式对待服务器的更新?如果你以可变的方式执行某些更改,它们可能会破坏资源。为了减轻失败的风险,你可以创建一个全新的资源,包含更新,然后用不可变性移除旧的一个。

不可变性依赖于一系列的创建和删除更改。创建新的资源可以缓解漂移(实际配置与预期配置之间的差异),因为新的资源与创建它的 IaC 相一致。你可以将此扩展到服务器资源,甚至到无服务器函数或整个基础设施集群。你选择通过创建新的资源来应用更改,而不是更新现有的资源。

注意:机器镜像构建器与不可变基础设施的概念一起工作。对服务器的任何更新都需要一个新的机器镜像,构建器生成并提供这个镜像。对服务器的修改,如 IP 地址注册,应作为参数传递给由镜像构建器定义的启动脚本。

不可变性的强制执行会影响你进行更改的方式。创建新的资源需要可重复性的原则。因此,IaC(基础设施即代码)在执行更改时很好地支持强制执行不可变性。例如,每次你需要更新防火墙时,你可能都会创建一个新的防火墙。新的防火墙会覆盖任何在 IaC 之外添加的手动规则,从而促进安全并减少漂移。

不可变性还促进了系统可用性,并减轻了对关键任务应用程序的任何故障。而不是原地更新现有资源,创建一个新的资源可以将更改隔离到新的资源中,如果出现问题,可以限制影响范围。我在第九章中对此有更多讨论。

然而,不可变性有时是以时间和精力为代价的。图 2.3 比较了可变与不可变基础设施的影响。当你将服务器视为可变资源时,你将 Python 原地更新的影响局部化。更新 Python 只会影响服务器整体状态的一小部分。当你以不可变的方式对待服务器时,你会替换掉整个服务器的状态,影响任何依赖于该服务器的资源。

在这里,为了不可变性而替换整个状态可能比改变为可变基础设施需要更长的时间!你不能期望一直以不可变的方式处理所有基础设施。如果你以不可变的方式更改成千上万的服务器,你可能需要花几天时间重新创建它们。如果一切顺利,原地更新可能只需要一天。

图片

图 2.3 对可变资源的更改影响基础设施状态的一小部分,而不可变资源则替换整个资源状态。

您会发现,根据情况,您将在将基础设施视为可变和不可变之间切换。不可变基础设施有助于减轻系统潜在的故障风险,而可变基础设施则促进更快的变化。当您需要修复系统时,您通常将基础设施视为可变。您如何在这两种基础设施之间迁移?

2.2.1 修复带外更改

您不能期望每次更改都部署新的资源。有时更改可能看起来范围和影响很小。因此,您决定以可变的方式做出更改。

想象你和你的朋友在咖啡馆见面。你的朋友点了一杯不含乳制品的卡布奇诺。然而,咖啡师加了一些牛奶。咖啡师随后需要为你的朋友制作一杯新的卡布奇诺,因为牛奶影响了整杯咖啡。你的朋友等待了另外 5 到 10 分钟。你得到一杯咖啡,并根据自己的口味添加牛奶和糖。如果你没有足够的糖,你只需再加一些。

您更改您可变咖啡的时间比您朋友更改他们不可变的卡布奇诺的时间要少。同样,对可变资源执行更改所需的时间、精力和成本要低得多。当您暂时将基础设施视为可变资源时,您进行了一次带外更改。

定义:带外更改是一种快速实施的变化,暂时将不可变基础设施视为可变基础设施。

当您通过带外更改破坏不可变性时,您减少了更改时间,但增加了将来影响另一个更改的风险。在您进行带外更改后,您需要更新您的真相源以返回到不可变基础设施。您如何开始这个修复过程?

在进行带外更改时,您必须协调实际状态和所需配置。让我们将此应用于图 2.4 中的我的服务器示例。首先,您登录到服务器并将 Python 升级到版本 3。然后,您在版本控制中更改配置,以便新服务器安装 Python 版本 3。配置将服务器的状态与版本控制中的真相源相匹配。

图片

图 2.4 更新可变资源后,您需要更新版本控制以考虑带外更改。

为什么您应该为带外更改更新 IaC?记住,从第一章中,手动更改可能会影响可重复性。确保将针对可变基础设施所做的更改过渡到未来的不可变基础设施,以保持可重复性。在修复带外更改并将其添加到 IaC 后,您可以重复部署更改到我的服务器,而且不应该有任何变化。这种行为符合幂等性!

如果你进行许多可变更改,你将不断协调状态和事实来源。你应该优先考虑不可变性以促进可重复性。在我的咖啡例子中,即使你把糖罐洒在你的可变咖啡里,咖啡师也可以总是替换我的饮料。我建议使用你组织的变更程序来限制带外更改,并确保更新与 IaC 中的配置一致。你总是可以使用不可变的基础设施配置来修复失败的可变更改。

练习 2.2

以下哪些更改受益于不可变性的原则?(选择所有适用的选项。)

A) 减少网络以拥有更少的 IP 地址

B) 向关系型数据库添加列

C) 向现有的 DNS 条目添加新的 IP 地址

D) 将服务器的软件包更新为向后不兼容的版本

E) 将基础设施资源迁移到另一个区域

请参阅附录 B 以获取练习的答案。

2.2.2 迁移到基础设施即代码

通过 IaC 的不可变性,版本控制可以管理基础设施配置作为事实来源,并促进未来的复制。事实上,遵守不可变性意味着你一直在创建新的资源。这对于没有活动资源的绿色地带环境来说效果很好。

然而,大多数组织都有棕色地带环境,这是一个包含活动服务器、负载均衡器和网络的现有环境。回想一下,本章的示例包括一个名为 hello-world 的棕色地带开发环境。你进入你的基础设施提供商并手动创建了一系列资源。

通常,棕色地带环境将基础设施视为可变的。你需要一种方法来改变你手动更改可变基础设施的做法,以自动更新不可变的 IaC。你如何将环境的基础设施资源迁移到不可变性?

让我们将 hello-world 开发环境迁移到不可变的 IaC。在你开始之前,你列出环境中基础设施资源清单。它包括网络、服务器、负载均衡器、防火墙和域名系统(DNS)条目。

基础设施

要开始,你找到其他资源需要使用的基基础设施资源。例如,每个基础设施资源都依赖于开发环境中的网络。你开始编写数据库和开发网络的 IaC,因为服务器、负载均衡器和数据库都运行在其上。除非网络以代码的形式存在,否则你无法重建运行在网络上任何资源。

图片

图 2.5 首先逆向工程数据库和服务器网络,并将它们的配置编写为代码。

在图 2.5 中,你使用你的终端来访问基础设施提供者应用程序编程接口(API)。你的终端命令打印出开发数据库和开发网络的名称和 IP 地址范围(无类别域间路由,或 CIDR,块)。你通过将每个网络的名称和 CIDR 块复制到 IaC 中来重建每个网络。

为什么要在 IaC 中逆向工程和再现网络?你必须使 IaC 与网络的实际资源状态完全匹配。如果你有差异,即所谓的漂移,你可能会意外地发现你的 IaC 可能会破坏你的网络(以及它上面的任何东西)!

如果可能,将资源导入到 IaC 状态。资源已经存在,你需要你的配置工具识别这一点。导入步骤将现有资源迁移到 IaC 管理。为了完成网络资源迁移,再次运行 IaC 并检查你没有漂移。

许多配置工具都有导入资源的函数。例如,CloudFormation 使用resource import命令。同样,Terraform 提供terraform import

如果你没有使用配置工具编写 IaC,你不需要直接导入功能。相反,你编写代码来创建一个新的资源。有时使用可重复性来创建全新的资源更容易。如果你不能轻松创建新资源,编写带有条件语句的代码来检查资源是否存在。

图片

图 2.6 迁移决策流程帮助你决定如何使用配置工具导入你的基础设施,重新创建资源,或构建条件语句以检查资源是否存在。无论哪种选择,你都必须重新运行你的 IaC 并解决任何漂移。

图 2.6 捕捉了重建网络的全过程以及你是否可以使用配置工具将资源迁移到不可变性。该图包括创建新资源或为现有资源编写条件语句的考虑因素。在迁移过程中,你多次运行 IaC 以检查漂移。

为什么你有这么多迁移到不可变性决策流程?所有这些实践都遵循可重复性、幂等性和可组合性的原则。你希望尽可能准确地再现 IaC 中的资源。如果你无法导入资源,至少可以重新创建一个新的资源。

此外,重新运行代码使用幂等性原则,这确保了你不会重新创建资源(除非必要)。如果你解决漂移,幂等性不应改变活动网络。同样,可组合性允许你分别迁移每个资源,以避免破坏系统。

在处理其他资源时,请记住决策流程。你可以将其应用于你迁移到 IaC 的每个资源,直到完成迁移。当你第十章重构 IaC 时,你将重新访问这个决策流程的某些部分。

依赖于基础基础设施的资源

在重建基础网络基础设施之后,你可以开始处理服务器和其他组件。再次使用你的终端来打印出 hello-world 服务器的属性。它在区域 A 运行,使用 Ubuntu 操作系统和一颗 CPU。你将服务器规范写入其配置中,并注意其依赖于开发网络。同样,你使用终端了解到数据库使用了 10 GB 的内存。你将这个信息复制到 IaC 中,并记录其使用开发数据库网络。图 2.7 显示了将服务器和数据库迁移到代码的过程。

你想要迁移使用网络的第二组资源。使用可组合性来隔离这些基础设施资源,并进行迭代更新。对下一层次基础设施的小幅改动有助于防止更大的系统故障。在第七章中,你将学习更多关于向基础设施部署小幅改动的内容。

在迁移下一组资源之前,通过运行 IaC 并检查漂移来完成迁移周期。确保网络、服务器和数据库在其 IaC 中不显示任何变化。在解决任何新的漂移后,你可以继续迁移剩余的资源(DNS、防火墙规则和负载均衡器)。

图 2.7 在迁移了基础基础设施,如网络之后,迁移服务器和数据库资源。它们依赖于基础基础设施,但彼此之间并不依赖。

最后,图 2.8 重建了 DNS、防火墙规则和负载均衡器的剩余配置。它们依赖于服务器和数据库的现有配置。没有其他资源依赖于它们。

图 2.8 最后,迁移依赖性最少或需要服务器和数据库配置的资源。

为什么要经历重建各个层次基础设施的繁琐过程?你的棕色地带环境没有一个一致的真相来源,因此你需要建立一个。当你完成将基础设施资源添加到配置中后,你重建了环境的真相来源。IaC 中的真相来源允许你将棕色地带环境视为不可变基础设施。

除了示例之外,你总会从基础资源迁移到顶层资源的不可变性。在开始迁移时,识别出其他人高度依赖的资源。将这些低级资源——如网络、账户或项目以及 IAM——写入 IaC 中。

接下来,选择服务器、队列或数据库等资源。防火墙、负载均衡器、DNS 和警报依赖于服务器、队列和数据库的存在。你可以在过程结束时迁移依赖性最少的资源。我们将在第四章中讨论更多关于基础设施依赖的内容。

注意:依赖关系图表示基础设施资源之间的依赖关系。例如,Terraform 等 IaC 工具使用这个概念以结构化的方式应用更改。当您迁移资源时,您会重新构建依赖关系图。您可以调查工具,这些工具可以帮助您映射实时基础设施状态并突出显示依赖关系,从而使这更容易。

迁移步骤

我通常遵循以下一般步骤来评估依赖关系并将现有资源结构化迁移到 IaC:

  1. 迁移初始登录、账户和提供者资源隔离结构。例如,我为云提供商的账户或项目以及我的初始服务账户编写了自动化配置。

  2. 如果适用,迁移网络、子网络、路由和根 DNS配置。根 DNS 配置可以包括安全套接字层(SSL)证书。例如,我创建了根域名 hello-world.net 及其 SSL 证书,为 dev.hello-world.net 等子域做准备。

  3. 迁移计算资源,例如应用程序服务器或数据库。

  4. 如果您使用计算编排平台,则需要迁移计算编排平台及其组件。例如,我将我的 Kubernetes 集群迁移以跨服务器调度工作负载。

  5. 如果您使用计算编排平台,则需要将应用程序部署迁移到计算编排平台。例如,我将部署在 Kubernetes 上的 hello-world 应用程序的配置回滚。

  6. 迁移消息队列缓存事件流平台。在您能够重新构建它们之前,这些服务具有应用程序依赖项。例如,我为 hello-world 和另一个应用程序之间的通信编写了消息队列的配置。

  7. 迁移DNS 子域负载均衡器防火墙。例如,我为 hello-world 应用程序及其数据库之间的防火墙规则重新创建了一个配置。

  8. 迁移与资源相关的警报或监控。例如,我重构了我的配置,以便在 hello-world 应用程序失败时通知我。

  9. 最后,迁移SaaS 资源,例如不依赖于应用程序的数据处理或存储库。例如,这可能是 GCP 上对数据库有单一依赖的数据转换作业。

在每个步骤之间,确保您通过重新运行配置来测试您是否已正确迁移了初始资源。您很少能在第一次尝试时就获得所有所需的参数和依赖项。

注意:重新运行迁移后的配置不应因为幂等性而更改现有基础设施。您应该重新应用配置并检查 dry run。dry run 中的更改意味着您的配置没有准确捕捉到资源的实际状态。

如果您运行配置并输出更改,您必须纠正您的配置!这个过程需要反复试验。因此,我建议您测试和验证每一组资源。

迁移到不可变性成为减少漂移的练习。这个过程展示了配置与状态相差甚远的极端情况。你通过在版本控制中更新其配置来努力调和真相来源。将现有资源导入新真相来源的过程适用于重构 IaC,我们将在第十章中讨论这一点。

2.3 编写干净的基础设施代码

除了使用不可变性之外,通过干净地编写配置,你还可以通过以下方式促进可重复性:代码卫生指的是一套旨在提高代码可读性和结构的实践。

定义:代码卫生是一套旨在提高代码可读性和可维护性的实践和风格。

基础设施即代码的卫生有助于在需要重用配置时节省时间。我经常发现基础设施配置被复制、粘贴并使用硬编码的值进行编辑。硬编码的值降低了可读性和可重复性。虽然许多这些实践来自软件开发,但我建议一些特定于基础设施的实践。

2.3.1 版本控制传达上下文

你如何有效地使用版本控制来实现可重复性?围绕版本控制的系统化实践可以帮助你快速重现配置并做出明智的更改。例如,你可能在开发中更新了防火墙规则,允许app-network网络流量访问shared-services-network。你添加以下提交信息来描述为什么添加了权限:

$ git commit -m "Allow app to connect to queues
app-network needs to connect to shared-services-network 
because the application uses queues. 
All ports need to be open."

几周后,你在生产环境中重新创建了网络。然而,你忘记了为什么添加了权限。当你检查提交历史时,你记得你的描述性信息。你现在有了应用程序需要访问队列的信息。

当你为 IaC 编写提交信息时,你不需要解释配置。更改已经捕捉到了配置将是什么样子。相反,使用提交信息来解释你想要进行更改的原因以及它将如何影响其他基础设施。

注意:在这本书中,我讨论了与基础设施即代码(IaC)相关的特定版本控制实践。要了解更多关于版本控制的信息,请查看“入门指南——关于版本控制”的 Git 教程,网址为mng.bz/pOBR。有关编写良好的提交信息,请查看“分布式 Git——为项目做贡献”的相关内容,网址为mng.bz/OoMj。这两部分内容均来自 Scott Chacon 和 Ben Straub 所著的《Pro Git》(Apress,2014 年)。

你可能还有审计要求,需要在提交信息的前面加上问题编号或工单编号以实现可追溯性。例如,你可能正在处理编号为 TICKET-002 的工单。它包含允许应用程序和共享服务之间流量请求。为了将工单与你的提交关联起来,你将工单添加到提交信息的开头:

$ git commit -m "TICKET-002 Allow app to connect to queues
app-network needs to connect to shared-services-network 
because the application uses queues. 
All ports need to be open."

将工作项或票据信息添加到提交信息中可以使跟踪更改变得更容易。配置成为变更文档,因为它是基础设施资源的真相来源。版本控制也变成了记录变更的机制。您可以通过检查版本控制和配置来重建变更历史并重现环境。

2.3.2 代码检查和格式化

在提交代码之前,您希望对其进行代码检查和格式化。IaC(基础设施即代码)通常不会执行,因为您遗漏了一个或两个空格,或者使用了错误的字段名称。错误的字段名称可能会导致错误。代码对齐不当通常会导致您误读或跳过配置行。

想象一下您配置了一个服务器,它需要一个名为ip_address的字段。相反,您将字段命名为ip,后来意识到您无法使用 IaC 创建该服务器。您如何确保您已将字段编写为ip_address

您可以使用代码检查来分析您的代码并验证非标准或不正确的配置。大多数工具都提供了一种检查配置或代码的方法。对ip_address的代码检查可以在开发早期捕捉到ip字段名称的错误。

定义 代码检查 自动检查代码的非标准配置风格。

为什么要检查非标准或不正确的配置?您希望确保您编写了正确的配置,并且没有遗漏关键的语法。如果工具没有代码检查功能,您总是可以找到一个社区扩展或使用编程语言编写自己的代码检查规则。您应该包括解决安全标准的代码检查规则,例如不要将秘密提交到版本控制(第八章)。

除了代码检查之外,您还可以使用格式化来检查空格和配置格式。格式化在软件开发实践中可能看起来很显然,但在 IaC 中变得更为关键。

定义 格式化 自动对齐您的代码以正确设置空格和配置格式。

大多数工具使用特定于域的语言(DSLs),这为编程语言提供了更高层次的表达。如果您不了解编程语言,DSL 提供了一个较低的入门门槛。这些语言使用具有特定格式要求的 YAML 或 JSON 数据格式。拥有检查格式的工具,例如检查您的 YAML 文件中是否遗漏了空格,是有帮助的!

您还可以添加版本控制钩子,在提交代码之前运行格式化检查。例如,您可能使用 YAML 数据格式使用 CloudFormation 创建您的基础设施资源。为了验证基础设施资源字段和值,您使用 AWS CloudFormation Linter(mng.bz/YGrj)。您还使用 AWS CloudFormation Template Formatter(mng.bz/GEVA)对 YAML 文件进行格式化。

而不是每次都记得输入这些命令,你可以将这些命令添加为预提交的 Git 钩。每次你运行 git commit,该命令都会在将它们推送到仓库之前检查配置和格式是否正确。你还可以将它们添加到持续交付工作流程中,这我在第七章中有所介绍。

2.3.3 资源命名

当你的基础设施即代码(IaC)成为文档时,你的资源、配置和变量需要具有描述性的名称。我曾经创建了一个防火墙规则来测试某些内容,并将其命名为 firewall-rule-1。两周后,当我想要将其复制到生产环境中时,我记不起为什么我在开发环境中创建了这个规则。

回顾起来,我应该给防火墙规则起一个更具描述性的名字。我花了另外 30 分钟追踪该规则的 IP 地址和权限。命名可能会影响你花费在解析基础设施做什么以及它在另一个环境中如何不同的时间。

资源名称应包括环境基础设施资源类型和其用途。图 2.9 将防火墙规则命名为 dev-firewall-rule-allow-hello-world-to-database,其中包括环境(dev)、资源类型(firewall-rule)和用途(allow-hello-world-to-database)。

图片

图 2.9 资源名称应包括环境、类型和用途。

为什么名字需要包含这么多细节呢?你希望快速识别资源以进行故障排除、共享和审计。一眼就能识别环境可以确保你配置了正确的环境(而不是意外地配置了生产环境)。目的告诉其他人并提醒自己资源的作用。

可选地,你可以包括资源类型。我通常在名称中省略资源类型,因为我从资源元数据中识别它。省略资源类型可以使你符合云提供商的字符限制。如果你想包含更多关于资源用途或类型的信息,你总是可以在资源的标签(第八章)中包含它。

向其他人描述资源

当我为资源命名时,我会尝试向其他人描述它。如果其他人根据名称就能理解资源,我知道这是一个好名字。然而,如果有人需要就环境或资源类型提出更多问题,我知道它还需要更多信息。

这个练习可能会使名称变得稍微长一些,但我更倾向于更详细的描述。根据名称识别资源的用途可以节省宝贵的时间来重建环境。

除了资源名称之外,你还希望变量和配置尽可能具有描述性。大多数基础设施提供商都有特定的资源属性命名。AWS 将网络的 IP 地址称为 CidrBlock,而 Azure 则将其称为 address_space

我倾向于使用提供者的特定命名来方便查找提供者的文档,以便稍后进行更改和复制。如果我将 Azure 的配置重命名为 cidr_block,我必须记得将其参数转换为 address_space 以供 Azure 使用。你需要记得将更通用的字段名称翻译为变量或配置的另一个提供者或环境。

2.3.4 变量和常量

除了命名变量,你如何知道哪些值应该作为变量?比如说,hello-world 应用程序始终在端口 8080 上提供服务。你并不打算经常更改端口,所以你在配置文件的开头将其设置为 application_port = 8080。然而,你直接将 hello-world 编译到你的基础设施资源的 name 属性中。

一年后,你为 hello-world 的新版本在端口 3000 上重新创建了环境。你希望新的 name 值为 hello-world-v2。你将配置文件开头的 application_port 更新为 3000。将端口放入变量中允许你在整个配置中引用 application_port 并将值存储在一个地方。你为自己不需要在配置中查找并替换 8080 的实例而感到自豪。然而,你花了整整一个小时在基础设施配置中寻找所有 hello-world 的实例以更改其名称。

在这个例子中,你有两种类型的输入。一个 变量 存储一个由基础设施配置引用的值。大多数基础设施值最好存储在变量中并由配置引用。

定义 A 变量 存储一个由基础设施配置引用的值。你期望在创建新资源或环境时随时更改变量的值。

你应该将应用程序的名称 hello-world 设置为变量,因为它将根据环境、版本或目的而变化。然而,端口不会根据环境或目的而变化。一个 常量 变量在一系列资源中设置一个共同的值,并且很少随着环境或目的而变化。

定义 A 常量 变量在基础设施配置中建立了一个共同的值。你不会经常更改常量。

在决定何时将配置值设置为变量或常量时,考虑更改值的影响和安全影响。变化的频率并不重要。如果更改值会影响基础设施依赖项或泄露敏感信息,则将其设置为变量。你应该始终将名称或环境设置为变量。

与软件开发不同,软件开发倾向于使用更少的常量,IaC 更重视常量而非变量。避免设置过多的变量,因为它们会使配置难以维护。相反,你可以通过定义具有静态配置的本地变量来设置常量。

例如,Terraform 使用本地值(www.terraform.io/docs/language/values/locals.xhtml)来存储常量。常见定义的常量包括操作系统、标签、账户标识符或域名。在internalexternal等基础设施提供商上标准化的值,用于描述网络类型,也可以是常量。

2.3.5 参数化依赖

当你创建服务器时,你需要指定它需要使用的网络。你最初通过硬编码你想要的网络名称来表达这一点,特别是development。当你读取配置时,你确切地知道服务器使用的是哪个网络。

然而,当你需要为生产环境重现此操作时,你需要搜索并替换所有对development的引用。问题在于,你有多个对development的引用!你的搜索和替换任务变成了几个小时的繁琐工作。

代码示例

你决定将 GCP 网络参数化为变量,这样你就可以在另一个环境中使用不同的网络重现服务器。当你传递网络名称作为变量时,你更改了任何引用该网络的任何服务器的网络。让我们按照以下方式在代码中将网络名称作为变量传递。

列表 2.1 将网络作为变量参数化

import json

def hello_server(name, network):                                       ❶
   return {
       'resource': [
           {
               'google_compute_instance': [                            ❷
                   {
                       name: [
                           {
                               'allow_stopping_for_update': True,
                               'zone': 'us-central1-a',
                               'boot_disk': [
                                   {
                                       'initialize_params': [
                                           {
                                               'image': 'ubuntu-1804-lts'
                                           }
                                       ]
                                   }
                               ],
                               'machine_type': 'f1-micro',
                               'name': name,
                               'network_interface': [
                                   {
                                       'network': network              ❸
                                   }
                               ],
                               'labels': {
                                   'name': name,
                                   'purpose': 'manning-infrastructure-as-code'
                               }
                           }
                       ]
                   }
               ]
           }
       ]
   }

if __name__ == "__main__":
   config = hello_server(name='hello-world', network='default')        ❹

   with open('main.tf.json', 'w') as outfile:                          ❺
       json.dump(config, outfile, sort_keys=True, indent=4)            ❺

❶ 将名称和网络作为参数传递给配置

❷ 使用 Terraform 的 google_compute_instance 资源配置服务器

使用“network”变量设置网络

❹ 当你运行脚本时,将网络依赖设置为默认网络

❺ 创建包含服务器对象的 JSON 文件,并使用 Terraform 运行它

AWS 和 Azure 等效

在 AWS 中,你会使用带有要使用网络引用的aws_instance Terraform 资源(mng.bz/z4j6)。你可以在默认的虚拟专用云(VPC)上创建此资源。

在 Azure 中,你需要创建虚拟网络和子网,然后在网络上创建azurerm_linux_virtual_machine Terraform 资源(mng.bz/064E)。

为什么传递名称和网络作为变量?你经常根据环境更改名称和网络。参数化这些值有助于可重复性和可组合性。你可以在不同的网络上创建新资源,并且可以构建多个资源而不用担心冲突。

运行示例

我将逐步运行示例,以庆祝我们的第一个 hello-world 服务器。有关示例所需的工具的更多信息,请参阅第一章,有关详细使用说明,请参阅附录 A。以下是步骤:

  1. 通过在终端中输入命令来使用 Python 运行脚本:

    $ python main.py
    

    命令创建一个扩展名为*.tf.json 的文件。Terraform 将自动搜索此文件扩展名以创建资源。

  2. 通过在终端中列出文件来检查文件是否存在:

    $ ls *.tf.json
    

    输出应如下所示:

    main.tf.json
    
  3. 在终端中验证 GCP:

    $ gcloud auth login
    
  4. 将您想要使用的 GCP 项目设置为CLOUDSDK_CORE_PROJECT环境变量:

    $ export CLOUDSDK_CORE_PROJECT=<your GCP project>
    
  5. 在终端中初始化 Terraform 以检索 GCP 插件:

    $ terraform init
    

    输出应包括以下内容:

    Initializing the backend...
    
    Initializing provider plugins...
    - Finding latest version of hashicorp/google...
    - Installing hashicorp/google v3.58.0...
    - Installed hashicorp/google v3.58.0 (signed by HashiCorp)
    
    Terraform has created a lock file .terraform.lock.hcl to 
    ➥record the provider selections it made above.
    ➥Include this file in your version control repository
    ➥so that Terraform can guarantee to make the same
    ➥selections by default when
    ➥you run "terraform init" in the future.
    
    Terraform has been successfully initialized!
    
    You may now begin working with Terraform. Try running 
    ➥"terraform plan" to see any changes that are
    ➥required for your infrastructure. All Terraform commands
    ➥should now work.
    
    If you ever set or change modules or backend configuration
    ➥for Terraform, rerun this command to reinitialize
    ➥your working directory. If you forget, other
    ➥commands will detect it and remind you to do so if necessary.
    
  6. 在终端中应用 Terraform 配置。确保您输入yes以应用更改并创建实例:

    $ terraform apply
    

    您的输出应包括服务器实例的配置和名称:

    Do you want to perform these actions?
      Terraform will perform the actions described above.
      Only 'yes' will be accepted to approve.
    
      Enter a value: yes
    
    google_compute_instance.hello-world: Creating...
    google_compute_instance.hello-world: Still creating... [10s elapsed]
    google_compute_instance.hello-world: Still creating... [20s elapsed]
    google_compute_instance.hello-world: Creation complete after 24s 
    ➥[id=projects/infrastructure-as-code-book/zones
    ➥/us-central1-a/instances/hello-world]
    
    Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
    

    注意:在这本书中,我不会详细介绍 Terraform 的所有细微差别。有关开始使用 Terraform 的详细信息,请查看 HashiCorp 的“入门”教程learn.hashicorp.com/terraform。您可以在mng.bz/Kx2g找到有关 Terraform 如何与 GCP 一起工作的附加文档。

  7. 您可以在 GCP 控制台中检查服务器的网络和元数据。否则,您可以使用 Cloud SDK 命令行界面(CLI)在终端中检查网络。输入命令以过滤出 hello-world 服务器:

    $ gcloud compute instances list --filter="name=( 'hello-world' )" \
      --format="table(name,networkInterfaces.network)"
    

    输出应包括网络的 GCP URL:

    NAME         NETWORK
    hello-world  ['https://www.googleapis.com/compute/v1/projects/
    ➥<your GCP project>/global/networks/default']
    

GCP 服务器使用default网络,您将其作为变量传递给示例。如果您想更改网络,请更新新变量。您的 IaC 工具将检测到更改并创建新的服务器。

要销毁服务器,您可以在终端中使用 Terraform。确保您输入yes以完全删除服务器:

$ terraform destroy

当您将依赖项定义为变量时,您将两个基础设施资源松散耦合。第四章介绍了您可以用来进一步解耦基础设施资源和依赖项的具体模式。如果可能,您应该避免硬编码依赖项,并将它们作为参数传递。

2.3.6 保持秘密

IaC 通常需要使用密钥,如令牌、密码或密钥来执行对提供者的更改。

定义 A 密钥 是像密码、令牌或密钥这样的敏感信息。

当您在 GCP 中创建服务器时,您需要一个访问项目和服务器资源的服务帐户密钥或令牌。为了确保您可以创建资源,您将密钥作为基础设施配置的一部分维护。配置中的密钥可能存在问题。如果有人可以读取我的密钥,他们可以使用它来访问我的 GCP 帐户以创建资源并访问受限制的数据!

您可能还需要将密钥作为配置的一部分传递。例如,您使用 IaC 设置负载均衡器的 SSL 证书。SSL 证书在两年后到期。两年后,您重新创建环境。然而,您发现证书的加密字符串已过期。您无法解密它,现在必须颁发新的证书。

图 2.10 展示了如何最佳地保护你的证书,同时在未来提高其可扩展性。你将证书作为输入变量传递,以便为每个环境提供不同的证书。然后你将新的证书放入密钥管理器中,它为你存储和管理证书。

图片

图 2.10 从密钥管理器检索敏感信息以使用基础设施提供者更改资源。

每当证书发生变化时,你需要在密钥管理器中更新它。当基础设施即代码(IaC)从密钥管理器读取证书时,它会更新其配置。将证书管理的关注点与配置分离可以减轻你以后在证书过期时遇到的问题。

为什么要在 IaC 之外存储密钥?你刚刚应用了将密钥与其他基础设施资源分离的可组合性和可扩展性原则。这种分离确保了没有人可以通过检查你的 IaC 来获取密码或用户名。你还可以通过重新运行 IaC 来轮换密钥时最小化失败的影响。

总是将密钥作为变量传递到 IaC 中,并在内存中使用它。这包括安全外壳协议(SSH)密钥、证书、私钥、API 令牌、密码以及其他登录信息。应有一个单独的实体来存储和管理敏感的认证数据,例如密钥管理器。分离密钥管理有助于重现,尤其是在你需要为每个环境提供不同的密码和令牌时。你绝不应该以明文形式硬编码或提交密钥到版本控制中。

摘要

  • 优先考虑不可变性可以减少配置漂移,维护一个真相来源,并提高可重复性。

  • 为了符合不可变性,对资源的更改将创建一个全新的资源并替换其状态。

  • 如果你进行可变更改,你必须协调基础设施状态中的本地化更改与你的配置。

  • 在编写 IaC 时,使用版本控制中的提交来传达更改和上下文,并格式化代码以提高可读性。

  • 对名称、环境和依赖其他基础设施进行参数化。如果你将配置属性范围限定在资源上,你可以将其设置为常量。

  • 密钥应始终作为变量传递,绝不能硬编码或以明文形式提交到版本控制中。

  • 在编写脚本时,始终简化创建、读取、更新和删除命令以重现资源。

3 种基础设施模块的模式

本章涵盖

  • 根据功能将基础设施资源分组为可组合的模块

  • 使用软件开发设计模式构建基础设施模块

  • 将模块模式应用于常见的基础设施用例

在上一章中,我介绍了基础设施即代码的基本实践。尽管我知道这些基本实践,但我的第一个 Python 自动化脚本将代码分组到一个文件中,其中包含混乱的函数。多年以后,我学习了软件设计模式。它们提供了一套标准的模式,使我更容易修改脚本并将其转交给另一位队友进行维护。

在接下来的两个章节中,我将展示如何将设计模式应用于 IaC 配置和依赖关系。软件设计模式可以帮助您识别常见问题并构建可重用、面向对象的解决方案。

定义 A 设计模式是针对软件中常见问题的可重复解决方案。

将软件设计模式应用于 IaC 有其陷阱。IaC 有可重用对象(作为基础设施资源)。然而,其有偏见的特性和 DSL 并不直接映射到软件设计模式。

IaC 提供了一层不可变的抽象层,这就是为什么本章借鉴了创建型(用于创建对象)和结构型(用于结构化对象)设计模式来对基础设施进行近似。大多数 IaC 都关注不可变性,这会在更改时自动创建新的资源。因此,依赖于可变性的设计模式不适用。

注意:我改编了许多来自 Erich Gamma 等人所著的《设计模式:可复用面向对象软件元素》(Addison-Wesley Professional,1994 年)的图案应用到 IaC 中。如果您想了解更多关于原始软件设计模式的信息,我建议您参考那本书。

我包括创建 Terraform JSON 文件的 Python 代码示例。它们引用了 GCP 资源。您可以将这些模式扩展到 DSL,如 Terraform、CloudFormation 或 Bicep。根据您选择的 DSL 和工具,它可能使用不同的机制或功能。在可能的情况下,我会指出 DSL 的限制以及 AWS 和 Azure 的等效功能。

3.1 单例

假设您需要从头开始在 GCP 中创建一组数据库服务器。数据库系统需要一个 GCP 项目、自定义数据库网络、服务器模板和服务器组。服务器模板会在每个服务器上安装软件包,而服务器组描述了您需要的数据库服务器数量。

图 3.1 显示了如何将项目、数据库网络、服务器模板和服务器组添加到一个目录中。您确定 GCP 项目名称及其组织的属性。接下来,您确定网络应具有名为development-database-network的名称,其 IP 地址范围为 10.0.3.0/16。最后,您表达数据库应具有三个使用 MySQL 模板的服务器。您将这些属性作为代码写入一个配置文件中。

数据库系统配置使用单例模式,该模式将一组资源声明为系统中的单个实例。

定义 单例模式将一组资源声明为系统中的单个实例。它使用单个命令部署所有资源。

我们为什么称之为单例模式?您创建一个具有静态配置的单个文件或目录。此外,您定义几个参数以单行方式创建所有基础设施资源。该配置表达了资源在由配置创建的环境中的独特性和特定性。

图片

图 3.1 单例模式表达了一组初始资源(如项目、网络和数据库)的配置,这些资源在一个文件中。这种模式捕捉了资源之间的关系。

单例模式简化了 IaC(基础设施即代码)的编写,因为您将所有内容放入一个配置中。当您在一个配置中表达每个基础设施资源时,您将获得一个单独的引用来调试和解决供应顺序和所需参数。

然而,从单例模式开始通常会导致后续的挑战。当我开始使用单例模式时,我把基础设施配置当作一个杂物抽屉——存放那些没有其他地方可放的随机物品。如果您找不到某物,抽屉将成为您首先查看的地方(图 3.2)。

图片

图 3.2 如果您不知道将对象放在哪里,请将其添加到杂物抽屉中,该抽屉使用单例模式来聚合所有资源。

由于我不知道其他地方可以配置基础设施资源,所以我只是将它们添加到一个文件中。最终,单例模式变得像杂物抽屉一样混乱!我不得不在单例中搜索基础设施资源。此外,单例中的基础设施资源数量意味着识别、更改和创建资源需要花费时间。

随着系统随着更多资源的增长而扩展,单例模式对可重复性提出了挑战。为生产环境生成配置意味着将其复制并粘贴到新文件中。当您有更多资源需要更新时,复制和粘贴配置无法扩展!单例以可扩展性和可组合性为代价。它适用于少量基础设施资源,但不能与复杂系统一起扩展。

你应该在何时使用单例?当您有一个只需要单个实例且很少更改的资源时,它效果最好,例如 GCP 项目。网络、数据库服务器模板和服务器组必须放入另一个配置中。

所有 GCP 项目都必须有一个唯一的标识符,这使得单例模式非常理想。项目只能有一个实例。例如,你可以创建一个名为 databases 的项目,并根据当前系统用户名生成一个唯一标识符。以下列表显示了实现使用系统用户名创建 GCP 项目的单例模式的代码。

列表 3.1 使用单例模式在 GCP 中创建项目

import json
import os

class DatabaseGoogleProject:
   def __init__(self):                                                   ❶
       self.name = 'databases'                                           ❷
       self.organization = os.environ.get('USER')                        ❸
       self.project_id = f'{self.name}-{self.organization}'              ❹
       self.resource = self._build()                                     ❺

   def _build(self):
       return {
           'resource': [
               {
                   'google_project': [                                   ❷
                       {
                           'databases': [
                               {
                                   'name': self.name,                    ❷
                                   'project_id': self.project_id         ❹
                               }
                           ]
                       }
                   ]
               }
           ]
       }

if __name__ == "__main__":
   project = DatabaseGoogleProject()                                     ❺

   with open('main.tf.json', 'w') as outfile:                            ❻
       json.dump(project.resource, outfile, sort_keys=True, indent=4)    ❻

❶ 为数据库 Google 项目创建一个对象

❷ 使用名为“databases”的 Terraform 资源设置 Google 项目

❸ 获取操作系统用户并将其设置为组织变量

❹ 根据项目名称和操作系统用户创建一个唯一的项目 ID,以便 GCP 可以创建项目

❺ 创建一个 DatabaseGoogleProject 来生成项目的 JSON 配置

❻ 将 Python 字典写入 JSON 文件,以便 Terraform 以后执行

AWS 和 Azure 等效

你可以将 GCP 项目等同于 AWS 账户。为了自动化 AWS 账户的创建,你需要使用 AWS Organizations (aws.amazon.com/organizations/)。

在 Azure 中,你会创建一个订阅和一个资源组。你可以通过使用 azurerm_resource_group Terraform 资源来创建资源组 (mng.bz/1orq)。

假设你想要在你的数据库项目中创建一个服务器。你可以调用 DatabaseGoogleProject 单例并从 JSON 配置中提取项目标识符。单例包含独特的资源,你可以通过模块的调用来引用它们。例如,如果你引用 database 项目,你将始终得到正确的项目,而不会是另一个项目。

你使用一个单例来表示 GCP 项目,因为你只创建它一次,并且很少更改。你可以将单例模式应用于很少更改的 全局 资源,例如提供者账户、项目、域名注册或根 SSL 证书。它也适用于静态环境,例如低使用率的数据中心环境。

3.2 组合

你可以将数据库系统组织到单个单例中,而不是用模块来组织组件。模块 将具有相同功能或业务领域的基础设施资源分组在一起。模块允许你更改部分自动化,而不会影响整体。

定义 A 模块 通过功能或业务领域组织基础设施资源。其他工具或资源可能将它们称为 基础设施堆栈集合

你可以使用模块作为构建块来构建你的系统。其他团队可以将模块作为构建块来构建他们独特的基础设施系统。

图 3.3 展示了你的公司如何组织团队并创建报告结构。每个团队或经理向另一个经理报告,最终达到管理层。公司使用团队的组合来实现共同目标。

为什么您的公司会将报告结构分解为模块?这种模式确保了新的倡议或商业机会有一个团队来支持它们。随着公司的发展,它促进了可组合性和可扩展性。

图片

图 3.3 您的公司可能会使用组合模式将员工分组到报告结构中,从而允许管理者组织团队及其目标。

以类似的方式,大多数 IaC 都依赖于组合模式来分组、排序和结构化一组模块。您通常会找到组合模式被归类为结构模式,因为它以层次结构结构化对象。

定义 组合模式 将基础设施模块视为单个实例,并允许您在系统中组装、分组和排序模块。

工具通常都有自己的模块化功能。Terraform 和 Bicep 使用它们自己的模块框架来嵌套和组织模块。您可以在 CloudFormation 中使用嵌套堆栈或 StackSets 来重用模板(模块)或跨区域创建堆栈。像 Ansible 这样的配置管理工具可以让您构建顶层剧本,导入其他任务。

您如何实现一个模块?想象一下,您需要为数据库服务器设置网络。然而,服务器需要一个子网络(子网)。您将网络和子网络组合到一个模块中,如图 3.4 所示。您首先确定如何设置网络,并将其写入模块中。然后,您写下子网络的配置并将其添加到模块中。

图片

图 3.4 网络模块可以使用组合模式将网络和子网络资源分组。

模块包含网络和子网络的配置。如果您需要在生产环境中重现网络系统,您始终可以复制并粘贴整个模块来创建新的网络和子网络。网络组合模式确保您可以始终重现一组相互依赖的资源。

您可以在模块中实现网络配置的组合模式。如下所示,模块创建了一个网络和子网络。您传递网络和子网络的 CIDR 范围,模块为网络生成一个标准化的名称。

列表 3.2 创建网络和子网络

import json

class Network:                                                              ❶
   def __init__(self, region='us-central1'):                                ❷
       self._network_name = 'my-network'                                    ❸
       self._subnet_name = f'{self._network_name}-subnet'                   ❹
       self._subnet_cidr = '10.0.0.0/28'                                    ❺
       self._region = region                                                ❷
       self.resource = self._build()                                        ❻

   def _build(self):                                                        ❻
       return {
           'resource': [
               {
                   'google_compute_network': [                              ❸
                       {
                           f'{self._network_name}': [                       ❸
                               {
                                   'name': self._network_name               ❸
                               }
                           ]
                       }
                   ]
               },
               {
                   'google_compute_subnetwork': [                           ❼
                       {
                           f'{self._subnet_name}': [                        ❽
                               {                                            ❽
                                   'name': self._subnet_name,               ❽
                                   'ip_cidr_range': self._subnet_cidr,      ❾
                                   'region': self._region,                  ❷
                                   'network': f'${{google_compute_network
                                   ➥.{self._network_name}.name}}'          ❼
                               }
                           ]
                       }
                   ]
               }
           ]
       }

if __name__ == "__main__":
   network = Network()                                                      ❿

   with open('main.tf.json', 'w') as outfile:                               ⓫
       json.dump(network.resource, outfile, sort_keys=True, indent=4)       ⓫

❶ 创建一个网络模块,该模块使用组合模式将网络和子网络捆绑在一起

❷ 将区域设置为默认区域,us-central1

❸ 使用名为“my-network”的 Terraform 资源设置 Google 网络。GCP 不需要定义网络 CIDR 块。

❹ 使用名为“my-network-subnet”的 Terraform 资源设置 Google 子网络

❺ 将子网的 CIDR 块设置为 10.0.0.0/28

❻ 使用模块创建网络和子网络的 JSON 配置

❼ 通过使用 Terraform 变量在网络上创建 Google 子网。Terraform 会动态引用网络 ID,并将其插入到子网配置中,供您使用。

❽ 使用名为“my-network-subnet”的 Terraform 资源设置 Google 子网

❾ 将子网的 CIDR 块设置为 10.0.0.0/28

❿ 使用模块创建网络的 JSON 配置和子网

⓫ 将 Python 字典写入 JSON 文件,以便稍后由 Terraform 执行

AWS 和 Azure 的等效模式

您可以将 GCP 网络和子网等同于 AWS VPC 和子网,或者等同于 Azure 虚拟网络和子网。然而,在 AWS 和 Azure 中,您需要在每个子网中定义网关和路由表。当您创建网络时,GCP 会自动为您定义这些。

为什么需要将模块与网络和子网组合?除非您有一个子网,否则您无法使用 GCP 网络!组合允许您一起创建一组资源。将所需的资源捆绑在一起可以帮助您的队友,他们可能不太了解网络。组合模式改进了可组合性原则,因为它将必须作为一个单元部署的常见资源分组和组织。

组合模式对于基础设施来说效果很好,因为基础设施资源具有层次结构。遵循此模式的模块反映了资源之间的关系,并促进了它们的管理。如果您需要更新路由,您将更新网络组合配置。您可以通过参考网络配置来确定子网 CIDR 范围并计算网络地址空间。

您可以通过根据功能、业务单元或运营责任对资源进行分组来应用组合模式。在您起草初始模块时,您可能需要添加变量以允许更灵活的参数或分发配置给其他团队。我在第五章中讨论了如何共享模块。然而,您可以在通用组合模式之外应用其他模式,以进一步提高 IaC 的可重复性。

3.3 工厂

之前,您使用单例模式为数据库系统创建了一个 GCP 项目。然后您应用了组合模式,并在不同的模块中构建了网络。然而,现在您意识到您需要一个分为三个子网的数据库网络。与其复制和粘贴三个子网,您希望创建一个接受子网名称和 IP 地址范围输入的配置。

创建三个子网和一个网络配置需要许多参数,这些参数可能很繁琐,难以包含和维护。如果有一个类似工厂的设施来制造所有具有默认意见的资源会怎样?图 3.5 显示,您可以创建一个网络工厂来批量生产类似网络。您可以将所需的参数减少到两个输入,并将其他配置设置为默认值。

图 3.5 工厂模式模块包含一组最小资源配置的默认值,并通过接受输入变量来启用定制。

当你知道网络有特定的默认属性时,你可以最小化输入,并更轻松地生成多个资源。我称这种方法为工厂模式。使用工厂模式的模块接受一组输入,例如名称和 IP 地址范围,并根据输入创建一组基础设施资源。

定义:工厂模式接受一组输入变量,并根据输入变量和默认常量创建一组基础设施资源。

你希望提供足够的灵活性以进行更改,例如子网的 IP 地址和名称。通常,你需要在模块中在提供足够的定制和使用有偏见的默认属性之间找到平衡。毕竟,你希望促进可重复性原则,同时保持资源的可发展性。我们将在第五章中讨论更多关于共享资源,如模块的内容,并在第八章中标准化模块的安全实践。

让我们回到我们的例子。如何在不传递名称列表的情况下创建三个子网?模块可以自动命名子网,这样你就可以避免硬编码它们。图 3.6 显示了如何将一些逻辑添加到网络工厂模块中,以根据网络地址标准化子网的名称。

图片

图 3.6 网络工厂模块可以包括计算子网寻址并创建网络多个子网资源的转换。

使用工厂模式的模块可以将输入变量转换为标准模板,这是一种常见的生成名称或标识符的做法。当你在代码中实现使用工厂模式的网络模块时,你会添加一个SubnetFactory模块。列表 3.3 构建了一个工厂模块来生成子网的名称。

列表 3.3 使用工厂模式在 GCP 中创建三个子网

import json
import ipaddress

def _generate_subnet_name(address):                                    ❶
   address_identifier = format(ipaddress.ip_network(                   ❶
       address).network_address).replace('.', '-')                     ❶
   return f'network-{address_identifier}'                              ❶

class SubnetFactory:                                                   ❷
   def __init__(self, address, region):
       self.name = _generate_subnet_name(address)                      ❶
       self.address = address                                          ❸
       self.region = region                                            ❹
       self.network = 'default'                                        ❺
       self.resource = self._build()                                   ❻

   def _build(self):                                                   ❻
       return {
           'resource': [
               {
                   'google_compute_subnetwork': [                      ❼
                       {                                               ❼
                           f'{self.name}': [                           ❼
                               {                                       ❼
                                   'name': self.name,                  ❼
                                   'ip_cidr_range': self.address,      ❼
                                   'region': self.region,              ❼
                                   'network': self.network             ❼
                               }                                       ❼
                           ]                                           ❼
                       }                                               ❼
                   ]                                                   ❼
               }
           ]
       }

if __name__ == "__main__":
   subnets_and_regions = {                                             ❽
       '10.0.0.0/24': 'us-central1',                                   ❽
       '10.0.1.0/24': 'us-west1',                                      ❽
       '10.0.2.0/24': 'us-east1',                                      ❽
   }                                                                   ❽

   for address, region in subnets_and_regions.items():                 ❽

       subnetwork = SubnetFactory(address, region)                     ❻

       with open(f'{_generate_subnet_name(address)}.tf.json',
                 'w') as outfile:                                      ❾
           json.dump(subnetwork.resource, outfile,                     ❾
                 sort_keys=True, indent=4)                             ❾

❶ 对于给定的子网,通过用短横线分隔 IP 地址范围并将其附加到“网络”来生成子网名称

❷ 为子网创建一个模块,该模块使用工厂模式生成任意数量的子网

❸ 将子网的地址传递给工厂

❹ 将子网的区域传递给工厂

❺ 在此示例中,在“默认”网络上创建子网

❻ 使用该模块创建网络和子网络的 JSON 配置

❼ 使用基于名称、地址、区域和网络的基础设施资源创建 Google 子网

❽ 对于每个使用其 IP 地址范围和区域定义的子网,使用工厂模块创建子网

❾ 将 Python 字典写入 JSON 文件,以便 Terraform 稍后执行

为什么我会将子网分离到自己的工厂模块中?为子网创建单独的模块促进了可扩展性的原则。我可以更改生成任何数量子网名称的逻辑。我还可以更新名称格式,而不会影响网络。

大多数工厂模块包括转换或动态生成属性。例如,你可以修改网络工厂模块以计算子网的 IP 地址范围。计算自动构建正确数量的私有或公共子网。

然而,我建议尽可能减少添加到工厂模块中的转换。它们可能会增加资源配置的复杂性。你的转换逻辑越复杂,你需要进行测试以检查转换的情况就越多。在第六章中,我将介绍如何测试模块和基础设施配置。

工厂模式平衡了基础设施资源的可重复性和可扩展性。它通过名称、大小或其他属性上的微小差异制造出相似的基础设施。如果你需要构建适用于常见资源(如网络或服务器)的配置模块,你将想要构建一个工厂模块。

每次运行工厂模块时,你都可以期待获得你请求的具体资源集合。该模块不包含很多逻辑来确定哪些资源要构建。相反,工厂模块专注于设置资源的属性。

我经常编写具有许多默认常量少量输入变量的工厂模块。这样,我减少了维护和验证输入的开销。通常使用工厂模块的基础设施包括网络和子网络、服务器集群、托管数据库、托管队列或托管缓存等。

3.4 原型

现在你已经创建了一个构建数据库网络的模块,你可以创建数据库服务器。但是,你必须使用客户名称、业务单元和成本中心对数据库系统中的所有资源进行标记。审计团队还要求你包含automated=true以标识自动化资源。

理想情况下,标签(或在 GCP 中的标签)必须在所有资源上保持一致。如果你更新了标签,你的自动化应该将它们复制到每个资源上。你将在第八章中了解更多关于标签重要性的内容。

如果你能将所有标签放在一个地方并一次性更新会怎样?图 3.7 显示你可以将所有标签放入一个模块中。数据库服务器引用标签的通用模块,并将静态值应用于服务器。

图片

图 3.7 使用原型模式的模块返回静态值(如标签)的副本,供其他基础设施资源使用。

而不是为每个标签硬编码,你创建了一个实现原型模式的模块,以表达一组供其他模块使用的静态默认值。原型模块生成配置的副本,以附加到其他资源。

定义 原型模式使用一组输入变量来构建一组静态默认值,供其他模块消费。它们通常不会直接创建基础设施资源,而是导出配置值。

您可以将原型视为一个存储单词和定义的字典(图 3.8)。字典的创建者会更改单词和定义。您可以引用它并更新您的文本或词汇。

图 3.8 您使用字典作为原型来引用单词和定义,并在您的写作中更新它们。

为什么使用原型模块来引用常用元数据?原型模式促进了可扩展性和可重复性原则。它确保资源之间配置的一致性,并简化了常用配置的演变。您不需要在文件中查找和替换字符串!

让我们使用原型模式实现标签模块。列表 3.4 创建了一个使用原型模式返回一组标准标签的模块。在后续的基础设施资源中,您可以通过引用模块中的 StandardTags 来包含所需的任何标签。该模块不会创建标签资源。相反,它返回预定义标签的副本。

列表 3.4 使用原型模式创建标签模块

import json

class StandardTags():                                                     ❶
   def __init__(self):                                                    ❶
       self.resource = {                                                  ❶
           'customer': 'my-company',                                      ❶
           'automated': True,                                             ❶
           'cost_center': 123456,                                         ❶
           'business_unit': 'ecommerce'                                   ❶
       }                                                                  ❶

class ServerFactory:                                                      ❷
   def __init__(self, name, network, zone='us-central1-a', tags={}):      ❸
       self.name = name
       self.network = network
       self.zone = zone
       self.tags = tags                                                   ❸
       self.resource = self._build()                                      ❹

   def _build(self):
       return {
           'resource': [
               {
                   'google_compute_instance': [                           ❺
                       {
                           self.name: [
                               {
                                   'allow_stopping_for_update': True,
                                   'boot_disk': [
                                       {
                                           'initialize_params': [
                                               {
                                                   'image': 'ubuntu-1804-lts'
                                               }
                                           ]
                                       }
                                   ],
                                   'machine_type': 'f1-micro',
                                   'name': self.name,
                                   'network_interface': [
                                       {
                                           'network': self.network
                                       }
                                   ],
                                   'zone': self.zone,
                                   'labels': self.tags                   ❻
                               }
                           ]
                       }
                   ]
               }
           ]
       }

if __name__ == "__main__":
   config = ServerFactory(                                               ❼
       name='database-server', network='default',                        ❼
       tags=StandardTags().resource)                                     ❽

   with open('main.tf.json', 'w') as outfile:                            ❾
       json.dump(config.resource, outfile,                               ❾
                 sort_keys=True, indent=4)                               ❾

❶ 使用原型模式创建一个返回标准标签副本的模块,例如客户、成本中心和业务单元

❷ 使用工厂模式创建一个基于名称、网络和标签的 Google 计算实例(服务器)模块

❸ 将标签作为变量传递给服务器模块

❹ 使用模块创建“默认”网络上的服务器 JSON 配置

❺ 使用 Terraform 资源创建 Google 计算实例(服务器)

❻ 将存储在变量中的标签添加到 Google 计算实例资源

❼ 使用模块创建“默认”网络上的服务器 JSON 配置

❽ 使用标准标签模块向服务器添加标签

❾ 将 Python 字典写入 JSON 文件,供 Terraform 后续执行

AWS 和 Azure 等效

要将列表 3.4 转换为其他云提供商,将资源更改为 Amazon EC2 实例或 Azure Linux 虚拟机。然后,将 self.tags 传递给 AWS 或 Azure 资源的 tags 属性。

让我们运行 Python 脚本以创建服务器配置,如列表 3.5 所示。当您检查服务器的 JSON 输出时,您会注意到服务器包含一组标签。这些标签与您的原型模块中的标准标签相匹配!

列表 3.5 使用模块中的标签创建服务器配置

{
   "resource": [
       {
           "google_compute_instance": [                             ❶
               {
                   "database-server": [                             ❷
                       {
                           "allow_stopping_for_update": true,
                           "boot_disk": [
                               {
                                   "initialize_params": [
                                       {
                                           "image": "ubuntu-1804-lts"
                                       }
                                   ]
                               }
                           ],
                           "labels": {                              ❸
                               "automated": true,                   ❸
                               "business_unit": "ecommerce",        ❸
                               "cost_center": 123456,               ❸
                               "customer": "my-company"             ❸
                           },     
                           "machine_type": "f1-micro",
                           "name": "database-server",               ❷
                           "network_interface": [
                               {
                                   "network": "default"
                               }
                           ],
                           "zone": "us-central1-a"                  ❹
                       }
                   ]
               }
           ]
       }
   ]
}

❶ JSON 文件使用 Terraform 资源定义了一个 Google 计算实例。

❷ Terraform 将资源识别为数据库服务器。JSON 配置与您在服务器工厂模块中使用 Python 定义的配置相匹配。

❸ 将标准标签原型模块中的标签添加到服务器配置中的标签字段

❹ JSON 配置检索区域变量并将其填充到 JSON 文件中。

你会注意到你的服务器配置中包含了很多硬编码的值,比如操作系统和机器类型。这些值作为全局默认值存在。随着时间的推移,你会在你的工厂模块中不断增加全局默认值,并发现它们已经超出了模块的容量!

为了理清和整理全局默认值,你可以在原型模块中定义它们。该模块使得随着时间的推移演变默认值并与其他值组合变得更加容易。原型成为资源的静态、明确定义的默认值。

在这样的情况中,我开始编写一个工厂模块来在基础设施上创建一组警报。最初,我传递环境名称和指标阈值来参数化警报及其配置。我发现警报不需要环境名称,并且指标阈值在各个环境中没有变化。

因此,我将这个模块转换成了原型。需要将指标添加到他们系统的团队导入了这个模块。该模块向他们的配置中添加了预定义的警报资源。

领域特定语言

Terraform、Kubernetes、CloudFormation 和 Bicep 等工具的领域特定语言没有像编程语言那样的全局常量。然而,它们确实支持模块引用和对象结构。你可以通过创建一个作为对象的原型来使用与编程语言相同的模式。

原型使得创建一组标准的资源或配置变得更容易。它消除了设置输入值的不确定性。然而,你会有一些标准值之外的例外。作为解决方案,你可以根据资源覆盖或添加配置。例如,我通常将特定于资源的自定义标签与标准标签列表合并。

除了标签之外,我通常还会使用原型模块来处理区域、可用区域或账户标识符。当我需要具有许多全局默认值或复杂转换的静态配置时,我会使用原型模式来创建模块。例如,你可能有一个在启用 SSL 时运行的服务器初始化脚本。你可以创建一个原型模块来根据是否使用 SSL 来模板化脚本。

3.5 构建器

你学习了如何应用单例模式来创建项目,工厂来创建网络,原型来设置数据库服务器的标签。接下来,你将构建一个连接到数据库的负载均衡器。

但首先,你会遇到一个具有挑战性的需求。该模块必须允许你创建私有或公共负载均衡器!私有负载均衡器需要不同的服务器和网络配置。你必须构建一个模块,它能够提供选择私有或公共负载均衡器的灵活性,并根据你的选择配置服务器和网络。

图 3.9 演示了一个模块,该模块根据您的负载均衡器类型选择防火墙和服务器配置。您可以使用相同的模块创建外部或内部负载均衡器。该模块处理负载均衡器及其所需防火墙规则的正确配置。

图 3.9 数据库的构建器模块将包括参数以选择模块必须创建的负载均衡器类型和防火墙规则。

该模块为您提供了选择构建所需系统的选项,有助于系统的可扩展性和可组合性。该模块遵循构建器模式,它捕获一些默认值,但允许您组合所需的系统。构建器模式组织了一组相关的资源,您可以根据所需的系统启用或禁用这些资源。

定义 构建器模式 组装一组您可以选择启用或禁用以实现所需配置的基础设施资源。

在数据库模块中实现构建器模式允许您根据选择生成资源组合。构建器模式使用输入来决定需要构建哪些资源,而工厂模块根据输入变量配置资源属性。该模式就像为房地产开发建造房屋。您从预设的蓝图中选择,并告诉构建者您想要的布局更改(图 3.10)。例如,一些构建者可能会通过移除车库来增加一个额外的房间。

图 3.10 一个构建器模块使用预设蓝图构建房屋,允许进行布局更改,例如增加一个房间。

让我们开始实现构建器模式,如下所示列表。首先,您通过使用工厂模式定义一个负载均衡器模块。您使用工厂模式来自定义负载均衡器(在 GCP 中也称为计算转发规则)。该模块将负载均衡器的方案设置为外部或内部。

列表 3.6 使用工厂模块为负载均衡器

class LoadBalancerFactory:                                                ❶
   def __init__(self, name, region='us-central1', external=False):        ❶
       self.name = name
       self.region = region
       self.external = external                                           ❶
       self.resources = self._build()                                     ❷

   def _build(self):
       scheme = 'EXTERNAL' if self.external else 'INTERNAL'               ❸
       resources = []
       resources.append({
           'google_compute_forwarding_rule': [{                           ❹
               'db': [
                   {
                       'name': self.name,
                       'target': r'${google_compute_target_pool.db.id}',  ❺
                       'port_range': '3306',                              ❻
                       'region': self.region,
                       'load_balancing_scheme': scheme,
                       'network_tier': 'STANDARD'
                   }
               ]
           }
           ]
       })
       return resources

❶ 创建一个负载均衡器模块,该模块使用工厂模式生成内部或外部负载均衡器

❷ 使用该模块创建负载均衡器的 JSON 配置

❸ 将方案设置为内部或外部负载均衡。负载均衡器默认为内部配置。

❹ 使用 Terraform 资源创建 Google 计算转发规则。这是 GCP 中负载均衡的等效功能。

❺ 将负载均衡器的目标设置为数据库服务器组。这使用 Terraform 内置的变量插值功能来动态解析数据库服务器组的 ID。

❻ 允许流量通过 3306 端口,这是 MySQL 数据库端口

AWS 和 Azure 等效功能

您可以将 GCP 计算转发规则等同于 AWS 弹性负载均衡器 (ELB) 或 Azure 负载均衡器。同样,AWS 安全组或 Azure 网络安全组大致等同于 GCP 防火墙规则。有关 AWS 的示例,请参阅代码仓库github.com/joatmon08/manning-book

然而,外部负载均衡器需要额外的防火墙规则配置。您必须允许外部源流量访问数据库端口。让我们定义一个使用工厂模式的模块,以允许外部源流量,如下面的列表所示。

列表 3.7 使用工厂模块创建防火墙规则

class FirewallFactory:                                ❶
   def __init__(self, name, network='default'):
       self.name = name
       self.network = network
       self.resources = self._build()                 ❷

   def _build(self):
       resources = []
       resources.append({
           'google_compute_firewall': [{              ❸
               'db': [
                   {
                       'allow': [                     ❹
                           {                          ❹
                               'protocol': 'tcp',     ❹
                               'ports': ['3306']      ❹
                           }                          ❹
                       ],                             ❹
                       'name': self.name,
                       'network': self.network
                   }
               ]
           }]
       })
       return resources

❶ 创建了一个使用工厂模式生成防火墙规则的防火墙模块

❷ 使用该模块创建负载均衡器的 JSON 配置

❸ 使用 Terraform 资源创建 Google 计算防火墙。这是 GCP 中防火墙规则的等效物。

❹ 防火墙规则应默认允许 TCP 流量访问端口 3306。

多亏了可组合性原则,您将负载均衡器和工厂模块放入数据库构建器模块中。该模块需要一个变量,帮助您选择负载均衡器的类型以及是否应包含防火墙规则以允许流量访问负载均衡器。

当您在列表 3.8 中实现数据库构建器模块时,您将其设置为默认创建数据库服务器组和网络。然后构建器接受两个选项:内部或外部负载均衡器以及额外的防火墙规则。

列表 3.8 使用构建器模式构建数据库

import json
from server import DatabaseServerFactory                               ❶
from loadbalancer import LoadBalancerFactory                           ❶
from firewall import FirewallFactory                                   ❶

class DatabaseModule:                                                  ❷
   def __init__(self, name):

       self._resources = []
       self._name = name
       self._resources = DatabaseServerFactory(self._name).resources   ❸

   def add_internal_load_balancer(self):                               ❹
       self._resources.extend(
           LoadBalancerFactory(
               self._name, external=False).resources)

   def add_external_load_balancer(self):                               ❺
       self._resources.extend(
           LoadBalancerFactory(
               self._name, external=True).resources)

   def add_google_firewall_rule(self):                                 ❻
       self._resources.extend(
           FirewallFactory(
               self._name).resources)

   def build(self):                                                    ❼
       return {                                                        ❼
           'resource': self._resources                                 ❼
       }                                                               ❼

if __name__ == "__main__":
   database_module = DatabaseModule('development-database')            ❽
   database_module.add_external_load_balancer()                        ❽
   database_module.add_google_firewall_rule()                          ❽

   with open('main.tf.json', 'w') as outfile:                          ❾
       json.dump(database_module.build(), outfile,                     ❾
                 sort_keys=True, indent=4)                             ❾

❶ 导入工厂模块以创建数据库服务器组、负载均衡器和防火墙

❷ 为数据库创建了一个模块,该模块使用构建器模式生成所需的数据库服务器组、网络、负载均衡器和防火墙

❸ 总是通过使用工厂模块创建数据库服务器组和网络。构建器模块需要数据库服务器组。

❹ 添加了一个方法,以便您可以选择构建内部负载均衡器

❺ 添加了一个方法,以便您可以选择构建外部负载均衡器

❻ 添加了一个方法,以便您可以选择构建防火墙规则以允许流量访问数据库

❼ 使用构建器模块返回您自定义数据库资源的 JSON 配置

❽ 使用数据库构建器模块创建具有外部访问(负载均衡器和防火墙规则)的数据库服务器组

❾ 将 Python 字典写入 JSON 文件,以便 Terraform 后续执行

运行 Python 脚本后,您将找到一个包含实例模板、服务器组、服务器组管理器、外部负载均衡器和防火墙规则的冗长 JSON 配置。构建器生成了构建外部可访问数据库所需的所有资源。请注意,列表为了清晰起见省略了其他组件。

列表 3.9 截断的数据库系统配置

[
  {
    "google_compute_forwarding_rule": [                             ❶
      {
        "db": [
          {
            "load_balancing_scheme": "EXTERNAL",                    ❷
            "name": "development-database",
            "network_tier": "STANDARD",
            "port_range": "3306",                                   ❸
            "region": "us-central1",
            "target": "${google_compute_target_pool.db.id}"         ❹
          }
        ]
      }
    ]
  },
  {
    "google_compute_firewall": [                                    ❺
      {
        "db": [
          {
            "allow": [                                              ❸
              {                                                     ❸
                "ports": [                                          ❸
                  "3306"                                            ❸
                ],                                                  ❸
                "protocol": "tcp"                                   ❸
              }                                                     ❸
            ],
            "name": "development-database",
            "network": "default"
          }
        ]
      }
    ]
  }
]

❶ JSON 文件使用 Terraform 资源定义了一个 Google 计算转发规则和防火墙,为了清晰起见,文件省略了实例模板、服务器组和服务器组。

❷ 使用 EXTERNAL 方案创建一个负载均衡器,使其可以从外部源访问

❸ 创建一个防火墙,允许在端口 3306 上进行 TCP 流量,这是 MySQL 数据库端口

❹ 将负载均衡器的目标设置为数据库服务器组。这使用 Terraform 内置的变量插值功能来动态解析数据库服务器组的 ID。

❺ JSON 文件使用 Terraform 资源定义了一个 Google 计算转发规则和防火墙,为了清晰起见,文件省略了实例模板和服务器组。

构建者模式帮助你遵循可扩展性原则。你可以选择你需要的一组资源。采用这种模式的模块可以消除配置正确属性和资源组合的挑战。

此外,你可以使用构建者模式在云提供商的资源周围包装一个通用接口。Python 示例提供了 add_external_load_balancer 构建方法,它围绕 GCP 计算转发规则进行包装。当你使用该模块时,该选项描述了创建通用负载均衡器的意图,而不是 GCP 转发规则。

领域特定语言

一些 DSL 提供了 if-else(条件)语句或循环(迭代),你可以用于构建者模式。Terraform 提供了 count 参数,可以根据条件语句创建一定数量的资源。CloudFormation 支持用户输入的条件,可以用于选择堆栈。Bicep 使用部署条件。对于 Ansible,你可以使用条件导入来选择任务或剧本。

例如,你可以设置一个名为 add_external_load_balancer 的布尔变量。如果你将 true 传递给该变量,DSL 将添加一个条件语句来构建外部负载均衡器资源。否则,它将创建一个内部负载均衡器。

一些领域特定语言(DSL)不提供条件语句。你可能需要一些类似于本书中代码示例的代码,以模板化 DSL。例如,你可以使用 Helm 来模板化和发布 Kubernetes YAML 文件。

构建者模式最适合创建多个资源的模块。这些用例包括 Kubernetes 等容器编排器的配置、具有集群架构的平台、应用程序和系统指标的仪表板等。这些用例的构建者模块允许你选择你想要的资源,而无需传递特定的输入属性。

然而,构建者模块可能很复杂,因为它们引用其他模块和多个资源。模块配置错误的风险可能非常高。第六章涵盖了测试策略,以确保构建者模块的功能性和稳定性。

3.6 选择一个模式

在本章中,我展示了如何将一些资源分组到数据库系统的各种模块模式中。你如何选择使用哪种模块模式?关于我未提及的数据库系统中的其他资源呢?

你可以为具有不同业务功能和目的的新基础设施资源创建单独的模块。本章中的数据库示例将 Google 项目的配置(单例)、网络(工厂)和数据库集群(工厂)分离成模块。每个模块作为不同的资源独立发展,具有不同的输入变量和默认值。

示例使用组合模式来组合系统中所有的模块模式。它使用工厂模式来处理网络、负载均衡器和数据库集群模块,以传递属性并定制每个资源。标签通常使用原型模式,因为它们涉及将一致的元数据复制到其他资源。你将使用工厂和原型模式来编写大多数模块,因为它们提供了可组合性、可复制性和可扩展性。

相比之下,你将 Google 项目作为一个单例来构建,因为没有其他人会更改我项目单例的属性。项目变化不大,所以你使用了一个更简单的模式。然而,你解决了使用构建器模式创建数据库系统的复杂问题。构建器模块允许你选择要创建的特定资源。

图 3.11

图 3.11 为了决定你想使用哪种模块模式,你必须评估你如何使用资源及其行为。

图 3.11 提供了一个决策树,用于确定使用哪种模式。你将针对目的、重用和更新频率以及多个资源的组合提出一系列问题。基于这些标准,你创建一个具有特定模式的模块。

遵循决策树可以帮助构建更可组合和可扩展的模块。你希望平衡标准属性的预测性与覆盖特定资源配置的灵活性。然而,保持开放的心态。模块可能会超出其功能并发生变化。仅仅因为你用一种模式构建了一个模块,并不意味着你未来不会将其转换为另一种模式!

练习 3.1

以下 IaC(基础设施即代码)适用于哪些模块模式?(选择所有适用的。)

if __name__ == "__main__":
  environment = 'development'
  name = f'{environment}-hello-world'
  cidr_block = '10.0.0.0/16'

  # NetworkModule returns a subnet and network
  network = NetworkModule(name, cidr_block)

  # Tags returns a default list of tags
  tags = TagsModule()

  # ServerModule returns a single server
  server = ServerModule(name, network, tags)

A) 工厂

B) 单例

C) 原型

D) 构建器

E) 组合

请参阅附录 B 以获取练习题的答案。

注意,本章中的许多模式都侧重于使用 IaC 工具构建模块。有时你可能需要在编程语言中编写自动化脚本,因为你找不到 IaC 支持。这种情况在遗留基础设施中最为常见。例如,想象你需要创建一个 GCP 中的数据库系统。然而,你没有 IaC 工具,只能直接访问 GCP API。

要使用 GCP API 创建数据库系统,您将每个基础设施资源分离成具有四个函数(创建、读取、更新和删除)的工厂模块。对资源的更改使用这些函数的组合。您可以根据您想要对每个资源执行的操作检查每个函数中的错误。

图 3.12 实现了服务器、网络和负载均衡器的工厂模块。您可以创建、读取、更新和删除每个模块。数据库的构建者模块使用组合模式来创建、读取、更新和删除网络、服务器和负载均衡器。

图 3.12

图 3.12 要编写自动化脚本,为单个资源创建工厂模块,然后构建创建、读取、更新和删除资源的函数。

将对资源的更新拆分为四个函数可以组织自动化流程。即使是构建者模式也使用创建、读取、更新和删除函数。这些函数定义了您想要用于配置资源的自动化行为。然而,您应该测试每个函数以确保幂等性。每次运行函数时,它都应该导致相同的配置。

您可以将本章中的模块模式应用于任何基础设施的自动化和实施基础设施即代码(IaC)。随着您开发 IaC,确定您可以将基础设施系统拆分为模块的地方。在决定何时以及如何模块化时,请考虑以下因素:

  • 资源是否已共享?

  • 它服务于哪个业务领域?

  • 哪个环境使用该资源?

  • 哪个团队管理基础设施?

  • 资源是否使用不同的工具?

  • 您如何更改资源而不影响模块中的其他内容?

通过评估哪些资源与不同的业务单元、团队或功能相匹配,您可以构建更小的基础设施集合。作为一般做法,尽可能少地编写模块。资源较少的模块可以加快供应速度并最小化故障的影响范围。更重要的是,您可以在将它们应用于更广泛的系统之前,对较小的模块进行部署、测试和调试。

将资源分组到模块中为您、您的团队和您的公司带来了一些好处。对于您来说,模块可以提高基础设施资源的可扩展性和弹性。您通过最小化模块更改的影响范围来提高整体系统的弹性。

对于您的团队来说,模块为其他团队成员提供了一个自助机制,以便复制您的模块并创建基础设施。您的队友可以使用模块并传递他们想要定制的变量,而不是寻找和替换属性。您将在第五章中了解更多关于模块共享的内容。

对于你的组织来说,模块可以帮助你在资源之间标准化更好的基础设施和安全实践。你可以使用相同的配置来批量生成类似的负载均衡器和受限防火墙规则。模块还有助于你的安全团队审核和强制执行这些实践,如第八章所述。

摘要

  • 应用模块模式,如单例、工厂、原型和构建器,以便你可以构建可组合的基础设施配置。

  • 使用组合模式将基础设施资源分组到层次结构中,并为自动化对其进行结构化。

  • 你可以使用单例模式来管理基础设施的单个实例,这适用于很少更改的基础设施资源。

  • 使用原型模式来复制和应用全局配置参数,例如标签或通用配置。

  • 工厂模块接收输入以使用特定配置构建基础设施资源。

  • 构建模块接收输入以决定要创建哪些资源。构建模块可以由工厂模块组成。

  • 在决定如何模块化以及如何模块化时,评估基础设施配置服务于哪些功能或业务领域。

  • 如果你编写脚本来自动化基础设施,构建具有创建、读取、更新和删除功能的工厂模块,并在构建模块中引用它们。

基础设施依赖关系的 4 种模式

本章涵盖

  • 使用依赖模式编写松散耦合的基础设施模块

  • 识别解耦基础设施依赖关系的方法

  • 识别依赖模式的基础设施用例

基础设施系统涉及一组相互依赖的资源。例如,服务器依赖于网络的存在。您如何在创建服务器之前知道网络是否存在?您可以使用基础设施依赖来表示这一点。当资源在创建或修改第一个资源之前需要另一个资源存在时,就会发生基础设施依赖。

定义 一个基础设施依赖表达了基础设施资源依赖于另一个资源的存在和属性的关系。

通常,您通过硬编码网络标识符来识别服务器对网络的依赖。然而,硬编码更紧密地绑定服务器和网络之间的依赖关系。每次您更改网络时,都必须更新硬编码的依赖关系。

在第二章中,您学习了如何使用变量避免硬编码值以促进可重复性和可演化性。将网络标识符作为变量传递可以更好地解耦服务器和网络。然而,变量仅在相同模块的资源之间工作。您如何表达模块之间的依赖关系?

前一章将资源分组到模块中以提高可组合性。本章涵盖了管理基础设施依赖关系以增强可演化的模式(变更)。当它们具有松散依赖关系时,您可以更容易地用另一个模块替换一个模块。

在现实中,基础设施系统可能相当复杂,而且在不造成某些干扰的情况下无法交换模块。松散耦合的依赖关系可以减轻变更失败的风险,但并不能保证 100%的可用性!

4.1 单向关系

不同的依赖关系会影响基础设施变更。想象一下,每次您创建一个新的应用程序时,都会添加一个防火墙规则。防火墙规则对应用程序 IP 地址有一个单向依赖,以允许流量。任何对应用程序的更改都会反映在防火墙规则中。

定义 一个单向依赖表达了一种单向关系,其中只有一个资源引用另一个资源。

您可以在任何一组资源或模块之间表达单向依赖关系。图 4.1 描述了防火墙规则与应用程序之间的单向关系。规则依赖于应用程序,这使得它在基础设施堆栈中的位置高于较低级别的应用程序。

图片

图 4.1 防火墙规则单向依赖于应用程序的 IP 地址。

当您表达依赖关系时,您有一个高级 资源,如防火墙,它依赖于低级 资源,如应用程序的存在。

定义 一个高级资源依赖于另一个资源或模块。一个低级资源有高级资源依赖于它。

假设一个报告应用程序需要一个防火墙的规则列表。它将规则发送到审计应用程序。然而,防火墙需要知道报告应用程序的 IP 地址。您应该先更新报告应用程序的 IP 地址还是防火墙规则?图 4.2 展示了决定应该先更新哪个应用程序的困境。

图片

图 4.2 报告应用程序和防火墙相互之间存在循环依赖。变更阻止了对应用程序的连接。

此示例遇到了循环依赖,这引入了一个“先有鸡还是先有蛋”的问题。您不能更改一个资源而不影响另一个资源。如果您首先更改报告应用程序的地址,防火墙规则必须更改。然而,报告应用程序失败,因为它无法连接。您可能已经阻止了它的请求!

循环依赖在变更过程中会导致意外的行为,这最终会影响可组合性和可扩展性。您不知道应该先更新哪个资源。相比之下,您可以确定低级模块的变更可能会如何影响高级模块。单向依赖关系使变更更加可预测。毕竟,成功的基础设施变更取决于两个因素:可预测性和隔离性。

4.2 依赖注入

单向依赖有助于您设计方法以最小化低级模块变更对高级模块的影响。例如,网络变更不应干扰高级资源,如队列、应用程序或数据库。本节将软件开发中的依赖注入概念应用于基础设施,并进一步解耦单向依赖。依赖注入涉及两个原则:控制反转和依赖反转。

4.2.1 控制反转

当您在基础设施依赖中强制执行单向关系时,您的高级资源会获取有关低级资源的信息。然后它可以运行其变更。例如,服务器在声明 IP 地址之前会获取有关网络 ID 和 IP 地址范围的详细信息(图 4.3)。

图片

图 4.3 在控制反转中,高级资源或模块调用低级模块以获取信息,并解析其元数据以查找任何依赖项。

服务器调用网络,自然应用了称为控制反转的软件开发原则。在更新之前,高级资源会调用低级资源以获取信息。

定义 控制反转是高级资源调用低级资源以获取属性或引用的原则。

作为非技术示例,当你通过电话预约医生而不是由医生办公室自动预约时,你使用控制反转。

让我们应用控制反转来实现服务器对网络的依赖。你通过使用网络模块创建网络。在下面的列表中,网络模块输出一个网络名称并将其保存到名为 terraform.tfstate 的文件中。高级资源,如服务器,可以从此 JSON 文件中解析网络名称。

列表 4.1 网络模块在 JSON 文件中的输出

{
 "outputs": {                           ❶
   "name": {                            ❷
     "value": "hello-world-subnet",     ❷
     "type": "string"                   ❷
   }
 }                                      ❸
}

❶ 使用 Terraform 创建网络生成一个包含输出列表的 JSON 文件。Terraform 使用此文件来跟踪它创建的资源。

❷ 网络模块以字符串形式输出子网名称。

❸ 为了清晰起见,省略了 JSON 文件的其余部分。

使用控制反转,服务器在列表 4.2 中调用网络的 terraform.tfstate 文件并读取子网名称。由于模块在 JSON 文件中表达输出,你的服务器模块需要解析子网名称的值(hello-world-subnet)。

列表 4.2 应用控制反转在网络中创建服务器

import json

class NetworkModuleOutput:                                             ❶
   def __init__(self):
       with open('network/terraform.tfstate', 'r') as network_state:
           network_attributes = json.load(network_state)
       self.name = network_attributes['outputs']['name']['value']      ❷

class ServerFactoryModule:                                             ❸
   def __init__(self, name, zone='us-central1-a'):                     ❹
       self._name = name
       self._network = NetworkModuleOutput()                           ❺
       self._zone = zone
       self.resources = self._build()                                  ❻

   def _build(self):                                                   ❻
       return {
           'resource': [{
               'google_compute_instance': [{                           ❹
                   self._name: [{
                       'allow_stopping_for_update': True,
                       'boot_disk': [{
                           'initialize_params': [{
                               'image': 'ubuntu-1804-lts'
                           }]
                       }],
                       'machine_type': 'f1-micro',
                       'name': self._name,                             ❹
                       'zone': self._zone,                             ❹
                       'network_interface': [{
                           'subnetwork': self._network.name            ❼
                       }]
                   }]
               }]
           }]
       }

if __name__ == "__main__":                                             ❽
   server = ServerFactoryModule(name='hello-world')                    ❽
   with open('main.tf.json', 'w') as outfile:                          ❽
       json.dump(server.resources, outfile, sort_keys=True, indent=4)    

❶ 创建一个对象来捕获网络模块输出的模式。这使得服务器更容易检索子网名称。

❷ 解析网络输出对象的子网名称值。

❸ 创建一个服务器模块,它使用工厂模式

❹ 使用具有名称和区域的 Terraform 资源创建 Google 计算实例

❺ 服务器模块调用包含从网络模块的 JSON 文件中解析出的子网名称的网络输出对象。

❻ 使用模块根据子网名称创建服务器的 JSON 配置

❽ 服务器模块引用网络输出的名称并将其传递给“子网”字段。

❽ 将 Python 字典写入 JSON 文件,以便 Terraform 稍后执行

AWS 和 Azure 的等效方案

在 AWS 中,你会使用带有网络引用的aws_instance Terraform 资源(mng.bz/PnPR)。在 Azure 中,使用网络上的azurerm_linux_virtual_machine Terraform 资源(mng.bz/J2DZ)。

实现控制反转可以消除服务器模块中对子网的直接引用。你也可以控制并限制网络为高级资源返回的信息。更重要的是,你提高了我的可组合性,因为你可以使用网络模块提供的子网名称创建其他服务器和高级资源。

如果其他高级资源需要其他低级属性怎么办?例如,你可能创建一个需要子网 IP 地址范围的队列。为了解决这个问题,你将网络模块进化为输出子网 IP 地址范围。队列可以引用它需要的地址的输出。

控制反转通过高级资源需要不同的属性来提高可进化性。你可以不重写高级资源的基础设施代码来进化低级资源。然而,你需要一种方法来保护高级资源免受低级资源任何属性更新或重命名的影响。

4.2.2 依赖倒置

虽然控制反转使得高级模块能够进化,但它并不能保护它们免受低级模块变更的影响。让我们想象一下,你将网络名称更改为其 ID。下次你部署服务器模块的变更时,它就崩溃了!服务器模块无法识别网络 ID。

为了保护你的服务器模块免受网络输出的变更影响,你在网络输出和服务器之间添加了一层抽象。在图 4.4 中,服务器通过 API 或存储的配置访问网络的属性,而不是网络输出。所有这些接口都作为抽象来检索网络元数据。

图 4.4 依赖倒置将低级资源元数据的抽象返回给依赖它的资源。

你可以使用依赖倒置来隔离对低级模块的变更,并减轻对其依赖的破坏。依赖倒置原则规定,高级和低级资源应通过抽象表达依赖关系。

定义依赖倒置是通过抽象表达高级和低级模块或资源之间依赖关系的原则。

抽象层充当一个翻译者,用于传达所需的属性。它作为低级模块对高级模块变更的缓冲。一般来说,你可以从三种类型的抽象中进行选择:

  • 资源属性插值(模块内)

  • 模块输出(模块之间)

  • 基础设施状态(模块之间)

一些抽象,如属性插值或模块输出,取决于你的工具。通过基础设施状态进行的抽象将取决于你的工具或基础设施 API。图 4.5 显示了通过属性插值、模块输出或基础设施状态来传递网络元数据到服务器的抽象。

图 4.5 根据工具和依赖关系,依赖倒置的抽象可以使用属性插值、模块输出或基础设施状态。

让我们通过构建列表 4.3 中的网络和服务器模块来检查如何通过实现三种类型的抽象。我将从属性插值开始。属性插值处理模块或配置内资源或任务之间的属性传递。使用 Python,子网通过访问分配给网络对象的name属性来插值网络名称。

列表 4.3 使用属性插值获取网络名称

import json

class Network:                                                    ❶
   def __init__(self, name="hello-network"):                      ❶
       self.name = name
       self.resource = self._build()                              ❷

   def _build(self):                                              ❷
       return {
           'google_compute_network': [                            ❶
               {
                   f'{self.name}': [
                       {
                           'name': self.name                      ❶
                       }
                   ]
               }
           ]
       }

class Subnet:                                                     ❸
   def __init__(self, network, region='us-central1'):             ❹
       self.network = network                                     ❹
       self.name = region                                         ❸
       self.subnet_cidr = '10.0.0.0/28'
       self.region = region
       self.resource = self._build()

   def _build(self):
       return {
           'google_compute_subnetwork': [                         ❸
               {
                   f'{self.name}': [
                       {
                           'name': self.name,                     ❸
                           'ip_cidr_range': self.subnet_cidr,
                           'region': self.region,
                           'network': self.network.name           ❺
                       }
                   ]
               }
           ]
       }

if __name__ == "__main__":
   network = Network()                                            ❻
   subnet = Subnet(network)                                       ❼

   resources = {                                                  ❽
       "resource": [                                              ❽
           network.resource,                                      ❻
           subnet.resource                                        ❼
       ]                                                          ❽
   }                                                              ❽

   with open(f'main.tf.json', 'w') as outfile:                    ❾
       json.dump(resources, outfile, sort_keys=True, indent=4)    ❾

❶ 使用名为“hello-network”的 Terraform 资源创建 Google 网络

❷ 使用模块创建网络的 JSON 配置

❸ 使用名为 us-central1 的区域名称的 Terraform 资源创建 Google 子网

❹ 将整个网络对象传递给子网。子网调用网络对象以获取它需要的属性。

❺ 通过从对象中检索来插值网络名称

❻ 使用模块创建网络的 JSON 配置

❼ 使用模块创建子网和网络 JSON 配置,并将网络对象传递给子网

❽ 将网络和子网 JSON 对象合并为 Terraform 兼容的 JSON 结构

❾ 将 Python 字典写入 JSON 文件,供 Terraform 后续执行

领域特定语言

使用 DSL 的 IaC 工具提供自己的变量插值格式。在 Terraform 中的示例将使用 google_compute_network.hello-world-network .name 动态传递网络名称到子网。CloudFormation 允许您使用 Ref 引用参数。在 Bicep 中,您可以引用资源的 properties

在配置中的模块或资源之间可以进行属性插值。然而,插值仅适用于特定工具,并不一定适用于所有工具。当在组合中有更多资源和模块时,您不能使用插值。

属性插值的另一种替代方法是使用显式的模块输出在模块之间传递资源属性。您可以自定义输出以符合任何所需的模式或参数。例如,您可以将子网和网络组合到一个模块中,并导出其属性以供服务器使用。让我们重构子网和网络,并添加服务器,如下所示。

列表 4.4 将子网名称设置为模块的输出

import json

                                                                   ❶
class NetworkModule:                                               ❷
   def __init__(self, region='us-central1'):
       self._region = region
       self._network = Network()                                   ❷
       self._subnet = Subnet(self._network)                        ❷
       self.resource = self._build()                               ❸

   def _build(self):                                               ❸
       return [                                                    ❸
           self._network.resource,                                 ❸
           self._subnet.resource                                   ❸
       ]                                                           ❸

   class Output:                                                   ❹
       def __init__(self, subnet_name):                            ❹
           self.subnet_name = subnet_name                          ❹

   def output(self):                                               ❺
       return self.Output(self._subnet.name)                       ❺

class ServerModule:                                                ❻
   def __init__(self, name, network,                               ❼
                zone='us-central1-a'):
       self._name = name
       self._subnet_name = network.subnet_name                     ❽
       self._zone = zone
       self.resource = self._build()                               ❾

   def _build(self):                                               ❾
       return [{
           'google_compute_instance': [{
               self._name: [{
                   'allow_stopping_for_update': True,
                   'boot_disk': [{
                       'initialize_params': [{
                           'image': 'ubuntu-1804-lts'
                       }]
                   }],
                   'machine_type': 'e2-micro',
                   'name': self._name,
                   'zone': self._zone,
                   'network_interface': [{
                       'subnetwork': self._subnet_name
                   }]
               }]
           }]
       }]

if __name__ == "__main__":
   network = NetworkModule()                                       ❿
   server = ServerModule("hello-world",                            ❻
                         network.output())                         ❼
   resources = {                                                   ⓫
       "resource": network.resource + server.resource              ⓫
   }                                                               ⓫

   with open(f'main.tf.json', 'w') as outfile:                     ⓬
       json.dump(resources, outfile, sort_keys=True, indent=4)     ⓬

❶ 为了清晰起见省略了网络和子网对象

❷ 将网络和子网创建重构为一个模块。这遵循了组合模式。该模块使用 Terraform 资源创建 Google 网络和子网。

❸ 使用模块创建网络和子网的 JSON 配置

❹ 为网络模块输出创建一个嵌套类。该嵌套类导出子网名称,以便高级属性可以使用。

❺ 为网络模块创建一个输出函数,以检索和导出所有网络输出

❻ 此模块使用 Terraform 资源创建 Google 计算实例(服务器)。

❼ 将网络输出作为输入变量传递给服务器模块。服务器将选择它需要的属性。

❽ 使用网络输出对象,获取子网名称并将其设置为服务器的子网名称属性

❾ 使用模块创建服务器的 JSON 配置

❿ 将网络和子网创建重构为一个模块。这遵循了组合模式。该模块使用 Terraform 资源创建 Google 网络和子网。

⓫ 将网络和服务器 JSON 对象合并为 Terraform 兼容的 JSON 结构

⓬ 将 Python 字典写入 JSON 文件,供 Terraform 后续执行

专用领域语言

对于像 CloudFormation、Bicep 或 Terraform 这样的配置工具,您为模块或堆栈生成输出,以便高级工具可以消费。例如,Ansible 这样的配置管理工具通过标准输出在自动化任务之间传递变量。

模块输出有助于暴露高级资源的特定参数。这种方法复制并重复值。然而,模块输出可能会变得复杂!您通常会忘记您暴露了哪些输出以及它们的名称。第六章中的合同测试可能有助于您强制执行所需的模块输出。

而不是使用输出,您可以使用基础设施状态作为状态文件或基础设施提供者的 API 元数据。许多工具保留基础设施状态的一个副本,我称之为 工具状态,以检测实际资源状态与配置之间的偏差,并跟踪它管理的资源。

定义 工具状态 是 IaC 工具存储的基础设施状态表示。它跟踪工具管理的资源配置。

工具通常将它们的状态存储在文件中。您已经在列表 4.2 中遇到了使用工具状态的例子。您从名为 terraform.tfstate 的文件中解析了网络名称,这是 Terraform 的工具状态。然而,并非所有工具都提供状态文件。因此,您可能难以在工具之间解析低级资源属性。

如果您的系统中有多达多个工具和提供者,您有两个主要选项。首先,考虑使用配置管理器作为标准接口来传递元数据。配置管理器,如键值存储,管理一组字段及其值。

配置管理器帮助您为工具状态创建自己的抽象层。例如,一些网络自动化脚本可能读取存储在键值存储中的 IP 地址值。但是,您必须维护配置管理器,并确保您的 IaC 可以访问它。

作为第二个选项,考虑使用基础设施提供者的 API。基础设施 API 不常更改;它们提供详细的信息,并考虑到状态文件可能不包括的带外更改。您可以使用客户端库从基础设施 API 中访问信息。

专用领域语言

许多配置工具提供了一种能力,可以向基础设施 API 发起 API 调用。例如,AWS 特定的参数类型和 CloudFormation 中的 Fn::ImportValue 从 AWS API 或其他堆栈检索值。Bicep 提供了一个名为 existing 的关键字,用于导入当前文件之外的资源属性。

Terraform 提供数据源,可以从 API 中读取基础设施资源的元数据。同样,模块可以引用 Ansible 事实,这些事实收集有关资源或您环境的元数据。

使用基础设施 API 时,您会遇到一些缺点。不幸的是,您的 IaC 需要网络访问。您只有在运行 IaC 之后才知道属性的,因为代码必须向 API 发出请求。如果基础设施 API 出现故障,您的 IaC 可能无法解析低级资源的属性。

当您使用依赖反转添加抽象时,您会保护高级资源免受低级资源属性更改的影响。虽然您不能防止所有故障或中断,但您可以通过更新低级资源最小化潜在故障的破坏范围。将其视为一项合同:如果高级和低级资源就它们需要的属性达成一致,它们可以独立于彼此发展。

4.2.3 应用依赖注入

当您结合控制反转和依赖反转时会发生什么?图 4.6 显示了如何结合这两个原则来解耦服务器和网络示例。服务器调用网络以获取属性,并使用基础设施 API 或状态解析元数据。如果您更改网络名称,它将更新元数据。服务器检索更新后的元数据并单独调整其配置。

图 4.6 依赖注入结合了控制反转和控制依赖反转,以放宽基础设施依赖并隔离低级和高级资源。

利用这两个原则的力量有助于促进演化和可组合性,因为抽象层充当系统每个构建块之间的缓冲区。您使用 依赖注入 来结合控制反转和依赖反转。控制反转隔离了高级模块或资源的更改,而依赖反转隔离了低级资源的更改。

定义 依赖注入 结合了控制反转和控制依赖反转的原则。高级模块或资源通过抽象从低级资源请求属性。

让我们使用 Apache Libcloud 库为服务器和网络示例实现依赖注入,Apache Libcloud 是一个用于 GCP API 的库,如列表 4.5 所示。您使用 Libcloud 来搜索网络。服务器调用 GCP API 获取子网名称,解析 GCP API 元数据,并将网络范围内的第五个 IP 地址分配给自己。

列表 4.5 使用依赖注入在网络上创建服务器

import credentials
import ipaddress
import json
from libcloud.compute.types import Provider                              ❶
from libcloud.compute.providers import get_driver                        ❶

def get_network(name):                                                   ❷
   ComputeEngine = get_driver(Provider.GCE)                              ❸
   driver = ComputeEngine(                                               ❹
       credentials.GOOGLE_SERVICE_ACCOUNT,                               ❹
       credentials.GOOGLE_SERVICE_ACCOUNT_FILE,                          ❹
       project=credentials.GOOGLE_PROJECT,                               ❹
       datacenter=credentials.GOOGLE_REGION)                             ❹
   return driver.ex_get_subnetwork(                                      ❺
       name, credentials.GOOGLE_REGION)                                  ❺

class ServerFactoryModule:                                               ❻
   def __init__(self, name, network, zone='us-central1-a'):
       self._name = name
       gcp_network_object = get_network(network)                         ❷
       self._network = gcp_network_object.name                           ❼
       self._network_ip = self._allocate_fifth_ip_address_in_range(      ❽
           gcp_network_object.cidr)                                      ❽
       self._zone = zone
       self.resources = self._build() 

   def _allocate_fifth_ip_address_in_range(self, ip_range):              ❽
       ip = ipaddress.IPv4Network(ip_range)                              ❽
       return format(ip[-2])                                             ❽

   def _build(self):                                                     ❾
       return {
           'resource': [{
               'google_compute_instance': [{                             ❻
                   self._name: [{
                       'allow_stopping_for_update': True,
                       'boot_disk': [{
                           'initialize_params': [{
                               'image': 'ubuntu-1804-lts'
                           }]
                       }],
                       'machine_type': 'f1-micro',
                       'name': self._name,
                       'zone': self._zone,
                       'network_interface': [{
                           'subnetwork': self._network,                 ❿
                           'network_ip': self._network_ip               ⓫
                       }]
                   }]
               }]
           }]
       }

if __name__ == "__main__":
   server = ServerFactoryModule(name='hello-world', network='default')  ⓬
   with open('main.tf.json', 'w') as outfile:                           ⓭
       json.dump(server.resources, outfile, sort_keys=True, indent=4)   ⓭

❶ 导入 Libcloud 库,该库允许您访问 GCP API。您必须导入提供者对象和 Google 驱动程序。

❷ 此函数使用 Libcloud 库检索网络信息。网络和子网是分别创建的。为了清晰起见,它们的代码已被省略。

❸ 导入 Libcloud 的 Google Compute Engine 驱动程序

❹ 将您希望 Libcloud 用于访问 GCP API 的 GCP 服务帐户凭据传递过去

❺ 使用 Libcloud 驱动程序通过名称获取子网信息

❻ 此模块使用 Terraform 资源创建 Google 计算实例(服务器)

❼ 从 Libcloud 返回的 GCP 网络对象中解析子网名称,并使用它来创建服务器

❽ 从 Libcloud 返回的 GCP 网络对象中解析 CIDR 块,并使用它来计算网络上的第五个 IP 地址。服务器使用此结果作为其网络 IP 地址。

❾ 使用该模块创建服务器的 JSON 配置

❿ 从 Libcloud 返回的 GCP 网络对象中解析子网名称,并使用它来创建服务器

⓫ 从 Libcloud 返回的 GCP 网络对象中解析 CIDR 块,并使用它来计算网络上的第五个 IP 地址。服务器使用此结果作为其网络 IP 地址。

⓬ 此模块使用 Terraform 资源创建 Google 计算实例(服务器)。

⓭ 将 Python 字典写入 JSON 文件,供 Terraform 稍后执行

AWS 和 Azure 等效

要转换列表 4.5,你需要更新 IaC 以创建 Amazon Elastic Compute Cloud (EC2)实例或 Azure Linux 虚拟机。你需要更新 Libcloud 驱动程序以使用 Amazon EC2 Driver (mng.bz/wo95)或 Azure ARM Compute Driver (mng.bz/qY9x)。

使用基础设施 API 作为抽象层,你可以独立于服务器进化网络。例如,当你更改网络的 IP 地址范围时会发生什么?在运行服务器的 IaC 之前,你将更新部署到网络的更新。服务器调用基础设施 API 以获取网络属性,并识别新的 IP 地址范围。然后它重新计算第五个 IP 地址。

图 4.7 显示了由于依赖注入而导致的服务器对变化的响应。当你更改网络的 IP 地址范围时,你的服务器会获取更新的地址范围,并在需要时重新分配 IP 地址。

图 4.7 依赖注入允许我更改低级模块(网络)并自动将更改传播到高级模块(服务器)。

多亏了依赖反转,你可以独立于依赖关系进化低级资源。控制反转有助于高级资源响应低级资源的变化。将两者结合为依赖注入确保了系统的可组合性,因为你可以向低级资源添加更多高级资源。由于依赖注入导致的解耦有助于你最小化系统模块中失败更改的爆炸半径。

通常,你应该将依赖注入作为基础设施依赖管理的基本原则。如果你在编写基础设施配置时应用依赖注入,你就可以充分解耦依赖关系,以便你可以独立地更改它们而不会影响其他基础设施。随着你的模块增长,你可以继续重构以使用更具体的模式,并进一步根据资源类型和模块类型解耦基础设施。

4.3 外观模式

应用依赖注入原则生成表达依赖关系的类似模式。这些模式与软件开发中的结构设计模式相一致。在追求解耦依赖的过程中,我经常在我的基础设施即代码(IaC)中重复使用相同的三个模式。

想象一下,你想要创建一个存储桶来存储静态文件。你可以使用 GCP 中的访问控制 API 来控制谁可以访问文件。图 4.8 创建了桶并设置输出以包括桶的名称。桶的访问控制规则可以使用输出获取桶的名称。

图片

图 4.8 外观模式简化了属性,将其作为存储桶的名称供访问控制模块使用。

使用输出和抽象层的模式看起来 非常熟悉。实际上,你在章节的前半部分就遇到了它。你一直在不知不觉中使用外观模式在模块之间传递多个属性!

外观模式 使用模块输出作为依赖注入的抽象。它像一个镜子,将属性反射到其他模块和资源。

定义 外观模式 从模块的资源中输出属性以进行依赖注入。

外观模式只反映属性,没有更多。该模式解耦了高层和底层资源之间的依赖关系,并符合依赖注入的原则。高层资源仍然调用底层资源以获取信息,而输出充当抽象。

以下代码示例通过构建一个输出方法实现了外观模式。你的桶模块在其输出方法中返回桶对象和名称。你的访问模块使用输出方法来检索桶对象并访问其名称。

列表 4.6 将桶名称作为外观用于访问控制规则

import json
import re

class StorageBucketFacade:                                  ❶
   def __init__(self, name):                                ❶
       self.name = name                                     ❶

class StorageBucketModule:                                  ❷
   def __init__(self, name, location='US'):                 ❸
       self.name = f'{name}-storage-bucket'
       self.location = location
       self.resources = self._build()

   def _build(self):
       return {
           'resource': [
               {
                   'google_storage_bucket': [{              ❸
                       self.name: [{
                           'name': self.name,
                           'location': self.location,
                           'force_destroy': True            ❹
                       }]
                   }]
               }
           ]
       }

   def outputs(self):                                       ❺
       return StorageBucketFacade(self.name)   

class StorageBucketAccessModule:                            ❻
   def __init__(self, bucket, user, role):                  ❼
       if not self._validate_user(user):                    ❽
           print("Please enter valid user or group ID")
           exit()
       if not self._validate_role(role):                    ❾
           print("Please enter valid role")
           exit()
       self.bucket = bucket                                 ❼
       self.user = user
       self.role = role
       self.resources = self._build()

   def _validate_role(self, role):                          ❾
       valid_roles = ['READER', 'OWNER', 'WRITER']
       if role in valid_roles:
           return True
       return False

   def _validate_user(self, user):                          ❽
       valid_users_group = ['allUsers', 'allAuthenticatedUsers']
       if user in valid_users_group:
           return True
       regex = r'^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,3}$'
       if(re.search(regex, user)):
           return True
       return False

   def _change_case(self):
       return re.sub('[⁰-9a-zA-Z]+', '_', self.user)

   def _build(self):
       return {
           'resource': [{
               'google_storage_bucket_access_control': [{
                   self._change_case(): [{
                       'bucket': self.bucket.name,          ❿
                       'role': self.role,
                       'entity': self.user
                   }]
               }]
           }]
       }

if __name__ == "__main__":
   bucket = StorageBucketModule('hello-world')
   with open('bucket.tf.json', 'w') as outfile:
       json.dump(bucket.resources, outfile, sort_keys=True, indent=4)

   server = StorageBucketAccessModule(
       bucket.outputs(), 'allAuthenticatedUsers', 'READER')
   with open('bucket_access.tf.json', 'w') as outfile:
       json.dump(server.resources, outfile, sort_keys=True, indent=4)

❶ 使用外观模式,将桶名称作为存储输出对象的一部分输出。这实现了依赖反转,以抽象掉不必要的桶属性。

❷ 为 GCP 存储桶创建一个低级模块,该模块使用工厂模式生成一个桶

❸ 使用基于名称和位置的 Terraform 资源创建 Google 存储桶

❹ 在 Google 存储桶上设置一个属性,当删除 Terraform 资源时销毁它

❺ 为模块创建一个输出方法,返回存储桶的属性列表

❻ 创建一个高级模块,向存储桶添加访问控制规则

❼ 将桶的输出外观传递给高级模块

❽ 验证传递给模块的用户是否与所有用户或所有认证用户的有效用户组类型匹配

❾ 验证传递给模块的角色是否与 GCP 中的有效角色匹配

❿ 使用 Terraform 资源创建 Google 存储桶访问控制规则

AWS 和 Azure 的等效产品

Google 云平台(GCP)存储桶类似于 Amazon Simple Storage Service(S3)桶或 Azure Blob Storage。

为什么输出整个桶对象而不是只输出名称?记住,你想要构建一个抽象层,以符合依赖反转原则。如果你创建了一个依赖于桶位置的模块,你可以更新桶对象的外观以输出名称和位置。更新不会影响访问模块。

你可以以低努力成本实现外观,同时仍然从依赖解耦中获得好处。这样的好处之一包括在一个模块中执行隔离、自包含更新而不会影响其他模块的灵活性。添加新的高级依赖关系不需要太多努力。

外观模式还使得调试问题更加容易。它镜像输出而不添加解析逻辑,这使得追踪问题到源头并修复系统变得简单。你将在第十一章中了解更多关于回滚失败更改的内容。

领域特定语言

使用领域特定语言(DSL),你可以通过使用具有自定义名称的输出变量来模拟外观。高级资源引用自定义输出名称。

作为一般做法,你应该从一个或两个字段开始创建外观。始终将其保持为所需的最小字段数,用于高级资源。每几周审查并修剪不需要的字段。

外观模式适用于更简单的依赖关系,例如几个高级模块到一个低级模块。然而,当你添加许多高级模块并且依赖关系的深度增加时,你将难以维护低级模块的外观模式。当你需要更改输出中的字段名称时,你必须更改引用它的每个模块。当你有数百个资源依赖于一个低级模块时,更改每个模块引用的缩放性不好。

4.4 适配器

外观将一个基础设施模块的值作为输出镜像到上一节的高级模块。这对于简单的依赖关系工作良好,但在更复杂的模块中会崩溃。更复杂的模块通常涉及一对一或多对一的依赖关系,或者跨越多个基础设施提供者。

假设你有一个身份模块,它传递一个用户和角色的列表来配置基础设施。该身份模块需要在多个平台上工作。在图 4.9 中,你设置模块以输出一个 JSON 格式的对象,将权限如 readwriteadmin 与相应的用户名相对应。团队必须将这些用户名及其通用权限映射到 GCP 特定的术语。GCP 的访问管理使用 viewereditorowner,它们分别转换为 readwriteadmin

图 4.9 适配器模式将属性转换为高级模块可以消费的不同接口。

如何将一组通用角色映射到特定基础设施提供商的角色?映射需要确保你可以在多个基础设施提供商上复制和扩展模块。你希望在将来扩展模块,以添加跨平台的等效角色中的用户。

作为解决方案,适配器模式 将低级资源的元数据转换,以便任何高级资源都可以使用它。适配器的作用就像旅行插头。你可以根据国家的插座更换插头,仍然可以使用你的电子设备。

定义 适配器模式 将低级资源或模块的元数据转换并输出,以便任何高级资源或模块都可以使用它。

首先,你创建一个字典,将通用角色名称映射到用户。在列表 4.7 中,你想要将只读角色分配给审计团队和两个用户。这些通用角色和用户名与 GCP 的权限和角色不匹配。

列表 4.7 创建将通用角色映射到用户名的静态对象

class Infrastructure:
   def __init__(self):
       self.resources = {
           'read': [                     ❶
               'audit-team',             ❶
               'user-01',                ❶
               'user-02'                 ❶
           ],
           'write': [                    ❷
               'infrastructure-team',    ❷
               'user-03',                ❷
               'automation-01'           ❷
           ],
           'admin': [                    ❸
               'manager-team'            ❸
           ]                             ❸
       }

❶ 将 audit-team、user-01 和 user-02 分配到只读角色。映射描述了用户只能读取任何基础设施提供商上的信息。

❷ 将 infrastructure-team、user-02 和 automation-01 分配到写角色。映射描述了用户可以更新任何基础设施提供商上的信息。

❸ 将 manager team 分配到管理员角色。映射描述了用户可以管理任何基础设施提供商。

AWS 和 Azure 的等效项

对于那些更熟悉 AWS 的人来说,每个权限集的等效策略将是 adminAdministratorAccesswritePowerUserAccess,以及 readViewOnlyAccess。Azure 基于角色的访问控制使用 Owner 代表 adminContributor 代表 writeReader 代表 read

然而,你无法在角色映射中的静态对象上做任何事情。GCP 不理解用户名或角色!实现适配器模式以将通用权限映射到特定于基础设施的权限。

下面的列表构建了一个针对 GCP 特定的身份适配器,它将通用权限如 read 映射到 GCP 特定的术语如 roles/viewer。GCP 可以使用此映射将用户、服务帐户和组添加到正确的角色中。

列表 4.8 使用适配器模式转换通用权限

import json
import access

class GCPIdentityAdapter:                                                  ❶
   EMAIL_DOMAIN = 'example.com'                                            ❷

   def __init__(self, metadata):
       gcp_roles = {                                                       ❸
           'read': 'roles/viewer',                                         ❸
           'write': 'roles/editor',                                        ❸
           'admin': 'roles/owner'                                          ❸
       }  
       self.gcp_users = []
       for permission, users in metadata.items():                          ❹
           for user in users:                                              ❹
               self.gcp_users.append(                                      ❹
                   (user, self._get_gcp_identity(user),                    ❺
                       gcp_roles.get(permission)))                         ❹

   def _get_gcp_identity(self, user):                                      ❺
       if 'team' in user:                                                  ❻
           return f'group:{user}@{self.EMAIL_DOMAIN}'                      ❻
       elif 'automation' in user:                                          ❼
           return f'serviceAccount:{user}@{self.EMAIL_DOMAIN}'             ❼
       else:                                                               ❽
           return f'user:{user}@{self.EMAIL_DOMAIN}'                       ❽

   def outputs(self):                                                      ❾
       return self.gcp_users                                               ❾

class GCPProjectUsers:                                                     ❿
   def __init__(self, project, users):
       self._project = project
       self._users = users
       self.resources = self._build()                                      ⓫

   def _build(self):                                                       ⓫
       resources = []
       for (user, member, role) in self._users:                            ⓬
           resources.append({
               'google_project_iam_member': [{                             ⓭
                   user: [{                                                ⓭
                       'role': role,                                       ⓭
                       'member': member,                                   ⓭
                       'project': self._project                            ⓭
                   }]                                                      ⓭
               }]                                                          ⓭
           })
       return {
           'resource': resources
       }

if __name__ == "__main__":
   users = GCPIdentityAdapter(access.Infrastructure().resources).outputs() ⓮

   with open('main.tf.json', 'w') as outfile:                              ⓯
       json.dump(                                                          ⓯
           GCPProjectUsers(                                                ⓯
               'infrastructure-as-code-book',                              ⓯
               users).resources, outfile, sort_keys=True, indent=4)        ⓯

❶ 创建一个适配器,将通用角色类型映射到 Google 角色类型

❷ 将电子邮件域名设置为常量,您将将其附加到每个用户

❸ 创建一个字典,将通用角色映射到 GCP 特定权限和角色

❹ 对于每个权限和用户,构建一个包含用户、GCP 身份和角色的元组

❺ 将用户名转换为 GCP 特定的成员术语,该术语使用用户类型和电子邮件地址

❻ 如果用户名包含“团队”,则 GCP 身份需要以“group”为前缀,以电子邮件域为后缀。

❼ 如果用户名包含“自动化”,则 GCP 身份需要以“serviceAccount”为前缀,以电子邮件域为后缀。

❽ 对于所有其他用户,GCP 身份需要以“user”为前缀,以电子邮件域为后缀。

❾ 输出包含用户、GCP 身份和角色的元组列表

❿ 创建一个 GCP 项目用户模块,该模块使用工厂模式将用户附加到给定项目的 GCP 角色中

⓫ 使用该模块创建项目的用户和角色的 JSON 配置

⓬ 创建一个字典,将通用角色映射到 GCP 特定权限和角色

⓭ 使用 Terraform 资源创建一个 Google 项目 IAM 成员列表。该列表检索 GCP 身份、角色和项目,以便将用户名附加到 GCP 中的读取、写入或管理员权限。

⓮ 创建一个适配器,将通用角色类型映射到 Google 角色类型

⓯ 将 Python 字典写入 JSON 文件,以便 Terraform 后续执行

AWS 和 Azure 等效

要将代码列表转换为 AWS,您需要将 GCP 项目的引用映射到 AWS 账户。GCP 项目用户与 AWS IAM 用户及其附加的角色相对应。同样,您会在 Azure 订阅中创建一个用户账户,并在 Azure Active Directory 中添加其 API 权限。

您可以将您的身份适配器扩展到将通用访问要求字典映射到另一个基础设施提供者,如 AWS 或 Azure。一般来说,适配器将特定提供者或原型模块特定的语言转换为通用术语。此模式最适合具有不同基础设施提供者或依赖项的模块。我还使用适配器模式为资源参数定义不佳的基础设施提供者创建一个一致的接口。

对于一个更复杂的示例,想象配置两个云之间的虚拟专用网络(VPN)连接。而不是通过外观传递每个提供者的网络信息,您使用适配器,如图 4.10 所示。每个提供者的网络模块输出一个具有更通用字段(如 nameIP address)的网络对象。此用例受益于适配器,因为它协调了两种不同语言的语义(例如,GCP 云 VPN 网关和 AWS 客户端网关)。

图片

图 4.10 一个适配器在两个云服务提供商之间转换语言和属性。

Azure 等效

Azure VPN 网关实现了与 AWS 客户网关和 GCP 云 VPN 网关类似的功能。

为什么使用适配器来提高可组合性和可扩展性?该模式严重依赖于依赖反转来抽象资源之间任何属性转换。适配器充当模块间的契约。只要两个模块都同意适配器概述的契约,你就可以在一定程度上独立地更改高级和低级模块。

专用领域语言

专用领域语言(DSL)将提供者或资源特定的语言或资源进行转换。DSL 在它们的框架内实现适配器,以表示基础设施状态。基础设施状态通常包括与基础设施 API 相同的资源元数据。一些工具将允许你与状态文件接口,并将模式视为高级模块的适配器。

然而,适配器模式仅在您维护模块间的契约时才有效。回想一下,你构建了一个适配器来将权限和用户名转换为 GCP。如果你的队友不小心将只读角色的映射更新为roles/reader,而这个角色不存在,会发生什么?图 4.11 展示了如果你不使用针对 GCP 的特定角色,你的基础设施即代码(IaC)将失败。

图片

图 4.11 你需要调试和测试适配器以正确映射字段。

在这个例子中,你破坏了通用角色和 GCP 角色之间的契约!破坏的契约导致你的 IaC 失败。确保你维护并更新适配器中的正确映射,以最大限度地减少失败。

此外,使用适配器后,调试变得更加困难。该模式模糊了依赖于特定适配器属性的资源。你需要调查错误是否由源模块输出的错误字段、适配器中的错误属性或依赖模块消费的错误字段引起的。第五章和第六章分别介绍了模块版本控制和测试,可以缓解适配器的挑战和调试问题。

4.5 中介者

适配器和外观模式隔离了变化,并使管理单个依赖变得容易。然而,基础设施即代码(IaC)通常包括复杂的资源依赖关系。为了解开依赖关系的网,你可以构建有见地的自动化,以确定何时以及如何创建资源。

想象一下,你想要在我们的标准服务器和网络示例中添加一条防火墙规则,允许 SSH 访问服务器的 IP 地址。然而,只有当服务器存在时,你才能创建防火墙规则。同样,只有当网络存在时,你才能创建服务器。你需要自动化来捕捉防火墙、服务器和网络之间关系的复杂性。

让我们尝试捕捉创建网络、服务器和防火墙的逻辑。自动化可以帮助 调解 首先创建哪些资源。图 4.12 绘制了自动化的工作流程。如果资源是服务器,IaC 首先创建网络,然后是服务器。如果资源是防火墙规则,IaC 首先创建网络,然后是服务器,最后是防火墙规则。

图 4.12 中介成为决定首先配置哪个资源的权威机构。

IaC 实现了依赖注入来抽象和控制网络、服务器和防火墙的依赖关系。它依赖于幂等性原则来持续运行并达到相同的目标状态(网络、服务器和防火墙),无论现有资源如何。可组合性还有助于建立基础设施资源和依赖关系的基本构建块。

这种 中介模式 的工作方式类似于机场的空中交通管制。它控制和管理工作站的进出航班。中介的唯一目的是组织这些资源之间的依赖关系,并根据需要创建或删除对象。

定义 中介模式 组织基础设施资源之间的依赖关系,并包含基于其依赖关系创建或删除对象的逻辑。

让我们实现网络、服务器和防火墙的中介模式。在 Python 中实现中介模式需要一些 if-else 语句来检查每种资源类型并构建其低级依赖。在列表 4.9 中,防火墙依赖于首先创建服务器和网络。

列表 4.9 使用中介模式组织服务器和依赖

import json
from server import ServerFactoryModule                                   ❶
from firewall import FirewallFactoryModule                               ❶
from network import NetworkFactoryModule                                 ❶

class Mediator:                                                          ❷
   def __init__(self, resource, **attributes):
       self.resources = self._create(resource, **attributes)

   def _create(self, resource, **attributes):                            ❸
       if isinstance(resource, FirewallFactoryModule):                   ❹
           server = ServerFactoryModule(resource._name)                  ❹
           resources = self._create(server)                              ❹
           firewall = FirewallFactoryModule(                             ❺
               resource._name, depends_on=resources[1].outputs())        ❺
           resources.append(firewall)                                    ❺
       elif isinstance(resource, ServerFactoryModule):                   ❻
           network = NetworkFactoryModule(resource._name)                ❻
           resources = self._create(network)                             ❻
           server = ServerFactoryModule(                                 ❼
               resource._name, depends_on=network.outputs())             ❼
           resources.append(server)                                      ❼
       else:                                                             ❽
           resources = [resource]                                        ❽
       return resources

   def build(self):                                                      ❾
       metadata = []                                                     ❾
       for resource in self.resources:                                   ❾
           metadata += resource.build()                                  ❾
       return {'resource': metadata}                                     ❾

if __name__ == "__main__":
   name = 'hello-world'
   resource = FirewallFactoryModule(name)                                ❿
   mediator = Mediator(resource)                                         ❿

   with open('main.tf.json', 'w') as outfile:                            ⓫
       json.dump(mediator.build(), outfile, sort_keys=True, indent=4)    ⓫

❶ 导入网络、服务器和防火墙的工厂模块

❷ 创建一个中介来决定如何以及按何种顺序自动化资源更改

❸ 当你调用中介来创建网络、服务器或防火墙等资源时,你允许中介决定所有要配置的资源。

❹ 如果你想创建一个作为资源的防火墙规则,中介将递归地调用自己以首先创建服务器。

❺ 在中介创建服务器配置后,它构建防火墙规则配置。

❻ 如果你想创建一个作为资源的服务器,中介将递归地调用自己以首先创建网络。

❼ 在中介创建网络配置后,它构建服务器配置。

❽ 如果你向中介传递任何其他资源,例如网络,它将构建其默认配置。

❾ 使用模块从中介创建资源列表并渲染 JSON 配置

❿ 将防火墙资源传递给中介。中介将创建网络、服务器,然后是防火墙配置。

将 Python 字典写入 JSON 文件,以便 Terraform 后续执行

AWS 和 Azure 的等效功能

GCP 的防火墙规则在行为上与 AWS 安全组或 Azure 网络安全组的规则相似。这些规则控制来自和发往标记目标的 IP 地址范围的网络流量。

如果你有一个新的资源,例如负载均衡器,你可以在服务器或防火墙之后扩展中介器来构建它。中介器模式最适合具有多层依赖关系和多个系统组件的模块。

然而,你可能发现实现中介器具有挑战性。中介器模式必须遵循幂等性。你需要多次运行并达到相同的目标状态。你必须编写和测试中介器中的所有逻辑。如果你不测试你的中介器,你可能会意外地破坏资源。编写自己的中介器需要大量的代码!

幸运的是,你通常不需要自己实现中介器。大多数基础设施即代码(IaC)工具都充当中介器,以解决复杂的依赖关系并决定如何创建资源。大多数配置工具都内置了中介器来识别依赖关系和操作顺序。例如,Kubernetes 的容器编排使用中介器来协调集群中资源的变更。Ansible 使用中介器来确定从各种配置模块中组合和运行哪些自动化步骤。

注意:一些 IaC 工具通过使用图论来映射资源之间的依赖关系来实现中介器模式。资源作为节点。链接将属性传递给依赖资源。如果你没有工具来创建资源,你可以手动在你的系统中绘制依赖关系图。图可以帮助组织你的自动化和代码。它们还可以确定你可以解耦哪些模块。绘制依赖关系的练习可能有助于你实现中介器。

我只在找不到工具或需要在工具之间添加某些内容时实现中介器模式。例如,我有时会编写一个中介器来控制在一个工具中创建 Kubernetes 集群,在另一个工具部署 Kubernetes 集群上的服务之前。中介器协调这两个工具之间的自动化,例如在第二个工具部署服务之前检查集群健康。

4.6 选择一个模式

门面、适配器和中介器都使用依赖注入来解耦高级模块和低级模块之间的更改。你可以应用任何一种模式,它们将表达模块之间的依赖关系并在其中隔离更改。随着你的系统增长,你可能需要根据模块的结构更改这些模式。

你选择模式取决于你对低级模块或资源的依赖数量。外观模式适用于一个低级模块到几个高级模块。如果你有一个具有许多高级模块依赖的低级模块,请考虑适配器。当你模块之间存在许多依赖时,你可能需要一个中介者来控制资源自动化。图 4.13 概述了确定使用哪种依赖模式的决策树。

图 4.13 选择你的抽象取决于依赖关系的关系,无论是模块内、一对一还是一对多。

所有模式都通过依赖注入促进幂等性、可组合性和可进化性。然而,为什么你会从外观(facade)开始,然后考虑适配器(adapter)或中介者(mediator)呢?随着你的系统增长,你需要优化你的依赖管理模式以减少变更的操作负担。

图 4.14 显示了故障排除和实施工作量与外观、中介者和适配器模式的可扩展性和隔离性之间的关系。例如,外观在实施和故障排除方面具有最小努力的优势,但不能随着更多资源的增加而扩展或隔离变更。适配器和中介者在故障排除和实施工作量的代价下提供了改进的可扩展性和隔离性。

图 4.14 一些模式可能有较低的故障排除和实施成本,但不能隔离模块的变更和扩展。

通过选择具有中介者实现的工具来降低初始工作量。然后使用工具内置的外观实现来管理模块或资源之间的依赖关系。当你发现难以管理外观,因为你有许多系统相互依赖时,你可以开始检查适配器或中介者。

适配器的实现需要更多的工作,但它为你扩展和增长基础设施系统提供了最佳基础。你总是可以添加新的基础设施提供者和系统,而不用担心更改低级模块。然而,你不能期望为每个模块都使用适配器,因为它需要时间来实现和故障排除。

使用中介者的工具选择哪些组件需要更新以及何时更新。现有的工具可以降低你的整体实施工作量,但在故障排除期间会引入一些问题。你需要了解你的工具行为来排除依赖失败的变更。根据你如何使用工具,具有中介者的工具允许你扩展,但可能无法完全隔离模块的变更。

练习 4.1

我们如何通过以下 IaC 更好地解耦数据库对网络的依赖?

class Database:
  def __init__(self, name):
    spec = {
      'name': name,
      'settings': {
        'ip_configuration': {
          'private_network': 'default'
        }
      }
    }

A) 该方法充分解耦了数据库和网络。

B) 将网络 ID 作为变量传递,而不是将其硬编码为 default

C) 为所有网络属性实现并传递一个NetworkOutput对象到数据库模块。

D) 向网络模块添加一个函数,将其网络 ID 推送到数据库模块。

E) 向数据库模块添加一个函数,用于调用基础设施 API 以获取default网络 ID。

请参阅附录 B 以获取练习题的答案。

摘要

  • 应用基础设施依赖模式,如外观模式、适配器模式和中介者模式,以解耦模块和资源,这样你可以独立地对模块进行更改。

  • 控制反转表明高级资源调用低级资源以获取属性。

  • 依赖倒置原则表明高级资源应该使用低级资源元数据的抽象。

  • 依赖注入结合了控制反转和依赖倒置的原则。

  • 如果你没有识别出适用的模式,你可以使用依赖注入,让高级资源调用低级资源,并解析其对象结构以获取所需的值。

  • 使用外观模式引用属性的简化接口。

  • 使用适配器模式将一种资源中的元数据转换为另一种资源以使用。这种模式与来自不同基础设施提供商或原型模块的资源配合得最好。

  • 中介者模式组织这些资源之间的依赖关系,并根据需要创建或删除对象。大多数工具都充当资源之间的中介。

第二部分:与团队一起扩展

在你学习了编写基础设施代码的实践和模式之后,你通常会想要与你的团队分享这些内容。本部分讨论了共享和协作 IaC 的考虑因素和指南。在多个团队之间建立模式可以最小化冲突和潜在失败。

第五章描述了如何构建、版本控制和发布模块,以确保每个团队成员都可以安全地更新它们。在第六章中,你将了解 IaC 的测试策略。这些测试将帮助你验证更改在推送到生产环境之前的稳定性。

在第七章中,你将学习如何将测试添加到交付管道中,该章节描述了开发和部署模型。本章提供了关于分支(或是否分支!)以及安全地将更改部署到生产的指导。最后,第八章讨论了你可以添加到 IaC 测试和交付管道中的实践,以确保安全且合规的配置。

5 结构化和共享模块

本章涵盖

  • 构建基础设施变更的模块版本和标签

  • 选择单个仓库与多个仓库

  • 在团队间组织共享的基础设施模块

  • 发布基础设施模块而不影响关键依赖

到目前为止,本书你已经学习了编写基础设施代码和实践以及将它们划分为基础设施组件组的模式。然而,即使你编写了最优的配置,仍然可能难以维护和减轻系统故障的风险。困难发生的原因是因为在更新基础设施模块时,你的团队没有标准化协作实践。

想象一家公司,蔬菜数据中心,它从自动化草药的种植操作开始。GCP 中的应用程序监控和调整以实现最佳草药生长。每个团队使用单例模式并创建独特的的基础设施配置。

随着时间的推移,蔬菜数据中心变得越来越受欢迎,并希望扩展到所有蔬菜。它雇佣了一个新的应用开发团队,专门为从草药到叶类蔬菜到根茎类蔬菜的各种蔬菜的种植软件。每个团队创建了一个独立于其他团队的基础设施配置。

蔬菜数据中心雇佣你来开发一个种植水果的应用程序。你意识到你不能重用任何基础设施配置,因为它们对每个蔬菜团队都是独特的。公司需要一个一致、可重用的方式来构建、安全和管理工作基础设施。

你意识到蔬菜数据中心可以利用第三章中的一些模块模式来组织基础设施配置,将其划分为模块以提高可组合性。你绘制了一个图,如图 5.1 所示,用于组织和协调多个团队的基础设施。草药、根茎类蔬菜、叶类蔬菜和水果的团队都可以使用标准化的网络、数据库和服务器配置。

图片

图 5.1 蔬菜数据中心可以使用模块来组织和标准化跨应用团队的基础设施配置。

在团队间共享模块促进可重复性、可组合性和可扩展性。团队不需要花费太多时间构建 IaC,因为他们可以复制已建立的配置。团队成员可以选择如何组合他们的系统,并覆盖特定需求的配置。

为了充分利用标准化模块的好处,你需要将它们视为一个独立于常规基础设施变更的开发生命周期。本章涵盖了共享和管理基础设施模块的实践。你将学习到如何发布稳定的模块,同时避免对高级依赖引入关键故障的技术和实践。

5.1 仓库结构

假设数据中心蔬菜的每个团队都为其基础设施使用单例模式。草药和叶绿蔬菜团队意识到他们使用的是类似配置的服务器、网络和数据库。他们能否将他们的基础设施配置合并到一个模块中?

与复制粘贴彼此的配置不同,草药和叶绿蔬菜团队希望在一个地方更新配置并在他们的配置中引用它。数据中心蔬菜应该将所有基础设施放在一个仓库中吗?还是应该将模块分散到多个仓库中?

5.1.1 单一仓库

起初,每个蔬菜数据中心团队都将其基础设施配置存储在单个代码仓库中。每个团队将其配置组织到一个专用目录中,以避免配置混淆。如果团队想要引用一个模块,团队将通过使用本地文件路径来导入模块。

图 5.2 显示了蔬菜数据中心如何构建其单一代码仓库。仓库包含顶级目录中的两个文件夹,分别用于模块和环境。公司为每个团队细分环境目录,例如叶绿蔬菜团队。叶绿蔬菜团队通过开发和生产环境来分离配置。

图片

图 5.2 叶绿蔬菜团队的生产和开发环境在一个单一仓库结构中使用了包含服务器、网络和数据库工厂模块的目录。

当叶绿蔬菜团队成员想要创建数据库时,他们可以使用模块文件夹中的一个模块。在他们的基础设施即代码(IaC)中,他们通过设置本地路径来导入模块。导入后,他们可以使用数据库工厂并在生产环境中构建资源。

蔬菜数据中心开始使用单一仓库(也称为单仓库,或monorepo)来包含每个团队的全部配置和模块。

定义 A 单一仓库结构(也称为单仓库,或monorepo)包含一个团队或功能的全部基础设施即代码(配置和模块)。

通常情况下,公司喜欢使用单一仓库结构。所有团队都可以通过复制粘贴来重现他们的配置,并且可以通过为模块添加一个新的文件夹来创建新的资源。在列表 5.1 中,叶绿蔬菜团队成员构建了一个新的数据库模块。他们使用 Python,通过sys.path方法将本地文件路径插入到模块中。他们通过将模块导入到代码库中来使用数据库。

列表 5.1 在不同目录中引用基础设施模块

import sys
sys.path.insert(1, '../../modules/gcp')                                   ❶

from database import DatabaseFactoryModule                                ❷
from server import ServerFactoryModule                                    ❷
from network import NetworkFactoryModule                                  ❷

import json

if __name__ == "__main__":
   environment = 'production'
   name = f'{environment}-hello-world'
   network = NetworkFactoryModule(name)                                   ❷
   server = ServerFactoryModule(name, environment, network)               ❷
   database = DatabaseFactoryModule(name, server, network, environment)   ❷
   resources = {                                                          ❸
       'resource': network.build() + server.build() + database.build()    ❸
   }                                                                      ❸

   with open('main.tf.json', 'w') as outfile:                             ❹
       json.dump(resources, outfile, sort_keys=True, indent=4)            ❹

❶ 导入包含模块的目录,因为它存在于同一个仓库中

❷ 导入生产环境的服务器、数据库和网络工厂模块

❸ 使用模块创建网络、服务器和数据库的 JSON 配置

❹ 将 Python 字典写入 JSON 文件,以便 Terraform 稍后执行

使用本地文件夹存储模块有助于团队引用他们想要的工具。每个人都可以在同一个存储库中查找模块或检查其他团队的配置。如果草药团队的某个人想了解水果团队的 IaC,他们可以使用tree命令来检查目录结构:

$ tree .
.
├── environments
│     ├── fruits
│     │     ├── development
│     │     └── production
│     ├── herbs
│     │     ├── development
│     │     └── production
│     ├── leafy-greens
│     │      ├── development
│     │      └── production
│     └── roots
│             ├── development
│             └── production
└── modules
 └── gcp
 ├── database.py
 ├── network.py
 ├── server.py
 └── tags.py

为了更好地组织配置,每个团队都将开发和生产环境配置放入单独的文件夹中。这些目录隔离了每个环境的配置和更改。理想情况下,所有环境都应该是相同的。现实情况下,您将在环境之间有差异,以解决成本或资源限制。

其他工具

单个存储库结构适用于许多其他 IaC 工具。您可以将单个存储库结构应用于重用角色和剧本到配置管理工具,如 Ansible。您可以根据单个存储库中的每个本地目录引用和构建剧本或配置管理模块。

CloudFormation 的工作方式略有不同。您可以将所有堆栈定义文件托管在单个存储库中。但是,您必须将子模板(我认为它是一个模块)发布到 S3 存储桶,并在AWS::CloudFormation::Stack资源中使用TemplateURL参数引用它。在本章的后面部分,您将了解如何交付和发布模块的更改。

蔬菜数据中心使用一个基础设施提供商,即 GCP。未来,团队可以为不同的基础设施工具或提供商添加新的目录。这些工具可以更新服务器或网络(ansible 目录),构建虚拟机镜像(packer 目录),或将数据库部署到 AWS(aws 目录):

$ tree .
.
├── environments
│     ├── development
│     └── production
└── modules
     ├── ansible
     ├── aws
     ├── gcp
     └── packer

您可能在其他 IaC 材料中遇到“不要重复自己”的原则(DRY)。DRY 原则促进重用和可组合性。基础设施模块减少了配置中的重复和冗余,符合 DRY 原则。如果您能够拥有相同的生产和开发环境,您就可以省略开发和生产目录,并引用一个模块而不是单独的环境文件。

您无法在基础设施中完全遵守 DRY 原则。根据基础设施或工具的语言和语法,您总会遇到重复的配置。因此,您可以在配置更清晰或工具或平台限制的范围内偶尔重复。

5.1.2 多个存储库

随着蔬菜数据中心的发展,其基础设施存储库有数百个文件夹。每个文件夹包含更多嵌套的文件夹。每周,您都要花费时间将所有存储库的更新与配置进行合并。每次您将更改推送到生产环境时,您也要等待 20 分钟,因为您的 CI 框架必须递归地搜索更改。安全团队也表达了担忧,因为与绿叶蔬菜团队合作的承包商可以访问水果团队的整个基础设施!

你将网络、标签、服务器和数据库模块分别划分到单独的仓库中。每个仓库都有自己的构建和交付模块的工作流程,这减少了 CI 框架的时间。你可以控制对每个仓库的访问,允许绿叶团队的外包人员只能访问绿叶配置。

蔬菜数据中心的不同团队可以使用模块的仓库或打包版本。每个团队将其配置和模块存储在单独的仓库中。公司中的任何人都可以下载和使用配置中的模块。

图 5.3 显示了蔬菜数据中心用于创建 IaC 的代码仓库。每个团队和模块都有自己的代码仓库。当绿叶团队想要创建数据库时,它会从 GitHub 仓库 URL 下载并导入数据库模块,而不是从本地文件夹中导入。如果团队有多个环境,他们可以将代码仓库细分到文件夹中。

图片

图 5.3 在多仓库结构中,你将每个模块存储在其自己的代码仓库中。配置引用仓库 URL 以使用模块。

蔬菜数据中心已从单一仓库结构迁移到多仓库结构,或称为多仓库结构。公司根据团队将模块分离到不同的仓库中。

定义 A 多仓库结构(也称为多仓库)根据团队或功能将 IaC(配置或模块)分离到不同的仓库中。

回想一下,单一仓库模式促进了可重复性和可组合性。多仓库模式有助于提高可进化性原则。将模块分离到各自的仓库中有助于结构化每个模块的生命周期和管理。

要实现多仓库结构,你需要将模块拆分到各自的版本控制仓库中。在下面的列表中,你通过在 requirements.txt 文件中将每个模块添加为库依赖项来配置 Python 的包管理器以下载每个模块。每个库依赖项必须包含一个指向版本控制仓库的 URL 和一个特定的标签以下载。

列表 5.2 Python requirements.txt 引用模块仓库

-e git+https://github.com/joatmon08/gcp-tags-module.git@1.0.0#egg=tags         ❶
-e git+https://github.com/joatmon08/gcp-network-module.git@1.0.0#egg=network   ❷
-e git+https://github.com/joatmon08/gcp-server-module.git@0.0.1#egg=server     ❷
-e git+https://github.com/joatmon08/gcp-database-module.git@1.0.0#egg=database ❷

❶ 从 GitHub 仓库下载标签的原型模块。根据标签选择模块版本。

❷ 从 GitHub 仓库下载网络、服务器和数据库的工厂模块。根据标签选择模块版本。

首先,你为水果应用程序基础设施的生产配置创建一个仓库。创建仓库后,你将其添加到 requirements.txt 文件中。然后,你运行 Python 的包安装管理器以下载基础设施配置的每个模块:

$ pip install -r requirements.txt
Obtaining tags from 
git+https://github.com/joatmon08/
➥gcp-tags-module.git@1.0.0#egg=tags
...
Successfully installed database network server tags

而不是设置本地路径并导入模块,你需要先运行 Python 的包安装管理器从远程仓库下载。下载模块后,团队可以使用列表 5.3 中的 Python 来在环境配置中导入它们。

列表 5.3 导入模块以用于基础设施配置

from tags import StandardTags                                             ❶
from server import ServerFactoryModule                                    ❶
from network import NetworkFactoryModule                                  ❶
from database import DatabaseFactoryModule                                ❶

import json

if __name__ == "__main__":
   environment = 'production'
   name = f'{environment}-hello-world'

   tags = StandardTags(environment)
   network = NetworkFactoryModule(name)
   server = ServerFactoryModule(name, environment, network, tags.tags)
   database = DatabaseFactoryModule(
       name, server, network, environment, tags.tags)
   resources = {
       'resource': network.build() + server.build() + database.build()    ❷
   }

   with open('main.tf.json', 'w') as outfile:                             ❸
       json.dump(resources, outfile, sort_keys=True, indent=4)            ❸

❶ 导入由包管理器下载的模块

❷ 使用模块创建网络、服务器和数据库的 JSON 配置

❸ 将 Python 字典写入 JSON 文件,以便 Terraform 稍后执行

记住,Veggies 数据中心分别配置开发和生产环境。团队将实现代码以引用版本控制中托管的相同工厂和原型模块。开发和生产环境的一致模块可以防止环境漂移,并帮助你在生产前测试模块更改。你将在第六章中了解更多关于测试和环境的内容。

多仓库的 IaC 实现与单仓库的实现差异不大。两种结构都支持可重复性和可组合性。然而,它们的不同之处在于你可以在外部仓库中独立地演进一个模块。

在多仓库结构中更新配置涉及使用包管理器重新下载新的模块。运行包管理器以使用新模块可能会在 IaC 工作流程中引入摩擦。有人可能更新了一个模块,除非你审查其仓库,否则你不会知道。在本章的后面部分,你将了解如何通过版本控制来解决这个问题。

领域特定语言

如果一个工具可以引用带有版本控制或工件 URL 的模块或库,它就可以支持多仓库结构。

当你采用多仓库结构时,你必须建立一些标准实践来共享和维护模块。首先,标准化模块文件结构和格式。这有助于组织中的团队在版本控制中识别和筛选模块。模块仓库的一致文件结构和命名也有助于审计和未来的自动化。

例如,Veggies 数据中心的基础设施模块遵循相同的模式和文件结构。它们的名称包括基础设施提供者、资源工具目的。在图 5.4 中,gcp-server-module描述 GCP 作为基础设施提供者,server作为资源类型,module作为目的。

图 5.4 仓库名称应包括基础设施提供者、资源类型和目的。

如果你的模块使用特定的工具或具有独特目的,你可以将其附加到仓库名称的末尾。将工具添加到名称中可以帮助识别模块类型。类似于第二章中概述的实践,你希望你的模块名称足够描述性,以便团队成员可以识别。

您可以将存储库命名方法应用于单个存储库中的文件夹命名。然而,单个存储库中的子目录使得嵌套和识别基础设施提供者和资源类型更加容易。根据您组织和团队的偏好,您始终可以向存储库名称添加更多字段。

5.1.3 选择存储库结构

您的系统可扩展性和 CI 框架决定了您是使用单个存储库还是多个存储库。数据中心从单个存储库开始,由于它有数十个模块和几个环境,所以效果很好。每个模块有两个环境,用于开发和生产。每个环境需要几台服务器、一个数据库、一个网络和一个监控系统。

使用单个存储库提供了一些好处。图 5.5 概述了一些优点和局限性。首先,您的团队中的任何人都可以访问一个存储库中的模块和配置。其次,您只需要去一个地方就可以比较和识别环境之间的差异。例如,您可以在存储库中比较两个文件,以检查开发是否使用三台服务器,而生产是否使用五台服务器。

借鉴 IaC 原则,单个存储库结构仍然提供可组合性、可扩展性和可重复性。任何人都可以进入一个文件夹并演进一个模块。您仍然可以在模块之间构建,因为您有一个对所有基础设施和配置的单一视图。

图片

图 5.5 单个存储库为所有模块和配置提供了一个视图,但限制了 CI 框架或细粒度访问控制。

另一方面,单个存储库结构有一些局限性。如果任何人都可以更改一个模块,它可能会破坏依赖于它的 IaC!此外,您的 CI 系统可能会崩溃,因为它递归地检查每个目录中的更改。

因此,您需要采用实践和工具来处理单个存储库。这包括有偏见的版本控制和专门的构建系统。如果您的组织无法构建或采用帮助减轻单个存储库管理的工具,您可以选择多个存储库结构。

注意:您会发现一些有助于构建和管理单个存储库的工具。它们包含额外的代码来处理嵌套子目录和单个构建工作流程。其中一些包括 Bazel、Pants 和 Yarn。

从单个存储库结构迁移到多个存储库结构的发生频率可能比你想象的要高。我不得不做两次!一个组织从三个环境和四个模块开始。几年后,IaC 增长到数百个模块和环境。

不幸的是,CI 框架(Jenkins)运行标准更改以扩展服务器需要近三个小时。框架的大部分时间都花在搜索每个目录和嵌套目录中的更改上!我们最终将配置和模块重构为多个存储库。

将配置重构为多仓库结构缓解了 CI 框架的一些问题。多仓库结构还提供了对特定模块的更细粒度的访问控制。安全团队可以向特定团队授予模块编辑访问权限。你将在第十章中了解更多关于重构的信息。

图 5.6 展示了多仓库的好处和局限性,包括细粒度的访问控制和可扩展的 CI 工作流程。然而,多仓库结构减少了你对组织模块和配置的单一视角。

图片

多仓库有助于减轻使用 CI 框架运行测试和配置的负担,但需要不断验证格式和故障排除的一致性。

通过将配置重构为多仓库结构,你可以隔离对特定团队的基础设施配置的访问和演进。你对模块的演进和生命周期有更大的控制权。大多数 CI 框架都支持多仓库,并且当框架检测到特定仓库的更改时,将并行运行工作流程。

然而,多仓库确实有一些缺点。想象一下“蔬菜数据中心”有十个或更多模块分布在不同的仓库中。你如何知道它们是否都符合相同的文件标准和命名?

图 5.7 展示了解决文件和标准一致性问题的一个解决方案。你可以将所有格式化和 linting 检查的测试捕获到一个原型模块中。然后,CI 框架在服务器、网络、数据库和 DNS 模块中下载测试并检查 README 和 Python 文件。

图片

图 5.7 创建一个包含所有模块仓库格式检查的原型模块将有助于修复不符合新标准的旧仓库。

带有测试的原型模块有助于强制执行不常使用的旧模块(如 DNS)的格式。如果你想添加一个新标准,你只需更新原型模块中的新测试。下次有人更新模块或配置时,他们需要更新他们的模块格式以符合标准。

标准化的检查集有助于减轻在数百个仓库中查找和替换文件的操作负担。它将更新模块仓库的责任分配给模块的维护者。有关模块一致性测试和将模块集成到工作流程中的更多信息,你可以在第 6、7 和 8 章中应用这些实践。

多仓库结构的第二个缺点涉及故障排除的挑战。当你在配置中引用一个模块时,你需要搜索模块仓库以确定它需要哪些输入和输出。在调试配置中的失败时,搜索增加了额外的努力和时间。

如果你有一个可以处理单个存储库构建要求的构建系统,你可以为所有内容使用单个存储库。然而,大多数构建系统不会随着递归目录搜索进行扩展。为了解决这个问题,你可以使用单存储库和多存储库的组合。

让我们将这个解决方案应用到蔬菜数据中心。他们为不同类型的果实和蔬菜分别分离每个配置。叶菜类蔬菜使用一个存储库,而水果使用另一个。它们都引用共享的网络、标签、数据库和 DNS 模块。

图 5.8 显示,水果团队需要一个队列,而叶菜类蔬菜团队不需要。因此,水果存储库包括一个用于创建队列的本地模块。水果团队使用单个存储库进行其独特的配置,但引用多个存储库的常见模块。

图片

图 5.8 你的组织可以使用单个存储库来组合多个存储库,以进行应用程序或系统特定的配置。

当你使用这种混合匹配方法时,要认识到你希望为单个存储库或共享配置设置哪种访问控制。如果你想提高其他团队的组合性和可重复性,你可能将一个模块放在它自己的存储库中。然而,如果你想保持特定配置的可扩展性,你可能需要使用你的配置在本地管理该模块。

当你选择你的存储库结构时,要认识到方法之间的权衡,并在模块和配置数量增长时进行重构。当你将更多配置和资源添加到单个存储库中时,你需要确保工具和流程与其一起扩展!

5.2 版本控制

在本章的整个过程中,你已经使用了将基础设施配置或代码保存在版本控制中的实践。例如,蔬菜数据中心团队可以始终根据提交哈希引用基础设施。有一天,蔬菜数据中心的安全团队对土壤监测数据库的用户名和密码的年龄表示了担忧。

团队建议使用密钥管理器存储和每 30 天轮换密码。问题在于,所有团队都使用土壤监测数据库模块。图 5.9 显示,应用程序当前引用了土壤监测模块的输出。该模块输出数据库的密码,应用程序使用该密码来写入和读取数据。安全团队希望你使用密钥管理器。

图片

图 5.9 应用程序引用了土壤监测模块的数据库端点和密码,但应使用密钥管理器的密码。

数据库模块的输出会影响密钥的可扩展性和安全性。我们如何更新数据库以使用密钥管理器而不干扰土壤数据收集?蔬菜数据中心的基础设施团队决定向数据库模块添加版本控制

定义 版本控制 是将唯一版本分配给代码迭代的过程。

让我们来看看蔬菜数据中心团队如何实现模块版本。该团队使用版本控制来标记数据库模块的当前版本为 v1.0.0。版本 v1.0.0 将为应用程序输出数据库密码:

$ git tag v1.0.0

他们将 v1.0.0 的标签推送到版本控制:

$ git push origin v1.0.0
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
 * [new tag]         v1.0.0 -> v1.0.0

你必须重构水果、叶菜、谷物和香草生长的配置,以使用名为 版本锁定 的过程中数据库模块的 v1.0.0 版本。版本锁定保持幂等性。当你运行 IaC 时,配置将继续使用数据库模块的输出。你不应该检测到锁定模块和现有基础设施之间的任何漂移。

在所有团队将版本锁定到 v1.0.0 之后,你可以重写模块以使用秘密管理器。数据库模块将密码存储在秘密管理器中。团队将新的数据库模块标记为 v2.0.0,该版本输出数据库端点和秘密管理器中密码的位置:

$ git tag v2.0.0

他们将 v2.0.0 的标签推送到版本控制:

$ git push origin v2.0.0
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
 * [new tag]         v2.0.0 -> v2.0.0

你可以根据提交历史来检查模块两个版本的差异:

$ git log --oneline
7157d3e (HEAD -> main, tag: v2.0.0, origin/main) 
➥Change database module to store password in secrets manager
5c5fd65 (tag: v1.0.0) Add database factory module

既然你已经创建了数据库工厂模块的新版本,你要求一些团队尝试使用它。水果团队勇敢地自告奋勇。水果团队目前使用版本 1.0.0。该模块版本输出数据库端点和密码。

当更新到模块版本 2.0.0 时,如图 5.10 所示,水果团队需要考虑模块工作流程的变化。团队不能使用模块输出中的数据库密码。模块输出一个指向存储在秘密管理器中的数据库密码的 API 路径。因此,水果团队重构其 IaC,在创建数据库之前从秘密管理器获取数据库密码。

你将应用一些基本的模块版本控制实践。首先,确保你在更新之前运行你的 IaC 并消除任何漂移。其次,建立一个不 引用 模块最新版本的版本控制方法。

蔬菜数据中心遵循 语义版本控制,分配传达配置基本信息的版本号。你可以通过几种方式指定模块版本,包括在版本控制中用数字标记提交,或在工件存储库中打包和标记模块。

图 5.10 你可以将水果应用程序重构为引用数据库模块的版本 2.0.0 并从秘密管理器检索数据库密码。

注意:我经常更新主版本,以删除输入、输出和资源的重要更新。如果我只更新配置值、输入或输出到不影响依赖项的模块,我通常更新次版本。最后,我将更改补丁版本,以针对模块及其资源范围内的次要配置值更改。有关语义版本化和其方法的更多详细信息,请参考semver.org/

使用一致的版本控制方法,你可以更有效地演进下游基础设施资源,而不会破坏上游资源,因为你控制了它们的依赖关系。版本控制还有助于对活跃版本进行审计。为了节省资源、减少混淆并推广最新更改,版本控制允许你识别和弃用模块的较旧、非活动版本。

然而,你必须持续记住并执行某些版本控制实践。你等待更新应用程序以使用数据库的 v2.0.0 版本的时长越长,它失败的可能性就越高。你可能需要考虑为模块版本 v1.0.0 的使用设定一个时间表。你不需要立即删除数据库模块的 v1.0.0 版本。通常,我在几个小版本更改内升级依赖模块。尝试在版本之间进行更广泛的“跳跃”升级会增加变更的风险和可能的失败率。

注意:如果你使用基于特性的开发或 Git Flow,你可以在软件开发的工作流程中容纳补丁或热修复。你可以基于版本标签创建一个分支,更新更改,增加补丁版本,并为热修复分支添加一个新的标签。你需要保留分支以保留提交历史。

此版本控制过程适用于多仓库结构。那么对于单一仓库呢?你仍然可以应用版本控制标签方法。你可能想在标签前添加模块名称的前缀(module-name-v2.0.0)。然后你可以将模块打包并发布到工件存储库。你的构建系统将打包模块子目录的内容,并在工件存储库中标记版本。你的配置引用工件存储库中的远程模块,而不是本地文件。

5.3 发布

我解释了模块版本控制的实践,以帮助模块演进并最小化对系统的干扰。然而,你不想每个团队都立即更新其 IaC 到最新的模块。相反,你想要确保模块在投入生产使用之前能够正常工作且不会破坏你的基础设施。

图 5.11 展示了在允许所有蔬菜数据中心团队使用之前,你如何评估你的数据库模块更新。在你将数据库模块更新为在密钥管理器中存储密码后,你将更改推送到版本控制。你要求水果团队在一个独立的环境中测试该模块,并确认模块可以正常工作。他们确认它工作正确。你使用新版本 2.0.0 标记发布,并更新了密钥管理器上的文档。

在上一节中,蔬菜数据中心的基础设施团队更新了模块,并首先与水果团队的开发环境进行了测试。现在模块通过了测试,其他团队可以使用带有密钥管理器的新数据库模块。团队遵循了一个发布流程来确保其他团队可以使用新模块。

定义 发布 是将软件分发给消费者的过程。

图片

图 5.11 当你进行模块更新时,确保在发布模块和更新其文档之前包括一个测试阶段。

发布流程识别并隔离了模块更新中可能出现的任何问题。除非测试证明新模块可以正常工作,否则你不会打包新模块。

我建议在一个专门的测试环境中运行模块测试,远离开发和生产工作负载。为模块测试设置一个单独的账户或项目可以帮助你跟踪测试的成本,并将故障隔离在活动环境中。你将在第六章中了解更多关于测试和测试环境的内容。

注意:有关发布模块的持续交付管道的详细代码列表,请参阅mng.bz/PnaR。GitHub Actions 管道在测试成功时根据提交消息自动构建 GitHub 发布。

测试模块后,你为团队使用的新版本标记发布。蔬菜数据中心发布数据库模块为版本 v2.0.0,并使用 Python 包管理器引用该标记。或者,你也可以打包模块并将其推送到工件存储库或存储桶。

例如,想象一下蔬菜数据中心有一些团队使用 CloudFormation。这些团队更喜欢引用存储在 Amazon S3 存储桶中的模块(或 CloudFormation 堆栈)。在图 5.12 中,团队在他们的交付管道中添加了一个步骤,以压缩他们的模块并将它们上传到 S3 存储桶。作为最后一步,他们更新了概述他们所做的更改的文档。

图片

图 5.12 测试后,你可以选择将模块打包并推送到一个工件存储库或存储桶。

一些组织更喜欢打包工件并将其存储在单独的存储库中,以实现额外的安全控制。如果你有一个安全的网络,无法访问外部版本控制端点,你可以引用工件存储库。只需确保将标签保留在版本控制中,以便有人可以将工件与正确的代码版本关联起来。

在打包和推送工件后,你应该更新概述你更改的文档。这种文档称为发布说明,概述了输出和输入的破坏性更改。发布说明向其他团队传达了更改的摘要。

定义 发布说明 列出了给定版本的代码更改。你应该将它们存储在存储库中的文档中,通常称为变更日志。

你可以手动更新发布说明,但我更喜欢使用自动语义发布工具(如 semantic-release)来检查提交历史并为我构建发布说明。确保你使用正确的提交信息格式,以便工具能够匹配和解析更改。第二章强调了编写描述性提交信息的重要性。你也会发现它们对自动发布工具很有帮助。

例如,数据库模块将密码存储在密钥管理器中。对于蔬菜数据中心来说,这是一个主要特性,因此你在提交信息前加上feat前缀:

$ git log -n 1 --oneline
1b65555 (HEAD -> main, tag: v0.0.1, origin/main, origin/HEAD) 
➥feat(security): store password in secrets manager

自动发布工具中的提交分析器会根据这个提交自动更新标签的主版本号为 v2.0.0。

图像构建

你可能会遇到使用图像构建工具来构建不可变服务器或容器镜像的做法。通过将你想要的软件包烘焙到服务器或容器镜像中,你可以创建带有更新的新服务器,而无需担心就地更新问题。当你发布不可变镜像时,使用工作流程基于该镜像创建测试服务器,检查其是否正常运行,并更新镜像标签的版本。第七章涵盖了这些工作流程的一些内容。

除了更新发布说明外,确保你更新常用文件和文档。常用文件有助于你的队友使用该模块。例如,蔬菜数据中心同意团队必须始终包含一个 README 文件。README 记录了每个模块的目的、输入和输出。

定义 README 是存储库中的一个文档,它解释了代码的使用和贡献说明。对于 IaC,使用它来记录模块的目的、输入和输出。

使用代码检查规则来检查 README 文件的存在。在第二章中,我讨论了一些代码检查实践,以确保干净的 IaC。将此模式应用于常用文件和文档有助于你格式化和组织大量的 IaC。

在 Python 示例中,模块包括常见的文件,如用于识别包的__init__.py和用于模块配置的setup.py。我经常将具有配置或元数据、有助于特定工具或语言的文件称为辅助文件。它们会根据你使用的工具和平台而变化。你希望在整个组织中标准化它们,以便可以通过自动化并行更改或搜索它们。

5.4 分享模块

随着数据中心为蔬菜生产更多产品,它增加了自动化谷物、茶叶、咖啡和豆类生长的新团队。公司还创建了一个研究野生蔬菜品种的新团队。每个团队都需要能够扩展现有模块并创建新的模块。

例如,豆类团队需要更改数据库模块以使用 PostgreSQL 版本 12。那些团队成员是否应该能够编辑模块并更新版本?或者他们应该向你,基础设施团队,提交工单以更新它?

你需要授权不同的团队使用 IaC(基础设施即代码)创建和更新模块。然而,你想要确保团队不会更改属性而损害安全性或功能性。你将发现一些可以帮助你在组织中共享模块的实践。

假设数据中心为蔬菜的各个团队都需要一个数据库。你创建了一个新的、有偏见的数据库模块,该模块建立了一组默认参数以提供安全性和功能性。数据库模块使用内嵌的默认值作为模块输入,以覆盖数据中心为蔬菜的许多用例。即使咖啡团队不知道如何创建数据库,该团队也可以使用该模块构建一个安全、可工作的数据库。

作为一项通用实践,在你的模块中设置有偏见的默认值。你希望偏向于规定性的一边。如果团队需要更多的灵活性,它可以更新模块或覆盖默认属性。预设的默认值有助于教授部署特定基础设施资源的安全和标准实践。

在这种情况下,豆类团队表达了对更多灵活性的需求。该模块没有使用新的数据库版本,即 PostgreSQL 版本 12。没有其他团队使用该版本的 PostgreSQL。豆类团队决定更新数据库版本并将更改推送到仓库。

然而,更改不会立即发布。构建系统向基础设施团队的模块审批者发送通知。在图 5.13 中,基础设施团队暂停了构建系统并审查了更改。如果更改通过了团队的批准,构建系统将发布模块。豆类团队可以使用带有 PostgreSQL 版本 12 的新版本的数据库模块。

为什么你应该允许 Beans 团队更改基础设施模块?自助服务的模块变更赋予所有团队更新其系统的权力,并减轻基础设施和平台团队的负担。你希望平衡他们的开发进度与安全和基础设施的可用性。在模块发布前添加审批可以识别潜在故障或非标准的基础设施变更。

图片

图 5.13 一个应用团队可以更新数据库模块。然而,该团队必须等待主题专家的批准才能使用新版本。

允许任何团队使用模块并经审批者编辑的做法,在建立了模块开发标准和流程的情况下效果最佳。如果你没有建立模块标准,这种方法就会失效,并给将基础设施变更交付到生产中增加摩擦。

让我们回到例子。基础设施团队对变更没有太多信心,因此团队请求数据库管理员进行额外审查。数据库管理员指出,如果 Beans 团队升级其模块版本,导致的行为将删除之前的数据库,并使用新版本创建一个空的数据库!这将严重干扰支持豆类生长的应用程序。

在图 5.14 中,Beans 团队向数据库团队提交了求助请求。管理员推荐了一些有助于更新数据库而不删除数据的做法。Beans 团队实施了这些做法,并请求模块审批者进行第二次审查。一旦模块发布,团队就可以使用该模块,而不用担心会干扰其应用程序。

图片

图 5.14 对于破坏性的模块更新,应用团队提交工单给数据库团队,以在发布新模块版本之前验证数据库迁移步骤。

如果你担心某个变更可能会特别破坏系统的架构、安全或可用性,在发布新版本之前,请向主题专家请求审查。主题专家可以帮助识别任何会影响使用该模块的其他团队的问题,并就如何最佳更新它提供建议。审查过程有助于你演进你的基础设施即代码(IaC),并识别来自基础设施变更的潜在故障。

通常,你需要一个流程,赋予你的团队能够进行基础设施变更的权力,并且提供团队完成这些变更所需的知识和支持,而不会干扰关键系统。手动审查可能看起来很繁琐,但有助于教育你的团队并防止生产中出现问题。你的团队必须在快速部署变更到生产中和等待主题专家手动审查之间找到平衡,这一点我将在第七章中进一步阐述。

通过在模块上协作工作,您可以在团队间共享基础设施即代码(IaC)知识,并集体识别对关键基础设施的潜在干扰。您可以将模块视为组织内使用的工件,类似于共享的应用程序库、容器镜像或虚拟机镜像。公司中的任何人都可使用和更新模块(如有需要,可提供额外帮助!)以演进基础设施架构、安全或可用性。

摘要

  • 在单一存储库或多个存储库中结构化和共享模块和配置。

  • 单一存储库结构将所有配置和模块组织在一个地方,这使得故障排除和识别可用资源变得更加容易。

  • 多存储库结构将所有配置和模块组织到各自的代码存储库中,按业务领域、功能、团队或环境划分。

  • 多存储库结构允许对单个基础设施配置或模块进行更好的访问控制,并简化每个存储库的管道执行。

  • 随着越来越多的人参与基础设施即代码(IaC)协作并需要额外的资源来快速处理更改,单一存储库可能无法扩展。

  • 将单一存储库重构为多个存储库,每个模块一个存储库。

  • 为模块选择一致的版本控制方法,并使用 Git 标签进行更新。

  • 将模块打包并发布到工件存储库,这将允许组织中的任何人都可检索特定模块版本。

  • 在团队间共享模块时,在模块中建立有见地的默认参数以维护安全和功能。

  • 允许组织中的任何人为模块提出更新建议,但添加治理机制以识别可能对架构、安全或基础设施可用性造成干扰的模块更改。

6 测试

本章涵盖

  • 确定为基础设施系统编写哪种类型的测试

  • 编写测试以验证基础设施配置或模块

  • 理解各种类型测试的成本

从第一章回忆起,基础设施即代码涉及将更改推送到系统的一个完整过程。您通过更新脚本或配置来应用基础设施更改,将它们推送到版本控制系统,并以自动化的方式应用更改。然而,您可以使用第三章和第四章中的每个模块和依赖模式,但仍可能遇到失败的更改!如何在将更改应用到生产之前捕捉到失败的更改?

您可以通过实现 IaC 的测试来解决此问题。测试是一个评估系统是否按预期工作的过程。本章回顾了与测试 IaC 相关的一些考虑因素和概念,以降低变更失败率并增强对基础设施变更的信心。

定义测试IaC 是一个评估基础设施是否按预期工作的过程。

想象您使用新的网络段配置了一个网络交换机。您可以通过对每个网络上的每台服务器进行 ping 测试并验证其连通性来手动测试现有网络。为了测试您是否正确设置了新网络,您创建了一个新的服务器并检查当您连接到它时它是否响应。这种手动测试对于两个或三个网络可能需要几个小时。

随着您创建更多的网络,您可能需要几天时间来验证您的网络连通性。对于每个网络段更新,您必须手动验证网络的连通性以及运行在网络上服务器、队列、数据库和其他资源。您不能测试一切,所以您只检查几个资源。不幸的是,这种方法可能会留下隐藏的虫子或问题,这些问题可能只会在几周甚至几个月后出现!

为了减轻手动测试的负担,您可以通过编写脚本来自动化测试,每个命令都编写一个脚本。您的脚本在新的网络上创建一个服务器,检查其连通性,并测试与现有网络的连接。您投入一些时间和精力来编写测试,但通过运行自动化脚本来对网络的任何后续更改进行验证,可以节省数小时的手动验证时间。

图片

图 6.1 手动测试可能最初需要较低的努力,但随着您系统中基础设施资源的增加,这种努力也会增加。自动化测试需要较高的初始努力,但随着您系统的扩展,这种努力会减少。

图 6.1 显示了手动和自动测试时,每小时所需的工作量与基础设施资源数量的对比。当你手动运行网络测试时,你必须在测试上花费大量时间。随着你向系统中添加更多资源,工作量也会增加。相比之下,编写自动化测试需要初始的努力。然而,随着系统的增长,维护测试所需的工作量通常会减少。你甚至可以并行运行自动化测试以减少总体测试工作量。

当然,测试并不能捕捉到每个问题或消除系统中的所有故障。然而,自动化测试作为每次你进行变更时你应该在系统中测试什么的文档。如果一个隐藏的 bug 选择出现,你需要花费一些努力编写一个新的测试来验证 bug 不会再次发生!随着时间的推移,测试可以降低总体运营工作量。

你可以使用基础设施提供商或工具的测试框架,或者使用编程语言中的原生测试库。代码示例使用了一个名为pytest的 Python 测试框架和 Apache Libcloud,这是一个连接到 GCP 的 Python 库。我编写的测试侧重于测试验证的内容而不是语法。你可以将这种方法应用于任何工具或框架。

更多关于 pytest 和 Apache Libcloud 的信息

要运行测试,请参考github.com/joatmon08/manning-book中的说明、示例和依赖项。它包括有关如何开始使用 pytest 和 Libcloud 的链接和参考。

不要为系统中每一块 IaC 编写测试。测试可能变得难以维护,有时甚至可能是冗余的。相反,我将解释如何评估何时编写测试以及适用于你正在更改的资源类型的测试类型。基础设施测试是一种启发式方法;你永远无法完全预测或模拟生产中的变更。一个有用的测试可以提供配置基础设施或变更如何影响系统的见解和实践。我还会区分哪些测试适用于如工厂、原型或构建器等模块,以及针对实时环境的通用复合或单例配置

6.1 基础设施测试周期

测试可以帮助你建立信心并评估对基础设施系统变更的影响。然而,在没有首先创建系统的情况下,你如何测试一个系统呢?此外,你如何知道在应用变更后系统是否正常工作?

你可以使用图 6.2 中的基础设施测试周期来构建你的测试工作流程。在你定义基础设施配置后,运行初始测试以检查你的配置。如果测试通过,你可以将变更应用到活动基础设施并测试系统。

在这个工作流程中,您运行两种类型的测试。一种是在您部署基础设施更改之前对配置进行静态分析,另一种是对基础设施资源进行动态分析,以确保它仍然正常工作。大多数测试都遵循在更改部署前后运行的这种模式。

图片

图 6.2 基础设施测试表明您是否可以应用更改到系统中。在应用更改后,您可以使用额外的测试来确认更改是否成功。

6.1.1 静态分析

您会如何将基础设施测试周期应用到我们的网络示例中?想象一下,您解析网络脚本以验证新的网络段是否具有正确的 IP 地址范围。您不需要将更改部署到网络中。相反,您分析脚本,这是一个静态文件。

在图 6.3 中,您定义网络脚本并运行静态分析。如果您发现错误的 IP 地址,测试将失败。您可以回滚或修复您的网络更改并重新运行测试。如果它们通过,您可以将正确的网络 IP 地址应用到活动网络中。

图片

图 6.3 您可以选择固定配置以通过测试,或者在静态分析失败时恢复到之前成功的配置。

在将更改部署到基础设施资源之前评估基础设施配置的测试执行静态分析

对于 IaC 的静态分析定义,在将更改部署到实际基础设施资源之前,验证明文基础设施配置。

静态分析的测试不需要基础设施资源,因为它们通常解析配置。它们不会对任何活动系统造成影响的风险。如果静态分析测试通过,我们更有信心可以应用更改。

我经常使用静态分析测试来检查基础设施命名标准和依赖关系。它们在应用更改之前运行,并在几秒钟内识别任何不一致的命名或配置问题。我可以纠正更改,重新运行测试以通过,并将更改应用到基础设施资源中。请参阅第二章以获取干净的 IaC、linting 和格式化规则。

静态分析的测试不会对活动基础设施进行更改,这使得回滚更加直接。如果静态分析的测试失败,您可以返回到基础设施配置,纠正问题,并再次提交更改。如果您无法修复配置以通过静态分析,您可以回滚您的提交到一个成功的版本!您将在第十一章中了解更多关于回滚更改的内容。

6.1.2 动态分析

如果静态分析通过,您可以部署更改到网络。然而,您不知道网络段是否实际上工作。毕竟,服务器需要连接到网络。为了测试连通性,您在网络中创建一个服务器并运行测试脚本以检查入站和出站连通性。

图 6.4 显示了测试网络功能性的周期。一旦你将更改应用到活动基础设施环境,你就运行测试来检查系统的功能。如果测试脚本失败并显示服务器无法连接,你将返回到配置并修复它以供系统使用。

注意,你的测试脚本需要一个活动网络来创建服务器并测试其连接性。在将更改应用到活动基础设施资源后验证基础设施功能性的测试执行动态分析

图片

图 6.4 当动态分析失败时,你可以通过更新配置或回滚到之前可工作的配置来修复测试环境。

定义 对于 IaC 的动态分析验证在将更改应用到活动基础设施资源后系统功能。

当这些测试通过时,我们更有信心更新成功。然而,如果它们失败,它们会识别系统中的问题。如果测试失败,你知道你需要调试,修复配置或脚本,并重新运行测试。它们为可能破坏基础设施资源和系统功能的变化提供了一个早期预警系统。

你只能动态分析一个活动环境。如果你不知道更新是否可行怎么办?你能将这些测试从生产环境中隔离出来吗?与其将所有更改应用到生产环境并测试它,你可以在中间测试环境中分离你的更新并测试它们。

6.1.3 基础设施测试环境

一些组织在单独的环境中复制整个网络,以便他们可以测试更大的网络更改。将更改应用到测试环境中可以更容易地识别和修复损坏的系统,更新配置,并提交新更改,而不会影响业务关键系统。

当你在提升到活动环境之前在单独的环境中运行测试时,你增加了基础设施测试周期。在图 6.5 中,你保留了静态分析步骤。然而,你在测试环境中应用网络更改并运行动态分析。如果它通过了测试环境,你就可以将更改应用到生产,并在生产中运行动态分析。

一个测试环境将更改和测试与生产环境隔离开。

定义 一个测试环境与生产环境分离,用于测试基础设施更改。

图片

图 6.5 在将更改应用到生产之前,你可以在测试环境中运行基础设施的静态和动态分析。

在生产之前设置一个测试环境可以帮助你在部署到生产环境之前练习检查更改。这样你就能更好地理解它们如何影响现有系统。如果你无法修复更新,你可以将测试环境回滚到一个可工作的配置版本。你可以使用测试环境进行以下操作:

  • 在将基础设施更改应用于生产系统之前检查其影响

  • 隔离基础设施模块的测试(有关模块共享实践,请参阅第五章)

然而,请注意,你必须像维护生产环境一样维护测试环境。当可能时,基础设施测试环境应遵守以下要求:

  • 它的配置必须尽可能接近生产环境。

  • 它必须与应用程序的开发环境不同。

  • 它必须是持久的(即,每次测试时不要创建和销毁它)。

在前面的章节中,我提到了减少环境漂移的重要性。如果你的基础设施测试环境与生产环境相同,你将获得更准确的测试行为。你还希望将基础设施更改的测试与专门用于应用程序的开发环境隔离。一旦你确认你的基础设施更改没有破坏任何东西,你就可以将它们推送到应用程序的开发环境。

拥有一个持久的测试基础设施环境很有帮助。这样,你可以测试运行中的基础设施更新是否可能影响业务关键系统。不幸的是,从成本或资源角度来看,维护一个基础设施测试环境可能并不实际。我在第十二章概述了一些测试环境成本管理的技巧。

在本章的剩余部分,我将讨论执行静态和动态分析的不同类型的测试以及它们如何适应你的测试环境。一些测试将允许你减少对测试环境的依赖。其他测试对于评估更改后的生产系统功能至关重要。在第十一章中,我涵盖了特定于生产的回滚技术,并将测试纳入持续基础设施交付。

6.2 单元测试

我提到了在基础设施即代码(IaC)上运行静态分析的重要性。静态分析评估文件的具体配置。你可以为静态分析编写哪些类型的测试?

假设你有一个工厂模块来创建一个名为hello-world-network的网络和三个具有 10.0.0.0/16 IP 地址范围的子网。你想要验证它们的网络名称和 IP 地址范围。你期望子网将 10.0.0.0/16 范围分配给自己。

作为解决方案,你可以在不创建网络和子网的情况下编写测试来检查网络名称和子网 IP 地址范围。这种静态分析可以在几秒钟内验证配置参数的预期值。

图 6.6 显示,你的静态分析包括同时运行的几个测试。你检查网络名称、子网数量和子网的 IP 地址范围。

图片

图 6.6 单元测试验证配置参数,例如网络名称,是否等于预期值。

我们刚刚对网络 IaC 运行了单元测试。单元测试 在隔离环境中运行,并静态分析基础设施配置或状态。这些测试不依赖于活动的基础设施资源或依赖,并检查配置的最小子集。

定义 单元测试 静态分析纯文本基础设施配置或状态。它们不依赖于实时基础设施资源或依赖。

注意,单元测试可以分析基础设施配置或状态文件中的元数据。一些工具直接在配置中提供信息,而其他工具则通过状态暴露值。接下来的几节将提供测试这两种类型文件的示例。根据你的 IaC 工具、测试框架和偏好,你可能测试其中一种、两种或全部。

6.2.1 测试基础设施配置

我们将首先编写使用模板生成基础设施配置的模块的单元测试。我们的网络工厂模块使用一个函数来创建具有网络配置的对象。你需要知道函数 _network_configuration 是否生成正确的配置。

对于网络工厂模块,你可以使用 pytest 编写单元测试来检查生成网络和子网 JSON 配置的函数。测试文件包括三个测试,一个用于网络名称,一个用于子网数量,一个用于 IP 范围。

Pytest 通过寻找以 test_ 前缀的文件和测试来识别测试。在列表 6.1 中,我们命名了测试文件为 test_network.py,这样 pytest 就能找到它。文件中的每个测试都有 test_ 前缀以及描述测试检查内容的说明信息。

列表 6.1 使用 pytest 在 test_network.py 中运行单元测试

import pytest                                                            ❶
from main import NetworkFactoryModule                                    ❷

NETWORK_PREFIX = 'hello-world'                                           ❸
NETWORK_IP_RANGE = '10.0.0.0/16'                                         ❸

@pytest.fixture(scope="module")                                          ❹
def network():                                                           ❹
   return NetworkFactoryModule(                                          ❹
       name=NETWORK_PREFIX,                                              ❹
       ip_range=NETWORK_IP_RANGE,                                        ❹
       number_of_subnets=3)                                              ❹

@pytest.fixture                                                          ❺
def network_configuration(network):                                      ❺
   return network._network_configuration()['google_compute_network'][0]  ❺

@pytest.fixture                                                          ❻
def subnet_configuration(network):                                       ❻
   return network._subnet_configuration()[                               ❻
       'google_compute_subnetwork']                                      ❻

def test_configuration_for_network_name(network, network_configuration): ❼
   assert network_configuration[network._network_name][
       0]['name'] == f"{NETWORK_PREFIX}-network"

def test_configuration_for_three_subnets(subnet_configuration):          ❽
   assert len(subnet_configuration) == 3

def test_configuration_for_subnet_ip_ranges(subnet_configuration):       ❾
   for i, subnet in enumerate(subnet_configuration):
       assert subnet[next(iter(subnet))
                     ][0]['ip_cidr_range'] == f"10.0.{i}.0/24"

❶ 导入 pytest,一个 Python 测试库。你需要命名文件和以 test_ 前缀的测试,以便 pytest 能够运行它们。

❷ 从 main.py 中导入网络工厂模块。你需要运行网络配置的方法。

❸ 将预期值设置为常量,例如网络前缀和 IP 范围

❹ 根据预期值从模块中创建网络作为测试固定装置。此装置为所有测试提供了一个一致的网络对象以供引用。

❺ 由于需要解析 google_compute_network,因此为网络配置创建了一个单独的固定装置。一个测试使用此装置来测试网络名称。

❻ 由于需要解析 google_compute_subnetwork,因此为子网配置创建了一个单独的固定装置。两个测试使用此装置来检查子网数量及其 IP 地址范围。

❼ Pytest 将运行此测试以检查网络名称是否匹配 hello-world-network。它引用了 network_configuration 固定装置。

❽ Pytest 将运行此测试以检查子网数量是否等于 3。它引用了 subnet_configuration 固定装置。

❾ Pytest 将检查网络示例配置中的正确子网 IP 范围。它引用了 subnet_configuration 固定装置。

AWS 和 Azure 等效

要将列表 6.1 转换为 AWS,请使用aws_subnet Terraform 资源(mng.bz/J2vZ)并检索cidr_block属性的值。

对于 Azure,请使用azurerm_subnet Terraform 资源(mng.bz/wo05)并检索address_prefixes属性的值。

测试文件包括在测试之间传递的静态网络对象。此 测试固定装置 创建一个一致的网络对象,每个测试都可以引用。它减少了构建测试资源时使用的重复代码。

定义 A 测试固定装置 是用于运行测试的已知配置。它通常反映了给定基础设施资源的已知或预期值。一些固定装置会分别解析网络和子网信息。每次我们添加新的测试时,我们不必复制和粘贴解析。相反,我们引用固定装置以获取配置。

您可以在命令行中运行 pytest 并传递一个带有测试文件的参数。pytest 运行一组三个测试并输出它们的成功:

$ pytest test_network.py
==================== test session starts ====================
collected 3 items

test_network.py ...                                    [100%]

===================== 3 passed in 0.06s =====================

在此示例中,您导入网络工厂模块,创建一个带有配置的网络对象,并对其进行测试。您不需要将任何配置写入文件。相反,您引用函数并测试对象。

此示例使用我用于单元测试应用程序代码的相同方法。这通常会导致更小、更模块化的函数,您可以更有效地进行测试。生成网络配置的函数需要输出测试配置。否则,测试无法解析和比较值。

6.2.2 测试特定领域语言

如果您使用 DSL,您如何测试您的网络和子网配置?您没有可以在测试中调用的函数。相反,您的单元测试必须从配置或干运行文件中解析值。这两种类型的文件都存储有关基础设施资源的一些 plaintext 元数据。

想象一下,您使用 DSL 而不是 Python 来创建您的网络。此示例创建一个与 Terraform 兼容的配置的 JSON 文件。该 JSON 文件包含所有三个子网、它们的 IP 地址范围和名称。在图 6.7 中,您决定针对网络的 JSON 配置文件运行单元测试。测试运行得很快,因为您没有部署网络。

图片

图 6.7 单元测试与干运行测试需要生成基础设施资源的更改预览,并检查其有效参数。

通常,您始终可以对您用于定义 IaC 的文件进行单元测试。如果工具使用配置文件,如 CloudFormation、Terraform、Bicep、Ansible、Puppet、Chef 等,您可以对配置中的任何行进行单元测试。

在列表 6.2 中,您可以在不生成干运行的情况下测试您的网络模块的网络名称、子网数量和子网 IP 地址范围。我用 pytest 运行类似的测试来检查相同的参数。

列表 6.2 使用 pytest 在 test_network_configuration.py 中运行单元测试

import json                                                     ❶
import pytest

NETWORK_CONFIGURATION_FILE = 'network.tf.json'                  ❷

expected_network_name = 'hello-world-network'                   ❸

@pytest.fixture(scope="module")                                 ❹
def configuration():                                            ❹
   with open(NETWORK_CONFIGURATION_FILE, 'r') as f:             ❹
       return json.load(f)                                      ❹

@pytest.fixture
def resource():                                                 ❺
   def _get_resource(configuration, resource_type):             ❺
       for resource in configuration['resource']:               ❺
           if resource_type in resource.keys():                 ❺
               return resource[resource_type]                   ❺
   return _get_resource                                         ❺

@pytest.fixture                                                 ❻
def network(configuration, resource):                           ❻
   return resource(configuration, 'google_compute_network')[0]  ❻

@pytest.fixture                                                 ❼
def subnets(configuration, resource):                           ❼
   return resource(configuration, 'google_compute_subnetwork')  ❼

def test_configuration_for_network_name(network):               ❽
   assert network[expected_network_name][0]['name'] \           ❽
       == expected_network_name                                 ❽

def test_configuration_for_three_subnets(subnets):              ❾
   assert len(subnets) == 3                                     ❾

def test_configuration_for_subnet_ip_ranges(subnets):           ❿
   for i, subnet in enumerate(subnets):                         ❿
       assert subnet[next(iter(subnet))                         ❿
                     ][0]['ip_cidr_range'] == f"10.0.{i}.0/24"  ❿

❶ 导入 Python 的 JSON 库,因为你需要加载 JSON 文件

❷ 设置一个常量,用于网络配置的预期文件名。测试从 network.tf.json 读取网络配置。

❸ 设置预期的网络名称为 hello-world-network

❹ 打开包含网络配置的 JSON 文件并将其作为测试固定配置加载

❺ 创建一个新的测试固定配置,它引用已加载的 JSON 配置并解析任何资源类型。它根据 Terraform 的 JSON 资源结构解析 JSON。

❻ 从 JSON 文件中获取 google_compute_network Terraform 资源

❼ 从 JSON 文件中获取 google_compute_subnetwork Terraform 资源

❽ Pytest 将运行此测试以检查网络名称配置是否与 hello-world-network 匹配。它引用了网络固定配置。

❾ Pytest 将运行此测试以检查子网数量配置是否等于 3。它引用了子网固定配置。

❿ Pytest 将检查正确的子网 IP 范围配置。它引用了子网固定配置。

AWS 和 Azure 等效

要将列表 6.2 转换为 AWS,使用 aws_subnet Terraform 资源 (mng.bz/J2vZ) 并检索 cidr_block 属性的值。

对于 Azure,使用 azurerm_subnet Terraform 资源 (mng.bz/wo05) 并检索 address_prefixes 属性的值。

你可能会注意到 DSL 的单元测试看起来与编程语言的测试相似。它们检查网络名称、子网数量和 IP 地址。一些工具具有专门的测试框架。它们通常使用相同的流程生成干运行或状态文件,并解析其中的值。

然而,你的配置文件可能不包含所有内容。例如,你不会在 Terraform 或 Ansible 中拥有某些配置,直到 之后 你进行了干运行。干运行 预览 IaC 变更而不部署它们,并内部识别和解决潜在问题。

定义 A 干运行 预览 IaC 变更而不部署它们。它内部识别并解决潜在问题。

干运行有不同的格式和标准。大多数干运行输出到终端,并且你可以将输出保存到文件。一些工具将自动将干运行生成到文件。

为单元测试生成干运行

一些工具将它们的干运行保存到文件中,而其他工具则在终端中输出更改。如果你使用 Terraform,你可以使用以下命令将 Terraform 计划写入 JSON 文件:

$ terraform plan -out=dry_run && terraform show -json dry_run > dry_run.json

AWS CloudFormation 提供更改集,你可以在完成后解析更改集描述。同样,你可以使用 kubectl run-–dry-run=client 选项获取 Kubernetes 干运行信息。

作为一般做法,我优先考虑检查配置文件的测试。当我无法从配置文件中获取值时,我会编写解析 dry-run 的测试。dry-run 通常需要访问基础设施提供者 API 的网络,并且运行需要一些时间。有时,输出或文件包含我不想让测试明确解析的敏感信息或标识符。

虽然 dry-run 配置可能不符合更传统的软件开发定义的单元测试,但解析 dry-run 不需要对活动基础设施进行任何更改。它仍然是一种静态分析。dry-run 本身作为单元测试,用于在应用更改之前验证和输出预期的更改行为。

6.2.3 你应该在什么时候编写单元测试?

单元测试帮助你验证你的逻辑生成正确的名称,产生正确数量的基础设施资源,并计算正确的 IP 范围或其他属性。一些单元测试可能与格式化和 linting 重叠,这些概念我在第二章中提到。我将 linting 和 formatting 归类为单元测试的一部分,因为它们帮助你了解如何命名和组织你的配置。

图 6.8 总结了单元测试的一些用例。你应该编写额外的单元测试来验证你用于生成基础设施配置的任何逻辑,特别是涉及循环或条件(if-else)语句的逻辑。单元测试还可以捕获错误或问题配置,例如错误的操作系统。

图片

图 6.8 编写单元测试以验证资源逻辑,突出潜在问题或确定团队标准。

由于单元测试是在隔离状态下检查配置,它们并不能精确反映更改将如何影响系统。因此,你不能期望单元测试在生产更改期间防止重大故障。然而,你仍然应该编写单元测试!虽然它们在运行更改时不会识别问题,但单元测试可以在生产之前预防问题配置。

例如,有人可能会不小心为 1,000 台服务器而不是 10 台服务器输入配置。一个验证配置中服务器最大数量的测试可以防止某人压倒基础设施并管理成本。单元测试还可以防止从生产环境中出现任何不安全或不合规的基础设施配置。我在第八章中介绍了如何将单元测试应用于安全和审计基础设施配置。

除了早期识别错误的配置值之外,单元测试还有助于自动化检查复杂系统。当你有许多由不同团队管理的基础设施资源时,你不能再手动搜索一个资源列表并检查每个配置。单元测试将最重要的或标准配置传达给其他团队。当你为基础设施模块编写单元测试时,你验证模块的内部逻辑是否产生了预期的资源。

单元测试自动化

优秀的单元测试需要一本书来描述!我在本节中限定了对基础设施配置的测试解释。然而,你可能会编写一个直接访问基础设施 API 的定制自动化工具。自动化使用更顺序的方法逐步配置资源(也称为命令式风格)。

你应该使用单元测试来检查单个步骤及其幂等性。单元测试应该在各种先决条件下运行单个步骤,并检查它们是否具有相同的结果。如果你需要访问基础设施 API,你可以在单元测试中模拟 API 响应。

单元测试的用例包括检查你是否创建了预期的数量的基础设施资源,固定了特定版本的基础设施,或者使用了正确的命名标准。单元测试运行迅速,几乎零成本(在你编写它们之后!)提供快速反馈。它们的运行时间在秒级,因为它们不需要向基础设施发布更新或创建活动的基础设施资源。如果你编写单元测试来检查干运行的结果,你会因为生成干运行所花费的初始时间而增加一点时间。

6.3 合同测试

单元测试验证配置或模块的独立性,但模块之间的依赖关系怎么办?在第四章中,我提到了依赖关系之间合同的想法。一个模块的输出必须与另一个模块的预期输入一致。你可以使用测试来强制执行这种一致性。

例如,让我们在网络上创建一个服务器。服务器通过使用外观来访问网络名称和 IP 地址,该外观反映了网络的名称和 IP 地址范围。你如何知道网络模块输出的是网络名称和 IP CIDR 范围,而不是另一个标识符或配置?

你在图 6.9 中使用了合同测试来测试网络模块是否正确输出外观。外观必须包含网络名称和 IP 地址范围。如果测试失败,则表明服务器无法在网络中创建自身。

图 6.9 合同测试可以快速验证配置参数是否等于预期值,例如具有正确输出的网络外观。

合同测试通过静态分析来检查模块的输入和输出是否与预期值或格式匹配。

定义 合同测试 静态分析并比较模块或资源的输入和输出,以匹配预期值或格式。

合同测试有助于使单个模块具有可扩展性,同时保持两者之间的集成。当你有许多基础设施依赖项时,你不能手动检查它们的所有共享属性。相反,合同测试自动化了模块之间属性的类型和值的验证。

你会发现合同测试对于检查高度参数化模块(如工厂、原型或构建器模式)的输入和输出最有用。编写和运行合同测试有助于检测错误的输入和输出,并记录模块的最小资源。当你没有为你的模块编写合同测试时,你不会知道你在系统中破坏了什么,直到你下一次将配置应用到实际环境中。

让我们在列表 6.3 中实现服务器和网络合同测试。使用 pytest,你通过创建一个带有工厂模块的网络来设置测试。然后你验证网络输出是否包含具有网络名称和 IP 地址范围的外观对象。你将这些测试添加到服务器的单元测试中。

列表 6.3 比较模块输出与输入的合同测试

from network import NetworkFactoryModule, NetworkFacade
import pytest

network_name = 'hello-world'                                      ❶
network_cidr_range = '10.0.0.0/16'                                ❷

@pytest.fixture
def network_outputs():                                            ❸
   network = NetworkFactoryModule(                                ❹
       name=network_name,                                         ❹
       ip_range=network_cidr_range)                               ❹
   return network.outputs()                                       ❺

def test_network_output_is_facade(network_outputs):               ❻
   assert isinstance(network_outputs, NetworkFacade)              ❻

def test_network_output_has_network_name(network_outputs):        ❼
   assert network_outputs._network == f"{network_name}-subnet"    ❼

def test_network_output_has_ip_cidr_range(network_outputs):       ❽
   assert network_outputs._ip_cidr_range == network_cidr_range    ❽

❶ Pytest 将运行此测试以检查网络名称是否与预期值匹配,hello-world。

❷ Pytest 将运行此测试以检查网络输出的 IP CIDR 范围是否与 10.0.0.0/16 匹配。

❸ 使用网络工厂模块和返回其输出的固定装置设置测试

❹ 使用带有名称和 IP 地址范围的网络工厂模块创建网络

❺ 测试固定装置应返回具有不同输出属性的网络外观。

❻ Pytest 将运行此测试以检查模块是否输出网络外观对象。

❼ Pytest 将运行此测试以检查网络名称是否与预期值匹配,hello-world。

❽ Pytest 将运行此测试以检查网络输出的 IP CIDR 范围是否与 10.0.0.0/16 匹配。

假设你更新了网络模块,以输出网络 ID 而不是名称。这破坏了上游服务器模块的功能,因为服务器期望网络名称!合同测试确保当你更新任何一个模块时,你不会破坏两个模块之间的 合同(或接口)。使用合同测试来验证你在表达资源之间的依赖关系时使用的外观和适配器。

为什么你应该将示例合同测试添加到服务器,一个更高级的资源?你的服务器期望从网络获得特定的输出。如果网络模块发生变化,你希望首先从高级模块检测到它。

通常,一个高级模块应该推迟对低级模块的更改,以保持可组合性和可扩展性。你希望避免对低级模块的接口进行重大更改,因为这可能会影响依赖于它的其他模块。

领域特定语言

列表 6.3 使用 Python 验证模块输出。如果你使用具有 DSL 的工具,你可能能够使用内置功能来验证输入是否遵循某些类型或正则表达式(例如检查有效的 ID 或名称格式)。如果一个工具没有验证函数,你可能需要使用单独的测试框架来解析一个模块配置的输出类型,并将其与高级模块输入进行比较。

基础设施合同测试需要一种方法来提取预期的输入和输出,这可能涉及对基础设施提供商的 API 调用,并验证模块的响应是否符合预期值。有时这涉及到创建测试资源来检查参数和理解字段如 ID 应该如何结构化。当你需要执行 API 调用或创建临时资源时,你的合同测试可能比单元测试运行时间更长。

6.4 集成测试

你如何知道你可以将你的配置或模块更改应用到基础设施系统中?你需要将更改应用到测试环境中,并动态分析运行中的基础设施。集成测试针对测试环境运行,以验证模块或配置的成功更改。

定义集成测试针对测试环境运行,并动态分析基础设施资源,以验证它们是否受到模块或配置更改的影响。

集成测试需要一个隔离的测试环境来验证模块和资源的集成。在接下来的章节中,你将了解可以为基础设施模块和配置编写的集成测试。

6.4.1 测试模块

想象一个创建 GCP 服务器的模块。你想要确保你可以成功创建和更新服务器,因此你编写了一个集成测试,如图 6.10 所示。

首先,你配置服务器并将更改应用到测试环境中。然后,你运行集成测试以检查你的配置更新是否成功,创建一个服务器,并将其命名为hello-world-test。测试的总运行时间需要几分钟,因为你需要等待服务器配置完成。

图片

图 6.10 集成测试通常在测试环境中创建和更新基础设施资源,测试其配置和状态的正确性或可用性,并在测试后删除它们。

当你实现集成测试时,你需要比较活动资源与你的 IaC。活动资源告诉你你的模块是否成功部署。如果有人无法部署模块,他们可能破坏了他们的基础设施。

集成测试必须使用基础设施提供者的 API 获取活动资源的有关信息。例如,你可以在服务器模块的集成测试中导入一个 Python 库来访问 GCP API。集成测试导入 Libcloud,一个 Python 库,作为 GCP API 的客户端 SDK。

列表 6.4 中的测试通过使用模块构建服务器的配置,等待服务器部署,并在 GCP API 中检查服务器的状态。如果服务器返回 running 状态,则测试通过。否则,测试失败并识别模块中的问题。最后,测试会拆除它创建的测试服务器。

列表 6.4 test_integration.py 中的服务器创建集成测试

from libcloud.compute.types import NodeState                    ❶
from main import generate_json, SERVER_CONFIGURATION_FILE
import os
import pytest
import subprocess
import test_utils

TEST_SERVER_NAME = 'hello-world-test'

@pytest.fixture(scope='session')                                ❷
def apply_changes():                                            ❷
   generate_json(TEST_SERVER_NAME)                              ❸
   assert os.path.exists(SERVER_CONFIGURATION_FILE)             ❸
   assert test_utils.initialize() == 0                          ❹
   yield test_utils.apply()                                     ❹
   assert test_utils.destroy() == 0                             ❺
   os.remove(SERVER_CONFIGURATION_FILE)                         ❺

def test_changes_have_successful_return_code(apply_changes):    ❻
   return_code = apply_changes[0]
   assert return_code == 0

def test_changes_should_have_no_errors(apply_changes):          ❼
   errors = apply_changes[2]
   assert errors == b''

def test_changes_should_add_1_resource(apply_changes):          ❽
   output = apply_changes[1].decode(encoding='utf-8').split('\n')
   assert 'Apply complete! Resources: 1 added, 0 changed, ' + \
      '0 destroyed' in output[-2]

def test_server_is_in_running_state(apply_changes):             ❾
   gcp_server = test_utils.get_server(TEST_SERVER_NAME)         ❾
   assert gcp_server.state == NodeState.RUNNING                 ❾

❶ Pytest 使用 Libcloud 调用 GCP API 并获取服务器的当前状态。它检查服务器是否正在运行。

❷ 在测试会话期间,使用 pytest 测试 fixture 应用配置并在 GCP 上创建测试服务器

❸ 生成一个使用服务器模块的 Terraform JSON 文件

❹ 使用 Terraform,通过 Terraform JSON 文件初始化和部署服务器

❺ 在测试会话结束时使用 Terraform 删除测试服务器并移除 JSON 配置文件

❻ Pytest 将运行此测试以验证更改的输出状态是否成功。

❼ Pytest 将运行此测试以验证更改不会返回错误。

❽ Pytest 将运行此测试并检查配置是否添加了一个资源,即服务器。

❾ Pytest 使用 Libcloud 调用 GCP API 并获取服务器的当前状态。它检查服务器是否正在运行。

AWS 和 Azure 的等效项

要转换列表 6.4,你需要更新 IaC 以创建一个 Amazon EC2 实例或 Azure Linux 虚拟机。然后你需要更新 Apache Libcloud 驱动程序以使用 Amazon EC2 驱动程序 (mng.bz/qYex) 或 Azure ARM Compute 驱动程序 (mng.bz/7yjQ)。驱动程序和 IaC 的初始化将发生变化,但测试不会。

当你在命令行中运行此文件中的测试时,你会注意到它需要几分钟的时间,因为测试会话会创建并删除服务器:

$ pytest test_integration.py
========================== test session starts =========================
collected 4 items

test_integration.py ....                                          [100%]

==================== 4 passed in 171.31s (0:02:51) =====================

服务器集成测试应用了两种主要实践。首先,测试遵循以下顺序:

  1. 如果适用,渲染配置

  2. 将更改部署到基础设施资源

  3. 运行测试,访问基础设施提供者的 API 进行比较

  4. 如果适用,删除基础设施资源。

此示例使用 fixture 实现了序列。你可以用它来应用任何任意的基础设施配置,并在测试后移除它。

注意 集成测试对于配置管理工具的工作方式非常相似。例如,你可以在服务器上安装软件包并运行进程。在运行测试后,你可以通过检查服务器的软件包和进程以及销毁服务器来扩展服务器集成测试。与其使用编程语言编写测试,我建议评估专门的服务器测试工具,该工具登录到服务器并针对系统运行测试。

其次,你在远离支持应用程序的测试或生产环境的单独 模块测试环境(例如测试账户或项目)中运行模块集成测试。为了避免与环境中其他模块测试发生冲突,你根据特定的模块类型、版本或提交哈希对资源进行标记和命名。

定义 A 模块测试环境 是与生产环境分开的,用于测试模块更改。

在与测试或生产环境不同的环境中测试模块有助于将失败的模块与具有应用程序的活跃环境隔离。你还可以从测试模块中测量和控制你的基础设施成本。第十二章更详细地介绍了云计算的成本。

6.4.2 环境配置测试

基础设施模块的集成测试可以在测试环境中创建和删除资源,但环境配置的集成测试则不能。想象一下,你需要向由组合或单例配置设置的当前域名添加一个 A 记录。你该如何编写集成测试来检查你是否正确添加了该记录?

你会遇到两个问题。首先,你不能简单地在集成测试中创建和销毁 DNS 记录,因为这可能会影响应用程序。其次,A 记录的存在依赖于在配置域名之前的服务器 IP 地址。

而不是在测试环境中创建和销毁服务器和 A 记录,你可以在与生产环境匹配的 持久性 测试环境中运行集成测试。在图 6.11 中,你更新测试环境的 IaC 中的 DNS 记录。你的集成测试检查测试环境中的 DNS 是否与预期的正确 DNS 记录匹配。测试通过后,你可以更新生产环境的 DNS 记录。

图片

图 6.11 你可以对具有长期资源的测试环境运行集成测试,以隔离生产环境中的更改并减少你需要为测试创建的依赖。

为什么要在 持久性 测试环境中运行 DNS 测试?首先,创建测试环境可能需要很长时间。作为高级资源,DNS 依赖于许多低级资源。其次,你希望在更新生产环境之前,对更改的行为有一个准确的表示。

测试环境捕获了生产系统的一部分依赖和复杂性,以便您可以检查您的配置是否按预期工作。保持与生产环境相似的测试环境意味着测试中的变化可以提供一个准确的生产行为视角。您希望尽早检测测试环境中的问题。

6.4.3 测试挑战

没有集成测试,您将不知道服务器模块或 DNS 记录更新是否成功,直到您手动检查。它们加快了验证您的 IaC 是否工作的过程。然而,您在集成测试中会遇到一些挑战。

您可能难以确定要测试哪些配置参数。您是否应该编写集成测试来验证您在基础设施即代码(IaC)中配置的每个配置参数是否与实际资源匹配?不一定!

大多数工具已经具有验收测试,这些测试会创建一个资源,更新其配置,然后销毁该资源。验收测试可以证明工具能够发布新的代码更改。这些测试必须通过,以便工具能够支持基础设施的更改。

您不希望花费额外的时间或精力编写与验收测试相匹配的测试。因此,您的集成测试应该涵盖多个资源是否具有正确的配置和依赖关系。如果您编写自定义自动化,您将需要编写集成测试来创建、更新和删除资源。

另一个挑战涉及决定在每个测试期间是否应该创建或删除资源,或者运行持久测试环境。图 6.12 显示了是否为集成测试创建、删除或使用持久测试环境的决策树。

图片

图 6.12 您的集成测试应根据模块或配置类型及其依赖关系创建和删除资源。

通常情况下,如果一个配置或模块没有太多依赖,您可以创建、测试和删除它。然而,如果您的配置或模块创建需要花费时间或需要许多其他资源的存在,您将需要使用持久测试环境。

并非所有模块都从创建和删除的集成测试方法中受益。我建议为低级模块,如网络或 DNS,运行集成测试,并避免删除资源。这些模块通常需要在具有最小财务成本的环境中就地更新。我经常发现测试更新比创建和删除资源更现实。

中级模块(如工作负载编排器)的集成测试创建的资源可能是持久的或临时的,这取决于模块和资源的大小。模块越大,需要长期存在的可能性就越高。你可以为高级模块(如应用部署或 SaaS)运行集成测试,每次都创建和删除资源。

持久的测试环境确实有其局限性。集成测试通常需要很长时间才能运行,因为创建或更新资源需要时间。一般来说,保持模块较小,资源较少。这种做法可以减少你需要进行模块集成测试的时间。

即使你保持配置和模块较小,资源较少,集成测试也常常成为你基础设施提供商账单成本增加的罪魁祸首。许多测试需要长期资源,如网络、网关等。权衡运行集成测试和发现问题与配置错误或损坏的基础设施资源成本之间的关系。

你可以考虑使用基础设施模拟来降低运行集成测试(或任何测试)的成本。一些框架为本地测试复制了基础设施提供者的 API。我不建议过度依赖模拟。基础设施提供者经常更改 API,并且经常有复杂的错误和行为,这些模拟通常无法捕捉。在第十二章中,我讨论了管理测试环境成本和避免模拟的技术。

6.5 端到端测试

虽然集成测试在资源创建或更新期间动态分析配置并捕获错误,但它们并不表明基础设施资源是否可用。可用性要求你或团队成员按预期使用该资源。

例如,你可能会使用一个模块在 GCP Cloud Run 上创建一个名为服务的应用程序。GCP Cloud Run 将任何服务部署到容器中,并返回一个 URL 端点。你的集成测试通过,表明你的模块正确创建了服务资源以及访问服务的权限。

你如何知道某人能否访问应用程序 URL?图 6.13 显示了如何检查服务端点是否工作。首先,你编写一个测试来从你的基础设施配置中检索应用程序 URL 作为输出。然后,你向该 URL 发出 HTTP 请求。总运行时间需要几分钟,其中大部分时间用于创建服务。

你已经创建了一个与集成测试不同的动态分析测试,称为端到端测试。它验证了基础设施的最终用户功能。

定义端到端测试动态分析基础设施资源和端到端系统功能,以验证它们是否受到 IaC 更改的影响。

示例端到端测试验证了最终用户访问页面的端到端工作流程。它检查基础设施配置的成功配置。

图 6.13 端到端测试通过访问应用程序的 URL 中的网页来验证最终用户的流程。

端到端测试对于确保你的更改不会破坏上游功能至关重要。例如,你可能会意外更新一个允许认证用户访问 GCP Cloud Run 服务 URL 的配置。更改后,你的端到端测试失败,表明有人可能无法再访问该服务。

让我们在下面的列表中实现一个在 Python 中针对应用程序 URL 的端到端测试。此示例的测试需要向服务的公共 URL 发送 API 请求。它使用 pytest 夹具创建 GCP Cloud Run 服务,测试运行页面的 URL,并从测试环境中删除服务。

列表 6.5 GCP Cloud Run 服务的端到端测试

from main import generate_json, SERVICE_CONFIGURATION_FILE
import os
import pytest
import requests                       
import test_utils                                                     ❶

TEST_SERVICE_NAME = 'hello-world-test'

@pytest.fixture(scope='session')                                      ❷
def apply_changes():                                                  ❷
   generate_json(TEST_SERVICE_NAME)                                   ❸
   assert os.path.exists(SERVICE_CONFIGURATION_FILE)                  ❸
   assert test_utils.initialize() == 0                                ❷
   yield test_utils.apply()                                           ❷
   assert test_utils.destroy() == 0                                   ❹
   os.remove(SERVICE_CONFIGURATION_FILE)                              ❹

@pytest.fixture                                                       ❺
def url():                                                            ❺
   output, error = test_utils.output('url')                           ❺
   assert error == b''                                                ❺
   service_url = output.decode(encoding='utf-8').split('\n')[0]       ❺
   return service_url                                                 ❺

def test_url_for_service_returns_running_page(apply_changes, url):    ❻
   response = requests.get(url)                                       ❻
   assert "It's running!" in response.text                            ❼

❶ 使用 Terraform,通过 Terraform JSON 文件初始化和部署服务

❷ 在测试会话期间,使用 pytest 测试夹具应用配置并在 GCP 上创建测试服务

❸ 生成一个使用 GCP Cloud Run 模块的 Terraform JSON 文件

❹ 在测试环境中销毁 GCP Cloud Run 服务,这样你就不需要在 GCP 项目中有一个持久的服务

❺ 使用 pytest 夹具解析服务 URL 配置的输出

❻ 在测试中,使用 Python 的 requests 库向服务的 URL 发送 API 请求

❼ 在测试中,检查包含特定字符串的服务 URL 响应,以指示服务正在运行

AWS 和 Azure 的等效产品

AWS Fargate 与 Amazon Elastic Kubernetes Service (EKS) 或 Azure Container Instances (ACI) 大约等同于 GCP Cloud Run。

注意,如果你想在生产环境中运行端到端测试,你不想删除服务。你通常会在现有环境中运行端到端测试,而不是创建新的或测试资源。你将对现有系统应用更改,并在活动基础设施资源上运行测试。

烟雾测试

作为一种端到端测试,烟雾测试可以快速提供有关更改是否破坏了关键业务功能的反馈。运行所有端到端测试可能需要时间,你需要快速修复更改失败。

如果你首先运行烟雾测试,你可以验证更改没有造成灾难性的后果,然后继续进行进一步测试。正如一位质量保证分析师曾经告诉我的,“如果你启动一些硬件并且它冒烟,你就知道有问题。不值得你花时间去进一步测试。”

更复杂的系统架构受益于端到端测试,因为它们成为更改是否影响关键业务功能的主要指标。因此,它们有助于测试组合或单例配置。除非模块具有许多资源和依赖项,否则通常不会在模块上运行端到端测试。

我为网络或计算资源编写了大部分端到端测试。例如,你可以编写一些测试来检查网络对等。这些测试在每个网络上配置一个服务器,并检查服务器是否可以连接。

端到端测试的另一个用例是向工作负载协调器提交一个作业并完成它。这个测试确定工作负载协调器是否在应用部署方面正常工作。我曾经包含了一些端到端测试,这些测试使用不同负载的 HTTP 请求来确保上游服务可以互相调用而不会中断,无论负载大小或协议。

除了网络或计算用例之外,端到端测试可以验证任何系统的预期行为。如果你使用配置管理工具进行配置,你的端到端测试将验证你是否可以连接到服务器并运行预期的功能。对于监控和警报,你可以运行端到端测试来模拟预期的系统行为,验证是否已收集指标,并测试警报的触发。

然而,端到端测试在时间和资源方面是最昂贵的测试。大多数端到端测试需要所有基础设施资源来完全评估系统。因此,你可能只针对生产基础设施运行端到端测试。你可能在测试环境中无法运行它们,因为为测试获取足够的资源通常花费太多金钱。

6.6 其他测试

你可能会遇到单元、契约、集成和端到端测试之外的测试类型。例如,假设你想要将减少内存的配置更改部署到生产服务器。然而,你不知道内存减少是否会影响整个系统。

图 6.14 显示,你可以通过使用系统监控来检查你的更改是否影响了系统。监控持续聚合服务器内存上的指标。如果你收到服务器内存达到其容量百分比的警报,你知道你可能影响了整个系统。

图 6.14 短时间间隔内运行的连续测试,以验证一组指标是否不超过阈值。

监控通过“测试”实现 连续测试,以检查指标是否在规律、频繁的时间间隔内超过阈值。

定义 连续测试(例如监控)在规律、频繁的时间间隔内运行,以检查当前值是否与预期值匹配。

连续测试包括监控系统指标和安全事件(当 root 用户登录到服务器时)。它们在活动基础设施环境中提供动态分析。大多数连续测试以警报的形式出现,通知你任何问题。

你可能会遇到另一种称为回归测试的测试类型。例如,你可能会在一段时间内运行测试,以检查你的服务器配置是否符合你组织的期望。回归测试定期运行,但频率低于监控或其他形式的持续测试。你可以选择每隔几周或几个月运行它们,以检查异常情况。

定义回归测试在较长时间内定期运行,以检查基础设施配置是否符合预期的状态或功能。它们可以帮助缓解配置漂移。

持续和回归测试通常需要特殊的软件或系统来运行。它们确保运行的基础设施具有预期的功能和性能。这些测试还为自动化系统以响应异常奠定了基础。

例如,配置了基础设施即代码(IaC)和持续测试的系统可以使用自动扩展来根据 CPU 或内存等指标调整资源。这些系统还可以实施其他自我修复机制,例如在出现错误时将流量重定向到应用程序的旧版本。

6.7 选择测试

我解释了基础设施中最常见的测试,从单元测试到端到端测试。然而,你是否需要编写所有这些测试?你在编写它们时应该在哪里投入时间和精力?你的基础设施测试策略将根据你系统的复杂性和增长而演变。因此,你将不断评估哪些测试能帮助你捕捉到生产前的配置问题。

我将金字塔形状作为基础设施测试策略的指南。在图 6.15 中,金字塔最宽的部分表示你应该有更多这种类型的测试,而最窄的部分表示你应该有更少的测试。金字塔的顶部是端到端测试,由于它们需要活跃的基础设施系统,因此可能需要更多的时间和金钱。金字塔的底部是单元测试,它们可以在几秒钟内运行,并且不需要整个基础设施系统。

图 6.15

图 6.15 根据测试金字塔,你应该有比端到端测试更多的单元测试,因为运行它们花费的时间、金钱和资源更少。

这个被称为测试金字塔的指南为不同类型的测试、它们的范围和频率提供了一个框架。我将测试金字塔从软件测试改编到基础设施,修改了它以适应基础设施工具和约束。

定义测试金字塔作为你整体测试策略的指南。随着你向上移动金字塔,测试类型将花费更多的时间和金钱。

实际上,你的测试金字塔可能更像是一个矩形或梨形,有时会有缺失的层级。你不会也不应该为每种基础设施配置编写每种类型的测试。在某个时候,测试变得重复且难以维护。

根据你想要测试的系统,可能无法在实际中完全遵循理想的测试金字塔。然而,避免我戏称为测试路标的做法。路标倾向于许多手动测试,而其他方面则很少。

6.7.1 模块测试策略

在第五章中,我提到了在发布模块之前测试模块的做法。让我们回到那个例子,你更新了一个数据库模块到 PostgreSQL 12。而不是手动创建模块并测试其是否工作,你添加了一系列自动化测试。它们检查模块的格式并创建一个在隔离的模块测试环境中的数据库。

图 6.16 更新了模块发布工作流程,其中包含了你可以添加以检查模块是否正常工作的单元测试、契约测试和集成测试。在契约测试通过后,你运行一个集成测试,在网络上设置数据库模块并检查数据库是否运行。完成集成测试后,你删除由模块创建的测试数据库并发布模块。

单元测试、契约测试和集成测试的组合可以充分代表模块是否能够正确工作。单元测试检查模块格式和你的团队的标准配置。你首先运行它们,以便快速获得关于格式或配置违规的反馈。

图 6.16

图 6.16 你可以将模块发布工作流程的测试阶段分解为包括单元测试、契约测试和集成测试。

接下来,你运行一些契约测试。在数据库模块的情况下,你检查输入到数据库模块的网络 ID 是否与网络模块输出的网络 ID 匹配。捕捉这些错误将有助于在部署过程中早期识别依赖项之间的问题。

专注于单元测试或契约测试,以确保适当的配置、正确的模块逻辑以及特定的输入和输出。图 6.16 中概述的测试工作流程最适合使用工厂、构建器或原型模式的模块。这些模式隔离了基础设施组件的最小子集,并为你的队友提供了一组灵活的变量,以便进行定制。

根据你的开发环境成本,你可以编写一些集成测试,针对临时基础设施资源运行,并在测试结束时删除这些资源。通过投入一些时间和精力来编写具有许多输入和输出的模块的测试,你可以确保更改不会影响上游配置,并且该模块可以独立成功运行。

练习 6.1

你注意到负载均衡器模块的新版本破坏了你的 DNS 配置。一位队友更新了该模块,使其输出私有 IP 地址而不是公共 IP 地址。你能做些什么来帮助你的团队更好地记住该模块需要公共 IP 地址?

A) 为私有 IP 地址创建一个单独的负载均衡器模块。

B) 添加模块合同测试,以验证模块输出私有和公开 IP 地址。

C) 在模块的文档中添加备注,说明它需要公开 IP 地址。

D) 在模块上运行集成测试,并检查 IP 地址是否公开可访问。

请参阅附录 B 以获取练习的答案。

6.7.2 配置测试策略

活动环境的配置使用更复杂的模式,如单例或组合。单例或组合配置有许多基础设施依赖项,并且经常引用其他模块。将端到端测试添加到你的测试工作流程中可以帮助识别基础设施和模块之间的问题。

想象一下,你有一个单例配置,其中包含一个网络上的应用服务器。图 6.17 概述了在更新服务器大小后的每个步骤。在将更改推送到版本控制后,你将更改部署到测试环境。你的测试工作流程从单元测试开始,以快速验证格式和配置。

接下来,你运行集成测试以应用更改并验证服务器是否仍在运行以及具有新的大小。你通过使用端到端测试来测试整个系统来完成你的验证。端到端测试向应用程序端点发出 HTTP GET 请求。图 6.17 在生产中重复此过程以确保系统没有损坏。

图 6.17

图 6.17 使用单例和组合模式进行 IaC 应该在将更改部署到生产之前在测试环境中运行单元、集成和端到端测试。

仅因为你成功创建或更新了服务器,并不意味着它所托管的应用程序可以处理请求!在复杂的系统架构中,你需要额外的测试来验证基础设施之间的依赖或通信。端到端测试可以帮助保持系统的功能。

在测试和生产环境之间重复相同的测试提供质量控制。如果你在测试和生产环境之间存在任何配置漂移,你的测试可能会反映这些差异。你可以根据环境启用或禁用特定测试。

镜像构建和配置管理

镜像构建和配置管理工具的测试遵循与测试配置工具配置类似的方法。单元测试镜像构建或配置管理元数据涉及检查配置。除非你模块化配置管理,并遵循模块的测试方法,否则你不需要合同测试。集成测试应在测试环境中运行,以测试服务器是否能够使用新镜像成功启动或应用正确的配置。端到端测试确保你的新镜像和配置不会影响系统的功能。

练习 6.2

你添加防火墙规则以允许应用程序访问新的队列。对于这个更改,哪种测试组合对你的团队最有价值?

A) 单元和集成测试

B) 合同和端到端测试

C) 合同和集成测试

D) 单元和端到端测试

请参阅附录 B 以获取练习的答案。

6.7.3 识别有用的测试

模块和配置的测试策略可以帮助指导你编写有价值测试的初始方法。图 6.18 总结了你可能考虑用于模块和配置的测试类型。模块依赖于单元、合同和集成测试,而配置依赖于单元、集成和端到端测试。

图片

图 6.18 你编写模块或环境配置时,你的测试方法会有所不同。

你如何知道何时编写测试?想象一下,你的队友可能知道数据库密码需要是 16 位数的字母数字字符。然而,你可能直到更新一个 24 位密码、部署更改并等待五分钟以查看更改是否失败,才知道这个事实。

我认为更新测试的做法是将系统中的未知已知转化为已知已知。毕竟,你使用可观察性来调试未知未知,使用监控来跟踪已知未知。在图 6.19 中,你将其他人可能知道但孤立的(未知已知)知识转化为测试(已知已知),以反映团队的知识。新的测试通常反映了团队应该知道并承认的孤岛知识。

图片

图 6.19 展示了基础设施测试如何将孤岛知识(其他人可能知道的知识)转化为测试,以反映团队的知识。

一个好的测试会将知识分享给团队的其他成员。你不必总是构建一个新的测试。相反,你可能找到一个现有的测试,它没有检查所有内容。使用测试来防止你的团队重复出现相同的问题。

除了添加测试,你还将移除测试。你可能编写了一个测试,但发现它有一半的时间会失败。由于其不可靠性,它并没有提供有用的信息,也没有增加你对系统的信心。移除测试可以清理你的测试套件,并帮助消除那些经常失败但并不表明系统真正失败的不可靠测试。

此外,你将移除测试,因为你可能不需要它们。例如,你可能不需要为每个模块编写合同测试,或者为每个环境配置编写集成测试。始终问自己测试是否提供了价值,以及它们是否运行得足够可靠,以便你能够获得足够的信息来了解系统。

下一章将展示如何为你的 IaC 添加测试到交付管道。即使你选择不自动化测试工作流程,你也有机会检查更改可能如何影响你的基础设施。

摘要

  • 测试金字塔概述了一种测试方法。金字塔中测试级别越高,测试成本越高。

  • 单元测试验证模块或配置中的静态参数。

  • 合同测试验证模块的输入和输出是否与预期值和格式匹配。

  • 集成测试创建测试资源,验证其配置和创建,然后删除它们。

  • 端到端测试验证基础设施系统的最终用户能否运行预期的功能。

  • 使用工厂、构建器或原型模式的模块可以从单元、合同和集成测试中受益。

  • 在环境中应用组合或单例模式配置可以从单元、集成和端到端测试中受益。

  • 其他测试包括持续监测系统指标、回归测试用于处理带外手动更改,或安全测试用于配置错误。

7 持续交付和分支模型

本章涵盖

  • 设计交付管道以避免将失败推送到生产系统

  • 为团队协作选择基础设施配置的分支模型

  • 审查和管理团队内部基础设施资源的变更

在前面的章节中,你学习了如何编写模块和依赖项的模式。你还应用了一些编写基础设施代码和共享模块的一般实践。这些模式、实践和工作流程有很多步骤。

此外,许多工作流程需要仔细协调变更。有一天,你可能会尝试进行一个变更,却发现你的队友的更新可能会覆盖你的变更!你如何确保你在开发过程中管理好冲突?

一种解决方案是向票务系统提交变更请求。例如,如果你想更改服务器,你需要在票务系统中填写变更请求。然后,这个变更请求将由你的同行(通常是你的团队)和变更咨询委员会(代表公司)进行审查。

大多数公司使用这个被称为变更管理的过程来确定哪些变更存在冲突。基础设施变更管理涉及提交一个变更请求,详细说明部署和回滚步骤,供同行审查批准。

定义变更管理对于基础设施来说是一个促进系统变更的过程。它通常涉及在批准投入生产之前,在整个公司中详细审查和审查变更。

变更管理依赖于同行审查,以防止变更相互覆盖。示例中的同行审查和变更咨询委员会充当质量门。质量门验证变更请求不会损害系统的安全或可用性。一旦你的变更通过了这些门,你就可以安排它并相应地更新服务器。

定义IaC质量门通过审查或测试来强制执行系统的安全性、可用性和弹性。

变更管理和质量门如何帮助解决变更之间的冲突?想象一下,你将变更管理流程应用于你和你的队友的冲突变更。在图 7.1 中,你和你的队友向票务系统提交变更请求。组织内的同行手动审查每个变更,并确定你的队友的变更对用户的影响最小。他们重新安排你的变更,以避免与你的队友的变更冲突。

图片

图 7.1 IaC 的协作涉及简化同行和组织审查变更的过程。

变更管理可能需要几周才能完成。人工审查无法发现每个问题或防止每个基础设施变更冲突。你仍然需要了解如何撤销变更。你将在第十一章学习如何修复变更失败。

而不是依赖变更管理,你可以使用基础设施即代码(IaC)通过代码来沟通变更并自动化变更管理。本章重点介绍通过扩展和自动化团队和公司内的 IaC 开发流程来简化变更管理。你将面临在尝试不破坏生产系统的情况下,同时处理同一或依赖资源上工作的挑战。

构建和配置管理

在整本书中,我专注于基础设施供应的使用案例。构建和配置管理的使用案例应遵循本章中交付管道的一般模式。评估基础设施更改和纳入自动化测试的模式和实践对所有使用案例都是一致的。

7.1 将更改交付到生产环境

你如何控制 IaC 对生产环境的更改?你应用软件开发实践,如持续集成、交付或部署(CI/CD),来组织来自各种协作者的代码更改,并准备将 IaC 发布到生产环境。

CI/CD 需要自动化测试来自动化更改的发布和管理。我将解释你如何自动化基础设施更改并充分利用其优势。它使用你在上一章中学到的测试实践以及本章中的交付管道模式。

7.1.1 持续集成

回想一下你与队友遇到的 IaC 冲突。你不知道你的队友的更改会影响你的,反之亦然。你如何在同行审查你的更改之前自动识别冲突?

图 7.2 中的一个解决方案是要求你的团队定期将其更改合并到主 IaC 中。如果你的团队持续地将更改集成到主配置中,你和你的队友可以更早地识别冲突,在它们覆盖你的更新之前。

你可以将持续集成(CI)的实践应用到合并更改到主配置中,每天多次,并检查它们是否与协作者冲突。

对于 IaC,持续集成(CI)的定义是在测试环境中验证更改后,定期和频繁地将更改合并到你的存储库中。

图片

图 7.2 持续集成涉及频繁地将更改合并到主配置中,从而允许更早地识别更改中的冲突。

你应该多久合并一次?知道何时合并需要一些经验,并取决于你想要进行的更改类型。作为一个一般规则,当我积累了几行配置更改(可能)不会破坏系统时,我会合并。有时,这意味着我一天内可能合并几次。有时,对于困难的更改,我可能一天只合并一次或两次。其余的团队也会继续每天几次地工作并合并他们的更改。

每当团队成员合并他们的更改时,构建工具(如 CI 框架)应该启动一个工作流程来测试更改并将它们部署。图 7.3 显示了构建工具可能运行的一个示例工作流程。该工作流程检查 IaC 的合并冲突,运行单元测试以验证格式,并暂停进行同行评审。一旦通过同行评审,构建工具将更改部署到生产环境。

图片

图 7.3 在交付管道中的 CI 包括在等待手动批准到生产之前自动化的单元测试。

你可以将这个工作流程表达为交付管道的一部分。管道组织并自动化一系列阶段,用于构建、测试、部署和发布你的基础设施即代码(IaC)。

定义 IaC 的交付管道表达并自动化一个工作流程,用于构建、测试、部署和发布对基础设施的更改。

基础设施交付管道首先检查配置冲突或语法问题。CI 管道中的单元测试让你有信心没有更改冲突。然后你可以将更改提交给团队或公司进行审查。管道会自动将其发布(或应用)到生产环境中。

为什么你应该设计一个交付管道并将其添加到构建工具中?你可能不会记得发布更改到生产所需的全部步骤。交付管道将过程编码化,这样你就不需要记住它。同意基础设施的交付管道可以帮助你一致且可重复地扩展基础设施更改,无论基础设施资源如何。

7.1.2 持续交付

你使用了持续集成(CI)来合并更改并检查冲突,但你如何知道系统是否按预期运行?CI 验证格式和标准,但你不知道配置是否在发布前工作。你对单元测试有信心,但你需要更多的测试来对更新感到舒适。

图 7.4 重新构想了你为 IaC 开始的 CI 工作流程。你在单元测试之后更新你的交付管道,添加额外的阶段。在提交更改进行同行评审之前,你将配置部署到测试环境,并使用集成和端到端测试进行测试。同行评审后,你交付更改到生产,并重新运行端到端测试以验证生产功能。

图片

图 7.4 持续交付自动在测试环境中部署和运行基础设施更改,并等待手动批准到生产。

你通过实践持续交付CD)扩展了你的交付管道。CD 在通过单元测试后,将你的基础设施配置部署到测试环境进行集成或端到端测试的步骤添加到你的交付管道中。

定义 持续交付 (CD) 对于基础设施即代码 (IaC),在将您的更改合并到存储库后,将基础设施更改部署到测试环境进行集成或端到端测试。在将更改发布到生产环境之前,它可能涉及一个手动质量关卡。

每当有人将更改推送到源代码控制时,它就会启动管道的工作流程,以在测试环境中验证这些更改。一旦集成和端到端测试通过,管道可以等待人工审批,然后再将更改部署到生产环境。

为什么使用 CD 而不是 CI?CD 包含了您在第六章中努力编写的所有自动化测试。此外,其交付管道包括 作为质量关卡 的测试。审查您更改的队友可能会对测试是否验证更改实现更有信心。

注意 持续交付需要整本书来介绍!我已经直接将其应用于基础设施,并试图在单节中涵盖它。如果您想查看一个更实际的例子,我创建了一个示例管道,mng.bz/mOy8。该管道使用 GitHub Actions 将 hello-world 服务部署到 Google Cloud Run。其阶段包括单元测试、测试环境部署和集成测试。

CD 应该涉及对代码的小幅度和频繁更改。您将这些更改自动推送到测试环境,并在将其推送到生产环境之前等待人工审批。然而,等待人工审批的更改会像交通堵塞一样积累。几辆减速的汽车可能会连锁反应到许多汽车,最终影响您的预期到达时间!

人工审批步骤会构建一批更改,这会引入一些问题。当您在图 7.5 中推送大量更改到生产环境时,您需要等待系统处理和部署这些更改。不幸的是,您也可能引入了意外的失败,因为一些更改存在冲突。您的团队可能需要花费数天时间追踪导致失败的更改组合。

当您使用 CD 时,尽可能快速地审批更改。为人工审批实施更短的反馈周期。您还可以限制一次审批的更改数量。这两种解决方案都减轻了人工审批引入的一些风险。我将在下一节中介绍另一种解决方案,该方案完全省略了人工审批过程。

7.1.3 持续部署

您能否通过在交付管道中消除人工阶段来防止大量更改?您可以!然而,在移除人工审批之前,您必须练习 CI/CD。

图片

图 7.5 避免在引入人工审批时推送大量更改到生产环境,以防止复杂的故障排除。

从您的管道中移除手动批准意味着您必须对您的测试有信心。图 7.6 中的管道增加了更多的集成和端到端测试来验证系统,并自动将更改推送到生产环境。您有信心认为您的测试充分检查了系统功能,并且您可以轻松地撤销更改。您移除了手动批准,并立即将更改提升到生产环境。

图片

图 7.6 持续部署完全自动化了测试和将更改应用到生产的过程。

持续部署消除了手动批准步骤,并直接从测试环境将更改提升到生产。

定义IaC 的持续部署:将基础设施更改部署和测试到测试环境,并在测试通过时自动将更改提升到生产。

自动部署防止了更改的拥堵。推送基础设施更改通常需要数小时,并影响未知依赖项。如果您有全面的测试策略和修复故障的熟悉度,您可以使用持续部署进行基础设施。

使用第十一章中的技术来修复故障可以帮助您练习持续部署。然而,大多数组织并没有完全接受持续部署用于他们的基础设施,因为他们对测试或撤销更改没有信心。在这些模式上投入时间和实践可以帮助您更接近持续部署模型。

7.1.4 选择交付方法

持续交付和部署创建了一个工作流程,用于测试和将 IaC 交付到生产环境。然而,您不能期望您的组织对直接自动化所有更改到生产感到舒适!我建议将持续交付和部署应用于基础设施变更管理。首先,您必须在选择交付方法之前对您实施的更改类型进行分类。

基础设施更改的类型

变更类型影响您将其交付到生产的方式。您需要与您组织的变更审查委员会合作,对变更类型进行分类,并为每个变更自动化测试和审查。否则,您可能会发现自己不符合审计要求。

想象一下,您每周都会对服务器进行常规更改。您使用新的标签更新服务器的 IaC。自动化从未改变,并且很少失败。当它确实失败时,您知道如何修复它。服务器的常规更改成为持续部署的良好候选。

在图 7.7 中,您将服务器更改直接连续部署到生产环境,无需手动批准。管道在测试环境之后替换了手动批准步骤,用测试来检查提交信息中的前缀。您的服务器更改具有标准更改的提交信息,因此管道绕过了手动批准。

你通常会定期对你的基础设施进行标准变更。标准变更的例子包括在编排器中升级容器镜像、部署新的队列或向你的监控系统添加新的警报。如果变更失败,你可以参考运行手册来回滚变更,而不会影响任何东西。

定义 基础设施的标准变更是一个常见实施、具有定义良好的行为和回滚计划的变更。

图片

图 7.7 在你的交付管道自动将变更推送到生产之前,标准或紧急变更可以有一个初始的同行评审。

为什么你应该考虑为标准变更使用持续部署?标准变更通常涉及一个常见的自动化、定义良好的修复。你不想让你的团队成员暂停来审查和批准重复的变更。标准变更会分散他们的注意力,使他们无法关注更重要的事。

其他类型的变更也受益于持续部署。想象一下,你发现服务器上的一个应用程序停止运行。你需要快速让应用程序重新运行。与其运行标准变更,不如在 IaC 中实施修复,并使用emergency更新其提交信息。在推送变更后,你的构建系统会绕过手动批准阶段,因为提交信息将变更标识为紧急。

除了持续交付标准变更外,你还可以选择为紧急情况持续交付紧急变更以修复生产。当可能时,使用 IaC 和提交信息来标识紧急变更,通过使用 IaC 推送修复。

定义 基础设施的紧急变更是指你必须快速在生产中实施以修复系统功能的变更。

紧急变更直接进入生产,无需手动批准,因为你通常需要快速修复系统。手动批准可能会阻碍问题的解决。因此,为紧急变更添加绕过有助于你快速解决问题并记录你的解决历史。

要持续部署标准和紧急变更,你必须在添加绕过手动批准步骤的能力之前在你的管道中进行自动化测试。此外,你需要标准化绕过提交信息的结构。绕过允许工程师在没有变更积压的情况下部署修复。它还允许合规性和安全团队审计变更序列。

我就不能手动运行一个紧急变更吗?

我强烈建议你使用 IaC 和你的交付管道来制作紧急变更。提交记录了解决步骤的历史,你的管道在运行自动化之前会测试你的变更,以防止系统变得更糟。

然而,当你试图快速推出修复时,你可能发现部署管道运行时间过长。你可能考虑进行手动变更,认识到你可能无法从管道中解决的自动化测试和检查中受益。

在进行手动变更后,将实际基础设施状态与预期的 IaC 进行核对。核对的做法涉及手动更新配置以匹配基础设施资源(请参阅第二章中介绍的技术)。

其他变更不应使用持续交付。想象一下,你被分配了一个新的项目。你需要启用所有网络上的 IPv6。通过进行这个网络变更,你可能会影响网络中的每个应用和系统!

对于这个新的和重大变更,你想跳过手动批准。你希望有经验的网络工程师审查你的基础设施即代码(IaC)。在图 7.8 中,你更新网络的 IaC 以 IPv6,并在生产前等待手动批准。手动批准步骤会通知其他应用和工程团队,如果变更失败,它们可能具有很大的影响范围。

图 7.8 新或重大变更在生产应用前应进行手动同行批准。

新或重大变更可能影响系统的架构、安全或可用性。这些变更应要求提交问题或工单,并提供一些理由和讨论。它们还涉及团队或公司内部同行对手动变更的审查。

定义一个对基础设施的新或重大变更可能影响系统的架构、安全或可用性。这些变更没有明确定义的实现或回滚计划。

重大变更可能产生重大影响或存在高风险失败的可能性。同样,新的或未知变更可能导致不可预测的结果和复杂的回滚步骤。请求手动批准表明,如果变更失败,你可能需要一些帮助。其他新的或重大变更的例子通常包括更新网络 CIDR 块(如果它们影响其他分配)、DNS、证书变更、升级工作负载编排器或平台重构(例如,向应用程序添加密钥管理器)。

基于变更类型的交付方法

在对所做的变更及其类型进行分类后,你可以决定你的交付方法。就大多数情况而言,标准和紧急变更使用持续交付,而新的和重大变更使用 CD。

表 7.1 概述了一些变更类型、它们的交付方法和一个示例。然而,这些一般实践有例外。一些标准变更可能需要持续交付,因为它们会影响其他资源。相比之下,与绿色(新)环境相关的变更可以实施持续交付方法,因为它不会影响其他系统。

表 7.1 变更类型和交付方法

变更类型 交付方法 生产前是否需要手动批准 示例
标准 持续交付 向扩展组添加服务器
紧急 持续交付或手动变更 将操作系统镜像回滚到先前的版本
主要 持续交付 为所有服务和基础设施启用 SSL
新增 持续交付 部署新的基础设施组件

持续集成、交付和部署也适用于软件开发生命周期。然而,将这些概念应用于基础设施生命周期将推动您组织变更和审查流程的极限。定期对您的变更进行分类并评估您的变更和审查流程可以帮助平衡生产力和治理,这是我作为模块共享实践的一部分提到的。

配置管理

配置管理工具应采用类似的方法来评估变更类型并应用持续交付或部署。

作为一般规则,确保快速审查和批准变更,并尽快将它们推入生产。大量变更的爆炸半径更大。如果您将每个变更作为一个批次推送并影响业务关键应用程序,您必须对批次中的每个变更进行故障排除。当您需要确定哪个变更影响了系统时,故障排除的复杂性会增加。

练习 7.1

在您的组织中选择一个标准的基础设施变更。您需要什么才能自信地将变更持续交付到生产环境中?持续部署呢?概述或绘制您的交付管道中的阶段。

请参阅附录 B 以获取练习的答案。

7.1.5 模块

模块的交付管道呢?您在第五章中学习了关于共享、发布和管理基础设施模块版本的内容,并在第六章中测试了它们的功能。我提到了自动化测试和发布模块过程的想法,但没有完全解释它们的交付管道。

基础设施模块的交付管道与我为生产配置概述的示例略有不同。您将图 7.9 中的交付管道更改,以便在测试后发布模块而不是交付到生产。您保留一个手动批准步骤,以便团队审查模块。

图片

图 7.9 在测试模块后,等待团队审查变更和测试结果,然后再更新和发布新模块版本。

您可以使用与配置相同的变更类型来对模块变更进行分类,包括标准、紧急、主要和新增变更。表 7.2 概述了变更类型、它们的交付方法和一个示例。大部分它们与为配置推荐的方法相匹配。

表 7.2 模块变更类型和交付方法

变更类型 交付方法 生产前手动批准? 示例
标准 持续部署 启用对现有默认参数的覆盖
紧急 持续部署或分支 将操作系统镜像回滚到先前版本
主要 持续交付 通过使用数据更新数据库或基础设施
新建 持续交付 部署新的服务器模块

一些模块更改可以从与主题专家(如数据库配置或其他涉及数据的专用基础设施)的审查或结对编程中受益。然而,模块上的紧急更改可能采用不同的交付方法。

模块上的紧急更改意味着将快速修复隔离到模块的不同版本。你可以通过两种方式实现隔离。你可以实施修复并持续部署和发布带有更改的新模块版本。或者,你可以创建模块存储库的 分支 并更新你的基础设施配置以引用该分支。

定义 版本控制中的 分支 是指向代码快照的指针。它允许你基于该快照单独实施更改。

验证分支后,你可以使用标准更改更新模块的主分支。分支模块可以帮助你快速实施紧急模块更改,并在以后协调模块更改。

如果我知道其他团队锁定他们的版本,我更喜欢持续部署带有修复的新模块版本。虽然分支可以隔离紧急更改,但我必须记得将其合并回模块的主发布版本。在下一节中,你将了解分支模型以及如何将它们应用于 IaC 更改。

图像构建和配置管理

图像构建和配置管理模块的交付管道遵循与提供工具模块类似的方法。在将它们部署到生产之前,确保对图像进行版本控制和测试更改。

7.2 分支模型

除了实施持续交付或部署之外,你还需要标准化更改合并到主配置的方式。版本控制中的主分支是配置的真相来源。更新配置需要团队内部额外的协调和协作。

假设你想要在队友刷新其许可证的同时减少防火墙的访问权限。你的团队有一个 CD 管道来测试和手动批准对生产防火墙的更改。然而,你和队友的更改提出了两个问题。

首先,你们两个人如何独立地工作和测试你们的更改?其次,你们如何控制哪个更改应该先进行?图 7.10 概述了你们和你们的队友在谁应该先部署他们的更改方面的困境。你们想要避免同时推送两个更改。如果防火墙导致网络访问失败,你们将不知道是哪个更改导致了问题。

图片

图 7.10 即使你有 CD 管道将更改部署到防火墙,你仍需要额外的开发协调来识别哪些更改必须首先应用。

分支模型 协调你的团队如何使用版本控制来启用并行工作,同时最小化中断和故障排除复杂性。你可以选择两种类型的分支模型:基于特征的或基于主干线的开发。

定义 分支模型 定义了你的团队如何使用版本控制来启用并行工作并解决他们努力中的冲突。

每种分支模型在实施过程中都伴随着其复杂性,尤其是在基础设施即代码(IaC)中。我将描述如何将这两种开发模型应用于协调你和你的队友之间的防火墙规则和许可证变更。然后,我将讨论每种方法的局限性以及你的团队如何进行选择。

7.2.1 基于特征的开发

如果你和你的队友在合并之前能够在隔离状态下对变更进行工作会怎样?如果你的队友创建了一个包含许可证变更的 分支,而你创建了一个包含防火墙变更的分支,你将你的变更彼此隔离。当你们都完成时,你们将变更合并到主分支,并解决彼此之间的冲突。

图 7.11 展示了你和你队友如何在不同的分支上编排你们的变更。你将你的分支命名为 TICKET-002 以表示防火墙规则,而你的队友将他们的分支命名为 TICKET-005 以表示许可证更新。你的防火墙规则变更首先获得批准,因此你将它们放入主分支并部署到生产环境中。你的队友继续进行许可证更新工作。他们在合并变更回主分支之前,将你的防火墙规则更新检索到他们的分支中进行进一步测试。

图片

图 7.11 当你使用基于特征的开发时,你将你的变更隔离到你的分支中,并与主配置进行冲突协调。

基于特征的开发 允许你通过将变更隔离到分支中来独立于你的队友进行变更的演进。

定义 基于特征的开发(也称为 功能分支Git Flow)是一种将不同的变更分离到单独分支的分支模式。在测试后,你将特定分支上的变更合并到主配置中。

基于特征的开发流程有助于你专注于你的变更的可组合性,而无需考虑其他人的变更。然而,你需要不断地从主分支获取变更,并将它们与你自己的分支进行协调。当每个团队成员勤奋地更新和测试他们的分支时,基于特征的开发效果最佳。

让我们看看基于特征的开发在实际中的应用。想象一下,你通过从版本控制中克隆配置的本地副本来开始基于特征的防火墙开发工作流程:

$ git clone git@github.com:myorganization/firewall.git

你创建一个分支,这将为你的更新创建一个指针。我建议将分支命名为与变更相关的票号(例如 TICKET-002),尽管你也可以使用描述性的破折号分隔的名称:

$ git checkout -b TICKET-002

你在分支上对防火墙规则进行更改。然后你使用命令行将你的更改提交到你的本地分支:

$ git commit -m "TICKET-002 Only allow traffic from database"
[TICKET-002 cdc9056] TICKET-002 Only allow traffic from database
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 firewall.py

你在本地有你的更改,但你想让别人审查你的更改。你将更改推送到远程分支:

$ git push --set-upstream origin TICKET-002
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (5/5), 1.06 KiB | 1.06 MiB/s, done.
Total 5 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To github.com:myorganization/firewall.git
 * [new branch]      TICKET-002 -> TICKET-002
Branch 'TICKET-002' set up to track remote branch 
➥'TICKET-002' from 'origin'.

同时,你的队友在TICKET-005上工作,更新许可证。他们创建了一个名为TICKET-005的新分支,其中包含的更改不包括你的防火墙规则更新。注意,你的分支不包括他们的更新许可证,他们的分支不包括你的更新防火墙规则。你可以审查两个分支之间的差异:

$ git diff TICKET-002..TICKET-005
diff --git a/firewall.py b/firewall.py
index 74daecd..aaf6cf4 100644
--- firewall.py
+++ firewall.py
@@ -1,3 +1,3 @@
-print("License number is 1234")
+print("License number is 5678")

-print("Firewall rules should allow from database and deny by default.")
\ No newline at end of file
+print("Firewall rules allow all.")
\ No newline at end of file

你打开一个拉取请求,通知你的团队你已经完成了你的更改。

定义 A 拉取请求通知存储库的维护者,你有一些外部更改希望合并到主配置中。

你将更改顾问委员会的成员添加到审查你的拉取请求中。他们批准更改,然后你将你的更改合并回主分支。

你的队友尚未获得更新许可证的批准。为了确保他们不会影响生产配置,他们需要从主分支检索所有更改,包括你在TICKET-002中的更改:

$ git checkout main
Switched to branch 'main'
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
  (use "git pull" to update your local branch)

$ git pull --rebase
Updating 22280e7..084855a
Fast-forward
 firewall.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

然后他们回到他们名为TICKET-005的分支,并将主分支的更改合并到TICKET-005分支:

$ git checkout TICKET-005
Switched to branch 'TICKET-005'
Your branch is up to date with 'origin/TICKET-005'.

$ git merge main
Auto-merging firewall.py
Merge made by the 'recursive' strategy.
 firewall.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

当你的队友审查防火墙配置时,他们会找到来自TICKET-002的你的更改。他们可以将主分支的更改更新到他们的分支:

$ git push --set-upstream origin TICKET-005
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (5/5), 1.06 KiB | 1.06 MiB/s, done.
Total 5 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To github.com:myorganization/firewall.git
 * [new branch]      TICKET-005 -> TICKET-005
Branch 'TICKET-005' set up to track remote branch 'TICKET-005' from 'origin'.

一旦你的队友的更改获得批准,你的队友可以将新的防火墙许可证合并到主分支。

基于特性的开发需要每个团队成员进行多个步骤。你可以通过自动化测试和合并过程来简化工作流程。使用交付管道来组织跨分支的更改。

图 7.12 为你和你的队友组织了基于特性的交付管道开发工作流程。你和你的队友各自获得一个带有自己测试环境的分支管道。例如,你有一个名为TICKET-002的分支和一个隔离你更改的新防火墙环境。你在TICKET-002防火墙环境中运行单元测试,部署更改,并运行集成和端到端测试。

图 7.12

图 7.12 你可以使用基于特性的开发来隔离分支上你的更改的测试。

一旦你的分支测试通过,你将更改合并到主分支。在你工作于你的更改的同时,你的队友分别创建了他们自己的名为TICKET-005的分支和防火墙环境。你的队友意识到你最近更新了防火墙配置。

在图 7.12 中,你的队友从主分支检索更改,并确保这些更改仍然与他们的分支和环境兼容。一旦你的队友在他们分支上运行相同的单元、集成和端到端测试,他们将合并TICKET-005更改到主分支以进行生产部署。

为什么为每个分支创建一个测试环境?每个分支的测试环境可以隔离您的更改,并相对于主分支进行测试。作为临时环境,分支的测试环境可以最小化对持久测试环境的需求,并降低整体基础设施成本。然而,创建测试环境可能需要一些时间。

您的团队从基于功能的开发中获得一些好处,包括以下内容:

  • 能够隔离分支上的更改。

  • 在分支内测试更改的能力。

  • 隐含的同行评审步骤。只有当有人批准更改时,您才能将更改合并到生产环境中。

  • 在分支上分离紧急更改。在验证紧急更改后,您可以将它合并到主分支。

幸运的是,代码库托管服务(例如,GitHub 或 GitLab)具有可以帮助您自动化基于功能的开发模型的功能。这些功能包括用于跟踪特定功能的标签、在合并分支之前进行集成测试的状态检查,以及自动删除旧分支。您还可以定义一个审阅者列表,并自动将他们添加到拉取请求中。

7.2.2 主干开发

假设您的组织不想为每个分支创建测试环境,并且许多工程师对基于功能的开发工作流程感到不舒服。相反,您和您的队友可以在主分支上一起工作。

图 7.13 展示了您和您的队友如何在主分支上进行协作。您首先更新防火墙规则并推送更改。然后您的队友更新他们的本地仓库以包含您的更改,并将他们的更改推送到主分支。

图 7.13 当您使用主干开发时,您维护一个主分支并直接更新生产配置。

由于您俩都推送到一个分支,工作流程看起来更加流畅。主干开发 意味着您直接将更改推送到主分支,而不在版本控制中隔离您的更改。

定义 主干开发(也称为 推送到主分支)是一种将所有更改直接推送到主分支的分支模式。它倾向于进行小改动,并使用测试环境来验证更改是否成功。

主干开发不允许您独立于队友进行更改的演变。然而,这种限制变成了一个优势。主干开发迫使您以 特定顺序 实施更改。您可以快速识别导致更改的提交并解决它。该模式提供了一种有见地的方法来编排和应用基础设施即代码(IaC)更改。

让我们将主干开发应用于您的防火墙规则和您的队友的防火墙许可证更新。您首先从版本控制中克隆防火墙配置的本地副本。当您克隆配置时,您可以检查主分支:

$ git clone git@github.com:myorganization/firewall.git
$ git branch --show-currentmain

你将防火墙规则更改提交到主分支。提交你的更改:

$ git commit -m "TICKET-002 Only allow traffic from database"
[TICKET-002 cdc9056] TICKET-002 Only allow traffic from database
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 firewall.py

更新你的本地副本以确保从主分支检索新更改。使用 git pull --rebase 从远程仓库获取更改,将它们合并到你的本地副本中,并与远程历史记录进行变基:

$ git pull --rebase
Already up to date.

现在,你可以将更改推送到主分支。你的推送应该启动图 7.14 所示的交付流水线。你的流水线会对测试环境执行单元和集成测试。在所有测试阶段通过后,流水线等待你的团队手动批准。你的队友可以审查你的更改并批准它们。一旦他们批准了你的更改,流水线就会将你的防火墙规则更改部署到生产环境中。

图 7.14 基于主干线的开发需要一条流水线来持续将其交付到生产环境中。

如果你的防火墙规则更改失败,它会在进入生产之前停止交付流水线。你会在测试环境阶段注意到失败并撤销更改。其他人可以继续将他们的更改推送到生产环境,而你则需要实施修复。

基于主干线的开发高度依赖于交付流水线来测试和部署更改。交付流水线应包括一个持久测试环境来评估更改之间的冲突。虽然持久测试环境会产生成本,但该环境更准确地反映了更改在生产中的行为。

基于主干线的开发的工作流程步骤非常少。大多数基础设施团队发现这个工作流程在做出更改时很有帮助,因为它按照特定的顺序排列更改。基于主干线的开发创建了一个持续反馈循环,展示了不同的更改如何相互影响。它还促进了一种实践,即进行小幅度更改,解决主配置的更新,并将更改推送到生产环境。当你的团队更改发生冲突时,你可以快速识别哪些依赖项影响了测试环境。

然而,基于主干线的开发确实需要练习来解决更改并保持纪律以协调更改。你不会隔离你的更改,在一个分支上工作可能会使协作变得具有挑战性。一旦你解决了最初的协作冲突,你可能会发现基于主干线的开发为你团队中的 IaC 更改提供了更好的可见性。

7.2.3 选择分支模型

我与软件和基础设施团队花费了数小时讨论基于功能或基于主干线的开发的优点。在这些会议结束时,我总是意识到分支模型的选择取决于团队的舒适度、规模和环境设置。本节涵盖了将这两种分支模型应用于基础设施时的某些局限性和担忧。

基于功能的开发挑战

许多应用程序和基础设施的开源项目成功使用了基于特性的开发。基于特性的开发提供了一个框架,可以独立测试和评估关键更改。它将更改分散到许多协作者之间,并在合并到主分支之前强制执行手动审查阶段。源代码控制或 CI 框架提供了原生集成以支持基于特性的开发。

IaC 从基于特性的开发中获得了相同的益处。团队在将基础设施更改推送到生产之前,可以将其隔离到分支中。图 7.15 测试了您在TICKET-002环境中对防火墙规则更改的验证,来自您的队友在TICKET-005环境中的许可更改。您可以在您的分支上应用更改,而不会与其他人冲突。

图片

图 7.15 您可以为每个特性分支创建一个新的测试环境,以验证单个更改。

然而,基于特性的开发有几个挑战。首先,创建新环境可能需要时间和金钱(有关成本管理,请参阅第十二章)。当多个团队成员在许多分支上配置时,您的管道可能难以启动新环境。

为了加快测试环境的创建,您可以为您的管道框架投资运行器以并行运行测试。或者,您也可以为所有分支创建一个持久测试环境。然而,基于特性的开发可能会在持久测试环境中引起冲突,因为每个分支都是异步应用更改的。

图片

图 7.16 您可以省略分支的集成和端到端测试,以减轻多个环境和管道并发成本。

而不是创建一个持久的测试环境,您还可以省略除了主分支之外的所有分支的集成和端到端测试,以优化成本。例如,您的防火墙更改可能只需要静态分析、单元测试和团队审查,然后才能合并到生产中,如图 7.16 所示。您不需要为分支创建唯一的测试环境,在人工审查后,将分支合并到生产中。

您在基于特性的开发中面临的另一个挑战涉及版本控制的纪律和熟悉度。如果您还没有使用版本控制,您需要习惯特性分支的工作流程。该工作流程增加了逆向工程合并更改和解决冲突的挑战。

例如,有人可能在本周末创建一个分支来修复防火墙规则。他们忘记合并热修复,而您不知道他们更改了防火墙。在更新防火墙规则时,您意外地覆盖了他们的配置!随着时间的推移,您积累了大量的分支,必须解决哪些分支已经应用。

你还会遇到长期分支的挑战。想象一下,你的队友已经花了一个月的时间更新许可证。他们创建了一个名为 TICKET-005 的新分支。每隔几天,他们需要检查主分支的更新并将它们添加到他们的修复中。

有一天,你需要进行一个依赖于你的队友许可证更新的变更。你开始在名为 TICKET-002 的分支上工作你的变更,如图 7.17 所示。你完成了工作,但意识到你的队友在 TICKET-005 上还有工作要做!你等待了另外两个月,直到你的队友完成他们的防火墙许可证更新。一旦他们完成,你花费数小时更新你的 TICKET-002 分支,以便最终将你的变更部署到生产环境中。

图片

图 7.17 长寿命的分支可能会阻止其他变更部署到生产环境,并在分支过程中引入复杂性。

基于功能的开发鼓励你长时间保留分支。你必须警惕地更新长期分支,以跟上主分支。否则,你将遇到难以轻易解决的冲突。有时,你唯一的解决方案是删除你废弃的分支,并在新的、更新的分支上重新开始你的变更。

基于主干开发的挑战

基于主干的开发与基础设施变更和缓解环境之间配置漂移的需求相得益彰。你省略了合并和管理功能分支的复杂性,特别是如果你需要建立 Git 技能的信心。

基于主干开发倾向于小变更而不是大而重要的变更。你逐步实施变更,而不是一次性测试它们。在第十章中,我将介绍使用功能切换来逐步实施一组变更并降低对基础设施的风险。

基于主干开发有几个缺点。在将变更推送到生产之前,它需要一个专门的测试环境。图 7.18 概述了基于主干开发的理想工作流程。在你运行单元测试后,你将变更部署到一个长期测试环境中进行集成和端到端测试。如果变更在测试环境中通过测试,它可以接受队友的审查。一旦他们批准变更,它将进入生产环境进行集成和端到端测试。

图片

图 7.18 基于主干的开发需要一个专门的测试环境来模拟生产中的变更,并帮助建立同行审查的信心。

你需要彻底的单元测试来格式化和检查团队的标准,以及集成测试来验证功能。一个持久的测试环境会增加基于主干开发的总体成本。更小、更模块化的基础设施配置可以减少资源或模块内的冲突,并降低测试所需基础设施的总体成本。

你可能会发现,基于主干的发展与手动变更审批相冲突。手动变更审批只能在有人将更改推送到主分支之后进行。你的审阅者需要知道你的变更是否成功,他们才能验证其格式和配置。如果你将一个损坏的配置推送到测试环境,你需要迅速识别并回滚,以免其他人审阅。

表 7.3 总结了基于特征和基于主干发展的优点和局限性。选择取决于你的团队配置的基础设施类型及其对版本控制的熟悉程度。

表 7.3 基于特征和基于主干的发展比较

开发模型 优点 局限性
基于特征的发展 使用分支隔离变更,使用分支隔离测试,组织代码的手动审查,跨多个团队和协作者扩展 需要勤奋和熟悉更新分支,鼓励长期分支,增加金钱和时间成本
基于主干的发展 提供更好的变更行为表示,使用一个版本控制工作流程处理所有变更,鼓励增量基础设施变更以减少影响范围 需要长期测试环境,不包括手动审查阶段,需要纪律和组织才能跨多个团队和协作者扩展

你必须在团队内部建立并达成一致的开发模型。对开发模型的达成一致有助于促进变更的可重复性和系统的整体可用性。注意每种方法的局限性,并始终尽量保持你的变更尽可能小。无论你的团队采用哪种模型,尽可能频繁地将变更应用到生产环境中,以减少变更的影响范围。

7.3 同行评审

在本章和第五章中,我强调了在交付管道和模块变更中包含审查步骤的重要性。为什么你应该花时间审查你的队友的 IaC?在审查时你应该寻找什么?

同行评审允许你的队友检查你的基础设施配置以获得建议、标准和格式。

定义 同行评审 是一种允许你的队友或其他团队检查你的基础设施配置以获得建议、标准和格式的实践。

作为审阅者,我关注的是配置是否能够在团队间扩展,保持安全,或者影响高级基础设施依赖。这种审阅视角有时会阻止变更合并到生产环境中。然而,同行评审过程为团队提供了标准化实践和新模式的教育机会。你和你的团队可能需要花时间讨论设计或实现的优点或缺点。

要理解同行评审的重要性和缺点,想象一个新的库存团队需要读取 GCP 项目的访问权限。在列表 7.1 中,您更新了访问管理规则的代码,从 JSON 对象中读取用户列表。新代码通过了所有测试,您等待了几天,等待您的队友审查更改。

列表 7.1 向 GCP 项目添加新团队的首次实现

import json

GCP_PROJECT_USERS = [                                   ❶
   (
       'operations',
       'group:team-operations@example.com',
       'roles/editor'
   ),
   (
       'inventory',                                     ❷
       'group:inventory@example.com',                   ❷
       'roles/viewer'                                   ❷
   )
]

class GCPProjectUsers:                                  ❸
   def __init__(self, project, users):
       self._project = project
       self._users = users
       self.resources = self._build()                   ❹

   def _build(self):                                    ❹
       resources = []
       for user, member, role in self._users:           ❺
           resources.append({                           ❺
               'google_project_iam_member': [{          ❺
                   user: [{                             ❺
                       'role': role,                    ❺
                       'member': member,                ❺
                       'project': self._project         ❺
                   }]                                   ❺
               }]                                       ❺
           })                                           ❺
       return {
           'resource': resources
       }

if __name__ == "__main__":
   with open('main.tf.json', 'w') as outfile:           ❻
       json.dump(GCPProjectUsers(                       ❻
           'infrastructure-as-code-book',               ❻
           GCP_PROJECT_USERS).resources, outfile,       ❻
           sort_keys=True, indent=2)                    ❼

❶ 定义要添加到 GCP 项目的用户和组列表

❷ 将库存团队作为只读组添加到项目中

❸ 为 GCP 项目用户创建一个模块,该模块使用工厂模式将用户附加到角色

❹ 使用该模块为要附加到 GCP 角色的用户列表创建 JSON 配置

❺ 对于列表中的每个组,创建一个 Google 项目 IAM 成员,并将用户附加到其分配的角色。此资源将用户添加到 GCP 的角色中。

❻ 将 Python 字典写入由 Terraform 稍后执行的 JSON 文件

❼ 当您将 JSON 文件写入由 Terraform 执行的文件时,使用两个空格缩进。

AWS 和 Azure 等效

要将代码列表转换为 AWS,您需要将 GCP 项目的引用映射到 AWS 账户。GCP 项目用户与 AWS IAM 用户相对应。同样,您会创建一个 Azure 订阅并将用户账户添加到 Azure Active Directory。

您等待三天后,您的队友返回并提供了以下反馈:

  • 您必须使用四个空格缩进您的 JSON 基础设施配置。

  • 您必须将组重命名为team-inventory@example.com

  • 您必须将库存团队添加到viewer角色的用户列表中,而不是为该组定义角色。

您的队友解释说,前两个符合团队标准。最后一个要求符合访问控制权威绑定安全标准(它定义了角色的用户列表而不是将角色添加到用户)。您已经因为等待同行评审而延迟了三天!现在,您需要修复它并等待另外几天以获得批准。

记住第六章中您想要将孤岛知识的未知已知捕获到测试中。您的队友有一些您不知道的知识。您决定添加一些单元测试来帮助您记住团队标准。

列表 7.2 中的新代码包括新的单元测试(linting 规则),以验证您的团队配置和安全标准。一个测试检查 JSON 中是否有四个空格的正确缩进。另一个测试检查所有组是否符合命名标准。最后一个测试检查您是否使用了正确的资源将用户绑定到角色。

列表 7.2 向团队开发标准添加单元测试以进行 lint

import pytest
from main import GCP_PROJECT_USERS, GCPProjectUsers                        ❶

GROUP_CONFIGURATION_FILE = 'main.tf.json'                                  ❷

@pytest.fixture                                                            ❷
def json():                                                                ❷
   with open(GROUP_CONFIGURATION_FILE, 'r') as f:                          ❷
       return f.readlines()                                                ❷

@pytest.fixture                                                            ❸
def users():                                                               ❸
   return GCP_PROJECT_USERS                                                ❸

@pytest.fixture                                                            ❹
def binding():                                                             ❹
   return GCPProjectUsers(                                                 ❹
       'testing',                                                          ❹
       [('test', 'test', 'roles/test')]).resources['resource'][0]          ❹

def test_json_configuration_for_indentation(json):                         ❺
   assert len(json[1]) - len(json[1].lstrip()) == 4, \                     ❺
       "output JSON with indent of 4"                                      ❺

def test_user_configuration_for_standard_team_name(users):                 ❸
   for _, member, _ in GCP_PROJECT_USERS:                                  ❸
       assert member.startswith('team-'), \                                ❸
           "group should always start with `team-`"                        ❸

def test_authoritative_project_iam_binding(binding):                       ❹
   assert 'google_project_iam_binding' in binding.keys(), \                ❻
       "use `google_project_iam_binding` to add team members to roles"     ❻

❶ 导入 GCP 用户和角色的列表

❷ 使用 Python 读取 Terraform JSON 配置文件。测试使用此固定值来验证 JSON 具有四个空格的缩进。

❸ 将 GCP 用户和角色的列表(包括库存团队)作为固定值导入测试。测试检查每个用户都有一个“team-”前缀来识别它作为一个组。

❹ 使用工厂模块创建一个示例 GCP 项目用户

❺ 使用 Python 读取 Terraform JSON 配置文件。测试使用此固定值来验证 JSON 有四个空格的缩进。

❻ 检查工厂模块是否使用了正确的 Google 项目 IAM 绑定 Terraform 资源而不是成员。这使用权威绑定将团队成员添加到特定角色

AWS 和 Azure 的等效资源

GCP 项目 IAM 绑定类似于aws_iam_policy_attachment Terraform 资源(mng.bz/5QW7)。绑定或附件会权威性地撤销任何未定义为 Terraform 资源一部分的用户。在发布时,Azure 的访问控制模型使用的是累加策略方法,并且没有明确的方式来定义权威的角色附加或绑定定义。

由于自动代码审查和单元测试,你可以在同行评审之前纠正错误并缩短反馈循环。你的队友不需要对格式和标准吹毛求疵。然而,你和你的队友仍在争论是否应该将用户添加到角色中,或者将角色添加到用户中。你决定将这个架构决策提交给更广泛的团队进行考虑。

图 7.19 演示了有效的同行评审遵循示例的工作流程,将自动化测试与更广泛的架构讨论相结合。在自动化测试、同行评审和协作之间,你维护了安全、弹性和可扩展的 IaC。

图 7.19 自动化一些检查并保持对任何手动审查过程的意识将有助于加快同行评审。

然而,测试自动化和审阅者并不能捕捉到所有内容。在开发过程的后期进行同行评审可能会令人沮丧。为了在 IaC 编写过程中尽早解决差距和提出架构问题,你可以与队友一起编程。这种技术称为结对编程,使用两位工程师来减轻同行评审的摩擦。

定义结对编程是两位程序员在一台工作站上共同工作的实践。

一位工程师可能会捕捉到另一位没有意识到的东西,反之亦然。结对编程有许多挑战,包括资源限制和个性冲突。大多数公司不采用它,因为它最初会减慢交付速度并影响团队容量。有些人不喜欢它,因为他们的配对伙伴可能工作速度不同。结对编程需要自我意识和纪律。

尽可能地进行 IaC 的结对编程。基础设施通常包括特定的术语和制度知识。例如,当前和未来的团队成员必须理解为什么有人为项目访问控制使用了权威绑定。结对编程促进了知识共享,并在开发过程中内置了变更审查。随着时间的推移,你的团队在快速交付基础设施变更方面变得更加熟练,而无需手动变更审查的摩擦。

注意同行(或代码)评审和结对编程应该为团队中的每个人提供一个安全的空间,以便学习如何考虑最佳实践来编写代码。关于这些过程的特定细节超出了本书的范围。有关代码评审的更多信息,我建议查看谷歌的工程实践mng.bz/6XNR。有关结对编程的更多信息,请参阅mng.bz/o2GD。你可以使用各种技术来平衡配对关系,例如每 30 分钟切换键盘或指定驾驶员和导航员。

基础设施的改变可能会影响关键业务系统的可用性。批量进行许多改变可能会加剧故障,因为难以追踪到一个根本原因。如果你能通过组合结对编程和测试自动化来缩短同行评审过程,你就可以专注于审查基础设施变更的架构和影响。

7.4 GitOps

当你结合持续部署、声明性配置、漂移检测和版本控制时会发生什么?所有这些模式似乎相当不同,但将它们一起使用为管理基础设施提供了一种有见地的方法。你声明你希望为基础设施设置的配置,将其添加到版本控制中,并将其部署到生产环境中。

想象一下,你想将支付服务从版本 3.0 更新到 3.2。支付服务运行在工作负载编排器(例如,Kubernetes)上。编排器提供了一个使用 DSL 的声明性配置接口。你可以传递 YAML 文件来配置编排器中的资源。

图 7.20 实现了一个通过结合持续部署、声明性配置和版本控制来响应变更的工作流程。你使用版本 3.2 更新声明性配置。一个控制器检测当前配置与版本控制中的配置之间的偏差。它启动一个交付管道来部署新版本并运行测试以检查其功能。

图 7.20

图 7.20 GitOps 的一个实现可以使用基于特性的开发来打开一个拉取请求,在分支上测试变更,添加审阅者,并合并变更。

为什么你要让控制器持续检测和应用变更?控制器减少了预期配置与实际状态之间的偏差。这确保了你的系统保持更新。

您可能会从这个工作流程中感受到似曾相识的感觉。毕竟,它结合了所有关于编写基础设施即代码(IaC)、测试和交付的实践。本书对 IaC 持有非常坚定的立场,与称为 GitOps 的概念相一致。GitOps定义了一种方法,允许团队通过版本控制管理基础设施变更,通过 IaC 进行声明性更改,并持续部署更新到基础设施。

定义:GitOps是一种使用声明性 IaC 通过版本控制管理基础设施变更并将其持续部署到生产的方法。

您最常将 GitOps 与 Kubernetes 生态系统联系起来。然而,GitOps 为在整个组织中扩展 IaC 实践提供了一个有见地的范例。您不再通过填写包含相关详细信息的工单来实施更改。

相反,组织中的任何人都可分支 IaC 并提交更改。持续部署减少漂移,保持基础设施更新,并始终运行测试。您可以通过拉取请求和提交历史记录跟踪谁请求并进行了更改。

注意:想了解更多关于 GitOps 和 Kubernetes 的信息,请参阅 Billy Yuen 等人所著的GitOps and Kubernetes(Manning,2021)。您可以在opengitops.dev找到关于 GitOps 的一般实践。

摘要

  • 将基础设施更改交付到生产通常涉及一个变更审查流程,该流程手动验证更改的架构和影响。

  • 持续集成涉及频繁地将更改合并到基础设施配置的主分支。

  • 持续交付将更改部署到测试环境进行自动测试,并在将其推送到生产之前等待手动审批。

  • 持续部署直接将更改部署到生产环境,无需手动审批阶段。

  • 您可以根据更改的类型和频率,使用持续集成、交付或部署来推送 IaC 更改,并带有自动测试。

  • 您的团队可以通过使用基于特性的开发或主干开发来协作进行 IaC。

  • 基于特性的开发为每个更改创建一个分支,允许隔离测试,但需要熟悉版本控制实践。

  • 基于主干的开发将所有更改应用于主分支,这会识别更改之间的冲突,但在生产之前需要测试环境。

  • 您可以自动化检查格式和标准,并手动审查配置架构和依赖关系。

  • 配对编程可以帮助在开发早期阶段识别更改中的冲突和问题。

  • GitOps 结合了版本控制、声明性基础设施配置和持续部署,使任何人都能通过代码提交自动化基础设施变更。

8 安全和合规

本章涵盖

  • 在 IaC 中选择凭证和秘密的保护措施

  • 实施政策以确保合规和安全的架构

  • 准备端到端测试以确保安全和合规

在前面的章节中,我提到了确保基础设施代码的安全以及检查其是否符合你组织的安全和合规要求的重要性。通常,你不会处理这些要求直到你的工程过程后期。到那时,你可能已经部署了一个不安全的配置或违反了关于数据隐私的合规要求!

例如,想象一下你为一家名为 uDress 的零售公司工作。你的团队有六个月的时间在 GCP 上构建一个新的前端应用程序。公司需要在假日季节前使其可用。你的团队非常努力地工作,开发出足够的功能以便上线。然而,在你部署和测试新应用程序的一个月前,合规和安全团队进行了一次审计——你失败了。

现在,你需要在你的待办事项中添加新的项目来解决安全和合规问题,并遵守公司政策。不幸的是,这些修复延迟了你的交付时间表,在最坏的情况下,破坏了功能。你可能希望从一开始就知道这些,至少这样你可以为它们做出计划!

你的公司的政策确保系统符合安全、审计和组织要求。此外,你的安全或合规团队通常根据行业、国家等因素定义政策。

定义 A 政策 是你组织中的一套规则和标准,以确保符合安全、行业或监管要求。

本章将教你如何保护凭证和秘密,并编写测试以强制执行安全和合规政策。如果你在编写 IaC 之前考虑这些实践,你可以构建安全、合规的基础设施,并避免交付时间表上的延误。受一位我以前共事过的经理的启发,“我们正在将安全烘焙到基础设施中,而不是在之后添加糖霜。”

8.1 管理访问和秘密

我已经在第二章中介绍了将“烘焙”安全理念融入 IaC 的想法。IaC 使用两套秘密。你使用 API 凭证来自动化基础设施,并将敏感变量(如密码)传递给资源。你可以在秘密管理器中存储这两个秘密以处理其保护和轮换。

在本节中,我专注于确保 IaC 交付管道的安全。IaC 表达了基础设施的预期状态,这通常包括根密码、用户名、私钥和其他敏感信息。基础设施交付管道控制需要这些信息的基础设施的部署和发布。

让我们设想你为新的 uDress 系统构建交付管道以部署基础设施。这些管道使用一组基础设施提供者凭证来创建和更新资源。每个管道还从密钥管理器中读取数据库密码,并将其作为属性传递以创建数据库。

你的安全团队指出你方法中的两个问题。首先,基础设施交付管道使用完整的管理凭证来配置 GCP。其次,你的团队的交付管道意外地在日志中打印出了根数据库密码!

你的交付管道增加了你系统攻击面(不同攻击点的总和)。

定义 攻击面 描述了未经授权的用户可以损害系统的不同攻击点的总和。

任何人都可以使用管理凭证或根数据库密码来获取信息并损害你的系统。你需要一个解决方案来更好地保护凭证和数据库密码。该解决方案应有望最小化攻击面。

8.1.1 最小权限原则

IaC 交付管道存在攻击点,允许未经授权的用户使用具有提升访问权限的凭证。例如,你使用第七章构建了一个管道,该管道持续将基础设施更改交付到生产环境中。该管道需要一些权限来更改 GCP 中的基础设施。

初始时,你的团队授予管道完整的管理凭证,以便它可以创建 GCP 中的所有资源。如果有人访问这些凭证,他们可以在 uDress 系统中创建和更新任何内容!有人可能会利用你团队的管道来运行机器学习模型或访问其他客户数据!

管道不需要访问每个资源。你决定更新凭证,使其仅使用更新特定资源所需的最小权限集。你确定 IaC 仅创建网络、Google App Engine 和 Cloud SQL 资源。你从凭证中移除了管理访问权限,并用对三个资源的写入访问权限替换它们。

当管道运行时,如图 8.1 所示,新的凭证恰好有足够的访问权限来更新三组资源。它还在部署网络、应用程序和数据库更新之前从密钥管理器中检索数据库密码。在将更改部署到测试环境后,你添加了一个单元测试来验证凭证不再具有管理访问权限。

图片

图 8.1 从 uDress 前端交付管道中移除管理凭证,并限制其访问网络、应用程序和数据库。

你通过使用最小权限原则解决了管道凭证的安全问题。这个原则确保用户或服务帐户只能获得完成其任务所需的最小访问权限。

定义 最小权限原则 指出,用户或服务帐户应具有对系统的最低访问要求。他们应该只有完成其任务所需的最少权限。

维护最小权限原则需要时间和精力。您通常在向 IaC 添加新资源时更改访问权限。通常,将角色附加到交付管道凭证。将访问权限分组到角色中有助于提高可组合性,因此您可以按需添加和删除访问权限。

将第三章中的模块实践应用于提供权限集模块。例如,您可以为 uDress 的 Web 应用程序提供一个工厂模块来自定义网络、应用程序和数据库的写入访问权限。任何 Web 应用程序都可以使用该模块,并正确地复制它所需的最低权限集。

让我们使用访问管理模块在列表 8.1 中实现 uDress 前端交付管道的最小权限访问管理。您将管道限制为网络、应用程序和 Cloud SQL 管理凭证。这些凭证允许管道创建、删除和更新网络、应用程序和数据库,但不能更新其他资源类型。

列表 8.1 前端最小权限访问管理策略

import json
import iam                                                      ❶

def build_frontend_configuration():
   name = 'frontend'
   roles = [
       'roles/compute.networkAdmin',                            ❷
       'roles/appengine.appAdmin',                              ❷
       'roles/cloudsql.admin'                                   ❷
   ]

   frontend = iam.ApplicationFactoryModule(name, roles)         ❶
   resources = {
       'resource': frontend._build()                            ❸
   }
   return resources

if __name__ == "__main__":
   resources = build_frontend_configuration()                   ❸

   with open('main.tf.json', 'w') as outfile:                   ❹
       json.dump(resources, outfile, sort_keys=True, indent=4)  ❹

❶ 根据服务帐户的角色列表创建角色配置,包括网络、App Engine 和 Cloud SQL

❷ 导入应用程序访问管理工厂模块以创建前端应用程序的访问管理角色

❸ 使用该方法创建管道访问权限的 JSON 配置

❹ 将 Python 字典写入 JSON 文件,以便稍后由 Terraform 执行

AWS 和 Azure 的等效功能

Google App Engine 类似于 AWS Elastic Beanstalk 或 Azure App Service,它们将 Web 应用程序和服务部署到提供商管理的环境中。

Google Cloud SQL 类似于 Amazon 关系数据库服务(RDS),它部署不同的托管数据库。Azure 为特定数据库提供不同的服务,例如 Azure Database for PostgreSQL 或 Azure SQL Database 服务。

在遵循最小权限原则的同时,移除权限时要小心。有时,管道需要更具体的权限来读取或更新依赖项。如果没有足够的权限,可能会破坏基础设施或应用程序。

一些基础设施提供商,包括 GCP,分析服务帐户或用户使用的权限,并输出一组多余的权限。您还可以运行其他第三方工具来分析访问并识别未使用的权限。我建议在每次添加新的基础设施资源时,使用这些工具检查和更新您的访问控制。

8.1.2 配置中的保密性保护

除了使用来自管道的管理凭证访问基础设施提供商外,有人可能会更改管道以打印有关基础设施的敏感信息。例如,前端交付管道在日志中输出根数据库密码。任何从管道访问日志的人都可以使用根密码登录数据库!

为了解决这个安全问题,你决定通过使用你的 IaC 工具将密码标记为敏感变量。该工具在日志中删除密码。你还在管道工具中安装了一个插件,用于识别和删除任何敏感信息,例如密码。你将这些两个配置添加到图 8.2 中的管道中,以避免在管道日志中泄露数据库密码。作为安全预防措施,你在密钥管理器中轮换数据库密码,并直接更改数据库中的密码,而不是使用 IaC。

你可以通过抑制或删除明文信息来使用工具在交付管道中掩码密码。

定义掩码你的敏感信息意味着抑制或删除其明文格式,以防止某人读取信息。

使用一种或两种机制可以防止敏感信息出现在管道日志中。敏感信息可能包括密码、加密密钥或像 IP 地址这样的基础设施标识符。如果你认为有人可以利用这些信息访问你的系统,考虑在管道中掩码这些值。

然而,掩码敏感信息并不能保证防止未授权访问。你仍然需要一个工作流程,以尽可能快地修复暴露的凭证。作为一个解决方案,在使用它们配置 IaC 之后,使用密钥管理器存储和轮换凭证。

图片

图 8.2 你可以通过使用工具掩码值并在应用 IaC 更改后轮换凭证来保护根数据库密码。

分别管理密钥会在你的基础设施即代码(IaC)中引入可变性,或者说是在原地更改。虽然这会在实际根数据库密码和 IaC 中表达的密码之间引入漂移,但以可变方式管理密码可以防止某人利用 IaC 管道并使用凭证。

当你构建 IaC 时,考虑以下安全要求清单,以最小化你的交付管道的攻击面:

  1. 从一开始就检查基础设施提供商凭证的最小权限访问。你应该提供足够的权限来应用和确保你的 IaC。

  2. 通过使用一个函数来生成随机字符串或从密钥管理器中读取密钥来生成一个密钥。避免将密钥作为静态变量传递到你的配置中。

  3. 检查你的管道在模拟运行能力或命令输出中是否掩码敏感配置数据

  4. 提供一种机制,以便快速撤销和旋转受损的凭证或数据。

你可以用秘密管理器解决清单中的许多要求。秘密管理器可以省略在配置中静态定义秘密的需要。虽然一些要求作为交付管道的一般安全实践,但它们也适用于安全的 IaC。你可以查看第二章以了解使用秘密管理器保护秘密的模式。

8.2 标签化基础设施

在确保你的基础设施安全之后,你面临运行和支持它的挑战。操作基础设施需要一组故障排除和审计模式和惯例。随着你继续向系统中添加基础设施,你需要一种方法来识别资源的目的和生命周期。

想象一下,uDress 前端应用程序上线了。然而,你的团队收到了来自财务团队的消息。你的基础设施提供商在过去两三个月的账单已经超过了预期的预算。你搜索提供商的界面以确定哪些资源对成本贡献最大。你如何知道每个资源的所有者和环境?

GCP 提供了使用标签的功能,这允许你为你的资源添加元数据以用于标识和审计目的。你更新这些标签以包括所有者和环境。在图 8.3 中,uDress 包括所有者和环境的标识、标签格式的标准以及自动化元数据。你决定使用短横线分隔标签名称和值,以便标签与 GCP 兼容。

图 8.3 标签应包括所有者、环境和自动化标识,以便于故障排除。

在 GCP 之外,其他基础设施提供商允许你添加元数据以标识资源。在你的组织中,你将制定一个标签策略来定义一组用于审计基础设施系统的标准元数据。

定义标签策略定义了一组用于审计、管理和保护组织中基础设施资源的元数据(也称为标签)。

为什么使用标签形式的元数据?标签可以帮助你搜索和审计资源,这对于计费和合规性是必要的。你还可以使用标签来对基础设施资源进行批量自动化。批量自动化包括清理或破窗(手动更改以稳定或修复系统故障)对资源子集的更新。

让我们在以下列表中为 uDress 实现标准标签。从第三章开始,你应用原型模式来定义一组标准标签。你参考 uDress 标签模块来在你的代码中创建 GCP 服务器的标签列表。

列表 8.2 使用标签模块为服务器设置标准标签

class TagsPrototypeModule():                                        ❶
   def __init__(
           self, service, department,
           business_unit, company, team_email,
           environment):
       self.resource = {                                            ❷
           'service': service,                                      ❷
           'department': department,                                ❷
           'business-unit': business_unit,                          ❷
           'company': company,                                      ❷
           'email': team_email,                                     ❷
           'environment': environment,                              ❷
           'automated': True,                                       ❷
           'repository': f"${company}-${service}-infrastructure"    ❷
       }   

class ServerFactory:
   def __init__(self, name, network, zone='us-central1-a', tags={}):
       self.name = name
       self.network = network
       self.zone = zone
       self.tags = TagsPrototypeModule(                             ❸
           'frontend', 'web', 12345, 'udress',                      ❸
           'frontend@udress.net', 'production')                     ❸
       self.resource = self._build()

   def _build(self):                                                ❹
       return {
           'resource': [
               {
                   'google_compute_instance': [                     ❺
                       {
                           self.name: [
                               {
                                   'allow_stopping_for_update': True,
                                   'boot_disk': [
                                       {
                                           'initialize_params': [
                                               {
                                                   'image': 'ubuntu-1804-lts'
                                               }
                                           ]
                                       }
                                   ],
                                   'machine_type': 'f1-micro',
                                   'name': self.name,
                                   'network_interface': [
                                       {
                                           'network': self.network
                                       }
                                   ],
                                   'zone': self.zone,
                                   'labels': self.tags             ❻
                               }
                           ]
                       }
                   ]
               }
           ]
       }

❶ 标签模块使用原型模式定义一组标准的标签。

❷ 设置标签以标识所有者、部门、业务单元用于计费,以及资源存储库

❸ 将必需的参数传递给前端应用程序设置标签

❹ 使用模块创建服务器的 JSON 配置

❺ 通过 Terraform 资源创建 Google 计算实例(服务器)

❻ 将标签模块中的标签添加为 Google 计算实例的标签

AWS 和 Azure 等效项

要将列表 8.2 转换为另一个云服务提供商,将资源更改为 Amazon EC2 实例或 Azure Linux 虚拟机。然后,将self.tags传递给 AWS 或 Azure 资源的tags属性。

你如何知道要添加哪些标签?回想第二章,你必须标准化你的基础设施资源的命名标签。与合规性、安全和财务团队讨论这些考虑因素。这将有助于确定你需要哪些标签以及如何使用它们。至少,我总是为以下内容添加一个标签:

  • 服务或团队

  • 团队电子邮件或通讯渠道

  • 环境(开发或生产)

例如,假设 uDress 安全团队审计前端资源并发现一些配置错误的基础设施。团队成员可以检查标签,识别有问题的服务和环境,并联系创建该资源的团队。

你还可以包括以下标签:

  • 自动化,帮助你识别手动创建的资源与自动化资源

  • 仓库,允许你将资源与其在版本控制中的原始配置相关联

  • 业务单元,用于识别会计的计费或冲销标识符

  • 合规性,用于识别资源是否具有处理个人信息的合规性或策略要求

在你决定标签时,确保它符合一组通用的约束条件,这样你就可以在任何基础设施提供商上应用相同的标签。大多数基础设施提供商对标签有字符限制。我通常更喜欢短横线命名法,它使用小写标签名称,值由短横线分隔。虽然你可以使用驼峰命名法(从风格上讲,camelCase),但并非所有提供商都有大小写敏感的标签。

标签字符限制也因基础设施提供商而异。大多数提供商支持标签键的最大长度为 128 个字符,标签值的最大长度为 256 个字符。你必须平衡描述性名称的冗长性(在第二章中描述)与提供商的标签限制!

你的标签策略的另一部分涉及决定是否删除未标记的资源。考虑在生产环境中强制对所有资源进行标记。测试环境可以支持未标记的资源进行手动测试。一般来说,我不建议在没有仔细检查的情况下立即删除未标记的资源。你不希望意外删除一个重要的资源。

8.3 编码策略

在基础设施交付管道中保护访问和秘密以及管理基础设施提供者中的标签可以提高安全和合规实践。然而,你可能希望在配置进入生产之前就识别不安全或不合规的基础设施配置。你希望在有人在你生产系统中发现问题之前就捕捉到问题。

想象一下将 uDress 前端应用程序连接到另一个数据库。你打开防火墙规则以允许所有流量进入用于测试的管理数据库。测试完成后,你预计会删除数据库,因此你没有对其进行标记。

你忘记了防火墙和标签配置,并将其发送进行审查。不幸的是,你的队友在代码审查中错过了它们,并将更改推送到生产环境。两周后,你发现一个未知实体访问了一些数据!然而,你没有标签来识别受损害的数据库。

你本可以做什么不同?回想一下第六章中提到的单元测试或基础设施配置的静态分析的重要性。你可以应用 相同 的技术来编写专门针对安全和策略的测试。

而不是依赖队友来发现问题,你可以将策略表达为代码以静态分析配置中的宽松防火墙规则或缺少标签。策略即代码 测试基础设施元数据,并验证其是否符合安全或合规要求。

定义 策略即代码(也称为 左移安全测试基础设施即代码的静态分析)在推送更改到生产之前测试基础设施元数据,并验证其值是否符合安全或合规要求。策略即代码包括你为动态分析工具或漏洞扫描编写的规则。

我在第一章和第六章中讨论了自动化和测试基础设施的长期好处。你同样需要初始的短期时间投资来编写策略即代码。策略检查会持续验证你想要对生产环境进行的每个更改的合规性。你最小化了合规性和安全团队审计你的系统后的惊喜。随着时间的推移,你通过缩短生产时间来减少长期的时间投资。

8.3.1 策略引擎和标准

工具可以通过根据一组规则评估元数据来帮助运行策略即代码。这个领域的大多数测试工具都使用策略引擎。策略引擎 以策略作为输入,并评估基础设施资源以验证其合规性。

定义 A 策略引擎 以策略作为输入,并评估资源元数据是否符合策略。

许多策略引擎会解析和检查基础设施配置或状态中的字段。在图 8.4 中,策略引擎从基础设施即代码(IaC)或系统状态中提取 JSON 或其他元数据。然后,它将这些元数据传递给安全或策略测试。引擎运行测试以解析字段,检查它们的值,如果实际值与预期值不匹配,则失败。

图片

图 8.4 安全和策略测试解析系统的配置或状态以获取正确的字段值,如果它们与预期值不匹配则失败。

此工作流程适用于策略代码工具以及您自己编写的任何测试。策略代码工具使得测试值更加直接,因为这些工具抽象了解析字段和检查值的复杂性。然而,工具并不涵盖您想要测试的所有值或用例。

因此,您通常需要编写自己的策略引擎以满足您的需求。在本章的示例中,我使用 pytest,一个 Python 测试框架,作为原始的“策略引擎”来检查安全且合规的配置。

策略引擎

策略代码生态系统为不同的目的提供了不同的工具。大多数工具都分为三个用例之一,所有这些用例都解决非常不同的功能,并且行为差异很大:

  1. 特定平台的网络安全测试

  2. 行业或监管标准的策略测试

  3. 自定义策略

表 8.1 包含了一个非详尽的策略引擎列表,用于配置工具,包括供应商和开源。我概述了每个工具的一些技术集成和用例类别。

表 8.1 配置工具的策略引擎示例

工具 用例 技术集成
AWS CloudFormation Guard 特定平台的网络安全测试自定义策略 AWS CloudFormation
HashiCorp Sentinel 特定平台的网络安全测试自定义策略 HashiCorp Terraform
Pulumi CrossGuard 特定平台的网络安全测试自定义策略 Pulumi SDK
Open Policy Agent(Fugue、Conftest、Kubernetes Gatekeeper 等底层技术) 特定平台的网络安全测试(工具相关)行业或监管标准的策略测试(工具相关)自定义策略 各种(要获取完整列表,请参阅www.openpolicyagent.org/docs/latest/ecosystem/
Chef InSpec 特定平台的网络安全测试自定义策略 各种(要获取完整列表,请搜索 Chef 市场supermarket.chef.io
Kyverno 特定平台的网络安全测试自定义策略 Kubernetes

您通常需要混合搭配工具以覆盖所有用例。没有单个工具可以覆盖所有用例。一些工具提供定制化,您可以使用它来构建自己的策略。通常,考虑通过自定义策略扩展现有工具,以便您可以与您的安全、合规和工程团队建立有见地的模式和默认值。实际上,您可能需要采用五到六个策略引擎来覆盖所需的工具、平台和政策。

注意,我没有包括任何特定于数据中心设备的特定安全或策略工具,这些工具通常依赖于你组织的采购要求。你也许还会在表 8.1 中列出的示例之外找到一些社区项目。我经常发现这些工具及其集成被更新的工具所取代,因为生态系统变化迅速。

镜像构建和配置管理

镜像构建工具没有太多安全或策略工具,因为你倾向于为他们编写测试。配置管理工具遵循与配置工具类似的方法。你需要找到社区或内置工具来验证安全和策略配置。

行业或监管标准

你可能会查看表 8.1 并发现,很少有工具包括针对行业或监管标准的策略测试。大多数这些策略以文档形式存在,你通常需要自己编写它们。有时,你可以找到社区创建的策略测试套件,你需要用你自己的内容来补充。

例如,美国国家标准与技术研究院(NIST)作为国家清单计划的一部分发布了安全基准列表(ncp.nist.gov/repository)。这本书的审稿人也推荐了美国国防部发布的《安全技术实施指南》(STIGs),包括技术测试和配置标准。

注意:是的,我在这一节中遗漏了许多工具或标准。我包括的标准适用于美国,但不一定适用于全球。在你阅读这段文字的时候,策略引擎可能已经改变了功能、集成或开源状态,行业或监管标准也可能已经更新了草案。如果你想推荐一个,请通过github.com/joatmon08/tdd-infrastructure告诉我。

8.3.2 安全测试

你应该测试什么来确保你的基础设施安全?一些策略即代码工具提供了有见地的默认设置,这些设置捕获了安全系统的最佳实践。然而,你可能需要为你公司的特定平台和基础设施编写自己的设置。

让我们从修复你的数据库安全漏洞开始。幸运的是,测试数据中没有重要信息。然而,在未来,你不想你的队友复制并部署配置到生产环境中。为了防止测试环境的 IaC(基础设施即代码)部署到生产环境,你编写了一个测试来为数据库安全网络。

数据库需要一个非常严格的、最小权限(最小访问)防火墙规则。图 8.5 展示了如何实现一个测试来从 IaC 中检索防火墙配置。配置传递给一个测试,该测试解析防火墙规则中的源范围。如果该范围包含一个允许规则,0.0.0.0/0,则测试失败。

图片

图 8.5 从防火墙规则配置中检索源地址范围值并确定是否包含过于宽泛的范围。

GCP 使用 0.0.0.0/0 来表示任何 IP 地址都可以访问数据库。如果有人访问了你的网络,并且他们有用户名和密码,他们就可以访问你的数据库。你的新测试在过于宽泛的规则 0.0.0.0/0 进入生产之前就会失败。

列表 8.3 使用 Python 实现了防火墙规则的测试。在你的测试中,你实现代码以打开 JSON 配置文件,检索 source_ranges 列表,并检查列表中是否包含 0.0.0.0/0

列表 8.3 使用测试解析 0.0.0.0 的防火墙规则

import json
import pytest
from main import APP_NAME, CONFIGURATION_FILE

@pytest.fixture(scope="module")
def resources():
   with open(CONFIGURATION_FILE, 'r') as f:                            ❶
       config = json.load(f)                                           ❶
   return config['resource']                                           ❷

@pytest.fixture
def database_firewall_rule(resources):                                 ❸
   return resources[0][                                                ❸
       'google_compute_firewall'][0][APP_NAME][0]                      ❸

def test_database_firewall_rule_should_not_allow_everything(           ❹
       database_firewall_rule):
   assert '0.0.0.0/0' not in \                                         ❺
       database_firewall_rule['source_ranges'], \                      ❺
       'database firewall rule must not ' + \                          ❻
       'allow traffic from 0.0.0.0/0, specify source_ranges ' + \      ❻
       'with exact IP address ranges'                                  ❻

❶ 从 JSON 文件加载基础设施配置

❷ 从 JSON 配置文件中解析资源块

❸ 从 JSON 配置中解析 Terraform 定义的 Google 计算防火墙资源

❹ 使用描述性的测试名称解释防火墙规则的政策,该规则不应允许所有流量

❺ 检查规则源地址范围中是否定义了 0.0.0.0/0 或允许所有流量

❻ 使用描述性的错误消息描述如何更正防火墙规则,例如从源地址范围中删除 0.0.0.0/0

AWS 和 Azure 的等效选项

GCP 中的防火墙规则相当于 AWS 安全组 (mng.bz/Qvvm) 或 Azure 网络安全组 (mng.bz/XZZY)。要更新代码,在您选择的云服务提供商中创建安全组资源。然后,编辑测试以将 GCP 的 source_ranges 与 Azure 的 security_rule.source_port_range 属性或 AWS 的 ingress.cidr_blocks 属性进行切换。

想象一下你的新队友想要从他们的笔记本电脑上对数据库进行一些测试。他们在 IaC 中更改了防火墙规则,将其设置为 0.0.0.0/0。管道运行 Python 代码以生成 JSON:

$ python main.py

管道运行单元测试,检查配置的 JSON 文件。它识别到防火墙规则在允许的源地址范围列表中包含 0.0.0.0/0 并抛出错误:

$ pytest test_security.py
====== short test summary info ======
FAILED test_security.py::test_database_firewall_rule_should_not_allow_everything - 
     ➥AssertionError: database firewall rule must not allow traffic 
     ➥from 0.0.0.0/0, specify source_ranges with exact IP address ranges
===== 1 failed in 0.04s ======

你的队友阅读了错误描述,并意识到防火墙规则不应该允许所有流量。他们可以更正他们的配置,将他们的笔记本电脑 IP 地址添加到源地址范围中。

就像第六章中的功能测试一样,安全测试 教育 你的团队了解基础设施的理想安全实践。虽然测试不一定能够捕获所有安全违规,但它们传达了关于安全期望的重要信息。将安全最佳实践的 未知已知 转变为 已知已知 消除了重复的错误。

这些测试也有助于扩大你组织中的安全实践。你的队友感到有力量更正配置。此外,你的安全团队对安全违规的调查和跟进更少。将安全作为每个人的责任可以减少未来补救的时间和精力。

正测试与负测试对比

在数据库 IP 地址范围的示例中,您检查了一个 IP 地址范围不匹配每个 IP 地址(0.0.0.0/0)。这被称为负测试,这个过程断言该值不匹配。您还可以使用正测试来断言属性确实与预期值匹配。

一些参考资料建议您使用一种类型来表达所有安全或策略测试。然而,我通常使用正负测试断言来编写测试。这种组合更好地表达了安全和策略要求的目的。例如,您可以使用负测试来检查任何 IP 地址范围是否与任何团队编写的任何基础设施配置匹配。另一方面,如果您有一个每个防火墙规则都必须包含的 IP 地址范围,例如 VPN 连接,您可以使用正测试。

您可以编写测试来检查其他安全配置,包括以下内容:

  • 其他网络策略上的端口、IP 范围或协议

  • 对基础设施资源、服务器或容器无管理或 root 访问的访问控制

  • 缓解实例元数据被滥用的元数据配置

  • 安全信息和事件管理(SIEM)的访问和审计日志配置,例如用于负载均衡器、IAM 或存储桶

  • 用于修复漏洞的软件包或配置版本

这个非详尽的列表涵盖了某些一般配置。然而,您应咨询您的安全团队或其他行业标准以获取更多信息并进行测试。

8.3.3 策略测试

安全测试验证您最小化了 IaC 中配置错误的攻击面。然而,您还需要其他测试来进行审计、报告、计费和故障排除。例如,您的测试数据库应该有一个标签,以便有人可以识别其所有者并报告安全漏洞。

uDress 合规团队提醒您为您的 GCP 数据库添加标签,以便他们可以识别数据库所有者。他们还通知您,安全漏洞导致数据库资源扩展,这增加了您的云计算账单。没有标签,合规团队很难确定联系谁以解决安全问题以及增加的账单。

您将标签添加到数据库配置中。为了在将来提醒自己进行标签操作,您使用图 8.6 所示的流程来实现一个单元测试来检查标签。与防火墙规则配置的安全测试一样,您解析包含数据库配置的 JSON 文件,以检查您是否正确地用标签填充了标签。如果测试中有空标签,则测试失败。

图片

图 8.6 您实现了一个测试,用于解析数据库配置并检查数据库用户标签中的标签列表。

策略测试的行为与安全测试相似。然而,它测试的是标签而不是 IP 源范围。虽然策略并不能更好地保护基础设施,但它提高了你排查问题和识别资源的能力。

让我们实现测试工作流程。在下面的列表中,你编写一个测试来检查 GCP user_labels参数下是否有超过零个标签。

列表 8.4 使用测试解析数据库配置以获取标签

import json
import pytest
from main import APP_NAME, CONFIGURATION_FILE

@pytest.fixture(scope="module")
def resources():                                                   ❶
   with open(CONFIGURATION_FILE, 'r') as f:                        ❶
       config = json.load(f)                                       ❶
   return config['resource']                                       ❷

@pytest.fixture
def database_instance(resources):
   return resources[2][                                            ❸
       'google_sql_database_instance'][0][APP_NAME][0]             ❸

def test_database_instance_should_have_tags(database_instance):    ❹
   assert database_instance['settings'][0]['user_labels'] \        ❺
       is not None                                                 ❺
   assert len(                                                     ❻
       database_instance['settings'][0]['user_labels']) > 0, \     ❻
       'database instance must have `user_labels`' + \             ❻
       'configuration with tags'                                   ❻

❶ 从 JSON 文件加载基础设施配置

❷ 从 JSON 配置文件中解析资源块

❸ 从 Terraform 定义的 JSON 配置中解析 Google SQL 数据库实例中的用户标签

❹ 使用描述性的测试名称解释数据库标签策略

❺ 检查数据库上的用户标签列表不为空或为 null 值

❻ 使用描述性的错误信息描述向 GCP 用户标签添加标签

AWS 和 Azure 的等效项

要将列表 8.4 转换为 AWS 或 Azure,将 Google SQL 数据库实例更改为来自任一云的 PostgreSQL 服务。你可以使用 AWS RDS (mng.bz/yvvJ) 或 Azure Database for PostgreSQL (mng.bz/M552)。然后,解析数据库实例资源中的tags属性。Azure 和 AWS 都使用标签。

你将测试添加到你的安全测试中。下次你的队友进行更改并忘记添加标签时,测试将失败。你的队友阅读错误信息并纠正他们的 IaC 以包含标签。你可以为其他组织策略实施测试,包括以下这些:

  • 所有资源所需标签

  • 变更的审批人数

  • 基础设施资源的地理位置

  • 审计日志输出和目标服务器

  • 将开发数据与生产数据分开

这个非详尽的列表涵盖了某些通用配置。然而,你应该咨询你的合规团队或其他行业标准以获取更多信息并进行测试。在你编写测试时,确保你包括清晰的错误信息,说明测试检查了哪些策略。

8.3.4 实践和模式

随着你编写更多的安全和策略测试,你将更有信心你的配置保持安全和合规。你如何在整个团队和公司中传授这些知识?你可以将一些测试的实践和模式应用于检查基础设施安全和合规性。接下来,我将更详细地介绍编写安全和策略测试的实践和模式。

使用详细的测试名称和错误信息

你会注意到 uDress 策略和安全测试的详细测试名称和错误消息。这些名称和消息看起来冗长,但精确地传达给队友策略寻找的内容以及他们应该如何纠正它!我在第二章介绍了一种技术来验证你的命名和代码的质量。试着让其他人阅读测试。如果他们能理解其目的,他们就可以更新他们的配置以符合策略作为代码。

模块化测试

你可以将第三章中的一些模块模式应用到策略作为代码中。例如,uDress 支付团队要求借用你的安全和策略测试来测试他们的基础设施。你将你的数据库策略划分为database-tests,并将防火墙策略划分为firewall-tests

安全团队还要求你添加一个互联网安全中心(CIS)基准。这个行业标准基准包括验证 GCP 上安全配置最佳实践的测试。在添加安全基准后,你意识到你在多个仓库中有太多测试需要跟踪。

图 8.7 将所有这些测试移动到一个名为gcp-security-test的仓库中。该仓库组织了 uDress 的 GCP 基础设施的所有测试。uDress 的前端和支付团队可以参考共享仓库,导入测试,并针对他们的配置运行它们。同时,安全团队可以在gcp-security-tests仓库的一个地方更新安全基准。

图 8.7 将策略作为代码添加到共享仓库,以便所有创建基础设施的团队进行分发。

就像你的基础设施方法到代码仓库结构一样,你可以选择将组织的策略作为代码放在单个仓库中,或者根据环境将其分散到多个仓库中。在任何结构中,确保所有团队都能看到组织的安全和策略测试,以便学习如何部署符合规定的基础设施。

此外,根据业务单元、功能、环境、基础设施提供商、基础设施资源、堆栈或基准来划分测试。你希望随着业务的变化单独发展测试类型。某些业务单元可能需要一种类型的测试,而另一种则不需要。划分测试并选择性运行有助于。

将策略作为代码添加到交付管道中

你的团队想要确保在推送到生产之前运行安全和策略测试,因此他们将它们添加为交付管道的一个阶段。策略作为代码在将更改部署到测试环境后运行,但在发布到生产之前。你可以快速获得关于基础设施更改的反馈,优先考虑功能,但在生产之前检查策略。

安全团队还将策略作为代码添加到扫描运行中的生产环境。这种动态分析持续验证对基础设施的任何紧急或破窗更改的安全性和合规性。

图 8.8 显示了交付管道中静态分析的工作流程和运行基础设施的动态分析,以检查配置更改并主动解决资源问题。在将更改部署到测试环境后,你运行安全和策略测试。在将更改发布到生产之前,它们应该通过测试。当资源获得更改时,你使用类似的测试扫描运行中的基础设施,以进行运行时安全和策略检查。

图片

图 8.8 对安全和策略检查进行测试,以防止不正确的基础设施配置更改并阻止更改进入生产。

你可能需要对静态分析和动态分析进行不同的测试。一些测试,例如端点访问的实时验证,只能在运行的基础设施上工作。因此,你希望在推送到生产之前和之后运行一些测试。

如果你的静态分析测试花费时间过长,你可以在将更改推送到生产后运行测试子集。然而,你必须迅速解决任何安全和合规违规问题。因此,我建议将最关键的安全和策略测试作为你管道的一部分运行。

图像构建

你可能会遇到构建不可变服务器或容器镜像的做法。通过将你想要的软件包烘焙到服务器或容器镜像中,你可以创建带有更新的新服务器,而不会遇到就地更新的问题。

使用策略代码的基础设施管道的相同工作流程来构建不可变镜像。该工作流程包括单元测试,用于检查特定安装要求(如公司软件包注册表)的脚本,以及针对测试服务器的集成测试,以验证软件包版本符合策略和安全要求。

你始终可以使用动态分析的形式,例如代理,扫描服务器并确保其配置符合规则。例如,Sysdig 提供 Falco,这是一个在服务器上运行的运行时安全工具,用于检查规则合规性。

大多数团队希望他们的安全或策略测试阻止所有更改进入生产环境。例如,如果客户需要访问基础设施的公共端点怎么办?仅检查私有网络的测试可能不适用。有时你会在你的安全策略中找到例外。

定义执行级别

随着你构建越来越多的策略代码,你必须确定最重要的那些,并对其他策略进行例外处理。例如,uDress 安全团队将数据库标记策略确定为强制执行。如果交付管道找不到标记,它必须失败,并且有人必须添加标记。

您定义了如图 8.9 所示的三个政策类别。安全团队要求您在生产之前修复数据库标签。然而,团队对您的防火墙规则做了例外,因为客户需要访问您的端点。团队还包括一些关于更安全的基础设施配置的建议。

图片

图 8.9 您可以将政策作为代码分为三个强制执行类别,以在生产之前阻止更改。

我将政策作为代码分为三个强制执行类别(借鉴自 HashiCorp Sentinel 的术语):

  • 强制执行 对于必需的政策

  • 软强制 对于可能需要手动分析以处理例外的政策

  • 建议 用于最佳实践的知识共享

安全团队将防火墙规则归类为软强制。一些公共负载均衡器必须允许从 0.0.0.0/0 访问。如果防火墙规则测试失败,安全团队中的人必须审查该规则,并手动批准更改进入生产流程。

安全团队将 CIS 基准作为知识共享和最佳实践的建议。他们要求您尽可能纠正配置,但他们在生产之前不要求强制执行。

在更改进入生产之前,您必须运行安全测试吗?毕竟,它们需要一段时间才能运行!如果您担心安全和政策测试会阻碍更改太长时间,请在部署到生产之前运行强制执行测试。

您可以异步运行软强制或建议测试,因此只有必要的测试会阻塞您的管道。我不建议异步运行所有安全和政策测试,因为您可能会在生产中临时引入一个不符合规定的配置,即使您在异步测试后快速修复它!

图 8.10 总结了测试模式和最佳实践,例如编写详细的测试名称和错误消息。类似于基础设施,您可以根据功能模块化测试,并将测试添加到生产交付管道中。

图片

图 8.10 测试安全性和检查不恰当的基础设施配置,以防止更改进入生产环境。

无论工具、安全基准或政策规则如何,您都应该以测试的形式表达和传达实践。遵循这些模式和最佳实践将帮助您和您的队友提高安全和合规知识。

随着您的组织成长并制定更多政策,您也会相应地扩展您的安全和政策测试。早期采用政策作为代码为将安全和合规实践融入您的 IaC 奠定基础。如果您找不到运行所需测试的工具,可以考虑编写自己的测试来解析 IaC。

摘要

  • 公司政策确保系统符合安全、审计和组织要求。您的公司根据行业、国家和其他因素制定政策。

  • 最小权限原则只授予用户或服务账户他们所需的最小访问权限。

  • 确保你的基础设施即代码(IaC)使用具有最小权限访问基础设施提供者的凭证。最小权限可以防止有人利用凭证在你的环境中创建未经授权的资源。

  • 使用工具来抑制或编辑交付管道中的明文、敏感信息。

  • 在管道中应用基础设施即代码后,旋转任何由基础设施即代码生成的用户名或密码。

  • 使用服务、所有者、电子邮件、会计信息、环境和自动化细节对基础设施进行标记,使其更容易识别和审计安全和计费。

  • 策略即代码测试一些基础设施元数据,并验证其是否符合安全或合规的配置。

  • 在将系统功能测试后但推送至生产之前,使用策略即代码来测试你基础设施的安全性和合规性。

  • 应用清洁的基础设施即代码和模块模式来管理和扩展策略即代码。

  • 将每个安全和策略测试分类到三个执行类别之一,例如强制性的(必须修复)、软性强制性的(手动审查)和咨询性的(最佳实践,但不会阻止生产)。

第三部分. 管理生产复杂性

当多个团队使用基础设施即代码并建立共同实践后,你必须学会如何进行更改和管理生产基础设施的复杂性。本部分最后描述了通过 IaC 更新基础设施以及更改 IaC 本身的技术。毕竟,你将改变你的基础设施,并使你的工具和配置不断进化!

在第九章中,你将学习如何更改基础设施并最小化更改失败的可能性。第十章描述了重构 IaC 的过程,这可能是你在扩展你的实践或系统后所需要的。如果你的重构导致基础设施故障,你可以使用第十一章中的一般技术来回滚、故障排除和修复你的更改。

然而,随着你继续扩展你的系统,你将遇到成本问题。第十二章提供了关于如何使用 IaC 技术来管理基础设施成本的指导。最后,第十三章通过使用开源工具和模块以及替换或升级现有工具的技术来结束。

9 进行变更

本章涵盖

  • 确定何时使用如蓝绿部署等模式来更新基础设施

  • 使用 IaC 在多个区域创建环境以建立不可变性

  • 确定更新有状态基础设施的变更策略

在前面的章节中,我们介绍了模块化、解耦、测试和部署基础设施变更的有用实践和模式。然而,您还需要使用基础设施即代码技术来管理基础设施的变更。在本章中,您将学习如何通过应用不可变性策略来最小化潜在失败的影响,从而改变 IaC。

让我们回到第五章中提到的“蔬菜数据中心”及其模块化 IaC 的挑战。该公司收购了“肉食植物数据中心”作为子公司。其肉食植物,如捕蝇草,需要特定的生长条件。

因此,肉食植物数据中心需要全球网络和网络优化的服务器和组件。大多数团队配置了他们的 IaC,但意识到他们的代码无法处理如此广泛的变化。他们向您,一位在 IaC 方面有一定经验的“蔬菜数据中心”工程师,寻求帮助。

工程团队指导您先更新“开普敦捕蝇草”团队的基础设施。作为一种耐寒的肉食植物,开普敦捕蝇草最能处理由温度和浇水波动引起的任何系统停机时间。开普敦捕蝇草基础设施的所有基础设施资源都存在于一个单一存储库中,只有几个配置文件。

您调查并绘制了捕蝇草系统的架构。图 9.1 显示了一个区域转发规则(负载均衡器),它将流量发送到具有容器集群和三个服务器的区域网络。所有流量都在同一区域内循环,而不是全球范围内。

图片

图 9.1 开普敦捕蝇草应用程序使用了一个具有共享负载均衡资源、三个服务器和一个容器集群的基础设施系统。

您希望将所有资源更改为将流量发送到全球,而不是在单个区域内路由。然而,捕蝇草团队在同一个 IaC 中定义了所有这些资源,也称为单例模式(参见第三章)。这些资源还共享基础设施状态。

您如何将网络变更部署到系统中并最小化其影响?如果您通过就地更改来以可变的方式处理基础设施,您会担心会干扰到浇水应用程序。例如,将您的网络更改为使用全局路由可能会影响支持浇水应用程序的服务器。

您可以从第二章中回忆起,您可以使用不可变性的概念来构建带有新变更的新基础设施。如果您可以将这些技术应用于系统,您可以在不影响旧环境的情况下,在一个新环境中隔离和测试变更。在本章中,您将学习如何隔离和更改 IaC。

注意 展示更改策略需要足够大(且复杂)的示例。如果你运行完整的示例,你将产生超过 GCP 免费层的成本。本书仅包括相关的代码行,为了可读性省略了其余部分。对于完整的代码列表,请参阅本书的代码仓库github.com/joatmon08/manning-book/tree/main/ch09。如果你将这些示例转换为 AWS 和 Azure,你也将产生成本。在可能的情况下,我提供将示例转换为所选云提供商的注释。

9.1 更改前的实践

你直接开始改变捕蝇草系统。不幸的是,你意外删除了一个配置属性,该属性用blue标记了一个服务器,这允许网络中所有蓝色实例之间的流量。你将你的更改推送到交付管道以测试配置并将其应用到生产中。

测试遗漏了已删除的标签。幸运的是,你的监控系统向你发送了警报,表示浇水应用程序无法与你的新服务器通信!你将所有请求重定向到副本服务器实例,以确保在调试过程中捕蝇草仍然得到浇水。

你意识到你不应该开始改变系统。捕蝇草系统有现有的架构和工具,在你开始之前你需要了解。你还需要知道,如果系统有备份或备选环境可供使用,以防你破坏了某些东西。在你进行更改之前,你应该做什么?

9.1.1 遵循检查清单

在更改 IaC 时,你总是存在引入错误和其他问题的风险。你需要测试、监控和可观察性(从系统的输出推断系统内部状态的能力)来确保你在更改过程中没有影响系统。如果你对你的系统没有一定的可见性,你就不能快速从破坏性更改中排除问题。

在你改变捕蝇草系统之前,你决定回顾一下你的系统。图 9.2 显示了你要回顾的内容。你首先添加一个测试来检查你的已删除标签。接下来,你检查你的监控系统,进行系统和应用程序的健康检查和指标。最后,你创建副本服务器作为备份,以防你破坏现有的服务器。如果你意外影响了更新的服务器,你可以将流量发送到备份服务器。

图片

在更新网络之前,你需要验证测试覆盖率、系统和应用程序监控以及冗余。

你为什么创建副本服务器作为备份?当主要资源失败时,拥有额外资源来复制你之前使用的配置是有帮助的。这种冗余使你的系统保持运行。

定义 冗余 是指复制资源以提高系统的性能。如果更新的组件失败,系统可以使用具有先前配置的工作资源。

通常,在您进行更改之前,请审查以下清单:

  • 您能否在每个更改之前预览并测试在隔离环境中进行的更改?

  • 系统是否配置了监控和警报以识别任何异常?

  • 应用程序是否通过健康检查、日志记录、可观察性或指标跟踪错误响应?

  • 应用程序及其系统是否有任何冗余

列表中的项目侧重于可见性和意识。如果没有监控系统或测试来帮助识别问题,您可能难以识别或解决损坏的更改。我曾经推送了一个破坏应用程序的更改,直到两周后才得知。由于我们没有在应用程序上设置警报,所以花了这么长时间才意识到问题!

在更改之前,检查清单为调试任何问题以及如果更改失败建立任何备份计划奠定了基础。您甚至可以使用第六章和第八章中的实践,将此检查清单构建到交付管道中作为质量门。

9.1.2 添加可靠性

在审查更改前的检查清单后,您意识到您需要在系统中有一个更好的备份环境。为了确保您在持续的重构工作中不会使整个捕蝇草系统崩溃,您需要额外的冗余。当您最终将更改部署到捕蝇草团队的模块时,您不需要担心会破坏系统。

不幸的是,捕蝇草系统仅存在于us-central1。如果该区域失败,捕蝇草不会得到浇水!您决定在另一个区域(us-west1)构建一个闲置的生产捕蝇草系统,这样您就可以重新启动浇水应用程序。您使用 IaC 将us-central1中的主动区域复制us-west1中的被动(闲置)区域。

您现在可以使用被动区域中的环境作为备份。在图 9.3 中,您更新捕蝇草团队的配置以使用服务器模块,并将更改推送到主动环境。如果它不起作用,您在调试问题时暂时将所有流量发送到被动环境。否则,您运行测试并使用模块更改更新被动环境。

图 9.3 您使用 IaC 为捕蝇草系统实现主动-被动配置,以在更改期间提高其可靠性。

现在的捕蝇草系统采用了一种主动-被动配置,其中一个环境处于闲置状态,作为备份。

定义在主动-被动配置中,一个系统是用于完成用户请求的主动环境,另一个是备份环境。

如果us-central1中的环境停止工作,您始终可以将流量发送到us-west1中的另一个被动环境。从失败的主动环境切换到工作的被动环境遵循故障转移的过程。

定义故障转移是在主要资源失败时使用被动(或备用)资源接管的做法。

你为什么想要一个活动-被动配置?在第二个区域构建一个被动环境可以提高系统的整体可靠性。可靠性衡量的是系统在一段时间内正确运行的时间。

定义 可靠性衡量的是系统在一段时间内正确运行的时间。

你希望在执行 IaC 更改时保持系统的可靠性。提高可靠性可以最小化对业务关键应用的干扰,并最终减少对最终用户的影响。你可以通过将流量切换到一个工作状态下的被动环境来减少对活动环境的破坏范围。

让我们在代码中创建一个活动-被动配置。在你的终端中,你将名为 blue.py 的文件(包含 sundew 的基础设施资源)复制到一个名为 passive.py 的新文件中:

$ cp blue.py passive.py

在列表 9.1 中的 passive.py 中,你更新了一些变量以创建被动 sundew 环境,包括区域和名称。

列表 9.1 更新us-west1的被动 sundew 环境

TEAM = 'sundew'
ENVIRONMENT = 'production'
VERSION = 'passive'                                                 ❶
REGION = 'us-west1'                                                 ❷
IP_RANGE = '10.0.1.0/24'                                            ❸

zone = f'{REGION}-a'
network_name = f'{TEAM}-{ENVIRONMENT}-network-{VERSION}'            ❹
server_name = f'{TEAM}-{ENVIRONMENT}-server-{VERSION}'              ❹

cluster_name = f'{TEAM}-{ENVIRONMENT}-cluster-{VERSION}'            ❹
cluster_nodes = f'{TEAM}-{ENVIRONMENT}-cluster-nodes-{VERSION}'     ❹
cluster_service_account = f'{TEAM}-{ENVIRONMENT}-sa-{VERSION}'      ❹

labels = {                                                          ❺
   'team': TEAM,                                                    ❺
   'environment': ENVIRONMENT,                                      ❺
   'automated': True                                                ❺
}                                                                   ❺

❶ 设置版本以识别被动环境。

❷ 将被动环境的区域从 us-central1 更改为 us-west1。

❸ 更新被动环境的不同 IP 地址范围,以避免发送请求。

❹ 剩余的变量和函数引用被动环境的常量,包括区域。

❺ 为资源定义标签,以便你可以识别生产环境。

AWS 和 Azure 等效项

GCP 标签类似于 AWS 和 Azure 标签。你可以将labels变量中定义的对象传递给 AWS 和 Azure 资源标签。

现在你有一个备份环境,以防你的模块更改出错。想象一下,你推送更改并破坏了us-central1中的活动环境。你可以更新并推送配置,以便生产全局负载均衡器故障切换并将所有流量发送到被动环境。在下面的列表中,你更改全局负载均衡器的权重,以便将 100%的流量发送到被动环境。

列表 9.2 在us-west1中故障切换到被动 sundew 环境

import blue                                                       ❶
import passive                                                    ❶

services_list = [
   {
       'version': 'blue',                                         ❷
       'zone': blue.zone,
       'name': f'{shared_name}-blue',
       'weight': 0                                                ❸
   }, {
       'version': 'passive',                                      ❷
       'zone': passive.zone,
       'name': f'{shared_name}-passive',
       'weight': 100                                              ❹
   }
]

def _generate_backend_services(services):                         ❺
   backend_services_list = []
   for service in services:
       version = service['version']
       weight = service['weight']
       backend_services_list.append({
           'backend_service': (                                   ❻
               '${google_compute_backend_service.'                ❻
               f'{version}.id}}'                                  ❻
           ),                                                     ❻
           'weight': weight,                                      ❻
       })
   return backend_services_list

def load_balancer(name, default_version, services):
   return [{
       'google_compute_url_map': {                                ❼
           TEAM: [{
               'name': name,
               'path_matcher': [{
                   'name': 'allpaths',
                   'path_rule': [{                                ❽
                       'paths': [                                 ❽
                           '/*'                                   ❽
                       ],                                         ❽
                       'route_action': {                          ❾
                           'weighted_backend_services':           ❾
                               _generate_backend_services(        ❾
                                   services)                      ❾
                       }                                          ❾
                   }]
               }]
           }]
       }
   }]

❶ 导入蓝色(活动环境)和被动环境的 IaC。

❷ 为每个环境定义一个版本列表,以便将其附加到负载均衡器,蓝色和被动。

❸ 配置负载均衡器,将 0%的流量发送到蓝色版本。

❹ 配置负载均衡器,将 100%的流量发送到被动版本。

❺ 将两个版本及其权重作为路由添加到负载均衡器的负载均衡规则中。

❻ 定义了蓝色和被动环境的后端服务,并为每个环境分配权重以引导流量。

❼ 使用基于路径、蓝色(活动)和被动服务器以及权重的 Terraform 资源创建 Google 计算 URL 映射(负载均衡规则)。

❽ 设置路径规则,将所有路径导向活动或被动服务器。

❾ 将两个版本及其权重作为路由添加到负载均衡器的负载均衡规则中。

AWS 和 Azure 等效项

Google Cloud URL 映射类似于 AWS 应用负载均衡器(ALB)或 Azure 流量管理器和应用程序网关。要将列表 9.2 转换为 AWS,您需要更新资源以创建 AWS ALB 和监听器规则。然后,向 ALB 监听器规则添加路径路由和权重属性。

对于 Azure,您需要将 Azure 流量管理器配置文件和端点链接到 Azure 应用程序网关。更新 Azure 流量管理器,设置权重并将它们路由到连接到 Azure 应用程序网关的正确后端地址池。

在将系统故障转移到被动环境后,Sundew 团队报告了浇水应用程序的回归。您有机会在蓝色(主动)环境中调试模块的问题。主动-被动配置将保护未来单个区域中的故障。

Sundew 团队成员告诉您,最终,他们希望将流量发送到两个区域。每个区域中的两个环境都处理请求。图 9.4 显示了他们的理想配置。下次,他们想更新模块并将其推送到一个区域。如果某个区域出现故障,大多数请求仍然会由系统处理。您减少对 Sundews 的浇水频率,但有机会修复损坏的区域。

图 9.4 在未来,Sundew 团队将进一步重构系统配置以支持活动-活动配置并向两个区域发送请求。

为什么要追求运行两个活跃环境?许多分布式系统都在 活动-活动配置 下运行,这意味着两个系统都处理和接受请求,并在它们之间复制数据。公共云架构建议使用多区域、活动-活动配置来提高系统的可靠性。

定义 在 活动-活动配置 中,多个系统是完成用户请求和它们之间数据复制的活跃环境。

您对 IaC 的更改将取决于主动-被动或主动-活动配置。Sundew 团队的主动-活动配置必须将 IaC 重构为更模块化的组件,并支持环境之间的数据复制。假设 Sundew 团队重构其应用程序以支持主动-活动配置,您需要在 IaC 中实现某种形式的全局负载均衡并将其连接到每个区域。

IaC 符合 可重复性,我们在第一章中讨论了这一点。多亏了这个原则,我们只需对属性进行少量更新,就可以在新的区域中创建一个新环境。您不必像我们在第二章中所做的那样,逐个资源地痛苦地重建环境资源。

然而,您可能会发现自己正在大量复制粘贴相同的配置。尝试将区域作为输入传递,并将您的 IaC 模块化以减少复制粘贴。将共享资源配置,如全局负载均衡器定义,从环境配置中分离出来。

多区域环境在金钱和时间上都会产生成本,但可以帮助提高系统可靠性。IaC 加速在其他区域创建新环境的副本,并强制跨区域保持一致配置。区域之间的不一致性可能导致重大的系统故障并增加维护工作量!第十二章讨论了成本管理和其考虑因素。

9.2 蓝绿部署

现在您有另一个环境可以作为备选,以防您意外损坏活动环境,您可以从更新 sundew 系统以使用全局网络和高级别网络访问服务器开始。sundew 系统使用主动-被动配置,这意味着在新的区域中复制整个新环境只是为了进行更改。

您意识到某些更改不需要在多个区域复制整个环境。为什么要有整个被动环境只是为了更新服务器?难道不能只更新一台服务器吗?毕竟,我们希望最小化故障的爆炸半径并优化资源效率。除了使用主动-被动配置外,您还可以将此模式应用于规模更小的少量资源。

在图 9.5 中,您使用全局网络而不是整个环境重新创建了一个新的网络。您将新的网络标记为绿色,并在其上部署了一组三台服务器和一个集群。在测试新资源后,您使用全局负载均衡器将一小部分流量发送到新资源。请求成功,表明全局路由的更新工作正常。

图片

图 9.5 使用蓝绿部署为全球网络创建绿色环境,将部分流量发送到新资源,并移除旧资源。

创建一组新资源并逐渐切换到它们的模式,应用了您系统的可组合性和可进化性原则。您向环境中添加一组新的绿色资源,并允许它们独立于旧资源独立进化。如果您想更改基础设施,重复此工作流程以减少更改的爆炸半径,并在将流量发送到它们之前测试新资源。

这种称为蓝绿部署的模式创建了一个新的基础设施资源子集,用于阶段化您想要进行的更改,标记为绿色。然后,您将一些请求定向到绿色阶段化基础设施资源,并确保一切正常。随着时间的推移,您将所有请求发送到绿色基础设施资源,并移除标记为蓝色的旧生产资源。

定义蓝绿部署是一种创建包含您想要进行更改的新子集基础设施资源的模式。您逐渐将流量从旧资源集(蓝色)转移到新资源集(绿色),并最终移除旧资源。

蓝绿部署允许您在发送请求之前在(暂时性)新的预发布环境中隔离和测试更改。在验证绿色环境后,您可以将新环境切换到生产环境并删除旧环境。您暂时为两个环境支付几周的费用,但最大限度地减少维护持久环境的总体成本。

注意:蓝绿部署有几个不同的标签,有时根据上下文会有细微的差别。无论您将环境标记为哪种颜色或名称,只要您能识别出哪个环境作为现有的生产环境或新的预发布环境即可。我还在蓝绿部署期间使用了生产/预发布和版本编号(v1/v2)来识别旧资源和新资源。

您应该使用蓝绿部署模式来重构或更新您的 IaC 超过一些最小配置。蓝绿部署依赖于不可变性原则来创建新资源,将流量或功能切换到它们,并删除旧资源。大多数重构(第十章)和更改 IaC 的模式通常涉及可重复性原则。

镜像构建和配置管理

您可以通过应用蓝绿部署模式类似地减轻失败机器镜像或配置的风险。将机器镜像或配置管理更新隔离到新的服务器(绿色),将其发送流量,并在删除旧服务器(蓝色)之前测试其功能。

您已经有一个用于现有 blue 网络的全局负载均衡器,您可以在以后用它来连接新的 green 网络。在接下来的章节中,我们将实现 sundew 系统的蓝绿部署的每个步骤。

9.2.1 部署绿色基础设施

要为 sundew 系统的全局网络和高级服务器启动蓝绿部署,您复制现有蓝色网络的配置。您创建名为 green.py 的文件并将蓝色网络配置粘贴进去。在下面的列表中,您对网络定义进行修改,使其使用全局路由模式。

列表 9.3 创建绿色网络

TEAM = 'sundew'
ENVIRONMENT = 'production'
VERSION = 'green'                                            ❶
REGION = 'us-central1'
IP_RANGE = '10.0.0.0/24'                                     ❷

zone = f'{REGION}-a'
network_name = f'{TEAM}-{ENVIRONMENT}-network-{VERSION}'     ❸

labels = {
   'team': TEAM,
   'environment': ENVIRONMENT,
   'automated': True
}

def build():                                                 ❸
   return network()                                          ❸

def network(name=network_name,                               ❸
           region=REGION,
           ip_range=IP_RANGE):
   return [
       {
           'google_compute_network': {                       ❹
               VERSION: [{
                   'name': name,
                   'auto_create_subnetworks': False,
                   'routing_mode': 'GLOBAL'                  ❺
               }]
           }
       },
       {
           'google_compute_subnetwork': {                    ❻
               VERSION: [{
                   'name': f'{name}-subnet',
                   'region': region,
                   'network': f'${{google_compute_network.{VERSION}.name}}',
                   'ip_cidr_range': ip_range
               }]
           }
       }
   ]

❶ 将新网络版本的名称设置为“绿色”

❷ 保持绿色网络的 IP 地址范围与蓝色网络相同。如果您没有设置对等连接,GCP 允许两个网络具有相同的 CIDR 块。

❸ 使用模块为绿色网络和子网络创建 JSON 配置

❹ 使用基于名称和全局路由模式的 Terraform 资源创建 Google 网络

❺ 将绿色网络的路由模式更新为全局,以在全球范围内暴露路由

❻ 使用基于名称、区域、网络和 IP 地址范围的 Terraform 资源创建 Google 子网络

AWS 和 Azure 的等效方案

如果你将列表 9.3 转换为 AWS 或 Azure,全局路由模式不适用。你仍然可以通过更改 Google 的网络和子网络为 VPC 或虚拟网络,以及更改子网和路由表来更新代码列表以适用于 AWS 或 Azure。

当可能时,你希望保持蓝色和绿色资源的相同配置。它们应该只在你想对绿色资源进行的更改上有所不同。然而,你可能有某些差异!

例如,如果我为我的一些网络有特定的对等配置,我不能使用蓝网络的 IP 地址范围用于绿色网络。相反,我需要一个不同的 IP 地址范围,如 10.0.1.0/24,并更新任何依赖以与另一个 IP 地址范围通信。

蓝绿部署倾向于不可变性,创建新的、更新的资源并将更改与旧资源隔离。然而,部署低级资源如网络的新版本并不意味着你可以立即向其发送实时流量。你总是从更改和测试你想要更新的基础设施资源开始。然后,你必须更改和测试其他依赖于它的资源。

9.2.2 将高级依赖部署到绿色基础设施

当你使用蓝绿部署模式时,你总是需要部署一个新的基础设施资源及其依赖的新一组高级资源。你完成了网络的更新,但除非你有服务器和应用程序在上面,否则你不能使用它。新的网络需要依赖于它的高级基础设施。

你向露珠团队传达部署新集群和服务器到绿色网络的指示,如图 9.6 所示。服务器必须在全局网络上使用高级网络。露珠团队还将其应用程序部署到集群和服务器上。

图片

图 9.6 在创建网络,一个低级基础设施资源之后,你还需要创建新的高级资源,如依赖于它的服务器和容器集群。

在这个例子中,更改低级基础设施资源如网络会影响高级资源。服务器必须使用高级网络运行。将原始blue网络就地从区域路由更新为全局路由可能会影响服务器和集群。使用蓝绿部署,你演进服务器的网络属性,而不影响实时环境。

让我们回顾一下露珠团队成员添加到以下列表中的 IaC 样本,用于将集群部署到green网络。他们从blue资源复制了集群配置,并更新了其属性以在green网络上运行。

列表 9.4 向绿色网络添加新的集群

VERSION = 'green'                                                                ❶

cluster_name = f'{TEAM}-{ENVIRONMENT}-cluster-{VERSION}'                         ❶
cluster_nodes = f'{TEAM}-{ENVIRONMENT}-cluster-nodes-{VERSION}'                  ❶
cluster_service_account = f'{TEAM}-{ENVIRONMENT}-sa-{VERSION}'                   ❶

def build():                                                                     ❷
   return network() + \
       cluster()                                                                 ❸

def cluster(name=cluster_name,                                                   ❹
           node_name=cluster_nodes,                                              ❹
           service_account=cluster_service_account,                              ❹
           region=REGION):                                                       ❹
   return [
       {
           'google_container_cluster': {                                         ❺
               VERSION: [
                   {
                       'initial_node_count': 1,   
                       'location': region,
                       'name': name,                                             ❺
                       'remove_default_node_pool': True,
                       'network': f'${{google_compute_network.{VERSION}.name}}', ❻
                       'subnetwork': \                                           ❻
                         f'${{google_compute_subnetwork.{VERSION}.name}}'        ❻
                   }
               ]
           }
       }
   ]

❶ 将集群的新版本标记为“green”

❷ 使用模块创建绿色网络的网络、子网络和集群的 JSON 配置

❸ 在绿色网络和子网络上构建集群

❹ 将所需的属性传递给集群,包括名称、节点名称、自动化服务账户和区域

❺ 使用具有一个节点和绿色网络的 Terraform 资源创建 Google 容器集群

❻ 在绿色网络和子网络上构建集群

AWS 和 Azure 等效

您可以通过将 Google 容器集群更改为 Amazon EKS 集群或 Azure Kubernetes 服务 (AKS) 集群来更新代码。您将需要一个 Amazon VPC 和 Azure 虚拟网络用于 Kubernetes 节点池(也称为组)。

集群不需要任何更改即可适应全局网络配置。然而,服务器需要高级网络。您从 blue 复制服务器配置,并将其更改为在 green.py 中使用高级网络属性。

列表 9.5 向绿色网络上的服务器添加高级网络


VERSION = 'green'                                           ❶

server_name = f'{TEAM}-{ENVIRONMENT}-server-{VERSION}'      ❷

def build():                                                ❸
   return network() + \
       cluster() + \
       server0() + \                                        ❹
       server1() + \                                        ❹
       server2()                                            ❹

def server0(name=f'{server_name}-0',                        ❺
           zone=zone):                                      ❺
   return [
       {
           'google_compute_instance': {                     ❻
               f'{VERSION}_0': [{
                   'allow_stopping_for_update': True,
                   'boot_disk': [{
                       'initialize_params': [{
                           'image': 'ubuntu-1804-lts'    
                       }]
                   }],
                   'machine_type': 'f1-micro',          
                   'name': name,
                   'zone': zone,
                   'network_interface': [{
                       'subnetwork': \
                            f'${{google_compute_subnetwork.{VERSION}.name}}',
                       'access_config': {
                           'network_tier': 'PREMIUM'        ❼
                       }
                   }]
               }]
           }
       }
   ] 

❶ 将新版本的“绿色”网络进行标记

❷ 创建服务器名称模板,包括团队、环境和版本(蓝色或绿色)

❸ 使用模块创建网络、子网络、集群和绿色网络的 JSON 配置

❹ 在绿色网络上构建与集群相关的三个服务器

❺ 复制并粘贴每个服务器配置。此代码片段展示了第一个服务器,server0。为了清晰起见,省略了其他服务器配置。

❻ 使用绿色网络上的 Terraform 资源创建一个小型 Google 计算实例(服务器)

❼ 设置网络层以使用高级网络。这使它与使用全局路由的底层子网兼容。

AWS 和 Azure 等效

如果将列表 9.5 转换为 AWS 或 Azure,则网络层不适用。您仍然可以通过将 Google 计算实例更改为 Amazon EC2 实例或带有 Ubuntu 18.04 映像的 Azure Linux 虚拟机来更新代码。首先您需要一个 Amazon VPC 和 Azure 虚拟网络。

将网络层更新为高级 不应 影响应用程序的功能,尽管您并不完全确定!绿色环境允许您在影响太阳草生长之前识别和缓解任何问题。在太阳草团队进行更新后,它将更改推送到交付管道并检查测试结果。

测试包括单元测试、集成测试和端到端测试,以确保您可以在新的容器集群上运行应用程序并向新的绿色服务器发送请求。幸运的是,测试通过了,您感觉准备好将实时流量发送到绿色资源。

9.2.3 使用金丝雀部署到绿色基础设施

您可以立即将所有流量发送到绿色网络、服务器和集群。然而,您不希望关闭 sundew 系统!理想情况下,当您发现系统有问题时,您希望将所有流量切换回蓝色。在图 9.7 中,您调整全局负载均衡器,将 90% 的流量发送到蓝色网络,并将 10% 的流量发送到绿色网络上的服务。

图 9.7 配置全局负载均衡器以运行金丝雀测试并向绿色资源发送少量流量。

如果发送到您系统的少量流量(称为 金丝雀部署)导致请求错误,您需要调试和修复您的更改。

定义 金丝雀部署 是一种模式,它将少量流量发送到系统中的更新资源。如果请求成功完成,您将随着时间的推移逐渐增加流量的百分比。

为什么首先发送少量流量?您不希望所有请求都失败。向更新的资源发送少量请求有助于在影响整个系统之前识别关键问题。

矿井中的金丝雀

在软件或基础设施中,金丝雀 作为新系统、功能或应用程序是否工作的第一个指标。这个术语来自表达“矿井中的金丝雀”。

您在软件开发中经常会发现对金丝雀测试的引用,它衡量应用程序或功能新版本的用户体验。我强烈推荐金丝雀部署,这是一种将少量流量发送到新资源的技术,您在做出重大基础设施更改时应该使用它。

注意,您不必使用负载均衡器来实现金丝雀部署。您可以使用任何方法向更新的基础设施资源发送少量请求。例如,您可以将一个更新的应用程序实例添加到现有的三个应用程序实例池中。轮询负载均衡方案会将大约 25% 的请求发送到新的、更新的实例,并将 75% 的请求发送到旧的、现有的应用程序实例。

对于 sundew 团队,您将全局负载均衡器配置与绿色和蓝色环境分开。这提高了负载均衡器的可扩展性。您将绿色服务器作为单独的后端服务添加到负载均衡器中,并控制绿色和蓝色环境之间的请求。

您在以下列表中定义了名为 shared.py 的文件中的负载均衡器。让我们将网络的绿色版本(包括服务器和集群)添加到具有 10 个权重的版本化环境列表中。

列表 9.6 将绿色版本添加到负载均衡服务列表中

import blue                                         ❶
import green                                        ❶

shared_name = f'{TEAM}-{ENVIRONMENT}-shared'        ❷

services_list = [                                   ❸
   {
       'version': 'blue',                           ❹
       'zone': blue.zone,                           ❹
       'name': f'{shared_name}-blue',               ❹
       'weight': 90                                 ❺
   },
   {
       'version': 'green',                          ❻
       'zone': green.zone,                          ❻
       'name': f'{shared_name}-green',              ❻
       'weight': 10                                 ❼
   }
]

def _generate_backend_services(services):           ❽
   backend_services_list = []
   for service in services:                         ❾
       version = service['version']                 ❾
       weight = service['weight']                   ❾
       backend_services_list.append({
           'backend_service': (                     ❾
               '${google_compute_backend_service.'  ❾
               f'{version}.id}}'                    ❾
           ),    
           'weight': weight,                        ❾
       })
   return backend_services_list

❶ 导入蓝色和绿色环境的 IaC

❷ 根据团队和环境创建共享全局负载均衡器的名称

❸ 定义每个环境的版本列表,以便附加到负载均衡器,包括蓝色和绿色环境

❸ 将蓝色网络、服务器和集群添加到负载均衡器列表中。从其 IaC 中检索蓝色环境的可用区。

❺ 将流量权重设置为蓝色服务器实例的 90%,代表 90% 的请求

❻ 将绿色网络、服务器和集群添加到负载均衡器列表中。从其 IaC 中检索绿色环境的可用区。

❷ 将流量权重设置为绿色服务器实例的 10%,代表 10% 的请求

❽ 创建一个函数以生成负载均衡器的后端服务列表

❾ 对于每个环境,定义一个具有版本和权重的 Google 负载均衡后端服务

AWS 和 Azure 的等效功能

列表 9.6 中的后端服务类似于 AWS ALB 的 AWS 目标组。然而,Azure 需要额外的资源。您需要创建 Azure Traffic Manager 配置文件和端点,并将其链接到附加到 Azure Application Gateway 的后端地址池。

shared.py 中的负载均衡器已经可以接受具有不同权重的后端服务列表。一旦您在以下列表中部署了权重和服务列表,负载均衡器配置开始将 10% 的流量发送到绿色网络。

列表 9.7 更新负载均衡器以将流量发送到绿色环境

default_version = 'blue'                                       ❶

def load_balancer(name, default_version, services):            ❷
   return [{
       'google_compute_url_map': {                             ❸
           TEAM: [{
               'default_service': (                            ❶
                   '${google_compute_backend_service.'         ❶
                   f'{default_version}.id}}'                   ❶
               ),    
               'description': f'URL Map for {TEAM}',
               'host_rule': [{
                   'hosts': [
                       f'{TEAM}.{COMPANY}.com'
                   ],
                   'path_matcher': 'allpaths'
               }],
               'name': name,
               'path_matcher': [{
                   'default_service': (                        ❶
                       '${google_compute_backend_service.'     ❶
                       f'{default_version}.id}}'               ❶
                   ),                                          ❶
                   'name': 'allpaths',
                   'path_rule': [{
                       'paths': [
                           '/*'
                       ],
                       'route_action': {
                           'weighted_backend_services':        ❹
                               _generate_backend_services(     ❹
                                   services)                   ❹
                       }
                   }]
               }]
           }]
       }
   }]

❶ 默认将所有流量从负载均衡器发送到蓝色环境

❸ 使用该模块创建负载均衡器的 JSON 配置,以将 10% 的流量发送到绿色,90% 的流量发送到蓝色

❸ 使用基于路径、蓝色和绿色环境以及权重的 Terraform 资源创建 Google 计算 URL 映射(负载均衡规则)

❹ 在负载均衡器上设置路由规则,将 10% 的流量发送到绿色,90% 的流量发送到蓝色。

AWS 和 Azure 的等效功能

Google Cloud URL 映射类似于 AWS ALB 或 Azure Traffic Manager 和 Application Gateway。要将列表 9.7 转换为 AWS,您需要更新资源以创建 AWS ALB 和监听器规则。然后,向 ALB 监听器规则添加路径路由和权重属性。

对于 Azure,您需要将 Azure Traffic Manager 配置文件和端点链接到 Azure Application Gateway。更新 Azure Traffic Manager,使用权重并将它们路由到附加到 Azure Application Gateway 的正确后端地址池。

您在列表 9.8 中运行 Python 以构建用于审查的 Terraform JSON 配置。负载均衡器的 JSON 配置包括组织蓝色服务器和绿色服务器的实例组、针对蓝色和绿色实例组的后端服务以及加权路由操作。

列表 9.8 负载均衡器的 JSON 配置

{
   "resource": [
       {
           "google_compute_url_map": {                                    ❶
               "sundew": [
                   {
                       "default_service": \                               ❷
                           "${google_compute_backend_service.blue.id}",   ❷
                       "description": "URL Map for sundew",
                       "host_rule": [
                           {
                               "hosts": [
                                   "sundew.dc4plants.com"
                               ],
                               "path_matcher": "allpaths"
                           }
                       ],
                       "name": "sundew-production-shared",
                       "path_matcher": [
                           {
                               "default_service": 
                                 "${google_compute_backend_service.blue.id}",
                               "name": "allpaths",
                               "path_rule": [
                                   {
                                       "paths": [                         ❸
                                           "/*"                           ❸
                                       ],                                 ❸
                                       "route_action": {
                                           "weighted_backend_services": [
                                               {
                                                   "backend_service": 
                                   "${google_compute_backend_             ❹
                                   service.blue.id}",                     ❹
                                                   "weight": 90,          ❹
                                               },
                                               {
                                                   "backend_service":
                                   "${google_compute_backend_             ❺
                                   service.green.id}",                    ❺
                                                   "weight": 10,          ❺
                                               }
                                           ]
                                       }
                                   }
                               ]
                           }
                       ]
                   }
               ]
           }
       }
   ]
}

❷ 使用基于路径、蓝色和绿色环境以及权重的 Terraform 资源定义 Google 计算 URL 映射(负载均衡规则)

❷ 为 Google 计算 URL 映射(负载均衡规则)定义默认服务为蓝色环境

❸ 根据权重将所有请求发送到蓝色或绿色环境

❹ 将 90%的流量发送到使用蓝色网络的蓝色后端服务

❺ 将 10%的流量发送到使用绿色网络的绿色后端服务

为什么默认将所有流量发送到蓝色环境?你知道蓝色环境可以成功处理请求。如果你的绿色环境出现问题,你可以快速切换负载均衡器,将流量发送到默认的蓝色环境。

通常,复制、粘贴并更新green资源。如果您在模块中表达blue资源,您只需更改传递给模块的属性。在可能的情况下,我将绿色和蓝色环境定义分开在单独的文件夹或文件中,这使得以后更容易识别环境。

您可能会注意到在 shared.py 中的一些 Python 代码,这些代码使得更容易进化环境列表和附加到负载均衡器的默认环境。我通常定义一个环境列表和一个默认环境变量。然后,我遍历环境列表,并将属性附加到一个负载均衡器上。这确保了高级负载均衡器可以进化以适应不同的资源和环境。

当您添加新资源时,您可以调整您的负载均衡器以将流量发送到额外的环境。您可能会发现自己每次想要运行蓝绿部署时都要更新负载均衡器的 IaC。花时间和精力配置负载均衡器有助于减轻更改带来的任何问题,并控制可能具有破坏性的更新的发布。

9.2.4 执行回归测试

如果您立即将所有流量发送到绿色网络并且它失败了,您可能会破坏开普敦捕蝇草的灌溉系统。因此,您从金丝雀部署开始,每天将发送到绿色网络的流量比例增加 10%。这个过程大约需要两周时间,但您对自己更新网络的正确性感到自信!如果您发现问题,您会减少发送到绿色网络的流量并调试。

图 9.8 显示了您逐渐增加流量并在一段时间内测试绿色环境的过程。您逐渐减少发送到蓝色环境的流量,直到达到 0%。然后,您反向增加发送到绿色环境的流量,直到达到 100%。在禁用蓝色网络之前,您运行绿色环境一周或两周,以防更改破坏了绿色环境中的系统。

图 9.8 在切断所有流量到新网络并移除旧网络之前,允许一周时间进行回归测试,这为验证功能提供了时间。

逐渐增加流量并在一周前进行的过程似乎很痛苦!然而,您需要让系统通过绿色环境运行足够的流量以确定是否可以继续。一些故障仅在系统通过足够的流量时出现,而其他故障则需要时间来检测。

您花费在测试、观察和监控系统错误的时间窗口成为系统回归测试的一部分。回归测试 检查系统更改是否影响现有或新功能。随着时间的推移逐渐增加流量允许您评估系统的功能,同时减轻潜在的失败影响。

定义 回归测试 检查系统更改是否影响现有或新功能。

您应该增加多少流量到绿色环境?每天增加 1%的流量不会提供太多信息,除非您的系统每天处理数百万个请求。逐渐增加并没有提供明确的增量标准。我建议评估您系统每天处理的请求数量和失败的成本(例如用户请求上的错误)。

我通常从 10%的增量开始,并检查这对系统意味着多少请求。如果我没有获得足够的请求样本量来识别失败,我会增加增量。您希望在每次百分比增加之间插入一个回归测试窗口以识别系统故障。

即使将负载均衡器增加到将所有流量发送到绿色网络,您仍然希望运行测试并监控系统功能一周或两周。为什么运行几周的回归测试?有时您可能会遇到来自应用程序请求的边缘情况,这会破坏功能。通过允许进行回归测试的期间,您可以观察系统是否可以处理意外或不常见的负载或请求。

9.2.5 删除蓝色基础设施

您观察捕蝇草系统两周并解决任何错误。您知道蓝色网络大约两周内没有处理任何请求或数据,这意味着您可以在不进行额外迁移步骤的情况下将其删除。您通过与同伴或变更咨询委员会审查来确认其不活跃状态。在从 IaC 中删除蓝色环境之前,图 9.9 更新了默认服务到绿色环境。

图片

图 9.9 通过从 IaC 中删除它并移除所有引用来退役旧网络。

我认为删除带有网络的蓝色环境是一个重大的变更,需要额外的同行审查。你可能不知道谁在使用该网络。其他你不会与其他团队共享的资源,如服务器,可能不需要额外的同行审查或变更批准。评估删除环境的潜在影响,并根据第七章中的模式对变更进行分类。让我们通过将默认服务设置为绿色网络并从其后端服务中删除蓝色网络来调整 shared.py 中的负载均衡器,如下所示。

列表 9.9 删除蓝色环境负载均衡器

import blue                             ❶
import green                            ❶

TEAM = 'sundew'
ENVIRONMENT = 'production'
PORT = 8080

shared_name = f'{TEAM}-{ENVIRONMENT}-shared'

default_version = 'green'               ❷

services_list = [                       ❸
   {
       'version': 'green',
       'zone': green.zone,
       'name': f'{shared_name}-green',
       'weight': 100                    ❹
   }
]

❶ 导入蓝色和绿色环境的 IaC

❷ 将负载均衡器的网络默认版本更改为绿色

❸ 从要生成的后端服务列表中删除蓝色网络和实例

❹ 将所有流量发送到绿色网络

AWS 和 Azure 等效物

列表 9.9 在 AWS 和 Azure 中保持不变。你将需要将版本、可用区、名称和权重映射到 AWS ALB 或 Azure Traffic Manager。

你将更改应用到负载均衡器。然而,你立即删除蓝色资源,因为你必须确保负载均衡器不引用任何蓝色资源。在测试更改后,你从 main.py 中删除构建蓝色环境的代码,并保留绿色环境,如下所示。

列表 9.10 从 main.py 中删除蓝色环境

import green
import json

if __name__ == "__main__":
   resources = {
       'resource':
       shared.build() +                              ❶
       green.build()                                 ❷
   }

   with open('main.tf.json', 'w') as outfile:        ❸
       json.dump(resources, outfile,                 ❸
                 sort_keys=True, indent=4)           ❸

❶ 使用共享模块创建全局负载均衡器的 JSON 配置

❷ 使用绿色模块创建具有全局路由、带高级网络的服务器和集群的网络的 JSON 配置

❸ 将 Python 字典写入 JSON 文件,供 Terraform 后续执行

你应用了更改,你的 IaC 工具删除了所有蓝色资源。你决定删除 blue.py 文件,以防止任何人创建新的蓝色资源。我建议删除你不再使用的任何文件,以减少未来队友的困惑。否则,你可能会拥有比你需要的更多资源的系统。

练习 9.1

考虑以下代码:

if __name__ == "__main__":
  network.build()
  queue.build(network)
  server.build(network, queue)
  load_balancer.build(server)
  dns.build(load_balancer)

队列依赖于网络。服务器依赖于网络和队列。你将如何运行蓝绿部署以升级带有 SSL 的队列?

请参阅附录 B 以获取练习答案。

9.2.6 其他考虑事项

假设 sundew 团队需要再次更改网络。而不是创建一个新的绿色网络,团队可以创建一个新的蓝色网络并重复部署、回归测试和删除过程!由于旧的蓝色网络不再存在,这次更新不会与现有环境冲突。

您为更改的版本或迭代命名的内容并不重要,只要您能够区分新旧资源即可。对于网络而言,您可以考虑分配两组 IP 地址范围。您应该永久保留一组用于蓝色网络,另一组用于绿色网络。这种分配将允许您通过使用蓝绿部署来灵活地做出更改,而无需寻找开放的网络安全空间。

通常,当我遇到以下情况时,我会决定使用蓝绿部署策略:

  • 回滚资源更改需要很长时间。

  • 我不确定我能否在部署后回滚资源更改。

  • 资源有许多我难以轻易识别的高级依赖项。

  • 资源更改会影响不能停机的时间关键应用程序。

并非所有基础设施资源都应该使用蓝绿部署。例如,您可以就地更新 IAM 策略,并在发现问题时快速回滚。您将在第十一章中了解更多关于回滚更改的内容。

蓝绿部署策略在时间和金钱上比维护多个环境成本更低。然而,当您必须部署像网络、项目或账户这样的低级基础设施资源时,这种策略将更贵!我通常认为这种模式值得成本。它将更改隔离到特定资源,并为部署更改和最小化系统中断提供一种风险较低的方法。

9.3 有状态基础设施

在本章中,示例省略了一个重要的基础设施资源类别。然而,捕蝇草系统包括许多处理、管理和存储数据的资源。例如,捕蝇草系统包括一个在具有区域路由的网络上运行的 Google SQL 数据库。

9.3.1 蓝绿部署

捕蝇草应用程序团队成员提醒您,他们需要更新数据库以使用具有全局路由的新网络。您在 IaC 中更新私有网络 ID 并将更改推送到您的仓库。您的部署管道在合规性测试(您在第八章中学到的)中失败。

您注意到测试检查了干运行(计划)是否失败,以确定数据库删除操作:

$ pytest . -q

F                                                 [100%]
====== FAILURES ======
_____ test_if_plan_deletes_database _____

database = {'address': 'google_sql_database_instance.blue', 'change': 
➥{'actions': ['delete'], 'after': None, 'after_sensitive': False, 
➥'after_unknown': {}, ...}, 'mode': 'managed', 'name': 'blue', ...}

    def test_if_plan_deletes_database(database):
>       assert database['change']['actions'][0] != 'delete'
E       AssertionError: assert 'delete' != 'delete'

test/test_database_plan.py:35: AssertionError
======= short test summary info =======
FAILED test/test_database_plan.py::test_if_plan_deletes_database - 
➥AssertionError: assert 'delete' != 'delete'
1 failed in 0.04s

合规性测试阻止您删除关键数据库!如果您在没有测试的情况下应用更改,您将删除所有的捕蝇草数据!捕蝇草团队对在原地更新数据库的网络表示担忧,因此您需要进行蓝绿部署。

在图 9.10 中,您手动验证是否可以将数据库迁移到绿色网络。然而,您发现无法迁移数据库。捕蝇草系统可以处理缺失的数据,因此您复制蓝色数据库的 IaC 并在高级网络中创建一个新的绿色数据库实例。在将数据从蓝色数据库迁移到绿色数据库后,您切换应用程序以使用新的数据库,并删除旧的数据库。

图片

图 9.10 如果你无法就地更新数据库,你必须部署一个新的绿色数据库,迁移和协调数据,并将数据库端点从蓝色更改为绿色。

蓝绿部署策略适用于数据库,这是一类关注状态的底层基础设施资源。有状态的基础设施资源,如数据库,存储和管理数据。实际上,所有应用程序都会处理和存储一定量的数据。然而,有状态的基础设施需要额外的关注,因为变化可能会直接影响到数据。这类基础设施包括数据库、队列、缓存或流处理工具。

定义 有状态的 基础设施描述的是存储和管理数据的底层基础设施资源。

为什么要在具有数据的底层基础设施上使用蓝绿部署?有时你无法使用 IaC 就地更新资源。替换资源可能会损坏或丢失数据,这会影响你的应用程序。蓝绿部署可以帮助你在应用程序使用之前测试新数据库的功能。

9.3.2 更新交付管道

让我们回到捕蝇草团队。你必须修复交付管道以自动化数据库的更新。在图 9.11 中,你通过添加一个步骤来更新你的交付管道,该步骤会自动将数据从蓝色数据库迁移到绿色数据库。当你添加绿色数据库并部署它时,你的管道会部署新的数据库,运行集成测试,自动将数据从蓝色迁移到绿色数据库,并通过端到端测试完成管道。

在自动化迁移步骤时保持幂等性。你的迁移脚本或自动化工具应该每次都产生相同的数据库状态。它应该避免在每次运行自动化时重复数据。数据迁移过程取决于你所拥有的有状态基础设施的类型(数据库、队列、缓存或流处理工具)。

图片

图 9.11 基础设施部署管道应该添加绿色数据库并将数据从蓝色复制到绿色。

注意 你可以写整本书来介绍如何迁移和管理有状态的基础设施,同时尽量减少(或零)停机时间。我推荐 Laine Campbell 和 Charity Majors 的《数据库可靠性工程》(O’Reilly,2017),其中包含其他关于管理数据库的模式和实践。你可以参考其他有状态基础设施资源迁移、升级和可用性的具体文档。

根据你更新有状态基础设施的频率,你应该将自动化的数据迁移捕获到你的部署管道中,而不是在 IaC 中。分离数据迁移允许你更改任何步骤并独立于创建和删除有状态资源来调试迁移问题。

9.3.3 金丝雀部署

要完成捕蝇草团队的数据库更新,你需要更改应用配置以使用绿色数据库。作为高级资源,应用依赖于数据库,就像负载均衡器将流量发送到服务器一样。你可以使用修改后的金丝雀部署形式切换到新数据库。

图 9.12 显示了回归测试的模式以及配置应用以使用数据库。经过一段时间的回归测试(以确保功能仍然正常),你更新应用以写入数据到绿色数据库。经过另一段时间的回归测试后,你更新应用以从绿色数据库读取数据。

图 9.12 你逐步更新应用部署管道以写入两个数据库,写入绿色数据库,然后从绿色数据库读取。

如果你在应用中遇到问题,可以逐步推出更改以恢复到蓝色数据库。由于应用写入两个数据库,你可能需要编写额外的自动化脚本来协调数据。然而,写入新的有状态基础设施确保你可以测试与存储和更新数据相关的任何关键功能。只有在这种情况下,应用才能正确读取和处理数据。

图 9.13 通过从 IaC 中删除蓝色数据库并将其推送到部署管道中来删除蓝色数据库。

现在,由于捕蝇草应用使用绿色数据库,图 9.13 通过从 IaC 中删除它来删除蓝色数据库。请注意,当你删除数据库时,合规性测试将失败,因为你删除了一个数据库!如果你计划删除绿色数据库而不是蓝色数据库,请更新测试以失败。手动覆盖允许你删除蓝色数据库,因为应用不再使用它。

像金丝雀部署这样的技术可以提供快速反馈以减轻失败的影响,尤其是在涉及数据处理的情况中。它可能意味着在数据库中修复几个错误条目与从备份中完全恢复数据库之间的区别!我知道我在一个隔离的绿色环境中对有状态基础设施进行更改,而不是在实时生产系统中,这让我感到安心。

使用不可变性(如蓝绿部署)的策略提供了一个结构化的过程来做出更改并最小化潜在失败的影响范围。多亏了可重复性原则,你通常可以通过复制和编辑配置来使用不可变方法更改 IaC。该原则还允许你通过类似的过程提高基础设施系统的冗余性。

摘要

  • 在开始任何基础设施更新之前,确保你的系统具有基础设施和应用的测试、监控和可观察性。

  • 在 IaC 中的冗余意味着向配置中添加额外的空闲资源,以便在组件故障时进行故障转移。

  • 向基础设施即代码(IaC)中添加冗余配置可以提高系统可靠性,衡量系统在一定时期内正确运行的时间。

  • 主动-被动配置包括一个主动环境处理请求,以及当主动环境失败时用于替换的重复空闲环境。

  • 故障转移将流量从失败的主动环境切换到空闲的被动环境。

  • 主动-主动配置设置两个主动环境来处理请求,这两个环境都可以使用 IaC 进行复制和管理。

  • 蓝绿部署创建了一个新的基础设施资源子集,用于阶段性地实施您想要进行的更改,并逐渐将请求切换到新的子集。然后您可以删除旧的资源集。

  • 在蓝绿部署中,部署您想要更改的资源以及依赖于它的顶级资源。

  • 金丝雀部署将一小部分流量发送到新的基础设施资源,以验证系统是否正确工作。随着时间的推移,您会增加流量的百分比。

  • 给出几周的时间进行回归测试,以检查系统更改是否影响现有或新的功能。

  • 有状态基础设施资源——如数据库、缓存、队列和流处理工具——存储和管理数据。

  • 将迁移步骤添加到有状态基础设施资源的蓝绿部署中。您必须在蓝色和绿色有状态基础设施之间复制数据。

10 重构

本章涵盖

  • 确定何时重构 IaC 以避免影响系统

  • 将功能标志应用于可变地更改基础设施属性

  • 解释滚动更新以完成就地更新

随着时间的推移,您可能会超出您用于协作基础设施代码的模式和实践。即使是像蓝绿部署这样的变更技术也无法解决在您的团队处理某些 IaC 时的配置或更改冲突。您必须对 IaC 进行一系列重大更改,并解决实践扩展中的问题。

例如,食肉植物数据中心团队表示,它不能再轻松自信地推出其系统的新更改。团队将所有基础设施资源放在一个仓库中(按照单例模式),以快速交付系统,并且只是不断地在上面添加新的更新。

sundew 团队概述了其系统的一些问题。首先,团队发现其对基础设施配置的更新不断重叠。一位队友正在更新服务器,却发现另一位队友已经更新了网络,这将影响他们的更改。

其次,运行单个更改需要超过 30 分钟。一个更改会对您的基础设施 API 进行数百次调用以检索资源状态,这会减慢反馈周期。

最后,安全团队表示担忧,sundew 基础设施可能存在不安全的配置。当前的配置没有使用标准化的、加固的公司基础设施模块。

您意识到需要更改 sundew 团队的配置。配置应使用安全团队批准的现有服务器模块。您还需要将配置分解为单独的资源,以最小化更改的影响范围。

本章讨论了一些 IaC 模式和技巧,用于分解具有数百个资源的大型单例仓库。作为 sundew 团队的 IaC 助手,您将重构系统的单例配置到单独的仓库中,并将服务器配置结构化以使用模块,以避免冲突并符合安全标准。

注意:演示重构需要足够大(且复杂)的示例。如果您运行完整示例,您将承担超出 GCP 免费层的费用。本书仅包含相关的代码行,为了可读性省略了其余部分。对于完整的列表,请参考本书的代码仓库github.com/joatmon08/manning-book/tree/main/ch10。如果您将这些示例转换为 AWS 和 Azure,您也将产生费用。在可能的情况下,我提供将示例转换为所选云提供商的注释。

10.1 最小化重构影响

捕蝇草团队需要帮助分解其基础设施配置。你决定重构基础设施即代码(IaC),以更好地隔离冲突,减少将更改应用到生产所需的时间,并按照公司标准进行安全加固。重构 IaC 涉及重新构建配置或代码,而不影响现有的基础设施资源。

定义重构 IaC 是指在不对现有基础设施资源造成影响的情况下重新构建配置或代码的实践。

你向捕蝇草团队传达,其配置需要进行重构以修复问题。虽然团队成员支持你的努力,但他们要求你尽量减少重构的影响。接受挑战:你在重构 IaC 时应用了一些技术来减少潜在的破坏范围。

技术债务

重构通常可以解决技术债务。技术债务最初是一个隐喻,用来描述任何使整体系统难以更改或扩展的代码或方法的成本。

为了理解应用于 IaC 的技术债务,回想一下捕蝇草团队将所有基础设施资源放入了一个仓库。捕蝇草团队在时间和精力上积累债务。一个本应花费一天的服务器更改需要四天,因为团队需要解决与其他更改的冲突并等待数百次对基础设施 API 的请求。请注意,在复杂的系统中,你总是会有些技术债务,但你需要持续的努力来最小化它。

管理团队害怕听到你需要解决技术债务,因为你没有在开发新功能。我争辩说,你在基础设施中积累的技术债务总是会回来困扰你。技术债务的鬼魂以某人更改基础设施并导致应用程序停机,或者更糟糕的是,导致个人信息泄露并产生货币成本的安全漏洞的形式出现。评估不修复技术债务的影响有助于证明努力的合理性。

10.1.1 通过滚动更新减少破坏范围

食肉植物数据中心和安全团队提供了一个具有安全配置的服务器模块,你可以用它来构建捕蝇草系统。捕蝇草团队的基础设施配置有三个服务器配置,但没有使用安全模块。你如何更改捕蝇草的 IaC 以使用该模块?

想象一下,你一起创建了三个新的服务器,并立即向它们发送流量。如果应用程序在服务器上运行不正确,你可能会完全破坏捕蝇草系统,而这些可怜的植物得不到浇水!相反,你可能通过逐个逐步更改服务器来减少服务器模块重构的破坏范围。

在图 10.1 中,你使用模块创建一个服务器配置,将应用程序部署到新服务器,验证应用程序是否正常工作,然后删除旧服务器。你为每个服务器重复此过程两次。你逐渐将更改推广到一台服务器,然后再更新下一台。

图片

图 10.1 使用滚动更新创建每个新服务器,部署应用程序,并测试其功能,同时最大限度地减少对其他资源的干扰。

滚动更新逐个逐渐更改相似资源,并在继续更新之前测试每个资源。

定义:滚动更新是一种逐个更改一组相似资源并测试它们,然后再将更改实施到下一个资源中的实践。

将滚动更新应用于雾水团队配置,每次更新时都会将故障隔离到单个服务器,并允许你在进行下一个更新之前测试服务器的功能。

滚动更新的实践可以让你避免处理大量失败更改或错误配置的 IaC(基础设施即代码)带来的痛苦。例如,如果食肉植物数据中心模块在一台服务器上不起作用,你还没有将其推广出去并影响到其他服务器。滚动更新让你在继续下一个服务器之前检查每个服务器是否都有适当的 IaC。渐进的方法还可以减轻应用程序的任何停机时间或服务器更新中的故障。

注意:我借鉴了来自工作负载协调器(如 Kubernetes)的滚动更新方法进行重构。当你需要为工作负载协调器更新新的节点(虚拟机)时,你可能发现它使用了一个自动化的滚动更新机制。协调器隔离旧节点,防止新的工作负载在其上运行,然后在新的节点上启动所有运行中的进程,将旧节点上的所有进程移除,并将工作负载和请求发送到新节点。当你重构时,你应该模仿这个工作流程!

多亏了滚动更新和增量测试,你知道服务器可以与安全模块一起运行。你告诉团队你已经完成了服务器的重构,并确认它们可以与内部服务一起工作。雾水团队现在可以将所有客户流量发送到新安全服务器。然而,团队成员告诉你,他们需要首先更新面向客户的负载均衡器!

10.1.2 使用功能标志进行阶段重构

你需要一种方法在几天内将新服务器从面向客户的负载均衡器中隐藏,并在团队批准后将其附加。然而,你已经有所有配置就绪!你希望用一个变量来隐藏服务器附加,以简化雾水团队的工作。当团队成员完成负载均衡器更新后,他们只需要更新一个变量,将新服务器添加到负载均衡器中。

图 10.2 概述了如何设置变量以添加由模块创建的新服务器。您创建一个布尔值来启用或禁用新服务器模块,使用 TrueFalse。然后,您在 IaC 中添加一个引用布尔值的 if 语句。True 变量将新服务器添加到负载均衡器。False 变量从负载均衡器中删除服务器。

图 10.2 概述了在基础设施即代码(IaC)中特性标志的创建、管理和删除过程。

布尔变量有助于 IaC 的可组合性或可进化性。对变量的单个更改会添加、删除或更新配置。这个变量被称为 特性标志(或 特性开关),用于启用或禁用基础设施资源、依赖项和属性。您通常在基于主干的开发模型(这些只在主分支上工作)的软件开发中找到特性标志。

定义 特性标志(也称为 特性开关)通过布尔值启用或禁用基础设施资源、依赖项或属性。

标志隐藏某些特性或代码,并防止它们影响主分支上的其他团队。对于捕蝇草团队,您将新服务器从负载均衡器中隐藏,直到团队完成负载均衡器更改。同样,您可以在 IaC 中使用特性标志进行配置阶段,并通过单个变量推送更新。

设置标志。

要开始实施特性标志并为新服务器进行阶段更改,您添加一个标志并将其设置为 False。您将特性标志默认设置为 False 以保留原始基础设施状态,如图 10.3 所示。捕蝇草配置默认禁用服务器模块,因此原始服务器不会受到影响。

让我们在 Python 中实现特性标志。您在名为 flags.py 的单独文件中将服务器模块标志的默认值设置为 False。该文件定义了标志 ENABLE_SERVER_MODULE 并将其设置为 False

ENABLE_SERVER_MODULE = False

图 10.3 通过将特性标志默认设置为 False 来保留基础设施资源的原始状态和依赖项。

您也可以在其他文件中嵌入特性标志作为变量,但您可能会失去对它们的跟踪!您决定将它们放在一个单独的 Python 文件中。

注意:我总是在一个文件中定义特性标志,以便在一个地方识别和更改它们。

以下列表在 main.py 中导入特性标志,并添加生成要添加到负载均衡器的服务器列表的逻辑。

列表 10.1 包含将服务器添加到负载均衡器的特性标志。

import flags                                                         ❶

def _generate_servers(version):
   instances = [                                                     ❷
       f'${{google_compute_instance.{version}_0.id}}',               ❷
       f'${{google_compute_instance.{version}_1.id}}',               ❷
       f'${{google_compute_instance.{version}_2.id}}'                ❷
   ]                                                                 ❷
   if flags.ENABLE_SERVER_MODULE:                                    ❸
       instances = [                                                 ❹
           f'${{google_compute_instance.module_{version}_0.id}}',    ❹
           f'${{google_compute_instance.module_{version}_1.id}}',    ❹
           f'${{google_compute_instance.module_{version}_2.id}}',    ❹
       ]                           
   return instances

❶ 导入定义所有特性标志的文件。

❷ 使用系统中的 Terraform 资源定义现有 Google 计算实例(服务器)列表。

❸ 使用条件语句评估特性标志,并将服务器模块的资源添加到负载均衡器。

❹ 将功能标志设置为 True 将会将模块创建的服务器附加到负载均衡器。否则,它将保持原始服务器

AWS 和 Azure 的等效功能

将列表 10.1 转换为 AWS 或 Azure,请使用 AWS EC2 Terraform 资源(mng.bz/VMMr)或 Azure Linux 虚拟机 Terraform 资源(mng.bz/xnnq)。您只需更新实例列表中的引用即可。

关闭功能标志运行 Python 生成 JSON 配置。生成的 JSON 配置仅将原始服务器添加到负载均衡器,从而保留现有基础设施资源的状态。

列表 10.2 禁用功能标志的 JSON 配置

{
   "resource": [
       {
           "google_compute_instance_group": {                          ❶
               "blue": [
                   {
                       "instances": [
                           "${google_compute_instance.blue_0.id}",     ❷
                           "${google_compute_instance.blue_1.id}",     ❷
                           "${google_compute_instance.blue_2.id}"      ❷
                       ]
                   }
               ]
           }
       }
   ]
}

❶ 使用 Terraform 资源创建一个 Google 计算实例组,并将其附加到负载均衡器

❷ 配置包括原始 Google 计算实例的列表,保留基础设施资源的当前状态

AWS 和 Azure 的等效功能

Google 计算实例组在 AWS 或 Azure 中没有直接的等效功能。相反,您需要将计算实例组替换为 AWS Target Group 的资源定义,以用于负载均衡器。对于 Azure,您需要一个后端地址池和三个虚拟机实例的地址(mng.bz/ZAAj)。

默认情况下,功能标志设置为 False 使用幂等原则。当您运行 IaC 时,您的基础设施状态不应发生变化。设置标志确保您不会意外更改现有基础设施。保留现有服务器的原始状态,最小化对依赖应用程序的干扰。

启用标志

Sundew 团队对其更改进行了修改,并批准将模块创建的新服务器添加到负载均衡器。您将功能标志设置为 True,如图 10.4 所示。当您部署更改时,您将模块创建的服务器附加到负载均衡器,并移除旧服务器。

图 10.4 将功能标志设置为 True,将模块创建的三个新服务器附加到负载均衡器,并断开旧服务器的连接。

让我们来看看更新后的功能标志在实际操作中的效果。您首先将服务器的功能标志设置为 True

ENABLE_SERVER_MODULE = True

运行 Python 生成新的 JSON 配置。以下列表中的配置现在包括您将附加到负载均衡器的模块创建的服务器。

列表 10.3 启用功能标志的 JSON 配置

{
   "resource": [
       {
           "google_compute_instance_group": {
               "blue": [
                   {
                       "instances": [
                           "${google_compute_instance.module_blue_0.id}",  ❶
                           "${google_compute_instance.module_blue_1.id}",  ❶
                           "${google_compute_instance.module_blue_2.id}"   ❶
                       ]
                   }
               ]
           }
       }
   ]
}

❶ 由于您启用了功能标志,模块创建的新服务器替换了旧服务器。

功能标志允许您在不影响负载均衡器高级依赖的情况下,分阶段部署模块的低级服务器资源。您可以通过关闭功能切换重新运行代码,以重新附加旧服务器。

为什么使用功能标志来切换到服务器模块?功能标志在准备好部署与其相关的资源之前,将功能隐藏在生产环境中。你提供一个变量来添加、删除或更新一组资源。你还可以使用相同的变量来撤销更改。

移除标志

在运行服务器一段时间后,sundew 团队报告称新的服务器模块工作正常。你现在可以移除列表 10.4 中的旧服务器。你不再需要功能标志,并且你不想在团队成员阅读代码时造成混淆。你重构负载均衡器的 Python 代码以移除旧服务器和删除功能标志。

列表 10.4:更改完成后移除功能标志

import blue                                                       ❶

def _generate_servers(version):
   instances = [                                                  ❷
       f'${{google_compute_instance.module_{version}_0.id}}',     ❷
       f'${{google_compute_instance.module_{version}_1.id}}',     ❷
       f'${{google_compute_instance.module_{version}_2.id}}',     ❷
   ]    
   return instances

❶ 你可以移除功能标志的导入,因为你不再需要它来配置你的服务器。

❷ 永久地将模块创建的服务器附加到负载均衡器并移除功能标志

领域特定语言

列表 10.4 展示了编程语言中的功能标志。你还可以在 DSL 中使用功能标志,尽管你必须根据你的工具语法进行适配。在 Terraform 中,你可以通过使用变量和 count 元参数来模拟功能标志(mng.bz/R44n):

variable "enable_server_module" {
 type        = bool
 default     = false
 description = "Choose true to build servers with a module."
}
module "server" {
 count   = var.enable_server_module ? 1 : 0
 ## omitted for clarity
}

在 AWS CloudFormation 中,你可以传递一个参数并设置一个条件(mng.bz/2nnN)来启用或禁用资源创建:

AWSTemplateFormatVersion: 2010-09-09
Description: Truncated example for CloudFormation feature flag
Parameters:
 EnableServerModule:
   AllowedValues:
     - 'true'
     - 'false'
   Default: 'false'
   Description: Choose true to build servers with a module.
   Type: String
Conditions:
 EnableServerModule: !Equals
   - !Ref EnableServerModule
   - true
Resources:
 ServerModule:
   Type: AWS::CloudFormation::Stack
   Condition: EnableServerModule
   ## omitted for clarity

除了使用功能标志来启用和禁用整个资源外,你还可以使用条件语句来启用或禁用资源的特定属性。

作为一般规则,在完成更改后移除功能标志。过多的功能标志会使你的基础设施配置逻辑变得复杂,难以排查问题。

用例

该示例使用功能标志将单例配置重构为基础设施模块。我经常将功能标志应用于此用例以简化基础设施资源的创建和删除。功能标志的其他用例包括以下内容:

  • 相同基础设施资源或依赖项上进行协作并避免更改冲突

  • 预先准备一系列更改,并通过更新标志进行快速部署

  • 测试更改并在失败时快速禁用

功能标志提供了一种在重构基础设施配置时隐藏或隔离基础设施资源、属性和依赖项更改的技术。然而,更改切换仍然可能破坏系统。在 sundew 团队服务器的例子中,我们不能简单地切换功能标志为 True 并期望服务器运行应用程序。相反,我们结合功能标志和其他技术,如滚动更新,以最小化对系统的影响。

10.2 分解单体

雨燕团队成员表示,他们仍然在系统中遇到问题。您将具有数百个资源和属性的单一配置识别为根本原因。每当有人进行更改时,团队成员必须与其他人解决冲突。他们还必须等待 30 分钟才能进行更改。

对于基础设施即代码(IaC)的单体架构意味着在同一个地方定义所有基础设施资源。您需要将 IaC 的单体分解成更小的、模块化的组件,以最小化团队成员之间的工作冲突并加快更改的部署速度。

定义 A 单体架构的 IaC 定义了单个配置和相同状态下的所有基础设施资源。

在本节中,我们将逐步介绍对雨燕团队单体架构的重构。最关键的一步是从识别和分组高级基础设施资源和依赖关系开始。我们通过完成对低级基础设施资源的重构来完成重构工作。

单体与单仓库

回想一下,您可以将您的基础设施配置放入单个存储库(第五章)。单个存储库是否意味着您有一个单体架构?不一定。您可以将单个存储库细分为单独的子目录。每个子目录包含独立的 IaC。

单体架构意味着您需要一起管理许多资源,并将它们紧密耦合在一起,这使得单独更改子集变得困难。单体通常是由一个初始的单例模式(所有配置都在一个地方)随着时间的推移而扩展形成的。

您可能已经注意到,我在第三章和第四章中立即开始使用模块化基础设施资源和依赖关系的模式。为什么不在更早的时候介绍这一章关于重构的内容?如果您能在 IaC 开发早期识别并应用一些模式,您就可以避免单体架构。然而,您有时会继承一个单体,并且通常需要对其进行重构。

10.2.1 重构高级资源

雨燕团队在一组配置文件中管理数百个资源。您应该从哪里开始分解 IaC?您决定寻找不依赖于其他资源的高级基础设施资源

雨燕团队在 GCP 项目级 IAM 服务账户和角色中有一套高级基础设施。IAM 服务账户和角色在设置项目中的用户和服务账户规则之前不需要创建网络或服务器。其他资源都不依赖于 IAM 角色和服务账户。您可以首先对它们进行分组和提取。

由于 GCP 不允许重复策略,因此您不能使用蓝绿部署方法。然而,您不能简单地从单体配置中删除角色和账户并将它们复制到新的存储库中。删除它们会阻止所有人登录项目!您该如何提取它们?

您可以将配置复制粘贴到其单独的存储库或目录中,初始化分离配置的状态,并将资源导入与新的配置相关联的基础设施状态。然后,您删除单体配置中的 IAM 配置。与滚动更新一样,您逐步更改每一组基础设施资源,测试更改,然后进行下一步。

图 10.5 概述了重构单体以处理高级资源的解决方案。您将代码从单体复制到新文件夹,并将实时基础设施资源导入新文件夹中的代码状态。您重新部署代码以确保它不会更改现有基础设施。最后,从单体中删除高级资源。

图片

图 10.5 Sundew 系统的 GCP 项目 IAM 策略没有依赖关系,您可以轻松重构而不会干扰其他基础设施。

与功能标志一样,我们使用幂等性原则来运行基础设施即代码(IaC)并验证我们不会影响活动基础设施状态。每次重构时,确保您部署更改并检查预览运行。您不希望意外更改现有资源并影响其依赖项。

我们将在接下来的几节中重构示例。请继续关注!我知道重构往往感觉枯燥,但逐步的方法可以确保您不会将广泛的故障引入您的系统。

从单体复制到单独的状态

您的初始重构开始于将代码复制到新目录以创建 IAM 角色和服务账户。Sundew 团队希望保持单个存储库结构,该结构存储所有 IaC 在一个源代码控制存储库中,但将配置分离到文件夹中。

您确定要复制的 IAM 角色和服务账户,并将团队的代码复制到新文件夹,如图 10.6 所示。GCP 中的活动 IAM 策略及其基础设施状态保持不变。

为什么要在单独的文件夹中重现 IAM 策略的 IaC?您希望分割您的单体 IaC 而不影响任何活动资源。重构时最重要的实践是保持幂等性。当您移动 IaC 时,您的活动状态不应发生变化。

图片

将 IAM 策略的文件复制到新的目录以用于 sundew-production-iam 配置,并避免更改 GCP 中的实时基础设施资源。

让我们开始从单体中重构 IAM 策略。创建一个仅管理 GCP 项目 IAM 策略的新目录:

$ mkdir -p sundew_production_iam

将 IAM 配置从单体复制到新目录:

$ cp iam.py sundew_production_iam/

您不需要更改任何内容,因为 IAM 策略不依赖于其他基础设施。以下列表中的文件 iam.py 分离了用户集合的创建和角色分配。

列表 10.5 从单体中分离的 IAM 配置

import json

TEAM = 'sundew'
TERRAFORM_GCP_SERVICE_ACCOUNT_TYPE = 'google_service_account'             ❶
TERRAFORM_GCP_ROLE_ASSIGNMENT_TYPE = 'google_project_iam_member'          ❶

users = {                                                                 ❷
   'audit-team': 'roles/viewer',                                          ❷
   'automation-watering': 'roles/editor',                                 ❷
   'user-02': 'roles/owner'                                               ❷
}                                                                         ❷

def get_user_id(user):
   return user.replace('-', '_')

def build():
   return iam()

def iam(users=users):                                                     ❸
   iam_members = []
   for user, role in users.items():
       user_id = get_user_id(user)
       iam_members.append({
           TERRAFORM_GCP_SERVICE_ACCOUNT_TYPE: [{                         ❹
               user_id: [{                                                ❹
                   'account_id': user,                                    ❹
                   'display_name': user                                   ❹
               }]                                                         ❹
           }]                                                             ❹
       })
       iam_members.append({
           TERRAFORM_GCP_ROLE_ASSIGNMENT_TYPE: [{                         ❺
               user_id: [{                                                ❺
                   'role': role,                                          ❺
                   'member': 'serviceAccount:${google_service_account.'   ❺
                   + f'{user_id}' + '.email}'                             ❺
               }]                                                         ❺
           }]                                                             ❺
       })
   return iam_members

❶ 将 Terraform 使用的资源类型设置为常量,以便您在需要时可以引用它们

❷ 将您添加到项目中的所有用户作为单体的一部分保留

❸ 使用模块创建 IAM 策略的 JSON 配置,这些配置位于单体之外

❹ 为 sundew 生产项目中的每个用户创建一个 GCP 服务账户

❺ 分配为每个服务账户定义的特定角色,例如查看者、编辑器或所有者

AWS 和 Azure 的等效功能

列表 10.5 创建所有用户和组作为 GCP 中的服务账户,以便您可以完成示例。您通常使用服务账户进行自动化。

GCP 中的服务账户类似于 AWS IAM 用户,专门用于服务自动化或与客户端密钥注册的 Azure Active Directory 应用。为了在 AWS 或 Azure 中重建代码,调整查看者、编辑器和所有者角色以适应 AWS 或 Azure 角色。

在分离配置时设置常量并创建输出资源类型和标识符的方法。您始终可以使用它们进行其他自动化和持续的系统维护,尤其是在您继续重构单体时!

在以下列表中,在 sundew_production_iam 文件夹中创建一个 main.py 文件,该文件引用 IAM 配置并输出其 Terraform JSON。

列表 10.6 构建单独 IAM JSON 配置的入口点

import iam                                       ❶
import json

if __name__ == "__main__":
   resources = {
       'resource': iam.build()                   ❶
   }

   with open('main.tf.json', 'w') as outfile:    ❷
       json.dump(resources, outfile,             ❷
                 sort_keys=True, indent=4)       ❷

❶ 导入 IAM 配置代码并构建 IAM 策略

❷ 将 Python 字典写入 JSON 文件,稍后由 Terraform 执行

不要运行 Python 来创建 Terraform JSON 或部署 IAM 策略!您已经将 IAM 策略定义为 GCP 的一部分。如果您运行 python main.py 并使用分离的 IAM 配置应用 Terraform JSON,GCP 会抛出一个错误,指出用户账户和分配已存在:

$ python main.py

$ terraform apply -auto-approve
## output omitted for clarity
| Error: Error creating service account: googleapi:
➥Error 409: Service account audit-team already exists within project 
➥projects/infrastructure-as-code-book., alreadyExists

雨燕团队成员不希望您删除并创建新的账户和角色。如果您删除并创建新账户,他们将无法登录到他们的 GCP 项目。您需要一种方法来迁移单体中定义的现有资源并将它们链接到其自己的文件夹中定义的代码。

将资源导入新状态

有时使用重构后的 IaC 创建新资源可能会干扰开发团队和业务关键系统。您不能使用不可变性的原则来删除旧资源并创建新资源。相反,您必须将活动资源从一个 IaC 定义迁移到另一个。

图 10.7 从基础设施提供者获取分离资源的当前状态并在重新应用 IaC 之前导入标识符。

在雾莲团队的情况下,你从单体配置中提取每个服务帐户的标识符,并将它们“移动”到新的状态中。图 10.7 演示了如何从单体中分离每个服务帐户及其角色分配,并将它们附加到 sundew_production_iam 目录中的 IaC。你调用 GCP API 以获取 IAM 策略的当前状态,并将实时基础设施资源导入到分离的配置和状态中。运行 IaC 应该不会显示任何干燥运行的变化。

为什么使用 GCP API 导入 IAM 策略信息?你想要导入资源的更新、活动状态。云提供商的 API 提供了资源的最最新配置。你可以调用 GCP API 来检索雾莲团队的用户电子邮件、角色和标识符。

而不是编写自己的导入功能并将标识符保存到文件中,你决定使用 Terraform 的导入功能将现有资源添加到状态中。你编写了一些 Python 代码,以下列表展示了如何封装 Terraform 来自动化 IAM 资源的批量导入,以便雾莲团队能够重用它。

列表 10.7 文件 import.py 单独导入雾莲 IAM 资源

import iam                                                              ❶
import os
import googleapiclient.discovery                                        ❷
import subprocess

PROJECT = os.environ['CLOUDSDK_CORE_PROJECT']                           ❸

def _get_members_from_gcp(project, roles):                              ❷
   roles_and_members = {}                                               ❷
   service = googleapiclient.discovery.build(                           ❷
       'cloudresourcemanager', 'v1')                                    ❷
   result = service.projects().getIamPolicy(                            ❷
       resource=project, body={}).execute()                             ❷
   bindings = result['bindings']                                        ❷
   for binding in bindings:                                             ❷
       if binding['role'] in roles:                                     ❷
           roles_and_members[binding['role']] = binding['members']      ❷
   return roles_and_members                                             ❷

def _set_emails_and_roles(users, all_members):                          ❹
   members = []                                                         ❹
   for username, role in users.items():                                 ❹
       members += [(iam.get_user_id(username), m, role)                 ❹
                   for m in all_members[role] if username in m]         ❹
   return members                                                       ❹

def check_import_status(ret, err):
   return ret != 0 and \
       'Resource already managed by Terraform' 
       ➥not in str(err)

def import_service_account(project_id, user_id, user_email):            ❺
   email = user_email.replace('serviceAccount:', '')                    ❺
   command = ['terraform', 'import', '-no-color',                       ❺
              f'{iam.TERRAFORM_GCP_SERVICE_ACCOUNT_TYPE}.{user_id}',    ❺
              f'projects/{project_id}/serviceAccounts/{email}']         ❺
   return _terraform(command)                                           ❺

def import_project_iam_member(project_id, role,                         ❻
                             user_id, user_email):                      ❻
   command = ['terraform', 'import', '-no-color',                       ❻
              f'{iam.TERRAFORM_GCP_ROLE_ASSIGNMENT_TYPE}.{user_id}',    ❻
              f'{project_id} {role} {user_email}']                      ❻
   return _terraform(command)                                           ❻

def _terraform(command):                                                ❼
   process = subprocess.Popen(                                          ❼
       command,                                                         ❼
       stdout=subprocess.PIPE,                                          ❼
       stderr=subprocess.PIPE)                                          ❼
   stdout, stderr = process.communicate()                               ❼
   return process.returncode, stdout, stderr                            ❼

if __name__ == "__main__":
   sundew_iam = iam.users                                               ❽
   all_members_for_roles = _get_members_from_gcp(                       ❾
       PROJECT, set(sundew_iam.values()))                               ❾
   import_members = _set_emails_and_roles(                              ❿
       sundew_iam, all_members_for_roles)                               ❿
   for user_id, email, role in import_members:
       ret, _, err = import_service_account(PROJECT, user_id, email)    ❺
       if check_import_status(ret, err):                                ⓫
           print(f'import service account failed: {err}')               ⓫
       ret, _, err = import_project_iam_member(PROJECT, role,           ❻
                                               user_id, email)          ❻
       if check_import_status(ret, err):                                ⓫
           print(f'import iam member failed: {err}')                    ⓫

❶ 从 sundew_production_iam 中的 iam.py 检索雾莲用户列表

❷ 使用 Google Cloud Client Libraries for Python 获取分配给 GCP 项目中角色的成员列表

❸ 从 CLOUDSDK_CORE_PROJECT 环境变量中检索 GCP 项目 ID

❹ 仅获取雾莲 IAM 成员的电子邮件和用户 ID

❺ 根据项目和使用者电子邮件,将服务帐户导入到 sundew_production_iam 状态中,使用你在 iam.py 中设置的资源类型常量

❻ 根据项目、角色和使用者电子邮件,将角色分配导入到 sundew_production_iam 状态中,使用你在 iam.py 中设置的资源类型常量

❼ 两种导入方法都封装了 Terraform CLI 命令,并返回任何错误和输出。

❽ 从 sundew_production_iam 中的 iam.py 检索雾莲用户列表

❾ 使用 Google Cloud Client Libraries for Python 获取分配给 GCP 项目中角色的成员列表

❿ 仅获取雾莲 IAM 成员的电子邮件和用户 ID

⓫ 如果导入失败且资源尚未导入,则输出错误

Libcloud 与云提供商 SDKs 的比较

本章中的示例需要使用 Google Cloud Client Library for Python 而不是我在第四章中展示的 Apache Libcloud。虽然 Apache Libcloud 可以用于检索虚拟机信息,但它不适用于 GCP 中的其他资源。有关 Google Cloud Client Library for Python 的更多信息,请参阅mng.bz/1ooZ

您可以将列表 10.7 更新为使用 Python 的 Azure 库(mng.bz/Pnn2)或 AWS SDK for Python(aws.amazon.com/sdk-for-python/)来检索有关用户的信息。这些将替换 GCP API 客户端库。

与定义依赖关系一样,您希望从基础设施提供者 API 动态检索您要导入的资源标识符。您永远不知道何时有人会更改资源,您认为需要的标识符可能不再存在!使用您的标签和命名约定在 API 响应中搜索所需的资源。

当您运行 python import.py 并使用分离的 IAM 配置对 Terraform JSON 进行干运行时,您会收到一条消息,表明您不需要做出任何更改。您已成功将现有的 IAM 资源导入到其单独的配置和状态中:

$ python main.py

$ terraform plan
No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration
➥and found no differences, so no changes are needed.

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

有时您的干运行指示活动资源状态和分离配置之间的漂移。您复制的配置与资源的活动状态不匹配。差异通常来自有人在手动更改期间或更改属性默认值时更改基础设施资源的活动状态。更新您的分离 IaC 以匹配活动基础设施资源的属性。

带和不带配置工具导入

许多配置工具都有一个导入资源的函数。例如,AWS CloudFormation 使用 resource import 命令。示例使用 Python 包装 terraform import 命令来移动服务账户。没有它,分解单体配置将变得繁琐。

如果您没有使用工具编写 IaC,则不需要直接导入功能。相反,您需要逻辑来检查资源是否存在。露水服务账户和角色分配可以在没有 Terraform 或 IaC 导入功能的情况下工作:

  1. 调用 GCP API 检查露水团队的服务账户和角色附件是否存在。

  2. 如果它们存在,检查服务账户属性的 API 响应是否与您期望的配置匹配。根据需要更新服务账户。

  3. 如果它们不存在,则创建服务账户和角色附件。

从单体中移除重构的资源

您成功提取并移动了露水团队的服务账户和角色分配到单独的 IaC。然而,您不希望资源留在单体中。在重新应用和更新工具之前,您从单体状态和配置中移除了资源,如图 10.8 所示。

图片

在应用更新并完成重构之前,从单体状态和配置中移除策略 10.8。

此步骤有助于维护 IaC 卫生。记得从第二章中,我们的 IaC 应该作为真相来源。您不希望用两套 IaC 管理一个资源。如果它们冲突,资源可能影响依赖关系和系统的配置的两个 IaC 定义。

您希望 IAM 策略目录作为真相来源。从现在起,sundew 团队需要在单独的目录中声明其 IAM 策略的更改,而不是在单体中。为了避免混淆,让我们从 IaC 单体中删除 IAM 资源。

要开始,您必须从 Terraform 状态中删除 sundew IAM 资源,表示为 JSON 文件。Terraform 包含一个状态删除命令,您可以使用它根据资源标识符从 JSON 中取出部分内容。列表 10.8 使用 Python 代码包装 Terraform 命令。该代码允许您传递您想要从基础设施状态中删除的任何资源类型和标识符。

列表 10.8 文件 remove.py 从单体状态中删除资源。

from sundew_production_iam import iam                                  ❶
import subprocess

def check_state_remove_status(ret, err):                               ❷
   return ret != 0 \                                                   ❷
       and 'No matching objects found' not in str(err)                 ❷

def state_remove(resource_type, resource_identifier):                  ❸
   command = ['terraform', 'state', 'rm', '-no-color',                 ❸
              f'{resource_type}.{resource_identifier}']                ❸
   return _terraform(command)                                          ❸

def _terraform(command):                                               ❹
   process = subprocess.Popen(
       command,
       stdout=subprocess.PIPE,
       stderr=subprocess.PIPE)
   stdout, stderr = process.communicate()
   return process.returncode, stdout, stderr

if __name__ == "__main__":
   sundew_iam = iam.users                                              ❶
   for user in iam.users:                                              ❺
       ret, _, err = state_remove(                                     ❻
           iam.TERRAFORM_GCP_SERVICE_ACCOUNT_TYPE,                     ❻
           iam.get_user_id(user))                                      ❻
       if check_state_remove_status(ret, err):                         ❼
           print(f'remove service account from state failed: {err}')   ❼
       ret, _, err = state_remove(                                     ❽
           iam.TERRAFORM_GCP_ROLE_ASSIGNMENT_TYPE,                     ❽
           iam.get_user_id(user))                                      ❽
       if check_state_remove_status(ret, err):                         ❼
           print(f'remove role assignment from state failed: {err}')   ❼

❶ 从 sundew_production_iam 中的 sundew_users.py 获取 sundew 用户列表。通过引用分离的 IaC 中的变量,您可以运行未来重构努力的删除自动化。

❷ 如果删除失败且尚未删除资源,则输出错误。

❸ 创建了一个围绕 Terraform 状态删除命令的方法。该命令传递资源类型,例如服务账户和标识符以进行删除。

❹ 打开一个子进程,运行 Terraform 命令以从状态中删除资源。

❺ 从 sundew_production_iam 中的每个用户中删除他们的服务账户和角色分配。

❻ 根据 GCP 服务账户的用户标识符从单体 Terraform 状态中删除 GCP 服务账户。

❼ 检查子进程的 Terraform 命令是否成功从单体状态中删除了资源。

❽ 根据 GCP 角色标识符从单体 Terraform 状态中删除 GCP 角色分配。

不要运行 python remove.py!您的单体仍然包含 IAM 策略的定义。打开您的单体 IaC 的 main.py。在以下列表中,删除为 sundew 团队构建 IAM 服务账户和角色分配的代码。

列表 10.9 从单体代码中删除 IAM 策略

import blue                                      ❶
import json                                      ❶

if __name__ == "__main__":
   resources = {
       'resource': blue.build()                  ❷
   }

   with open('main.tf.json', 'w') as outfile:    ❸
       json.dump(resources, outfile,             ❸
                 sort_keys=True, indent=4)       ❸

❶ 删除 IAM 策略的导入。

❷ 删除在单体中构建 IAM 策略的代码,并保留其他资源。

❸ 将配置写入 JSON 文件,供 Terraform 后续执行。配置不包括 IAM 策略。

您现在可以更新您的单体。首先,使用 python remove.py 从单体状态中删除 IAM 资源:

$ python remove.py

此步骤表示单体不再作为 IAM 策略和服务账户的真相来源。您删除 IAM 资源!您可以想象这是将 IAM 资源的所有权移交给单独文件夹中的新 IaC。

在你的终端中,你最终可以更新单体。生成一个不带 IAM 策略的新 Terraform JSON 并应用更新;你应该没有任何更改:

$ python main.py

$ terraform apply
google_service_account.blue: Refreshing state...
google_compute_network.blue: Refreshing state...
google_compute_subnetwork.blue: Refreshing state...
google_container_cluster.blue: Refreshing state...
google_container_node_pool.blue: Refreshing state...

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration 
➥and found no differences, so no changes are needed.

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

如果你的 dry run 包括 你重构的资源,你知道你没有从单体状态或配置中删除它。你需要检查资源并确定是否需要手动删除它们。

10.2.2 重构具有依赖关系的资源

现在,你可以开始处理具有依赖关系的低级基础设施资源,例如 sundew 团队的容器编排器。sundew 团队成员要求你避免创建新的编排器并销毁旧的编排器,因为他们不想中断应用程序。你需要重构并提取现有的低级容器编排器。

开始从单体中复制容器配置,重复你在重构 IAM 服务帐户和角色时使用的相同过程。你创建一个名为 sundew_production_orchestrator 的单独文件夹:

$ mkdir -p sundew_production_orchestrator

你选择并复制创建集群的方法到 sundew_production_orchestrator/cluster.py。然而,你遇到了一个问题。容器编排器 需要网络和子网名称。当容器编排器无法引用单体时,你如何获取网络和子网名称?

图 10.9 实现了使用基础设施提供者 API 作为抽象层的现有单体依赖注入。创建集群的 IaC 调用 GCP API 获取网络信息。你将网络 ID 传递给集群以使用。

图 10.9 复制基础设施并添加新方法以调用 GCP API 并获取集群的网络 ID。

一个单体通过资源显式传递依赖。当你创建一个新文件夹时,你的分离资源需要关于其低级依赖的信息。回想一下,你可以通过 依赖注入(之前在第四章中介绍)解耦基础设施模块。一个高级模块调用抽象层以获取低级依赖的标识符。

当你开始重构具有依赖关系的资源时,你必须实现一个用于依赖注入的接口。在 sundew 团队的代码列表 10.10 中,更新 sundew_production_orchestrator/cluster.py 以使用 Google Cloud Client Library 并检索集群配置的子网和网络名称。

注意:为了增加清晰度,列表 10.10 中已删除一些依赖项、变量和导入。有关完整示例,请参阅书籍的代码仓库 github.com/joatmon08/manning-book/tree/main/ch10/s03/s02

列表 10.10 使用依赖反转在集群中处理网络名称

import googleapiclient.discovery                                   ❶

def _get_network_from_gcp():                                       ❷
   service = googleapiclient.discovery.build(                      ❶
       'compute', 'v1')                                            ❶
   result = service.subnetworks().list(                            ❸
       project=PROJECT,                                            ❸
       region=REGION,                                              ❸
       filter=f'name:"{TEAM}-{ENVIRONMENT}-*"').execute()          ❸
   subnetworks = result['items'] if 'items' in result else None
   if len(subnetworks) != 1:                                       ❹
       print("Network not found")                                  ❹
       exit(1)                                                     ❹
   return subnetworks[0]['network'].split('/')[-1], \              ❺
       subnetworks[0]['name']                                      ❺

def cluster(name=cluster_name,                                     ❻
           node_name=cluster_nodes,
           service_account=cluster_service_account,
           region=REGION):
   network, subnet = _get_network_from_gcp()                       ❼
   return [
       {
           'google_container_cluster': {                           ❽
               VERSION: [
                   {
                       'name': name,
                       'network': network,                         ❾
                       'subnetwork': subnet                        ❾
                   }
               ]
           },
           'google_container_node_pool': {                         ❽
               VERSION: [                                          ❽
                   {                                               ❽
                       'cluster':                                  ❽
                       '${google_container_cluster.' +             ❽
                           f'{VERSION}' + '.name}'                 ❽
                   }                                               ❽
               ]                                                   ❽
           },                                                      ❽
           'google_service_account': {                             ❽
               VERSION: [
                   {
                       'account_id': service_account,
                       'display_name': service_account
                   }
               ]
           }
       }
   ]

❶ 使用 Python 的 Google Cloud Client Library 设置对 GCP API 的访问

❷ 创建一个方法,从 GCP 获取网络信息并实现依赖注入

❸ 查询 GCP API 以获取以 sundew-production 开头的子网名称列表

❹ 如果 GCP API 未找到子网,则抛出错误

❺ 返回网络名称和子网名称

❻ 已从代码列表中删除了几个依赖项、变量和导入,以增加清晰度。请参阅书籍的代码存储库以获取完整示例。

❼ 应用依赖倒置原则并调用 GCP API 以检索网络和子网名称

❽ 通过使用 Terraform 资源创建 Google 容器集群、节点池和服务帐户

❾ 使用网络和子网名称更新容器集群

AWS 和 Azure 等效

您可以将列表 10.10 更新为使用 Azure Python 库(mng.bz/Pnn2)或 AWS SDK for Python(aws.amazon.com/sdk-for-python/)来替换 GCP API 客户端库。

接下来,更新资源。为 Kubernetes 节点池(也称为 groups)创建一个 Amazon VPC 和 Azure 虚拟网络。然后,将 Google 容器集群切换到 Amazon EKS 集群或 AKS 集群。

当重构具有依赖关系的资源时,您必须实现依赖注入以检索低级资源属性。列表 10.10 使用了基础设施提供者的 API,但您可以使用您选择的任何抽象层。基础设施提供者的 API 通常提供了最直接的抽象。您可以使用它来避免实现自己的。

在将容器集群复制并更新以引用来自 GCP API 的网络和子网名称后,您将重复图 10.10 中显示的重构工作流程。您将实时基础设施资源导入 sundew_production_orchestrator,应用单独的配置,检查活动状态和 IaC 之间的任何偏差,并从单体状态中删除资源的配置和引用。

将高级资源重构与从单体中重构低级资源的主要区别在于依赖注入的实现。您可以选择要使用的依赖注入类型,例如基础设施提供者的 API、模块输出或基础设施状态。请注意,如果您不使用基础设施提供者的 API,可能需要更改单体 IaC 以输出属性。

否则,确保通过在重构后重新运行 IaC 来应用幂等性。您希望避免影响活动资源并将所有更改隔离到 IaC 中。如果您的 dry run 反映了更改,您必须在继续其他资源之前修复重构代码与基础设施状态之间的偏差。

图 10.10 在继续重构低级资源之前,使用 GCP API 重构高级资源以获取低级标识符。

10.2.3 重复重构工作流程

在你提取了 IAM 服务账户和角色以及容器编排器之后,你可以继续分解雨燕系统的单体 IaC 配置。图 10.11 中的工作流程总结了分解单体 IaC 的一般模式。你确定哪些资源相互依赖,提取它们的配置,并更新它们的依赖关系以使用依赖注入。

图 10.11 重构 IaC 单体的工作流程首先识别没有依赖关系的顶层资源。

识别不依赖于任何事物或没有事物依赖于它们的顶层基础设施资源。我使用顶层资源来测试复制、分离、导入和从单体中删除它们的流程。接下来,我识别依赖于其他资源的更高层资源。在复制过程中,我将它们重构为通过依赖注入引用属性。我通过系统识别并重复此过程,最终得出没有依赖关系的最低层资源。

配置管理

虽然本章主要关注 IaC 部署工具,但配置管理也可能变成自动化单体,并导致相同的挑战,包括运行时间过长或配置部分有冲突的更改。你可以应用类似的重构工作流程来处理单体配置管理:

  1. 提取没有依赖关系的最独立的部分,并将它们分离成模块。

  2. 运行配置管理器并确保你没有更改你的资源状态。

  3. 识别依赖于较低级别自动化输出或存在性的配置。提取它们,并应用依赖注入以检索配置所需的任何值。

  4. 运行配置管理器并确保你没有更改你的资源状态。

  5. 重复此过程,直到你有效地达到配置管理器的第一步。

当你重构 IaC 单体时,确定将资源相互解耦的方法。我发现重构是一个挑战,并且很少没有一些失败和错误。隔离单个组件并仔细测试它们将有助于识别问题并最小化对系统的影响。如果我真的遇到失败,我会使用第十一章中的技术来修复它们。

练习 10.1

给定以下代码,你会使用什么顺序和资源分组来重构和分解单体?

if __name__ == "__main__":
  zones = ['us-west1-a', 'us-west1-b', 'us-west1-c']
  project.build()
  network.build(project)
  for zone in zones:
    subnet.build(project, network, zone)
  database.build(project, network)
  for zone in zones:
    server.build(project, network, zone)
  load_balancer.build(project, network)
  dns.build()

A) DNS,负载均衡器,服务器,数据库,网络 + 子网,项目

B) 负载均衡器 + DNS,数据库,服务器,网络 + 子网,项目

C) 项目,网络 + 子网,服务器,数据库,负载均衡器 + DNS

D) 数据库,负载均衡器 + DNS,服务器,网络 + 子网,项目

请参阅附录 B 以获取练习答案。

摘要

  • 重构 IaC 涉及在不影响现有基础设施资源的情况下重构配置或代码。

  • 重构解决了技术债务,这是一个描述更改代码成本的隐喻。

  • 滚动更新会逐个更改相似的基础设施资源,并在移动到下一个资源之前测试每个资源。

  • 滚动更新允许你增量地实施和调试更改。

  • 功能标志(也称为功能开关)可以启用或禁用基础设施资源、依赖项或属性。

  • 在将更改应用到生产之前,应用功能标志以测试、预览和隐藏更改。

  • 在一个地方(例如文件或配置管理器)定义功能标志,以便一目了然地识别它们的值。

  • 当你不再需要功能标志时,请将其移除。

  • 当你在同一个地方定义所有的基础设施资源时,就会发生单体 IaC,移除一个资源会导致整个配置失败。

  • 将资源重构出单体架构涉及将配置分离并复制到新的目录或仓库中,将其导入新的独立状态,并从单体配置和状态中移除资源。

  • 如果你的资源依赖于另一个资源,请更新你的分离资源配置以使用依赖注入并从基础设施提供者 API 检索标识符。

  • 将单体拆分首先从重构无依赖的高层资源或配置开始,然后是具有依赖的资源或配置,最后是低层资源或无依赖的配置。

11 修复失败

本章涵盖

  • 确定如何回滚失败更改以恢复功能

  • 组织 IaC 故障排除的方法

  • 对失败更改的分类修复

我们花费了很多章节来讨论编写和协作基础设施即代码。您为 IaC 学习的所有实践和原则都积累到了一个关键时刻,那就是您推送更改,它导致您的系统失败,您需要将其回滚!然而,IaC 不支持回滚。您不能完全撤销 IaC 更改。如果不回滚,修复失败意味着什么呢?

本章重点介绍如何修复 IaC 中的失败更改。首先,我们将讨论通过“回滚”来“撤销”IaC 更改的含义。然后,您将学习故障排除和修复失败更改的工作流程。虽然本章中介绍的技术可能不适用于您在系统中遇到的所有场景,但它们建立了一套广泛的实践,您可以使用这些实践开始修复 IaC 失败。

故障排除和站点可靠性工程

在本书中,我不会深入探讨故障系统故障排除的过程和原则。关于故障排除的大部分讨论都集中在 IaC 的上下文中如何管理它。有关故障排除和构建可靠系统的更多信息,我推荐 Betsy Beyer 等人所著的《站点可靠性工程》(O’Reilly,2016 年)。

作为一般规则,在进一步调试根本原因(导致问题的那个问题)之前,优先考虑服务和客户的稳定性和功能恢复。临时修补提供了您进行故障排除并实施系统长期修复的机会。

11.1 恢复功能

想象一下,您在一家名为 Cool Caps for Keys 的公司工作。该公司创建定制的键盘帽,并将客户与艺术家联系起来以设计帽。作为安全工程师,您需要缩小 GCP 项目中应用程序和用户的访问控制。

您复制 Google Cloud SQL 数据库配置并更新访问控制,以实现团队成员和应用程序的最小权限访问。您选择不同应用程序使用基础设施所需的政策,并验证应用程序仍然可以工作。

接下来,您与推广团队交谈。其应用程序通过使用数据库用户名和密码直接访问数据库。直接访问数据库意味着您可以从推广应用程序的服务帐户中移除roles/cloudsql.admin策略。您移除策略,测试更改,与推广团队确认更改没有影响其在测试环境中的应用,然后将它推送到生产环境;见图 11.1。

图 11.1 在移除推广服务的数据库管理访问权限后,您发现更改破坏了服务访问数据库的能力。

一小时后,促销团队告诉您,其应用程序持续抛出错误,无法访问数据库!您怀疑您的更改可能引入了问题。虽然您可以开始挖掘问题,但您优先修复促销服务,以便在进一步调查之前能够访问数据库。

11.1.1 回滚以撤销更改

您需要修复服务,以便用户可以向系统发出请求。然而,您不能简单地将系统恢复到之前的工作状态。IaC 优先考虑不可变性,这意味着对系统的任何更改,包括 撤销 的更改,都必须创建新的资源!

例如,让我们通过撤销更改并添加角色到服务账户来修复 Cool Caps for Keys 的促销服务。在图 11.2 中,您撤销了提交并将 roles/cloudsql.admin 添加回服务账户。然后,您将更改推送到测试和生产环境。

图片

图 11.2 您为促销服务添加了管理数据库角色,以便将系统回滚到工作状态。

您撤销了提交并将更改推送到测试和生产环境。您通过 IaC 推进更改,因为它使用不可变性将系统恢复到工作状态。

定义 回滚 IaC 的实践是将系统中的更改撤销,并使用不可变性将系统恢复到工作状态。

回滚意味着您将基础设施回滚到之前的状态。实际上,IaC 的不可变性意味着您在每次更改时都会创建一个新的状态。您无法完全恢复基础设施到之前的状态。有时您实际上无法将基础设施恢复到之前的状态,因为您的更改具有很大的影响范围。

让我们撤销对服务账户的更改,并将更改回滚以恢复权限。首先,检查您的提交历史,因为版本控制会跟踪您所做的所有更改。带有 a31 前缀的提交包括您移除 roles/cloudsql.admin 的操作:

$ git log --oneline -2
a3119fc (HEAD -> main) Remove database admin access from promotions
6f84f5d Add database admin to promotions service

应用第七章中的 GitOps 实践,您希望避免进行手动、破坏性的更改。相反,您更倾向于通过基础设施即代码(IaC)进行操作更改!您撤销提交以推送更新,以恢复促销服务到工作状态:

$ git revert a3119fc

您推送了提交,并且管道将角色添加回服务账户。在您回滚后,应用程序再次工作。您已成功将基础设施状态恢复到工作状态。然而,您从未实现状态的完全恢复。相反,您推出了一个与之前工作状态匹配的新状态。

回滚 IaC 通常意味着将更改回滚到基础设施状态。您使用 git revert 作为向前移动的撤销来保留不可变性,并将撤销更新回滚到基础设施。

配置管理

配置管理不优先考虑不可变性,但仍然将撤销的变更向前推进到服务器或资源。例如,想象你安装了一个版本为 3.0.0 的包,需要回滚到 2.0.0。你的配置管理工具可能会选择卸载新版本并重新安装旧版本。你并没有将包及其配置恢复到先前的状态。你只是将服务器恢复到一个带有较旧包的新工作状态。

11.1.2 向前推进以实施新变更

采用向前推进的心态带来的好处是扩展了你的故障排除方法。在示例中,你撤销了一个损坏的提交,并通过将新状态与先前的有效状态匹配来恢复促销服务的功能。然而,有时撤销提交并不能修复你的系统,反而使情况变得更糟!相反,你可以向前推进新变更并恢复功能。

让我们假设在向前推进变更后,促销服务仍然无法工作。与其试图修复应用程序,不如创建一个新的环境,其中包含变更和新的促销服务。你从第九章开始使用金丝雀部署技术,逐渐增加流量以完全恢复应用程序,如图 11.3 所示。在所有请求都转到新服务实例后,你禁用失败的环境进行调试。

图片

图 11.3 当无法恢复促销应用程序时,你可以使用金丝雀部署将流量切换到新实例并恢复系统。

IaC(基础设施即代码)允许你以更少的努力重现环境。此外,遵循不可变性意味着你已经有一个创建新环境以进行变更的模式。这两个原则的结合有助于减轻具有更大影响范围的更高风险变更。

涉及数据或完全无法恢复的资源的使用案例不能回滚到先前的状态。在解决级联故障的过程中,你可能会损坏应用程序数据或影响其他基础设施。与其回滚以恢复,不如通过应用第九章中的变更技术,向前推进并实施新的变更。

你也可以通过撤销和完全新的变更的组合来恢复功能。将你的向前推进心态扩展到包括在撤销旧变更之外的新变更,为快速恢复功能并提供了一种有用的替代方案,同时最大限度地减少对系统其他部分的干扰。

11.2 故障排除

你给你的系统贴上了一个创可贴,这样促销团队仍然可以发送 Cool Caps for Keys 的促销活动。然而,你仍然需要确保应用程序的 IAM(身份和访问管理)安全!当你移除了促销团队不需要的管理权限时,你从哪里开始查找促销服务失败的原因?

故障排除您的 IaC 也遵循特定的模式。即使在最复杂的系统基础设施中,许多失败的 IaC 变更通常来自三个原因:漂移、依赖项或差异。检查您的配置以查找任何这些原因有助于您识别问题以及潜在的解决方案。

11.2.1 检查漂移

许多损坏的基础设施变更源于配置与资源状态之间的配置漂移。在图 11.4 中,您首先检查漂移。确保服务帐户的 IaC 与 GCP 中服务帐户的状态相匹配。

图片

图 11.4 首先检查基础设施即代码(IaC)与状态之间的漂移。

检查代码与状态之间的漂移可以确保您消除由于两者之间的差异而导致的任何故障。代码与状态之间的差异可能会引入意外问题。消除这些差异可以确保您的更改行为按预期工作。

在 Cool Caps for Keys 的情况下,您将审查在 IaC 中定义的推广服务帐户的权限。以下列表概述了定义服务和角色的 IaC。

列表 11.1 具有数据库管理员权限的推广服务帐户

from os import environ
import database                                                     ❶
import iam                                                          ❷
import network                                                      ❸
import server                                                       ❹
import json
import os

SERVICE = 'promotions'
ENVIRONMENT = 'prod'
REGION = 'us-central1'
ZONE = 'us-central1-a'
PROJECT = os.environ['CLOUDSDK_CORE_PROJECT']
role = 'roles/cloudsql.admin'                                       ❺

if __name__ == "__main__":
   resources = {                                                    ❻
       'resource':
       network.Module(SERVICE, ENVIRONMENT, REGION).build() +       ❼
       iam.Module(SERVICE, ENVIRONMENT, REGION, PROJECT,            ❽
                  role).build() +                                   ❽
       database.Module(SERVICE, ENVIRONMENT, REGION).build() +      ❾
       server.Module(SERVICE, ENVIRONMENT, ZONE).build()            ❿
   }

   with open('main.tf.json', 'w') as outfile:                       ⓫
       json.dump(resources, outfile,                                ⓫
                 sort_keys=True, indent=4)                          ⓫

❶ 导入数据库模块以构建 Google Cloud SQL 数据库

❷ 导入 Google 服务帐户模块并创建具有权限的配置

❸ 导入网络模块以构建 Google 网络

❹ 导入服务器模块以构建 Google 计算实例

❺ 推广服务帐户应具有访问数据库的“cloudsql.admin”角色的权限

❻ 使用模块创建数据库、网络、服务帐户和服务器的 JSON 配置

❼ 导入网络模块以构建 Google 网络

❽ 导入 Google 服务帐户模块并创建具有权限的配置

❾ 导入数据库模块以构建 Google Cloud SQL 数据库

❿ 导入服务器模块以构建 Google 计算实例

⓫ 将 Python 字典写入 JSON 文件,以便 Terraform 后续执行

AWS 和 Azure 等效

GCP Cloud SQL 管理员权限的 AWS 等效是 AmazonRDSFullAccess。Azure 没有确切的等效项。相反,您需要将 Azure Active Directory 帐户直接添加到数据库中,并授予 Azure SQL Database API 权限的管理同意。

然后,将代码与 GCP 中推广应用程序的服务帐户权限进行比较。服务帐户只有与您的 IaC 一致的 roles/cloudsql.admin 权限:

$ gcloud projects get-iam-policy $CLOUDSDK_CORE_PROJECT
bindings:
- members:
  - serviceAccount:promotions-prod@infrastructure-as-code-book
  ➥.iam.gserviceaccount.com
  role: roles/cloudsql.admin
version: 1

如果你发现 IaC 和活动资源状态之间存在配置漂移,你可以进一步调查它是否影响系统功能。你可以选择消除一些漂移,以确保它不会导致根本原因。然而,仅仅因为检测到一些漂移并不意味着它会破坏你的系统!一些漂移可能与失败无关。

11.2.2 检查依赖关系

如果你确定漂移没有导致失败,你可以检查依赖于你的更新资源的资源。在图 11.5 中,你开始绘制依赖于服务账户的资源。在 IaC 和生产环境中,服务器都依赖于服务账户。

图片

图 11.5 修复任何依赖于您想要更新的资源的错误。

你想要检查预期的依赖关系是否与实际匹配。意外的依赖关系会干扰更改行为。当你审查以下列表中的代码时,你验证服务账户的电子邮件是否传递给了服务器。

列表 11.2 推广服务器依赖于推广服务账户

class Module():                                                       ❶
   def __init__(self, service, environment,
                zone, machine_type='e2-micro'):
       self._name = f'{service}-{environment}'
       self._environment = environment
       self._zone = zone
       self._machine_type = machine_type

   def build(self):                                                   ❶
       return [
           {
               'google_compute_instance': {                           ❷
                   self._environment: {
                       'allow_stopping_for_update': True,
                       'boot_disk': [{
                           'initialize_params': [{
                               'image': 'ubuntu-1804-lts'
                           }]
                       }],
                       'machine_type': self._machine_type,
                       'name': self._name,
                       'zone': self._zone,
                       'network_interface': [{
                           'subnetwork':
                           '${google_compute_subnetwork.' +
                           f'{self._environment}' + '.name}',
                           'access_config': {
                               'network_tier': 'STANDARD'
                           }
                       }],
                       'service_account': [{                          ❸
                           'email': '${google_service_account.' +     ❸
                           f'{self._environment}' + '.email}',        ❸
                           'scopes': ['cloud-platform']               ❸
                       }]                                             ❸
                   }
               }
           }
       ]

❶ 使用模块创建服务器的 JSON 配置

❷ 通过基于名称、地址、区域和网络使用 Terraform 资源创建 Google 计算实例

❸ 推广应用程序服务器的工厂使用服务账户访问 GCP 服务。

AWS 和 Azure 的等效选项

在 AWS 或 Azure 中创建一个网络。然后,更新代码列表以使用带有托管身份块的 Azure Linux 虚拟机 Terraform 资源 (mng.bz/J22p)。identity块应包含具有访问 Azure 权限的用户 ID 列表。对于 AWS,您将为 AWS EC2 Terraform 资源定义 IAM 实例配置文件。

然而,推广团队提到,其应用程序直接通过使用其 IP 地址、用户名和密码访问数据库。如果应用程序从文件中读取数据库连接字符串,服务器为什么还需要服务账户?

你意识到这突显了一个差异。你要求推广团队向你展示应用程序代码。应用程序配置没有使用数据库 IP 地址、用户名或密码!

在与推广团队进行额外的调试后,你发现推广应用程序连接到本地的数据库。配置使用 Cloud SQL Auth 代理 (cloud.google.com/sql/docs/mysql/sql-proxy),该代理处理连接并登录到数据库!因此,连接到服务器的服务账户需要数据库访问权限。

图 11.6 显示,推广应用程序通过代理访问数据库。代理使用服务账户进行身份验证并访问数据库。服务账户需要具有策略的数据库访问权限。

图片

图 11.6 推广应用程序通过代理访问数据库,该代理需要一个具有数据库权限的服务账户。

AWS 和 Azure 的等效选项

GCP Cloud SQL Auth 代理的 AWS 等效选项是 Amazon RDS Proxy。代理有助于强制执行数据库连接,并避免在应用程序代码中需要数据库用户名和密码。

Azure 没有等效的 SQL 代理选项。相反,你必须设置一个指向数据库的 Azure Private Link。这在你选择的私有网络上分配了一个 IP 地址。你可以配置你的数据库,允许你的应用程序使用 Azure Active Directory 服务主体登录。

恭喜你,你发现了为什么当你移除服务账户时推广应用程序会损坏的原因!然而,你有点怀疑。你不应该在测试环境中发现相同的问题吗?毕竟,你在测试环境中测试了更改,应用程序并没有损坏。

11.2.3 检查环境差异

为什么更改在测试环境中有效但在生产环境中无效?你检查了测试环境中的推广应用程序。该应用程序连接到localhost上的数据库。相反,它使用数据库 IP 地址、用户名和密码。

你向应用程序团队解释说,生产 IaC 使用 Cloud SQL Auth 代理,而测试 IaC 直接调用数据库;参见图 11.7。两种配置都使用roles/cloudsql.admin权限。

图 11.7 检查测试和生产环境之间的差异,以解决任何失败的测试更改。

在与推广团队进一步讨论后,你发现该团队实施了一个紧急更改,使用 Cloud SQL Auth 代理来保护生产环境。然而,团队没有机会更新测试环境以匹配!这种不匹配使得你的更新在测试环境中成功,但在生产环境中失败。

你希望测试和生产环境尽可能相似。然而,你无法总是复制生产环境。因此,你将遇到由于两者之间的差异而失败的更改。系统地识别测试和生产环境之间的差异有助于突出测试和变更交付中的差距。

虽然 IaC 应该记录系统中所有的更改和配置,但你可能在 IaC 和环境之间仍然会发现一些惊喜。图 11.8 总结了你在推广应用程序 IaC 中调试失败更改的结构化方法。你检查漂移、依赖关系,最后检查环境之间的差异。

图 11.8 你通过检查漂移、意外的依赖关系和测试与生产之间的差异,使用 IaC 来调试你的损坏更改。

在确定根本原因后,你最终可以实施一个长期的修复方案。你现在必须协调测试环境和生产环境之间的差异,并重新审视对促销应用程序服务账户的最小权限访问。

练习 11.1

一支团队报告称,其应用程序无法连接到另一个应用程序。该应用程序上周还能正常工作,但自周一以来请求都失败了。该团队没有对其应用程序进行任何更改,并怀疑问题可能是防火墙规则。你可以采取哪些步骤来排查这个问题?(选择所有适用的选项。)

A) 登录云服务提供商并检查应用程序的防火墙规则。

B) 在绿色环境中部署新的基础设施和应用程序进行测试。

C) 检查应用程序的 IaC 更改。

D) 将云服务提供商中的防火墙规则与 IaC 进行比较。

E) 编辑防火墙规则,允许应用程序之间的所有流量。

请参阅附录 B 以获取练习的答案。

11.3 修复

你为 Cool Caps for Keys 的原任务是更新每个应用程序的服务账户权限,以确保对服务的最小权限访问。你尝试从促销应用程序的服务账户中移除数据库管理访问权限,但失败了。在排查了问题之后,你现在可以修复这个问题。

你可能会觉得有点不耐烦!毕竟,你还没有完成 Cool Caps for Keys 中其他应用程序的访问更新。然而,不要一次性改变所有东西。推送一批更改可能会使调试失败原因变得困难(如第七章中提到的)。你的测试环境仍然与生产环境不匹配,如果你一次性做出太多更改,你仍然可能会影响促销应用程序。

在整本书中,我提到了通过进行小改动来最小化潜在失败的影响范围的过程。同样,增量修复将系统需要做出的更改分解成更小的部分,以防止未来的失败。

定义 增量修复 将更改分解成更小的部分,以逐步改进系统并防止未来的失败。

进行小范围的配置更改并逐步部署有助于你识别麻烦的第一迹象,并为未来的 IaC 成功做好准备。

11.3.1 协调漂移

如我在第二章中提到的,你需要协调对基础设施状态进行的任何手动更改与 IaC。如果你发现一些漂移,你需要首先解决它!如果你优先考虑使用 IaC,你的系统不应该有太多的紧急更改。

回想一下,Cool Caps for Keys 的促销应用程序实现了一个玻璃破碎更改,导致测试和生产环境之间存在差异。生产应用程序使用 Cloud SQL Auth 代理连接到数据库,而测试应用程序直接通过 IP 地址和密码连接到数据库。你需要在测试环境中构建一个 Cloud SQL Auth 代理。

要开始修复漂移,你需要将基础设施的当前状态重构到配置中。图 11.9 根据生产服务器重构了 Cloud SQL Auth 代理的安装命令。然后,你将这些命令添加到 IaC 中,并应用到测试环境中。

图片

图 11.9 在测试中,你需要将 Cloud SQL Auth 代理包安装到促销应用程序的服务器上,以解决玻璃破碎更改的漂移。

在此示例中,团队没有为手动更改添加 IaC。因此,你需要额外的时间来重建 Cloud SQL Auth 代理的安装。像代理这样的带外更改导致更改失败,修复它需要更多的时间和精力。

为了帮助最小化这些问题,请使用第二章中描述的迁移到 IaC 的过程。将手动更改作为 IaC 捕获有助于最小化环境之间的差异以及 IaC 和实际状态之间的漂移。如果你需要重建基础设施的状态,请记住第二章包括将现有基础设施迁移到 IaC 的高级示例。然而,你通常需要找到或编写一个工具将状态转换为 IaC。

让我们编写安装代理的 IaC 脚本。你检查了生产环境中促销应用程序服务器的命令历史,并重新构建了 Cloud SQL Auth 代理的安装过程。以下列表自动化了促销应用程序服务器启动脚本中的命令和安装过程。

列表 11.3 在服务器启动脚本中安装 Cloud SQL Auth 代理

class Module():
   def _startup_script(self):                                            ❶
       proxy_download = 'https://dl.google.com/cloudsql/' + \            ❷
           'cloud_sql_proxy.linux.amd64'                                 ❷
       exec_start = '/usr/local/bin/cloud_sql_proxy ' + \                ❸
           '-instances=${google_sql_database_instance.' + \              ❸
           f'{self._environment}.connection_name}}=tcp:3306'             ❸

       return f"""                                                       ❹
       #!/bin/bash
       wget {proxy_download} -O /usr/local/bin/cloud_sql_proxy
       chmod +x /usr/local/bin/cloud_sql_proxy

       cat << EOF > /usr/lib/systemd/system/cloudsqlproxy.service        ❺
       [Install]
       WantedBy=multi-user.target

       [Unit]
       Description=Google Cloud Compute Engine SQL Proxy
       Requires=networking.service
       After=networking.service

       [Service]
       Type=simple
       WorkingDirectory=/usr/local/bin
       ExecStart={exec_start}
       Restart=always
       StandardOutput=journal
       User=root
       EOF

       systemctl daemon-reload                                           ❺
       systemctl start cloudsqlproxy                                     ❺
       """

   def build(self):                                                      ❻
       return [
           {
               'google_compute_instance': {                              ❼
                   self._environment: {                                  ❼
                       'metadata_startup_script': self._startup_script() ❼
                   }                                                     ❼
               }                                                         ❼
           }
       ]

❶ 创建一个启动脚本,以重构 Cloud SQL Auth 代理的手动安装命令

❷ 设置一个变量作为代理下载 URL

❸ 设置一个变量,在端口 3306 上运行 Cloud SQL Auth 代理二进制文件

❹ 返回一个安装代理并使用服务器启动它的 shell 脚本

❺ 配置 systemd 守护进程以启动和停止 Cloud SQL Auth 代理

❻ 使用模块创建 Google 计算实例的 JSON 配置,并包括一个启动脚本以安装代理

❼ 将启动脚本添加到服务器。为了清晰起见,我省略了其他属性。

AWS 和 Azure 的等效方案

对于 AWS 和 Azure,你不需要在实例上安装代理的软件。如果你想在 AWS 和 Azure 中练习重现列表 11.3,你可以将启动脚本作为user_data传递给 AWS 实例或custom_data传递给 Azure Linux 虚拟机。

更新服务账户以添加新权限!本着增量修复的精神,你希望在推向生产时避免添加更多需要跟踪的变更。你将启动脚本添加到促销应用程序的服务器,并更改测试环境而无需更多更新。

启动脚本、配置管理器,还是镜像构建器?

在这个例子中,我使用启动脚本字段来避免引入更多语法。相反,你应该使用配置管理器镜像构建器来实施任何新软件包或进程的配置。

例如,配置管理器会将 Cloud SQL Auth 代理安装过程推送到任何带有促销应用程序的服务器。同样,镜像构建器为每个你烘焙的镜像配置代理!每次你引用促销应用程序的镜像时,你总是会在服务器中内置了代理。

11.3.2 调和环境之间的差异

当你更新你的基础设施即代码(IaC)以处理漂移时,你还需要确保测试和生产环境使用你的新 IaC。对于 Cool Caps for Keys,你确保数据库连接在测试环境中工作。然后,你要求促销团队更新其应用程序配置,通过 localhost 上的代理连接到数据库。

促销团队将其应用程序配置推送到测试环境以使用 Cloud SQL Auth 代理,运行测试,并更新生产环境,如图 11.10 所示。你保留服务账户上的 roles/cloudsql.admin 权限,因为代理需要它。

图 11.10 你需要将你的 IaC 变更推送到测试和生产环境中的促销应用程序服务器。

推送重新创建带有新启动脚本的生成服务器。在为促销应用程序进行额外的端到端测试之后,你确认你成功更新了测试和生产环境。

为什么在生产环境和测试环境之间的差异之前先调和漂移?在这个例子中,你选择先调和漂移,因为你将花费更多时间手动在测试环境中安装软件包。如果你更新你的 IaC 并自动化软件包安装,你可以在将其推送到生产环境之前确保更改在测试环境中工作。

你可能会选择首先调和测试环境和生产环境,因为你有很多漂移。在这种情况下,在修复漂移之前先匹配测试环境和生产环境。在你实施调和变更之前,你希望有一个准确的测试环境。

调和漂移和环境差异,以帮助下一个人更新系统。他们不必担心了解配置差异或手动配置代理。你花费在更新你的 IaC 上的额外时间可以帮助你避免额外的调试时间!

11.3.3 实施原始变更

现在您通过对齐漂移和更新环境最小化了潜在失败的范围,您最终可以推进原始更改。您的调试和增量修复更改了您的基础设施。当您返回实施原始更改时,您可能需要调整您的代码。

让我们完成 Cool Caps for Keys 促销应用程序的原始更改。回想一下,安全团队要求您从服务账户中移除管理权限。这个过程确保了最小权限访问,并考虑到了使用 Cloud SQL Auth 代理。

您知道服务账户必须具有数据库访问权限,因为应用程序使用了 Cloud SQL Auth 代理。现在,您试图弄清楚应用程序应该使用什么样的最小访问权限。roles/cloudsql.client权限为服务账户提供了足够的访问权限,以便获取实例列表并连接到它们。

在图 11.11 中,您将服务账户的权限从管理访问更改为roles/cloudsql.client。您将此更改推送到测试环境,验证促销是否仍然工作,并将roles/cloudsql.client权限部署到生产环境。

图 11.11 您需要将 IaC 更改推送到测试和生产环境中的促销应用程序服务器。

您已对代理的测试和生产环境之间的差异进行了对齐。理论上,测试环境现在应该能够捕捉到您更改中的任何问题。任何失败的更改现在应该出现在测试环境中。让我们在以下列表中将服务账户的权限从roles/cloudsql.admin更改为roles/cloudsql.client

列表 11.4 将服务账户角色更改为数据库客户端

from os import environ
import database
import iam
import network
import server
import json
import os

SERVICE = 'promotions'
ENVIRONMENT = 'prod'
REGION = 'us-central1'
ZONE = 'us-central1-a'
PROJECT = os.environ['CLOUDSDK_CORE_PROJECT']
role = 'roles/cloudsql.client'                                    ❶

if __name__ == "__main__":
   resources = {                                                  ❷
       'resource':                                                ❷
       network.Module(SERVICE, ENVIRONMENT, REGION).build() +     ❷
       iam.Module(SERVICE, ENVIRONMENT, REGION, PROJECT,          ❸
                  role).build() +                                 ❸
       database.Module(SERVICE, ENVIRONMENT, REGION).build() +    ❷
       server.Module(SERVICE, ENVIRONMENT, ZONE).build()          ❷
   }                                                              ❷

   with open('main.tf.json', 'w') as outfile:
       json.dump(resources, outfile,
                 sort_keys=True, indent=4)

❶ 将促销服务账户角色更改为客户端访问,允许连接到数据库实例

❷ 不更改资源导入网络、数据库和服务器模块

❸ 导入服务账户并将其“roles/cloudsql.client”角色附加到其权限

AWS 和 Azure 的等效权限

AWS 中与 GCP 云 SQL 管理员权限等效的是AmazonRDSFullAccess。Azure 没有确切的等效权限。相反,您需要直接将 Azure Active Directory 账户添加到数据库中,并授予 Azure SQL 数据库 API 权限的管理权限。

在 AWS 中,您可以将rds-db:connect操作添加到附加到 EC2 实例的 IAM 角色中。在 Azure 中,您需要撤销管理访问权限,并授予与数据库用户链接的 Azure AD 用户的SELECT访问权限(mng.bz/woo7)。

您提交并应用更改。测试环境应用更改并验证应用程序仍然可以工作!您与促销团队确认,该团队批准了生产更改。

团队将新的权限更改推广到生产环境,运行端到端测试,并确认推广应用程序可以访问数据库!经过几周的调试和更改后,你终于可以修复 Cool Caps for Keys 中的其他应用程序了。

为什么用一个修复失败的更改的例子来单独占据一整章?这代表了修复基础设施即代码(IaC)的现实。你希望尽快解决问题,而不要使问题变得更糟。

向前滚动有助于恢复系统的正常工作状态并最小化对基础设施资源的影响。然后,你可以着手排查根本原因。许多基础设施故障源于漂移、依赖关系或测试和生产环境之间的差异。在你解决这些差异之后,你实施你的原始更改。

学习滚动前进基础设施即代码的艺术需要时间和经验。虽然你可以直接登录云服务提供商的控制台进行手动更改以使系统工作,但请记住,这种临时修补很快就会失效,而且不会促进长期系统的修复。使用 IaC 跟踪和逐步修复系统可以最小化维修的影响,并为其他更新系统的人提供上下文。

摘要

  • 对于基础设施即代码的故障修复,涉及向前滚动修复而不是回滚。

  • 向前滚动基础设施即代码使用不可变性将系统恢复到工作状态。

  • 在进行调试和实施长期修复之前,应优先稳定系统并恢复其到工作状态。

  • 在排查基础设施即代码的问题时,检查漂移、意外的依赖关系以及环境之间的差异,作为根本原因的一部分。

  • 专注于增量修复,以便快速识别并减少潜在故障的影响范围。

  • 在重新实施失败的原始更改之前,确保你解决了漂移和环境之间的差异,以便进行准确的测试和未来的系统更新。

  • 在基础设施即代码(IaC)中重建状态以解决漂移问题,涉及聚合服务器配置的手动命令或将基础设施元数据转换为 IaC。

12 云计算的成本

本章涵盖

  • 调查云成本的成本驱动因素

  • 比较成本优化实践

  • 实施符合成本规范的 IaC 测试

  • 计算基础设施成本估算

当您使用云服务提供商时,您会对配置的便捷性感到非常兴奋。毕竟,您只需点击鼠标或执行一个命令即可创建资源。然而,随着组织的规模扩大和增长,云计算的成本就会成为一个问题。您对基础设施即代码的更新可能会影响云的整体成本!

我们必须像在基础设施中建立安全性一样,将成本考虑纳入其中。如果您发现您的系统成本超支,您在尝试移除资源并减少您的云计算账单时可能会破坏系统。在第八章中,我建议像烘焙蛋糕一样将安全性融入 IaC。原料的成本会影响我们可以烘焙的蛋糕数量,这在您开始之前是至关重要的。

本章介绍了您可以与基础设施即代码(IaC)结合使用的实践,以管理云计算的成本并减少未使用的资源。您将发现一些高级的、通用的成本控制实践和模式,这些模式我在 IaC 的背景下进行了描述。然而,根据客户需求、组织规模和云服务提供商的计费情况,随着系统的演变,您需要定期应用这些实践以重新优化成本。

我的数据中心或托管服务的成本如何?

我主要关注云计算,因为它具有灵活性和按需计费。您通常使用组织的计费系统来计算数据中心计算的成本。每个业务单元为其数据中心资源设立预算,技术部门根据资源使用情况发出计费,并考虑数据中心运营成本。

您始终可以应用管理成本驱动因素的成本控制和估算实践。然而,我概述的成本降低和优化技术可能并不适用于所有情况(无论您是否使用云、数据中心或托管服务)。根据您的规模、地理架构、业务领域或数据中心使用情况,您的用例和系统可能需要专门的评估或重新平台化。

12.1 管理成本驱动因素

假设您是一家公司的顾问,该公司需要将其支持会议和活动的平台迁移到公共云。公司要求您将数据中心中的配置“迁移和转换”到公共云。您帮助公司的团队在 GCP 中构建基础设施,应用您从本书中学到的所有原则和实践。最终,您的团队在 GCP 上推出了平台,并成功支持了其第一位客户:一场为期三小时的社区会议。

事件几周后,您的客户安排了一场神秘的会议。当会议开始时,客户向您展示了他们的云账单。仅一个三小时的会议的开发和支持费用就超过$10,000!财务团队似乎对成本不太满意,尤其是公司举办会议还亏损了。您接下来的任务是:尽可能降低每场会议的成本

您是否使用实际的云账单?

上述示例使用了一个虚构的、非常简化的云账单,该账单基于 GCP 定价计算器(cloud.google.com/products/calculator)2021 年的价格,近似计算了会议平台服务的成本。这些估计可能不包括您需要的所有服务、平台的更新定价、环境之间的差异或您可能在类似系统中使用的尺寸。我将小计四舍五入以简化示例。

如果您运行示例,可能会达到 N2D 机器类型实例的 GCP 配额。服务器将超过平台的免费层!您可以将机器类型更改为免费层实例,以便免费运行示例。

感谢您从第八章借用的标记实践,账单使用标记来识别哪些资源属于社区会议及其环境。您设法将云计算账单分解,如表 12.1 所示,并按基础设施资源的类型和大小识别成本。

表 12.1 按服务和环境划分的云账单

服务 测试环境小计 生产环境小计 小计
计算机服务器 $400 | $3,600 $4,000
数据库(云 SQL) $250 | $2,250 $2,500
消息传递(Pub/Sub) $100 | $900 $1,000
对象存储(云存储) $100 | $900 $1,000
数据传输(网络出口) $100 | $900 $1,000
其他(云 CDN、支持) $50 | $450 $500
总计 $1,000 | $9,000 $10,000

AWS 和 Azure 等效服务

云账单主要抽象了等效的 Azure 和 AWS 服务的具体名称。为了清晰起见,我列出了一些 GCP 服务及其对 AWS 或 Azure 的近似值:

  • 数据库(云 SQL)—Amazon RDS、Azure SQL 数据库

  • 消息传递(Pub/Sub)—Amazon Simple Queue Service (SQS) 和 Simple Notification Service (SNS)、Azure Service Bus

  • 对象存储(云存储)—Amazon S3、Azure Blob 存储

  • 其他(云 CDN、支持)—Amazon CloudFront、Azure 内容分发网络(CDN)

按服务和环境划分成本有助于您确定哪些因素导致成本,以及您应该进一步调查的地方。为了开始减少账单,您必须确定成本驱动因素,即影响总成本的因素或活动。

定义成本驱动因素是影响您总云计算成本的因素或活动。

当你评估成本驱动因素时,计算云服务的百分比成本。某些服务始终比其他服务成本更高。你仍然可以使用这种分解来帮助你识别可以优化的服务。按环境分解成本有助于你识别测试环境和生产环境的足迹。比较这两个环境将为你提供一个更好的图景,了解哪个环境存在你可以减少的低效率。

根据你的分解,你计算每个服务和环境的百分比。在图 12.1 中,你绘制出计算资源占账单的 40%。你还发现,团队在测试环境中花费了总成本的 10%,在生产环境中花费了 90%。

图片

图 12.1 资源标签按服务和环境分解成本。

账单的大部分都花在了计算资源上——特别是服务器。如果你的团队成员需要支持更大型的会议,他们需要控制他们创建的资源类型,并根据使用情况优化资源大小。你决定调查控制团队可以使用的服务器和类型的方法。

12.1.1 实施测试以控制成本

你检查了会议的指标和每个服务器的资源使用情况。没有服务器超过它们的虚拟 CPU(vCPU)或内存使用量。大部分情况下,你确定你的生产环境最多需要 32 个 vCPU。你的客户的 IT 团队确认最大使用量不超过 32 个 vCPU。

注意 GCP 使用术语机器类型来指代一个预定义的虚拟机形状,具有特定的 vCPU 和内存比率,以适应你的工作负载需求。同样,AWS 使用术语实例类型,而 Azure 使用术语大小

然而,公共云使得任何人都可以轻松地将服务器调整为使用 48 个 vCPU。由于额外的 CPU,你的账单增加了 50%,而你甚至没有使用完所有这些 CPU。为了更积极地使用 IaC 来控制成本,你结合了第六章中的单元测试和第八章中的策略执行。

图片

图 12.2 你的测试应该从服务器配置中解析机器类型,检查 GCP API 中的 CPU 数量,并验证它们不超过限制。

图 12.2 在服务器的交付管道中添加了一个新的策略测试。该测试检查 IaC 中定义的每个服务器的 vCPU 数量,并将其与 GCP API 返回的值进行比较。如果 API 的信息超过了你 32 个最大 vCPU 限制,则测试失败。

为什么调用基础设施提供商的 API 来获取 vCPU 信息?许多基础设施提供商提供 API 或客户端库,可以从他们的目录中检索有关给定机器类型的信息。你可以使用它来动态获取关于 CPU 数量和内存的信息。

基础设施提供商经常更改其产品。此外,你无法计算每种可能的服务器类型。编写测试以调用基础设施提供商 API 以获取最新信息有助于提高测试的整体可扩展性。

在列表 12.1 中,让我们实现策略测试以检查最大 vCPU 限制。首先,你构建一个方法来调用 GCP API 以获取给定机器类型的 vCPU 数量。

列表 12.1 从 GCP API 检索机器类型的 vCPU 计数

import googleapiclient.discovery

class MachineType():                                     ❶
   def __init__(self, gcp_json):                         ❶
       self.name = gcp_json['name']                      ❶
       self.cpus = gcp_json['guestCpus']                 ❶
       self.ram = self._convert_mb_to_gb(                ❷
           gcp_json['memoryMb'])                         ❷
       self.maxPersistentDisks = gcp_json[               ❷
           'maximumPersistentDisks']                     ❷
       self.maxPersistentDiskSizeGb = gcp_json[          ❷
           'maximumPersistentDisksSizeGb']               ❷
       self.isSharedCpu = gcp_json['isSharedCpu']        ❷

   def _convert_mb_to_gb(self, mb):                      ❷
       GIGABYTE = 1.0/1024                               ❷
       return GIGABYTE * mb                              ❷

def get_machine_type(project, zone, type):
   service = googleapiclient.discovery.build(
       'compute', 'v1')
   result = service.machineTypes().list(                 ❸
       project=project,                                  ❸
       zone=zone,                                        ❸
       filter=f'name:"{type}"').execute()                ❸
   types = result['items'] if 'items' in result else None
   if len(types) != 1:
       return None
   return MachineType(types[0])                          ❹

❶ 定义一个机器类型对象以存储你可能需要检查的任何属性,包括 vCPU 数量

❷ 将兆字节内存转换为千兆字节以实现一致的单位度量

❸ 调用 GCP API 以检索给定机器类型的 vCPU 数量

❹ 返回一个包含 vCPU 和磁盘属性的机器类型对象

AWS 和 Azure 等效

你可以使用 AWS 的 Python SDK 来检索 EC2 实例并解析实例类型。然后,描述实例类型以获取 vCPU 和内存信息(mng.bz/qYYK)。

要从 Azure 获取机器类型和库存单位(SKU),请使用 Python 的 Azure 库。在从实例列表中检索大小后,你可以调用资源 SKU API 以获取 CPU 数量和内存信息(mng.bz/YG1N)。

每次使用新的机器类型时,你都可以使用相同的函数来检索 vCPU 和内存。接下来,你编写一个测试来解析配置中定义的每个服务器的机器类型。在下面的列表中,你检索服务器列表中机器类型的 vCPU 数量,并验证 vCPU 数量不超过 32 的限制。

列表 12.2 编写策略测试以检查服务器是否不超过 32 个 vCPU

import pytest
import os
import compute
import json

ENVIRONMENTS = ['testing', 'prod']                                      ❶
CONFIGURATION_FILE = 'main.tf.json'                                     ❶

PROJECT = os.environ['CLOUDSDK_CORE_PROJECT']                           ❶

@pytest.fixture(scope="module")                                         ❶
def configuration():                                                    ❶
   merged = []                                                          ❶
   for environment in ENVIRONMENTS:                                     ❶
       with open(f'{environment}/{CONFIGURATION_FILE}', 'r') as f:      ❶
           environment_configuration = json.load(f)                     ❶
           merged += environment_configuration['resource']              ❶
   return merged                                                        ❶

def resources(configuration, resource_type):                            ❶
   resource_list = []                                                   ❶
   for resource in configuration:                                       ❶
       if resource_type in resource.keys():                             ❶
           resource_name = list(                                        ❶
               resource[resource_type].keys())[0]                       ❶
           resource_list.append(                                        ❶
               resource[resource_type]                                  ❶
               [resource_name])                                         ❶
   return resource_list                                                 ❶

@pytest.fixture                                                         ❶
def servers(configuration):                                             ❶
   return resources(configuration,
                    'google_compute_instance')

def test_cpu_size_less_than_or_equal_to_limit(servers):
   CPU_LIMIT = 32                                                       ❷
   non_compliant_servers = []                                           ❸
   for server in servers:
       type = compute.get_machine_type(                                 ❹
           PROJECT, server['zone'],                                     ❹
           server['machine_type'])                                      ❹
       if type.cpus > CPU_LIMIT:                                        ❺
           non_compliant_servers.append(server['name'])                 ❺
   assert len(non_compliant_servers) == 0, \                            ❻
       f'Servers found using over {CPU_LIMIT}' + \                      ❻
       f' vCPUs: {non_compliant_servers}'                               ❻

❶ 解析和提取测试和生产环境中任何服务器的 JSON 配置

❷ 将 CPU 限制设置为 32,这是应用程序所需的最大值

❸ 初始化一个超过 32 个 vCPU 限制的不合规服务器列表

❹ 对于每个服务器配置,检索机器类型属性并调用 GCP API 获取更多信息

❺ 如果服务器配置中包含超过 32 个 vCPU 的机器类型,将其添加到不符合规定的服务器列表中。

❻ 检查所有服务器是否遵守 CPU 限制。如果不遵守,则测试失败,并对超过 32 个 CPU 的服务器抛出错误。

你使用软强制性执行策略配置测试。软强制性执行意味着在你创建之前,你的团队会审查和批准更昂贵的资源类型。如果你有业务理由,你可以将机器类型覆盖为更大的尺寸。

除了检查机器类型的 vCPU 和内存限制外,你可能还需要为适用于某些用例(如机器学习)的独特架构或机器类型添加覆盖。然而,它们比通用资源类型成本更高。

您可以通过测试 IaC 默认使用通用资源来验证。通用机器或资源类型提供更低成本的选项。如果有人需要专用、更昂贵的资源,您可以通过软强制性执行来启用它。

其他测试可能包括对特定配置的检查,如计划重启、自动扩展或私有网络。这些配置中的每一个都有助于优化您资源的成本。在 IaC 中表达它们可以让您验证配置是否符合最佳实践,从而在开发早期阶段降低成本。

12.1.2 自动化成本估算

您通过策略测试来应对过大或昂贵的资源变更以控制成本。那么,如果您想通过改变成本驱动因素来主动检查您的预算如何变化呢?想象一下,您想知道将您的生产服务器大小调整为n2d-standard-16(16 个 vCPU)的机器类型可能会如何影响未来不同三小时会议的成本。

图 12.3 概述了使用机器类型n2d-standard-16估算五台服务器成本的流程。一旦计算出价格,您就可以添加策略测试来验证总额不超过您的月度预算。

图片

图 12.3 成本估算解析 IaC 的机器类型,计算资源的月度成本,并生成一个与您预期预算进行比较的值。

成本估算 解析您的 IaC 以获取资源属性并生成其成本的估算。您可以使用成本估算来检查您的更改是否在预算范围内,或评估对成本驱动因素的调整。

定义 成本估算 从基础设施资源属性中提取并生成其总成本的估算。

成本估算如何帮助您演进您的基础设施?成本估算提供了对可能影响您架构的成本驱动因素的额外透明度。随着您改变系统,您可以使用这些测试来帮助预算和跨团队之间的费用报销沟通。

成本估算示例和工具

我编写了最少的代码来展示成本估算的一般工作流程。为了清晰起见,我从文本中省略了一些代码。您可以在github.com/joatmon08/manning-book/tree/main/ch12找到所有组织好的代码。

该示例使用 Google Cloud Billing Catalog API,该 API 提供了一个包含定价的服务目录。我还使用了一个专门的 Python 客户端库来访问 Cloud Catalog 的 API(mng.bz/mOOn)。该示例没有考虑专门的定价,例如持续使用折扣或预留实例(在 AWS 中相当于 spot instances)。

您会发现一些提供成本估算的工具。每个云服务提供商都为其资源提供自己的成本估算用户界面。其他工具实现了一个比我的示例代码更可扩展的工作流程,用于解析配置、调用云服务提供商的 API 并计算成本估算。我不会在本章中列出它们,因为它们经常变化,并且取决于云服务提供商和您的 IaC 工具。

获取单价

我建议动态地从云服务提供商的服务目录 API 请求信息。单价可能会变化,而硬编码价格通常会导致成本估算错误。要在示例中开始实现成本估算,您需要一些逻辑来调用云服务提供商的目录并获取基于您的机器类型的单价。

Google Cloud 计费目录 API 根据每单位 CPU 或内存(RAM)的价格提供服务和 SKU 列表。在列表 12.3 中,您获取了 Google Compute Engine 服务的服务标识符。Google Cloud 计费目录 API 根据服务标识符对价格进行分类,您必须动态检索这些标识符。

列表 12.3 从目录中获取 Google Compute Engine 服务

from google.cloud import billing_v1

class ComputeService:
   def __init__(self):
       self.billing = \                                               ❶
           billing_v1.services.cloud_catalog.CloudCatalogClient()     ❶
       for result in self.billing.list_services():
           if result.display_name == 'Compute Engine':                ❷
               self.name = result.name

❶ 使用 Python 库创建 Google Cloud 计费目录 API 的客户端

❷ 在目录中获取 Google Compute Engine 的服务标识符

AWS 和 Azure 的等效功能

将 GCP 客户端库更新为调用 AWS 成本探索器 API (mng.bz/5Qm4) 用于 AWS。另一方面,Azure 提供了一个用于零售价格的开放 REST API 端点 (mng.bz/6X9G)。您可以编写一些额外的代码来请求目录信息。

您可以在列表 12.4 中再次调用 Google Cloud 计费目录 API 来获取机器类型的成本。使用上一步中的服务标识符,您获取了 Google Compute Engine 服务的 SKU 列表。您编写一些代码来解析其 SKU 列表响应,以匹配机器类型和用途,并检索每 CPU 或每千兆内存的单元价格。

列表 12.4 获取 Compute Engine SKU 的 CPU 和 RAM 价格

from google.cloud import billing_v1

class ComputeSKU:
   def __init__(self, machine_type, service_name):
       self.billing = \                                              ❶
           billing_v1.services.cloud_catalog.CloudCatalogClient()    ❶
       self.service_name = service_name
       type_name = machine_type.split('-')                           ❷
       self.family = type_name[0]                                    ❷
       self.exclude = [                                              ❸
           'custom',                                                 ❸
           'preemptible',                                            ❸
           'sole tenancy',                                           ❸
           'commitment'                                              ❸
       ] if type_name[1] == 'standard' else []                       ❸

   def _filter(self, description):                                   ❸
       return not any(                                               ❸
           type in description for type in self.exclude              ❸
       )                                                             ❸

   def _get_unit_price(self, result):                                ❹
       expression = result.pricing_info[0]
       unit_price = expression. \
           pricing_expression.tiered_rates[0].unit_price.nanos \
           if expression else 0
       category = result.category.resource_group
       if category == 'CPU':
           self.cpu_pricing = unit_price
       if category == 'RAM':
           self.ram_pricing = unit_price

   def get_pricing(self, region):                                    ❺
       for result in self.billing.list_skus(parent=self.service_name):
           description = result.description.lower()                  ❻
           if region in result.service_regions and \                 ❻
                   self.family in description and \                  ❻
                   self._filter(description):                        ❻
               self._get_unit_price(result)
       return self.cpu_pricing, self.ram_pricing

❶ 使用 Python 库创建 Google Cloud 计费目录 API 的客户端

❷ 对于像 n2d-standard-16 这样的机器类型,提取机器系列(N2D)和用途(标准)以识别 SKU

❸ 如果您使用标准机器类型,不要在目录中搜索任何专门的计算服务 SKU。

❹ 获取每 CPU 或 RAM 的单价(以纳美元计,10^(-9))

❺ 调用 Google Cloud 计费目录并检索计算服务的 SKU 列表

❻ 根据机器类型的描述,找到与区域、机器系列和用途匹配的 SKU

Google Cloud 计费目录根据 CPU 数量和内存的千兆字节数设定单价。因此,您不能根据机器类型的名称进行搜索。相反,您需要将通用机器类型与目录描述相关联。

计算单个资源的月度成本

一旦你检索到给定机器类型的 CPU 和 RAM 单价,你可以用它来计算单个实例的月度成本。一些云目录通过一个因子设置单价。例如,GCP 使用纳单位,这意味着你还需要乘以这个因子。列表 12.5 实现了计算单个服务器月度成本的代码。你将单价乘以每月平均小时数,730,以及纳单位。

列表 12.5 计算单个服务器的月度成本

HOURS_IN_MONTH = 730                                      ❶
NANO_UNITS = 10**-9                                       ❶

def calculate_monthly_compute(machine_type, region):
   service_name = ComputeService().name                   ❷ 
   sku = ComputeSKU(machine_type.name, service_name)      ❸
   cpu_price, ram_price = sku.get_pricing(region)         ❸

   cpu_cost = machine_type.cpus * cpu_price * \           ❹
       HOURS_IN_MONTH if cpu_price else 0                 ❹
   ram_cost = machine_type.ram * ram_price * \            ❺
       HOURS_IN_MONTH if ram_price else 0                 ❺
   return (cpu_cost + ram_cost) * NANO_UNITS              ❻

❶ 设置一个常量,表示一个月 730 小时的平均值,并将纳美元转换为美元(10^(-9))。

❷ 从 Google Cloud Billing Catalog API 获取 Compute Engine 服务标识符

❸ 为机器类型设置 SKU 并获取其 CPU 和 RAM 单价

❹ 将机器类型的 CPU 数量乘以单价和该月的小时数

❺ 将机器类型的内存(RAM)容量(以千兆字节为单位)乘以单价和该月的小时数

❻ 将 CPU 和 RAM 成本相加,并将其转换为美元单位

现在你已经有一个最小化的成本估算形式,它可以计算单个服务器的成本。通过单个服务器的初始成本计算,你可以解析所有服务器的 IaC,检索它们的机器类型和区域,并计算总成本。在未来,你可以添加更多逻辑来检索其他服务的 SKU,如数据库或消息传递。

检查你的成本是否不超过预算

你决定你可以用你的成本估算做更多的事情。你编写了一个带有软强制性执行方法的测试,以检查你的估算成本是否超过了你的月度预算。例如,你的客户告诉你,一个会议不应超过 4500 美元的月度预算。你可以将你的成本估算与预算进行比较,并主动识别任何成本驱动因素。

让我们编写一个测试来估算服务器的新的成本,并将其与预算进行比较。在以下列表中,你解析所有服务器的 IaC 并计算具有特定机器类型和区域的服务器数量。

列表 12.6 解析所有服务器的 IaC

from compute import get_machine_type
import pytest
import os
import json

ENVIRONMENTS = ['testing', 'prod']
CONFIGURATION_FILE = 'main.tf.json'

@pytest.fixture(scope="module")
def configuration():                             ❶
   merged = []
   for environment in ENVIRONMENTS:
       with open(f'{environment}/{CONFIGURATION_FILE}', 'r') as f:
           environment_configuration = json.load(f)
           merged += environment_configuration['resource']
   return merged

@pytest.fixture
def servers(configuration):                      ❷
   servers = dict()
   server_configs = resources(configuration,
                              'google_compute_instance')
   for server in server_configs:
       region = server['zone'].rsplit('-', 1)[0]
       machine_type = server['machine_type']
       key = f'{region},{machine_type}'
       if key not in servers:
           type = get_machine_type(              ❸
               PROJECT, server['zone'],          ❸
               machine_type)                     ❸
           servers[key] = {                      ❹
               'type': type,                     ❹
               'num_servers': 1                  ❹
           }                                     ❹
       else:                                     ❹
           servers[key]['num_servers'] += 1      ❹
   return servers

❶ 读取定义每个环境的配置文件,例如测试和生产

❷ 对于配置文件中的每个服务器,创建它们的区域和机器类型列表

❸ 调用 Google Compute API 并获取有关机器类型的详细信息,例如其 CPU 数量和内存

❹ 跟踪具有特定机器类型和区域的服务器数量,以简化你需要检索的 SKU

你可以在测试中调用这些方法来检索特定区域中每种机器类型的成本信息,并计算总成本。以下列表中的测试检查总成本是否超过 4500 美元的月度预算。

列表 12.7 获取 Compute Engine SKU 的 CPU 和 RAM 价格

from estimation import calculate_monthly_compute

PROJECT = os.environ['CLOUDSDK_CORE_PROJECT']
MONTHTLY_COMPUTE_BUDGET = 4500                                           ❶

def test_monthly_compute_budget_not_exceeded(servers):                   ❷
   total = 0
   for key, value in servers.items():
       region, _ = key.split(',')
       total += calculate_monthly_compute(value['type'], region) * \     ❸
           value['num_servers']                                          ❸
   assert total < MONTHTLY_COMPUTE_BUDGET                                ❹

❶ 设置一个常量,以传达预期的月度计算预算

❷ 测试你的服务器成本是否不超过月度计算预算

❸ 根据机器类型和区域计算每台服务器的月度总成本,乘以该机器类型的服务器数量,然后汇总总成本

❹ 确认估算的总成本不超过月度计算预算

你现在有一个测试来估算计算资源的总月度成本,并将其与你的预算进行比较!每次有人更改基础设施时,测试都会重新计算系统的新的成本。

成本估算可以给你提供一个关于基础设施成本的一般视图,但可能无法准确反映你的实际账单。你必须考虑到一定的误差范围。如果你的估算超过了月度预算,这可能表明你需要重新评估规模和资源使用。你还将根据你系统的增长,随着时间的推移调整你的月度预算。

带有成本估算的持续交付

你如何检查基础设施变更是否不超过你的预算?每次你更改 IaC 并将其推送到仓库时,你的成本估算和预算测试都会运行。管道中的预算测试帮助你识别昂贵的基础设施变更并在测试环境中优化资源。这个过程可以防止生产环境中的费用追回。

例如,假设你想要为不同的会议向测试环境添加另一台服务器。在图 12.4 中,你创建了一个配置来添加一台具有n2d-standard-8机器类型的另一台服务器。管道运行一个测试来计算带有新服务器的月度成本,并将其与月度预算进行比较。

图片

图 12.4 添加另一台服务器超出了 4,500 美元的预算,导致测试失败。

你将配置推送到仓库,并且你的交付管道会运行测试来检查预算合规性。管道失败了!你检查日志并发现你的成本估算超过了预期的月度预算:

$ pytest test_budget.py
FAILED test_budget.py::test_monthly_compute_budget_not_exceeded – 
➥assert 4687.6161600000005 < 4500

你与财务团队进行了沟通。财务分析师确认预算可以增加以适应新的测试实例。你更新了测试中的月度预算,将其调整为 4,700 美元,以应对未来的变化!

无论你是编写成本估算机制还是使用工具,你应该考虑将其添加到你的交付管道中作为另一个策略测试。估算有助于指导实例大小和用量。它应该永远不会在生产前阻止变更。相反,它应该为你提供一个机会来重新评估资源的需求。

不要将每个成本驱动因素都计入你的成本估算中。相反,选择构成你账单大部分的资源。示例侧重于计算资源,如服务器,这些资源通常对成本贡献很大。你可能还需要对其他资源,如数据库或消息框架,实施成本估算。

总是质疑你的成本估算的准确性!你不能预测你将创建的资源或你如何使用它们。例如,你可能发现很难估算在不同区域或服务之间传输数据的成本,直到它发生。将你的成本估算与你的月度账单对账,并评估哪些成本驱动因素导致了差异。

每月比较可以帮助你识别任何变化,并根据估算的乘数预算实际成本。在本章的剩余部分,我们将讨论减少云浪费和优化成本的方法,这些方法超出了主动措施,如测试或估算。

练习 12.1

给定以下代码,以下哪些陈述是正确的?(选择所有适用的选项。)

HOURS_IN_MONTH = 730
MONTHLY_BUDGET = 5000
DATABASE_COST_PER_HOUR = 5
NUM_DATABASES = 2
BUFFER = 0.1

def test_monthly_budget_not_exceeded():
   total = HOURS_IN_MONTH * NUM_DATABASES * DATABASE_COST_PER_HOUR
   assert total < MONTHLY_BUDGET + MONTHLY_BUDGET * BUFFER

A) 测试将通过,因为数据库的成本在预算内。

B) 测试估算数据库的每月成本。

C) 测试不考虑不同数据库类型。

D) 测试计算每个数据库实例的每月成本。

E) 测试包括 10%的缓冲,作为软强制性政策,以应对任何成本超支。

请参阅附录 B 以获取练习的答案。

12.2 减少云浪费

你可以使用 IaC(基础设施即代码)来实施主动措施,以管理云计算的成本驱动因素。然而,你需要将它们与其他实践相结合,以继续减少和优化成本。毕竟,在示例中,你的客户仍然不欣赏为三小时会议支付 10,000 美元的云账单!

如果你配置了一个大服务器但未使用所有 CPU 或内存,你浪费了未使用的 CPU 或内存。你有机会降低你的云计算成本!你可以采取的一种改进账单状态的方法是消除云浪费,即未使用或利用率不足的基础设施资源。

定义云浪费是指未使用或利用率不足的基础设施资源。

你可以通过删除、过期或停止未使用的资源;根据使用情况安排或调整实例;以及评估适合你系统的正确资源大小或类型来减少云浪费;参见图 12.5。

图片

图 12.5 你可以通过删除未使用的资源,或安排和调整资源以适应使用模式来减少云浪费。

识别云浪费通常是从对公共云账单上令人惊讶的成本的第一反应开始的。然而,你可以在数据中心使用这些技术,特别是对于私有云。虽然它们不会提供即时的短期利益,但它们有助于优化数据中心资源使用和长期成本降低。

12.2.1 停止未标记或未使用的资源

有时你和你的团队会创建基础设施资源进行测试或其他配置。你最终会忘记它们,直到它们出现在你的云账单上。作为减少云浪费的第一步迭代,你可以识别未使用的资源并将它们删除。

回想一下,你有一个使命,那就是降低你客户举办会议的运营成本。你能通过识别未使用资源来降低成本吗?是的!有时我们的团队为测试创建了资源,却忘记了移除它们。

例如,你检索了你 Google Cloud 项目中的服务器列表,并在表 12.2 中检查它们。虽然许多测试和生产中的服务器都有标签,但你注意到有两个实例没有标签。n2d-standard-16 机器的费用约为 $700(占月度总账单的 7%)。

表 12.2 按类型和环境划分的服务器成本

机器类型 环境 服务器数量 小计
n2d-standard-8 测试 2 $400
n2d-standard-16 生产 2 $700
n2d-standard-32 生产 3 $2,900
总计 $4,000

你询问团队关于生产中的未标记实例。他们为沙盒创建了服务器以验证应用程序,但从未使用过它们。为了确保,你检查了整个月的服务器使用指标,它们都保持在零。你识别了一些云浪费!

团队确实使用基础设施即代码(IaC)创建了服务器。你删除了配置并推送更改以移除未使用的实例。删除配置会移除附加到实例的磁盘和资源。幸运的是,你下一次会议的云账单反映了这种减少。

为什么你会通过指标和团队成员确认服务器的使用情况?你不想意外地删除资源。有时你对看似未使用的资源有意外依赖。

确保你打算删除的资源没有其他依赖。如果你对删除未标记或未使用的资源有顾虑,你可以总是停止资源一周或两周,等待确定它是否破坏了系统,然后删除它。

12.2.2 按计划启动和停止资源

你的下一次云账单比上次减少了 7%,这得益于未使用服务器的移除。然而,财务团队希望你能进一步降低成本。你对此感到困惑,直到你与客户团队的一员交谈。他们提到,他们周末从不运行测试或使用任何基础设施资源。客户需要在会议前一周的平台可用。

你能否找到一种方法在周五晚上关闭服务器,并在每周一打开它们?你不会因为关闭服务器 48 小时而付费。安排定期关机意味着成本降低。

你发现 GCP 使用计算资源策略定义了一个实例关机时间表(mng.bz/o25N)。你每周一启动服务器,每周六关闭它们,如图 12.6 所示。

图片

图 12.6 你可以通过安排资源在不使用时启动和停止来降低成本。

按计划关闭实例可以减轻运行服务器的成本。然而,这项技术在你了解你系统的行为时才有效。按计划启动和停止资源可能会干扰开发工作。

一些应用程序没有容错机制,即使资源成功重启,也会继续失败。一般来说,大多数重启计划只在测试环境中运行。该计划为你提供了一个机会来验证你系统的弹性,因为每个周末你都有一个计划中的停机。

列表 12.8 在 GCP 中实现了实例调度的资源策略。该计划在会议前一周到期,因此周末不会关闭服务器。开发团队可能需要在会议前的几天内在该平台上工作。

列表 12.8 为实例调度创建资源策略

def build(name, region, week_before_conference):
   expiration_time = datetime.strptime(                      ❶
       week_before_conference,                               ❶
       '%Y-%m-%d').replace(                                  ❶
           tzinfo=timezone.utc).isoformat().replace(         ❶
               '+00:00', 'Z')                                ❶
   return {
       'google_compute_resource_policy': {
           'weekend': {
               'name': name,
               'region': region,
               'description':
               'start and stop instances over the weekend',
               'instance_schedule_policy': {                 ❷
                   'vm_start_schedule': {                    ❸
                       'schedule': '0 0 * * MON'             ❸
                   },                                        ❸

                   'vm_stop_schedule': {                     ❹
                       'schedule': '0 0 * * SAT'             ❹
                   },                                   
                   'time_zone': 'US/Central',                ❺
                   'expiration_time': expiration_time     
               }
           }
       }
   }

❶ 在会议前一周使用 RFC 3339 日期格式过期计划

❷ 创建一个具有实例调度的计算资源策略

❸ 每周一午夜启动虚拟机

❹ 每周六午夜停止虚拟机

❺ 在美国中部时区运行计划,因为开发团队在中部美国工作

AWS 和 Azure 的等效功能

其他公共云提供商通常提供类似的自动化功能,以按计划启动和停止虚拟机。例如,AWS 使用实例调度器来启动和停止服务器和数据库(mng.bz/nNev)。同样,Azure 基于 Azure 函数使用启动/停止虚拟机工作流(mng.bz/v6Xx)。

如果你的公共或私有云平台不提供计划关闭功能,你需要编写自动化代码以定期运行。我在使用各种工具之前实现了这一点,包括无服务器函数、容器编排器上的 cron 作业或持续集成框架上的计划运行。

与每月运行 730 小时的服务器相比,你大约少运行了 144 小时(假设一个月有三个周末和 48 小时的关闭时间)。使用你的成本估算代码,你更新了每月 586 小时的计算。它输出你将整体成本降低了 700 美元(总月账单的 7%)!

该示例将计划添加到测试环境中。然而,如果你有一个具有周期性使用模式的运行环境,你也可以将重启计划添加到生产环境中。例如,会议平台按需运行三小时,仅在周一至周五服务用户流量。关闭服务器和数据库 48 小时不会干扰用户流量。然而,你可能不希望在持续服务请求的生产环境中实施重启计划。

12.2.3 选择正确的资源类型和大小

如果您的生产环境需要每天 24 小时、每周 7 天服务客户,您仍然可以通过评估资源类型和大小来减少云浪费,而无需资源计划。许多资源并没有充分利用它们的 CPU 或内存。

许多时候,我们分配更大的资源是因为我们不知道需要多少。运行系统一段时间后,您可以调整资源的大小以适应其实际使用。您可以通过更改资源类型、大小、预留、副本甚至云服务提供商来降低成本,如图 12.7 所示。

图片

图 12.7 您可以通过更改资源的属性来更好地利用它并降低其成本。

您决定调查客户的会议平台以找到资源类型和大小上的云浪费。您意识到您无法降低计算资源的成本,因此您检查了数据库(Cloud SQL)。团队为生产数据库预留了 4 TB 的固态硬盘(SSD)存储,详情见表 12.3。

表 12.3 按提供和资源计算您的云账单

提供商 类型 环境 数量 小计
Cloud SQL $2,500
db-standard-1, 400 GB SSD, 600 GB backup 测试 1 $250
db-standard-4, 4 TB SSD, 6 TB backup 生产 1 $2,250

在检查指标和数据库使用情况后,您意识到它只需要 1 TB 的 SSD。您更新了 IaC 中数据库的磁盘大小。调整大小将数据库成本降低了$1,350(占月度总账单的 22.5%)!

您可能以许多其他方式没有充分利用资源。如果它使用更昂贵的机器类型,您可能需要考虑更改资源类型。您需要问自己和您的团队,“如果我们不运行性能测试,我们是否真的需要在测试环境中运行这个高性能数据库?”

可能不是!为特定环境选择正确的大小和类型可能需要几次迭代。您希望选择一个资源类型、大小和副本,以模拟生产环境,而不使其在成本上完全相同。

对于会议示例,您可能在生产中有三个n2d-standard-32实例,在测试环境中有三个n2d-standard-8实例。配置仍然测试了三个应用程序实例,而没有产生 72 个 CPU 的成本。

有时,您还可以更改资源的预留类型。GCP 和许多其他云服务提供商提供了一种短暂性(也称为现货可抢占性)的资源类型。这种资源成本较低,但云服务提供商保留停止资源并将 CPU 或内存分配给其他客户的权利。虽然短暂性资源预留可以降低成本,但您需要仔细考虑您的应用程序和系统是否能够处理这种中断。

12.2.4 启用自动扩展

您试图在环境中识别尽可能多的云浪费,但仍希望进一步降低成本。许多系统具有客户使用模式,这些模式不需要系统中的 CPU、内存或带宽在每天每小时都使用。

例如,会议平台只在会议的三小时内需要其全部容量!您能否根据需求自动增加或减少服务器数量?

图 12.8 将目标利用率设置为 CPU 使用量的 75%,以便 GCP 管理实例组启动和停止服务器以匹配目标指标。它根据需求增加和减少组的大小。

图 12.8 自动扩展组包括一个目标利用率,这允许它自动启动和停止资源以调整使用情况。

您为每个服务器组添加了自动扩展。自动扩展 根据指标(如 CPU 或内存)增加或减少组中的资源数量。许多公共云提供商提供自动扩展组资源,您可以使用基础设施即代码(IaC)创建。

定义 自动扩展 是根据指标自动增加或减少组中资源数量的实践。

GCP 自动扩展要求您设置一个目标指标以扩展或缩减资源以达到。在大多数月份的低流量期间,您预计只需使用一台服务器。然而,当高峰流量通过您的会议平台时,您需要最多三台。您决定使用 CPU 利用率作为指标,并将目标设置为 75%。

您更新了服务器的 IaC。在列表 12.9 中,您用托管实例组和自动扩展策略替换了原始的服务器和实例调度资源策略。自动扩展计划每天早上启动,增加或减少实例以达到 75% 的 CPU 利用率,并在每天晚上将实例扩展为零。

列表 12.9 基于 CPU 利用率创建自动扩展组

def build(name, machine_type, zone,
         min, max, cpu_utilization,
         cooldown=60,
         network='default'):
   region = zone.rsplit('-', 1)[0]
   return [{                                                             ❶
       'google_compute_autoscaler': {                                    ❶
           name: {                                                       ❶
               'name': name,                                             ❶
               'zone': zone,                                             ❶
               'target': '${google_compute_instance_group_manager.' +    ❶
               f'{name}.id}}',
               'autoscaling_policy': {
                   'max_replicas': max,                                  ❷
                   'min_replicas': 0,                                    ❸
                   'cooldown_period': cooldown,
                   'cpu_utilization': {
                       'target': cpu_utilization                         ❹
                   },
                   'scaling_schedules': {                                ❺
                       'name': 'weekday-scaleup',                        ❺
                       'min_required_replicas': min,                     ❺
                       'schedule': '0 6 * * MON-FRI',                    ❺
                       'duration_sec': '57600',                          ❺
                       'time_zone': 'US/Central'                         ❺
                   }                                                     ❺
               }
           }
       }
   }]

❶ 将实例组附加到自动扩展资源。我们省略了实例组以保持清晰。

❷ 当 CPU 利用率超过 75% 时,设置最大副本数以扩展

❸ 默认将最小副本数设置为零,这意味着它停止虚拟机

❹ 将 CPU 利用率作为自动扩展组的目标指标

❺ 设置一个扩展计划,每周一至周五上午增加开发团队使用模式下的最小副本数

AWS 和 Azure 等效

GCP 将托管实例组附加到自动扩展策略中。GCP 不允许您附加资源策略。您必须在自动扩展组中实现计划。

其他公共云提供商也提供服务器和有时数据库的自动扩展功能。AWS 使用自动扩展组。Azure 使用自动扩展规则进行规模集。

你可以在示例中设置一个扩展计划来模拟你之前实施的周末关闭。一般来说,使用模块的模式来创建自动扩展模块。该模块应根据你的工作负载设置一个有见地的默认目标指标。

如果你有一个不适合模块预设目标指标的独特工作负载,你可以将其默认设置为针对 CPU 利用率或内存,并评估其随时间的行为。当你推出实例组时,应用第九章中的蓝绿部署模式来替换活动工作负载或实例。推出计划和自动扩展组不应干扰应用程序。

为了鼓励团队使用自动扩展组和调度,你可以创建几个策略测试,以确保你的自动扩展组减少云浪费。例如,一个测试可以验证你的 IaC 没有单独的服务器,只包含自动扩展组。这个测试鼓励团队利用弹性。

你可以添加另一个测试,涉及检查最大副本限制。假设你的应用程序突然消耗了大量的 CPU 或内存,或者一个恶意行为者在你的机器上注入了加密货币挖矿的二进制文件。在这种情况下,你不想自动扩展组自动将其容量增加到 100 台机器。

12.2.5 设置资源过期标签

你可以通过根据利用率动态扩展和缩减资源来减少云浪费,但你还需要适应按需手动创建的资源。例如,客户端团队成员抱怨他们经常需要创建沙盒服务器进行进一步测试。然而,他们经常忘记服务器。如果没有人更新它们,你能在一段时间后“过期”服务器吗?

你决定更新你的标签模块,在测试环境中附加一个新的标签用于过期日期。回想一下,你可以使用原型模式(第三章)来建立标准标签。在应用第八章中的策略测试以检查标签合规性之后,你知道测试环境中的每个资源都将有一个过期日期。

例如,团队成员可能会创建一个初始过期日期为 2 月 2 日的服务器,如图 12.9 所示。然而,他们决定更新服务器。作为变更的一部分,标签模块检索当前日期(2 月 5 日),添加七天,并将服务器上的标签更新为新日期(2 月 12 日)。

图片

图 12.9 在标签模块中创建一个过期日期,将模块的过期日期重置为更改后的一个星期。

为什么要在标签模块中使用设置过期日期?你的标签模块应该应用于你所有的 IaC。这允许你建立一个默认持续时间为七天,并将其应用于所有基础设施资源。

您还可以控制何时应用到期标签作为模块的一部分。只有当您在生产环境中未创建资源或持续在测试环境中运行时,模块才会应用到期标签。以下列表更新了具有到期日期的默认标签的原型模块。

列表 12.10 带有到期日期的标签模块

import datetime

EXPIRATION_DATE_FORMAT = '%Y-%m-%d'                        ❶
EXPIRATION_NUMBER_OF_DAYS = 7                              ❷

class DefaultTags():
   def __init__(self, environment, long_term=False):
       self.tags = {
           'customer': 'community',
           'automated': True,
           'cost_center': 123456,
           'environment': environment
       }
       if environment != 'prod' and not long_term:         ❸
           self._set_expiration()                          ❸

   def get(self):
       return self.tags

   def _set_expiration(self):
       expiration_date = (                                ❹
           datetime.datetime.now() +                      ❹
           datetime.timedelta(                            ❹
               days=EXPIRATION_NUMBER_OF_DAYS)            ❹
       ).strftime(EXPIRATION_DATE_FORMAT)                 ❺
       self.tags['expiration'] = expiration_date

❶ 将日期格式化为年、月、日的字符串

❷ 从当前日期计算七天后的到期日期

❸ 如果在生产环境中未创建资源或将其标记为长期资源,则设置到期标签

❹ 从当前日期计算七天后的到期日期

❺ 将日期格式化为年、月、日的字符串

您将一周设为默认值,因为这给团队成员足够的时间开发和测试资源。他们可以通过运行交付管道自动更新标签来续订另一周。然而,您确实需要启用覆盖,以便在测试环境中允许长期资源。

如何默认强制执行到期日期标签,但豁免资源不设置到期日期?您可以创建一个具有软强制性执行的策略测试。软强制性策略允许对测试环境中的长期资源进行例外和审计。

让我们编写一个测试,强制执行列表 12.11 中每个服务器资源的到期标签。如果服务器不在免于策略的资源列表中,测试将失败,并停止交付管道部署所有更改到生产环境。

列表 12.11 测试检查测试资源是否具有到期日期

import pytest

def test_all_nonprod_resources_should_have_expiration_tag(
       servers, server_exemptions):                             ❶
   noncompliant = []
   for name, values in servers.items():
       if 'expiration' not in values['labels'].keys() and \     ❷
               name not in server_exemptions:                   ❸
           noncompliant.append(name)                            ❸
   assert len(noncompliant) == 0, \
       'all nonprod resources should have ' + \
       f'expiration tag, {noncompliant}'

❶ 从配置中检索服务器列表以及免于策略的服务器

❷ 检查服务器标签中是否存在到期标签

❸ 如果您未豁免服务器,您必须将服务器标记为不符合策略。

将资源添加到豁免列表意味着您的团队成员将仔细检查哪些资源持续存在于测试环境中。在同行评审(第七章)期间,您可以根据豁免列表的更改识别任何新的持久资源。测试环境中的持久资源单一来源确保您可以在开发早期审计和讨论成本控制。

在 IaC 中实现到期标签后,您需要编写一个每天运行的脚本。图 12.10 显示了脚本的流程。它检查到期日期是否与当前日期匹配。如果是,自动化会删除资源。

为什么使用 IaC 设置到期日期?使用标签模块设置到期日期的工作流程内置了更新资源到期日期的能力!而不是通过添加单独的自动化来引入开发摩擦,您将续订纳入开发过程。

图 12.10 设置到期标签允许每日自动化确定是否删除临时资源,从而降低成本。

例如,如果一个团队仍然需要资源,它总是可以重新运行其 IaC 交付管道来重置有效期,再延长七天。对资源的主动更改也会重置有效期。如果您更改了基础设施,您可能仍然需要该资源。

当资源到期而您仍然需要它时会发生什么?您总是可以重新运行您的 IaC 并创建一个新的。使用 IaC 添加和更新到期日期提供跨团队的成本合规性和功能可见性。

注意:有时您会发现针对自动标记的单独自动化。自动化在创建基础设施资源后添加到期日期。虽然自动标记意味着对成本合规性有更大的控制,但它也引入了实际配置和预期配置之间的偏差。此外,自动到期通常会让团队成员感到困惑。除非他们注意到了您的沟通,否则他们可能会在几天后发现他们的资源被删除了!

您总是可以将到期时间间隔设置为几天以外的其他时间。如果您想为团队提供更多灵活性,您可以通过标签模块提供一系列天数。我建议计算绝对到期日期并将其添加到标签中,而不是时间间隔,以便更容易进行清理自动化。

在示例中的所有更改之后,您的客户的云计算账单发生了什么变化?您的账单从略高于 10,000 美元降至约 6,500 美元(减少了 35%)!您的客户感谢他们云资源的有效使用。

在现实中,您可能无法实现与示例相同的戏剧性成本降低。然而,您始终可以将这些实践和技术应用于您的 IaC,以在可能的情况下引入减少成本的小幅变化。通过测试捕获成本降低实践确保每个人在考虑成本约束的情况下编写代码。

练习 12.2

假设您有三台服务器。您检查它们的利用率,并注意到以下情况:

  • 您需要一个服务器来处理最小流量。

  • 您需要三个服务器来处理最大流量。

  • 服务器每天 24 小时、每周 7 天处理流量。

您可以做什么来优化下个月服务器的成本?

A) 将资源安排在周末停止。

B) 添加一个基于内存的autoscaling_policy进行扩展。

C) 为所有服务器设置三小时的到期时间。

D) 将服务器更改为较小的 CPU 和内存机器类型。

E) 将应用程序迁移到容器中,并在服务器上更密集地打包应用程序。

请参阅附录 B 以获取练习的答案。

12.3 优化成本

您可以将其他基础设施即代码(IaC)原则和实践应用于减少云浪费和管理成本驱动因素。例如,按需构建环境、更新区域间的路由或在生产环境中进行测试等技术可以使用 IaC 进一步优化成本,如图 12.11 所示。

图 12.11 成本优化需要 IaC 实践来扩展和部署基础设施资源。

尤其是可重复性、可组合性和可进化性的原则可以帮助通过创造性的技术进一步优化成本。这些技术包括按需复制环境以减少持久测试环境、跨云提供商组合基础设施以及跨区域和云提供商演进生产基础设施。

回想一下,你通过减少客户用于会议的云计算成本 35%。一年后,财务团队请求你的帮助来优化他们平台的成本。他们的业务已经增长,并希望优化数百名客户和托管服务的成本。

12.3.1 按需构建环境

从更广泛的角度来看,你需要检查测试和生产中存在哪些环境。你可能从减少所有环境中的云浪费开始。然而,随着你的公司发展壮大,它需要添加更多环境来支持和支持更多产品。

想象一下你检查了客户的架构。客户有许多测试环境。你确定其中三到四个环境持续运行并支持专业测试。例如,质量保证(QA)团队每年使用其中一个环境进行性能测试两次。在其余的一年中,这些环境保持休眠状态。

你决定移除持续运行的环境。如果 QA 团队需要用于性能测试的环境,他们可以按需创建。团队会复制生产环境,包括其工厂和构建模块,这些模块允许输入。这些模块提供了指定不同环境变量和参数的灵活性。

图 12.12 显示了创建按需环境的其余工作流程。QA 团队将 IaC 复制到组织多仓库结构中针对测试环境的特定新仓库。团队更新参数和变量,运行其测试,并移除环境。

图片

图 12.12 你可以复制生产环境的配置来创建和定制用于测试的按需环境。

为什么使用可重复性按需创建新环境并删除它们?一个新的、更新的环境确保最新的配置与生产环境匹配。如果你一年只使用一次环境,你不想让它持续运行 11 个月。

虽然创建新环境需要时间,但你可能花费同样多的时间试图修复测试环境和生产环境之间的漂移。识别不必要的长时间运行的环境并将它们切换到按需模式可以帮助减轻成本,尤其是如果你可以轻松地重新创建它们。

12.3.2 使用多个云

在有限的云提供商选项中,您也可以考虑部署到其他云,并根据资源、工作负载和一天中的时间优化成本。IaC 可以帮助标准化和组织多个云的配置。部署到多个云可以满足需要特定基础设施资源的专业工作负载或团队。

例如,假设您的客户使用 Google Cloud Dataflow 进行流数据处理。然而,成本取决于管道的类型。您说服一些报告团队将一些批处理管道转换为 Amazon EMR 以降低整体成本。

Azure 等效

Azure 中与 Amazon EMR 或 Google Cloud Dataflow 等效的是 Azure HDInsight。

在图 12.13 中,报告服务团队将其 IaC 切换到使用 Amazon EMR 模块。为了最小化作业的中断,团队成员使用第九章中提到的蓝绿部署模式,逐渐增加他们在 Amazon EMR 中运行的作业数量。

图片

图 12.13 报告服务通过引用不同的模块将其批处理作业切换到使用 Amazon EMR 而不是 Google Cloud Dataflow。

可组合性原则成为多云配置的重要组成部分。IaC 使得管理并识别不同云提供商的基础设施资源变得更加容易。使用模块来表示云之间的依赖关系也有助于您随着时间的推移演进资源。

在第五章中,我们将 IaC 配置根据工具和提供商分到了不同的文件夹中。许多 IaC 工具不提供云提供商资源的统一数据模型。为每个计划支持的云构建不同的模块。按提供商分离模块可以降低复杂性并支持模块的独立测试。为每个云提供商维护独立的模块也有助于轻松识别基础设施资源和提供商。

12.3.3 评估区域和云之间的数据传输

随着多云的采用,您可能会发现您并没有降低整体云计算账单。您需要仔细考虑多云,因为提供商将根据区域间和云网络外的数据传输收费。数据传输成本以令人惊讶的方式累积!

您检查客户的云计算账单,发现大部分成本来自区域间和网络外的数据传输。经过一些调查,您发现许多服务和测试环境都在跨区域、可用区和公共互联网中进行通信。

例如,us-central1-a中聊天服务的集成测试使用了us-central1-b中用户配置文件服务的公网 IP 地址!你意识到集成测试环境中的所有服务都不需要跨区域、可用区或网络外进行测试。

集成测试仅测试服务相对于其他服务的功能,而不是系统本身。图 12.14 使用第十章中的重构技术将集成测试环境中的基础设施资源整合到一个可用区。

图片

图 12.14 重构您的 IaC 以支持单个可用区中的集成测试环境,并解析到私有 IP 地址。

如果可用区失败会怎样?您始终可以将 IaC 切换到不同的区域或地区。应用程序仍然通过私有网络进行通信,并且不会对谷歌云网络之外的数据传输或区域和区域之间的数据传输收费。

优先考虑私有网络而非公共网络,这不仅关乎安全,还关乎成本和效率。如果您使用多个云,了解哪些资源需要在云之间进行通信。有时,您可能会发现将整个服务集转移到另一个云比支付云之间的数据传输费用更经济高效。应用第九章和第十章中的更改和重构技术可以帮助整合服务和通信。

12.3.4 生产环境测试

即使在转移到多个云并优化数据传输之后,您可能会发现您的测试环境无法完全模拟生产环境,并且运行成本过高。在某个时候,您无法在隔离环境中进行测试。与其完全模拟生产环境,不如通过直接在生产环境中进行测试来继续优化成本。

在会议平台的案例中,您帮助视频服务团队实施更改以进行生产测试。在图 12.15 中,团队成员使用功能标志隐藏预期更改,并创建了一组新的基础设施资源。然后,他们切换标志以将所有应用程序和用户流量导向,并在生产环境中验证其功能。这两组资源同时运行几周。几周后,他们删除了旧的基础设施资源。

图片

图 12.15 展示了 IaC 在生产环境中的测试应用了蓝绿部署和功能标志来针对一组资源。

团队通过使用蓝绿服务而不是测试环境来在生产环境中测试其服务。生产环境测试涉及一系列实践,这些实践使您能够对生产数据和服务运行测试。

定义生产环境测试是一系列实践,允许您对生产数据和服务运行测试。

在软件开发中,您使用特征标志等技术来在生产环境中隐藏某些功能以进行测试。同样,您可以使用金丝雀部署在向平台上的所有人提供之前,先对一小组用户的功能进行测试。

对于基础设施即代码(IaC),在生产环境中进行测试并不完全符合软件开发实践。你不想测试代码是否与一小群用户兼容。你只想知道你是否创建了可能影响用户的损坏的基础设施系统!你可以应用一些技术,比如功能标志和金丝雀部署。我们在第九章和第十章中就是这样做的。

你可以直接在生产环境中测试 IaC,而不需要蓝绿部署模式或功能标志。然而,在出现故障的情况下,你需要一个已建立的回滚计划。我在一个组织工作,该组织在将更改推送到生产之前完全依赖于本地测试。如果我们的更改失败,我们会尝试将系统更新到之前的状态。如果所有其他方法都失败了,我们会创建一个全新的基础设施环境,并将所有应用程序和用户流量直接导向新环境。

你可能已经尝试了所有成本优化、云浪费减少和成本驱动控制技术,但仍然无法完全优化你的云账单!随着时间的推移,你组织的使用和产品需求会发生变化。

如果你系统中有适当的监控和仪表,你可能会发现你有需求更高或更低的时间段。例如,你的客户的平台在 5 月、6 月、10 月和 11 月需求最高,因为这是高峰会议季节。

注意:你可能需要重新架构你的系统,以利用公共云的弹性,即扩展或缩减的能力,并随着时间的推移降低成本。某些软件架构并不容易让你动态地扩展或缩减资源。通常,解决方案需要重构或重新平台化应用程序,以提高系统成本效率。

了解你的系统在一段时间内的资源使用和需求可以帮助你进一步优化成本,超出我在本章中描述的技术。你还可以通过与基础设施提供商协商合同来进一步降低成本。选择新的定价模型让你可以在一定数量的预留实例或基于容量的折扣上节省费用。

系统运行时间越长,你聚集的指标就越多。这些信息可以帮助你在基础设施即代码中迭代成本驱动控制、云浪费减少和整体成本优化。你还可以使用这些信息与云提供商协商,减少云计算账单中的意外费用!

摘要

  • 在更改 IaC 以优化成本之前,确定可能影响总成本的成本驱动因素(资源或活动)。

  • 通过添加策略测试来检查资源类型、大小和预留,管理基础设施中的成本驱动因素,例如计算资源。

  • 成本估算解析你的 IaC 以获取资源属性,并根据云提供商的 API 生成其成本的估算。

  • 你可以添加策略测试来检查成本估算的输出,以近似你是否超出了预算。

  • 云浪费描述了未使用或利用率不足的基础设施资源。

  • 消除云浪费可以帮助降低您的云计算成本。

  • 通过移除或停止未标记或未使用的资源、按计划启动和停止资源、为更好的利用调整基础设施资源的大小、启用自动扩展以及标记资源到期日期来减少云浪费。

  • 自动扩展根据如 CPU 或内存等指标增加或减少给定组中资源数量或数量。

  • 优化云计算成本的技术包括按需构建环境、使用多云、评估数据传输以及在生产中进行测试。

  • 生产测试使用如蓝绿部署和功能标志等实践来测试基础设施更改,而不需要测试环境。

13 管理工具

本章涵盖

  • 评估开源基础设施模块和工具的使用

  • 应用技术来更新或迁移 IaC 工具

  • 实施事件驱动 IaC 的模块模式

您已经学会了如何编写基础设施代码,通过使用交付管道和测试与团队一起更新它,并在组织内部管理其安全和成本。随着您的基础设施系统的发展,您会适应这些模式和策略,并调整它们以适应新的工作流程和用例。同样,工具会发生变化,但不应破坏扩展、协作和操作基础设施的模式和策略。

更新您的工具可能需要采取几个步骤。您可以选择升级到新版本,用新工具替换它,或者处理更动态的 IaC 用例。本章讨论了处理 IaC 工具更新的常见模式和策略。

这些模式适用于任何涵盖*提供配置管理镜像构建**用例的工具。您会发现它们也适用于软件开发,尽管我根据基础设施的观点对这些模式进行了调整。使用这些模式和策略来减轻更新的影响范围,跨团队扩展新工具,并继续发展您的系统以支持业务需求。

注意:本章不包括任何代码列表。添加示例意味着引入另一个工具。我在更高层次上描述了模式和策略。您可以将这些技术应用于支持 DSLs 或编程语言的任何工具。

您已经阅读了本书中的许多章节,并在不同行业和公司中实践了 IaC。您在建立和扩展 IaC 实践方面建立了声誉。有一天,一家社交媒体公司向您提供了一个平台团队的角色。

该公司已经建立了其 IaC 实践几年了。员工需要您的帮助来维护和更新他们用于 IaC 的工具。您接受了这个提议,并在第一天就接到了一系列待办项目。

13.1 使用开源工具和模块

版本控制和公共仓库的易用性使得在编写自己的工具或基础设施模块时,搜索现有工具或模块变得更加简单。您可以去 GitHub 或其他任何服务上寻找满足您需求的自动化和工具。然而,在将新工具引入任何组织之前,您需要确保您已经做了充分的尽职调查。

例如,让我们想象一下维护社交媒体的推送功能团队成员向您提出的情况。他们在网上搜索并找到了一个用于创建数据库的基础设施模块。他们希望使用它。他们希望加快他们的开发过程,避免等待另一个团队审查他们的数据库配置。毕竟,为什么要重新发明轮子呢?

您提出帮助审查该模块的安全性和最佳实践。在您将模块介绍给社交媒体公司的其他团队之前,您使用图 13.1 评估数据库模块的功能、安全和生命周期,然后正式采用它。

每次开源维护者发布新的数据库模块时,您都会重新评估该模块。您可以将此决策流程应用于安全地采用外部 IaC 模块和工具。您想确保您可以使用该工具或模块,并避免在您的基础设施系统中造成不安全的配置,从而允许恶意行为者利用您的系统。

13.1.1 功能性

您可能会发现一个模块或工具很有前景。它允许您非常灵活地配置所需的属性。然而,回想一下第二章和第三章,模块应包含一些具有意见的默认值。没有它们,过于灵活的模块或工具可能会导致一次性配置,最终破坏您的系统。

图片

图 13.1 在您使用工具或模块之前,您评估其功能、安全和生命周期。

您鼓励数据源团队验证数据库模块中的默认值。团队评估该模块,如图 13.2 所示。该模块使用非常具有意见的默认值,固定了数据库版本,并彻底测试了兼容性。

图片

图 13.2 基于功能评估模块或工具使用的决策流程

文档指出,该模块将数据库版本固定,以彻底测试配置及其与特定版本的兼容性。数据源团队成员确认他们使用该数据库版本,并批准了该模块为其设置的默认值。

接下来,他们评估模块的输入变量。数据库模块允许他们设置所需的属性,例如数据库名称。它还允许他们设置标签和网络。数据源团队成员确认他们不需要设置超过这些变量。

由于该模块并未将每个属性都作为输入变量提供,并且提供了适合团队的默认值,因此您根据功能批准该模块。一般来说,请审查模块的文档和提交历史。如果该模块测试了版本兼容性以及适合您功能的一组默认值,您就可以继续进行安全评估。

如果您发现该模块没有提供您需要更改的一些特定默认值或输入变量,您可以找到另一个模块,编写自己的模块,或者带着其限制使用该模块。同样,您可以将决策流程应用于工具,并发现其不足。一个工具不能做所有事情!您必须平衡其在功能上的灵活性与其在基础设施变更中的可预测性。

13.1.2 安全性

虽然你可能首先评估模块或工具的功能,但接下来你应该评估其安全性。安全性往往是决定你是否应该使用开源工具或模块的关键标准。如果没有仔细评估开源模块或工具的安全配置或代码,你可能会无意中为恶意行为者打开一扇门,使其能够损害系统。

在饲料团队成员可以使用他们的数据库模块之前,他们需要你检查该模块是否存在任何安全顾虑。你检查数据库模块是否暴露或输出敏感信息,是否向第三方端点发送信息,以及是否通过现有的安全性和合规性测试,如图 13.3 所示。

图片

图 13.3 基于安全性的模块或工具使用评估决策流程

在示例中,数据库模块不会输出密码或任何敏感配置,也不会将信息发送到第三方端点。该模块还通过了你在第八章中编写的数据库的安全和合规性测试。为什么在采用模块之前你应该验证这三个检查?

一个模块可能会意外地暴露或输出敏感信息。例如,在模拟运行期间,配置可能会意外地输出密码。如果确实如此,确保你有方法来缓解或隐藏密码并更换它。

类似地,一个模块不应写入或向未经授权的第三方发送信息。恶意行为者可能会添加一个小的配置,将你的网络信息发送到 HTTP 端点。审查每个资源并检查它们是否没有向第三方发送任何信息。

安全性和开源

软件供应链攻击发生在行为者在供应商的软件中包含恶意代码时,该软件被发送给客户并损害了他们的数据和系统。开源的好处在于,在作为客户的你决定使用它之前,你可以检查代码中包含的内容。

在本节中,我意识到我在防御供应链攻击的风险评估和防护措施上过于简化了。更多详细信息,请参阅 NIST 的白皮书(mng.bz/449B),它更好地组织了一些实践。

最后,运行你为基础设施资源编写的现有安全性和合规性测试。你希望拥有安全合规的资源。否则,你需要回过头来更新模块以满足你的要求。

在你的基础设施即代码(IaC)中,在一个隔离的测试环境中部署该模块。然后对该模块运行安全性和合规性测试。将模块隔离在其自己的环境中可以确保你不会向运行环境引入不合规的配置。

请记住,并非所有的安全性和合规性测试都会通过。那些失败的测试需要对模块进行一些小的重构才能工作。所有测试通过后,你可以批准该模块作为安全模块使用。

工具在评估其安全性时遵循类似的决策流程。然而,IaC 工具的安全性和合规性测试可能包括静态代码分析和来自您组织的额外审查。对于在干运行期间输出配置或其他信息的工具,您将希望应用第八章中的修复步骤。

13.1.3 生命周期

您检查了模块的功能和安全,但合规性团队成员提出了一个非常好的问题。他们询问 维护公共模块。如果维护者不再更新它,您的组织将需要对该模块或工具拥有私人(或可能是公共)所有权。

您检查了文档以了解模块的生命周期以及谁在维护它,如图 13.4 所示。如果数据库模块有公司赞助和适当的许可证,您很可能可以使用它。

图 13.4 基于工具或模块生命周期评估模块或工具使用的决策流程

您检查了数据库模块的维护者。维护者来自一家知名科技公司,并且有许多贡献者,这意味着项目培养了一个活跃的社区。他们每隔几个月就会更新和发布模块的新版本。每个贡献都必须通过测试套件来验证更改不会破坏模块。

接下来,您检索有关数据库模块开源许可证的信息。您不知道开源许可证如何影响您的公司,因此您联系了您的法律团队。法律团队在数据团队可以使用之前审查了许可证。

该模块包含一个 MIT 许可证。作为一种 宽松的开源许可证,MIT 许可证意味着如果您分叉(维护副本)或修改数据库模块,您必须包含许可证副本和原始版权声明。如果维护者废弃了模块或工具,宽松的许可证类型允许您自己修改和更新模块。

定义 A 宽松的开源许可证 允许您在包含许可证副本和原始版权声明的情况下分叉或修改代码。

您的法律团队批准了许可证,因为该模块对整体基础设施配置的风险极低。公司如果需要可以编辑该模块,但不必公开发布。也许您甚至可以自己为开源模块做出贡献,前提是获得法律批准。

模块或工具还可以包含属于版权左开类别的开源许可证。许可证的 版权左开 类别包含一个条款,您必须发布带有您修改的代码库。

定义 A 版权左开的开源许可证 允许您在发布带有您修改的代码库的情况下分叉或修改代码。

许可证的 copyleft 类别通常包括对修改和分发工具或模块的更多限制。您的公司的法律团队将评估公司是否可以使用具有更严格许可的开源 IaC。

注意:有关开源许可的更多信息,请参阅开源倡议概述的许可和标准(opensource.org/licenses)。

(兴奋的)饲料团队可以使用数据库模块。您建议团队通过将其镜像到内部工件存储库来锁定模块版本。镜像模块确保团队只能在内部存储库中使用经过批准的模块。如果模块的公共端点出现故障,团队始终可以在内部存储库中找到副本。每次维护者发布新的模块版本时,您都必须检查更改并批准最新版本。

如果您的组织允许,请考虑为开源项目做出贡献。从开源模块或工具中分叉并自行维护,同时保持与公共发布的同步,这将产生运营开销。您可能需要投入许多小时来协调开源版本和您的版本之间的更改。创建一个直接向公共发布贡献更改的流程有助于减少维护独特更改的开销,这些更改可能会破坏您的基础设施。

13.2 升级工具

当您运行 IaC 实践几年后,您不可避免地会达到一个需要升级工具或其使用的插件的点。工具版本与最新版本之间的差距越大,您在最小干扰的情况下更新基础设施就越困难!

我们在第五章中了解了模块版本化的挑战。在本节中,我将介绍一些您在需要升级工具时可以使用的考虑因素和模式。

注意:您不会找到可以完美迁移所有内容的魔法工具。升级工具始终会面临挑战。您 IaC 中的独特模式(如内联脚本)可能在升级过程中破坏系统!因此,尽可能限制您 IaC 逻辑的复杂性。

想象一下,您审计了公司的 IaC 工具(提供、配置管理和镜像构建)。公司的大部分 IaC 使用工具版本 1.7。然而,最新工具版本是 4.0。您的第一个主要项目涉及将 IaC 更新为使用工具版本 4.0。

13.2.1 升级前清单

在开始升级工具或插件之前,您需要检查一些基本实践,这些实践将有助于最大限度地减少对您基础设施的潜在干扰。您的清单应包括一些步骤来解耦、稳定和协调 IaC。

图 13.5 显示了此清单。您解耦所有依赖项,检查版本,锁定所有版本,并将 IaC 部署以减少漂移。

图片

图 13.5 在升级工具之前,你的清单应该包括固定和检查模块、插件和工具的所有版本。

如果工具升级添加或删除字段,你需要传递每个资源子集期望的正确信息。依赖注入为基础设施资源之间的配置属性提供了一层抽象(参见图 4)。它保护每个子集免受其他子集变更的影响。

你需要检查你是否为所有 IaC 模块(第五章)添加了模块版本号。同样,你也要确保任何使用该模块的 IaC 或存储库将其固定到特定版本。

例如,社交媒体公司的一个团队总是使用模块的最新版本。你将模块版本 2.3.1 添加到团队的存储库中。当你使用工具升级发布模块版本 3.0.0 时,团队的 IaC 不会因为破坏性变更而升级。你可以在不固定模块版本的情况下更新模块并将破坏性变更推送给所有使用它的人!

你还要验证每个团队是否将工具及其插件版本固定到当前版本。虽然插件可能没有向前兼容性,但你希望保留当前版本,并避免在另一个版本中添加新的配置。最后,将所有固定的模块、工具和插件版本推送到每个 IaC 模块和配置中。你确保版本固定不会引入新的变更。

在完成你的预升级清单后,你必须规划你的工具升级路径。图 13.6 显示,从 1.0 升级到 4.0 的工具引入了一些破坏性变更!你决定可以升级到 3.0,它提供了与 1.7 的向后兼容性。然后你可以从 3.0 升级到 4.0,这应该减少其他破坏性变更。

图片

图 13.6 你规划工具升级路径,并考虑向后兼容版本和具有破坏性变更的版本。

不要立即升级到最新版本。相反,检查你的工具中的破坏性变更列表,并评估你是否可以适应这些变更。我避免一次性升级超过两个版本(或 beta 发布中的子版本)。大多数工具在行为或语法上都会有破坏性变更。

在将升级部署到生产环境之前,考虑在测试环境中测试升级。根据你的系统和测试环境,你可以确定工具升级是否可能破坏基础设施。升级永远不会像你预期的那样顺利,测试环境有助于在升级生产之前识别重大问题。

13.2.2 向后兼容性

许多 IaC 工具为变更提供某种向后兼容性。它们通常在废弃旧功能之前,支持一个或两个版本的旧和新功能。即使一个工具支持向后兼容性,也要确保尽快将代码移植和重构到新功能。

你的示例将工具从版本 1.7 升级到 3.0。幸运的是,版本 3.0 确实支持与 1.7 的向后兼容性。它提供了新功能,但没有破坏性更改会影响你的 IaC。当然,你采取了谨慎的方法进行升级。

你从同意在升级期间提供帮助的饲料团队成员开始。饲料团队部署所有更改,并确保不要向 IaC 添加新的更改。然后,你检查配置,以找到升级工具而不中断社交媒体流量的最佳方式。

在图 13.7 中,你将第十章中提到的重构技术应用于工具的升级。你从高级资源开始,因为其他资源不依赖于它们,部署更改,测试系统,然后升级底层资源。

你通知饲料团队,你将首先升级 DNS 和负载均衡器基础设施资源。其他资源不依赖于它们。首先更新它们允许你测试你的升级模式是否有效。你可以通过更改版本并运行 IaC 来升级资源的工具版本。你检查交付管道中的干运行和测试,以确保你没有中断任何内容。

图片

图 13.7 将重构技术应用于从高级到低级资源的向后兼容性工具版本升级。

高级资源如 DNS 和负载均衡器更新时没有任何中断。接下来,你转向更底层的资源。你开始使用第十章中提到的滚动更新模式更新服务器。你不会同时升级所有服务器,而是从一台开始升级,很快遇到了问题。

生产服务器配置有一个覆盖脚本,在升级工具时会中断。幸运的是,由于你使用了滚动更新,你只影响了一台服务器。毕竟,你希望保持社交媒体流量的运行和可用性。

在图 13.8 中,你应用第十一章中提到的回滚技术来修复不再工作的服务器。你实施手动修复,并调试旧服务器以解决问题。一旦你在测试环境中修复了问题,你就将更改推送到覆盖脚本,并继续进行服务器的滚动更新。

图片

图 13.8 你可以使用滚动更新模式来最小化失败的工具升级的影响范围,并在更改失败时回滚。

服务器通过了测试。你转向最低级别的资源,即网络。以防万一,你使用第九章中提到的蓝绿部署模式部署了网络的第二个版本。将所有内容部署到新网络后,你运行了所有端到端测试,并检查系统是否仍然正常工作。你完成了工具升级!

为什么要回顾重构、蓝绿部署和滚动升级等模式?你希望最小化潜在失败的影响范围。许多这些模式看似重复,但它们提供了结构化、风险较低的升级系统的方法。改变你的工具确实会改变你的基础设施,因此你可以应用非常相似的技术并获得相同的结果。

通常,使用滚动升级进行 IaC 升级到服务器或其他计算资源。蓝绿部署有助于高风险、低级基础设施资源的工具升级。你通常可以在原地更新高级资源。

然而,你无法预防所有的失败。在这种情况下,如果系统崩溃,可以使用第十一章中的回滚实践和模式。高级资源可以回滚配置,而低级资源则通过创建具有先前更改的新资源来受益。

13.2.3 升级中的破坏性变更

有时,你会发现你的基础设施工具或插件发布了包含破坏性变更的新版本。带有破坏性变更的新版本通常发生在工具的早期版本中,例如当工具处理新的或边缘用例时。如果你需要执行带有破坏性变更或功能的工具升级,请使用第九章中描述的更改技术。

对于社交媒体公司,你将饲料团队的工具从版本 1.7 升级到 3.0。然而,从版本 3.0 升级到 4.0 涉及一些破坏性变更!版本 4.0 包含后端架构更改,可能会影响你的资源。你如何在不影响系统的情况下将你的基础设施更新到版本 4.0?

记住,在第四章中,我提到了工具状态的存在。工具保留基础设施状态的副本,以检测实际资源状态与配置之间的偏差,并跟踪它管理的资源。工具状态与实际基础设施状态不同。当你更新工具时,你希望将旧工具状态与新的工具状态分开。

在我们的例子中,你希望将版本 3.0 的旧工具状态与版本 4.0 的新工具状态隔离开。通过隔离工具状态,可以最小化潜在失败的影响范围,因为这样可以隔离工具需要更改的基础设施资源。资源越少,恢复越快,并且可能对系统其他部分的影响越小。

在图 13.9 中,社交媒体公司的每个团队都将其工具状态分离到不同的位置。分离的工具状态确保对饲料团队蓝色基础设施的更改不会影响绿色基础设施。

图 13.9

图 13.9 工具状态捕获了工具的基础设施状态,以便进行比较,并且可以存在于工具使用的不同位置。

首先,你在整个饲料团队的 IaC 中固定工具和模块版本。接下来,你更新基础设施模块到工具版本 4.0。你发布模块的新版本,并记录破坏性变更。

接下来,您将现有配置复制到一个新文件夹中。每个新文件夹创建一个新的工具状态。找到网络文件夹,并使用工具版本 4.0 创建一个新的网络。现在,您应该有来自工具版本 3.0 的原始“蓝色”资源,以及来自工具版本 4.0 的新“绿色”资源。

您将蓝绿部署策略应用于工具状态,以创建带有新工具的新资源!使用新版本创建一组新资源确保任何破坏性更改都不会影响现有基础设施。

定义工具状态蓝绿部署是一种模式,它创建了一个带有新工具版本的新基础设施资源子集。您逐渐将流量从旧资源集(蓝色)转移到新资源集(绿色)。该模式将破坏性更改隔离到新资源集以进行测试。

在创建低级资源之后,将高级资源复制到一个新文件夹中。更新其依赖项以使用来自工具版本 4.0 的低级资源。毕竟,你希望所有使用新工具版本的资源。

图 13.10 总结了该策略。创建高级资源,运行您的测试,并将流量发送到新资源。

图片

图 13.10 在出现破坏性更改时,考虑在另一种状态中创建一个新的资源,并将流量切换到使用工具最新版本的资源。

这种方法与第九章中提到的蓝绿部署不同。您创建了一组全新的资源,以及一个全新的工具状态。如果您松散耦合了依赖项,资源子集可以有不同的工具版本,而不会影响系统的功能。

回想一下,您可以使用不同的存储库结构(第五章)和分支模型(第七章)进行 IaC 的工作。根据您的存储库结构和分支模型,您可以以不同的方式隔离工具状态。您始终可以合并一个新的分支并修改其管道以部署基础设施,或者将单独的文件夹复制到存储库的主配置中。

假设您的组织对交付管道有特定的方法,并且只允许在主分支上进行生产部署。在这种情况下,您可以创建一个新的存储库用于工具升级,并将旧的存储库存档。

如果您彻底测试了更改并进行了干运行,则可以应用就地升级。如果失败,您可以在新的工具状态中创建一个全新的资源,并使用升级后的工具。如有疑问,请参考工具的升级文档。作为一个一般做法,如果工具提供某种类型的迁移工具或脚本以简化更新,我将尝试就地升级。

13.3 替换工具

我试图在提供具体示例以展示模式和做法的同时,使本书尽可能不依赖于任何工具!在运行 IaC 一段时间后,您不可避免地会为了改进功能或供应商支持而更改工具。在迁移到新工具时,我们应该使用哪些模式?

本书中的许多模式和做法应该有助于保护您的系统免受这些变化的影响。使用第三章中的模式对您的基础设施进行范围和模块化,以及使用第四章中的模式解耦您的依赖关系,允许您的团队使用适合其用例的工具,并在需要时替换它们。如果您没有这些模式,您在迁移到新工具时可能会遇到一些困难。

想象一下您完成了社交媒体公司的 IaC 工具升级。当网络团队成员请求您的帮助时,您考虑休息一下。他们希望从供应商的 DSL 迁移到开源 DSL。他们的配置需要社交媒体推送团队进行额外的审查,该团队对供应商一无所知。

您的研究无法找到供应商或开源脚本以简化直接的迁移。您希望有一种可以将供应商的 DSL 翻译为开源 DSL 的工具。没有它,您和网络团队需要谨慎进行迁移。

13.3.1 新工具支持导入

您找不到“翻译”工具之间的自动化工具。然而,您可以将本书中的模式应用于迁移到不同的工具。一些工具支持导入功能,可以将新资源添加到工具的状态中。您可以使用第二章中的实践将现有资源迁移到新工具。

在图 13.11 中,您将模块升级以使用新的开源 DSL。您还更新了其测试,并且测试通过。首先,确定一些可以更改的低级资源。您创建一个单独的文件夹、分支或仓库,以将新的 DSL 与供应商的 DSL 隔离。在为新 DSL 编写配置后,您将现有资源导入到新 DSL 的状态中。

再次重写测试以测试开源 DSL 的新语法。它们通过,然后您继续编写配置和导入高级资源。最后,您删除 IaC。

在使用开源 DSL 编写每个新的 IaC 周期中,您想要检查您的干运行并重写您的测试。干运行显示新工具的默认设置是否与您的现有状态不匹配。如果不匹配,您需要更新新的 IaC 并修复漂移。

图 13.11 支持导入资源的工具允许您在不更改现有资源的情况下迁移到新工具。

13.3.2 没有导入功能

一些工具不支持导入功能,你需要为新工具创建新的资源。想象一下,如果网络团队要求你帮助他们从一家供应商的 DSL 转换到另一家。然而,新的供应商 DSL 不允许将现有资源导入其状态。

如果你的新工具不支持导入现有资源,你需要使用蓝绿部署策略重新创建工具状态下的资源。在图 13.12 中,你首先编写新的基础设施即代码(IaC)用于低级资源,并对新工具进行测试重构。随着你重复此过程并完成高级资源的迁移,你将切换流量并测试整个系统。

图片

图 13.12 一个无法导入资源的全新工具需要使用蓝绿部署策略进行工具迁移。

工具迁移的模式保持一致,无论是否有导入能力。然而,没有导入能力的迁移需要更多努力,因为你需要重新创建系统。即使你有一个可以将从一个工具迁移到另一个工具的神奇脚本,你也可能考虑应用一些这些模式和做法,以避免破坏关键基础设施资源,如网络。

你总是会替换或添加工具到你的基础设施生态系统中。你的组织将选择适合其架构目标的工具。应用模块化、隔离和管理 IaC 的技术将适应你的 IaC 发展。我总是回到这些实践和模式,以使基础设施、IaC、模块、工具和组织的变化,并减轻它们对关键系统的风险。

无论工具是否具有导入功能,你都需要为每个重构的资源重写测试。图 13.13 显示,在迁移过程中,你必须对每个模块和资源子集的重构单元测试和契约测试进行重构。然而,你的端到端和集成测试可能保持不变。

图片

图 13.13 当你升级工具时,你需要重写单元测试和契约测试,而集成、端到端和手动测试可能保持基本不变。

新工具将影响单元测试和契约测试,因为该工具使用不同的状态和元数据格式。测试无法从新工具中解析出正确信息。集成和端到端测试可能保持不变,因为它们评估的是基础设施的功能,而不是工具本身。

在重构测试的同时进行重构,允许你添加更多测试,删除冗余测试,或对更广泛的安全或策略测试进行更新。由于你有不同的资源,你应该更新你的手册、集成和端到端测试,以包含新的输入参数。然而,测试本身的变化不大,因为它们测试的是系统的功能,而不是基础设施属性。

13.4 事件驱动 IaC

本书的大部分内容涵盖了协作编写 IaC 以减少关键系统潜在故障影响的实践。一旦你熟悉了原则和实践,你就可以将它们扩展到更多动态用例。

例如,一个开发团队希望在其 IaC 中实现一些非常动态的自动化。每当开发团队成员部署应用程序的新实例时,他们需要更新防火墙规则以允许从实例访问数据库。而不是推送新实例然后记住稍后更新防火墙规则,他们希望有一些自动化在应用程序实例启动后运行基础设施模块来配置防火墙规则。

图 13.14 展示了你实现的自动化。你部署了一个带有新 IP 地址的新应用程序。一个自动脚本来捕获新的 IP 地址并运行一些 IaC。配置更新了防火墙以包含新的 IP 地址。每次你部署带有新 IP 地址的新应用程序时,这种自动化都会重复。

图 13.14

图 13.14 每当应用程序实例获得新的 IP 地址时,基础设施模块都会使用新的 IP 地址更新防火墙规则。

当系统发生变化时,你可能考虑自动运行基础设施模块。事件驱动的 IaC意味着运行最小范围的基础设施模块以响应事件来配置基础设施。你可以使用自动化来更新其他资源或根据事件修复系统。

定义 事件驱动的 IaC 运行最小范围的基础设施模块以响应事件来配置基础设施。

更新应用程序等同于一个事件。某些脚本、应用程序或自动化检测到事件,并通过运行基础设施模块来响应!你可以编写自己的或找到开源工具来识别和响应事件。你可以使用的某些实际自动化工具包括 Kubernetes 的操作员、无服务器函数或从事件队列中消费的应用程序。

这不是 GitOps 吗?

在第七章中,我提到本书中的一些实践和模式倾向于 GitOps。GitOps 结合了声明性配置、漂移检测、版本控制和持续部署。这种方法实现了事件驱动的 IaC。我认为 GitOps 是事件驱动的 IaC 的一个子集,因为它的自动化响应配置漂移的事件。

如果 GitOps 框架检测到漂移,它会运行自动化操作来协调配置。例如,容器编排器 Kubernetes 使用控制器来自动协调声明性配置与资源状态。然而,事件驱动的 IaC 描述的是由更广泛的事件集自动化的 IaC,而不仅仅是漂移。

随着动态服务和应用程序的普及,事件驱动的 IaC 用例变得更加普遍。如果你确实使用事件驱动的 IaC,请记住以下几点:

  • 避免创建或配置时间较长的基础设施资源。你不想包含一个需要一小时来创建的基础设施资源。

  • 不要向事件驱动模块添加太多资源。否则,你将花费很长时间来创建许多实例。

  • 平衡运行更改所需的时间与模块之间的时间间隔。

  • 将第二章中的模块实践与第六章中的测试模式结合起来,以验证事件驱动 IaC 运行快速且正确。

一些事件发生频率很高。你需要比事件频率部署更快的基础设施,或者一个自动化脚本来在特定间隔批量更改。你应该选择与事件驱动 IaC 一起部署的最小基础设施资源子集。

从基于代码提交部署的静态基础设施即代码(IaC)到为事件运行动态 IaC,你应用相同的模式、实践和原则来管理和协作。你的目标、团队的需求以及你组织的业务将随着时间的推移而演变和改变——希望你的 IaC 实践也能随之增长。请记住你的测试策略、基础设施成本以及维护其安全和合规性。

摘要

  • 在你的组织中采用开源工具或模块之前,请审查其功能、安全性和生命周期。

  • 开源工具或模块应具有默认值或行为,这些值或行为在更改中提供可预测性和稳定性。否则,你将不得不编写一层代码,在你的组织中添加具有偏见的默认属性。

  • 通过扫描基础设施工具或模块以检查安全性、检查第三方数据收集以及运行你的安全和合规性测试来保护你的基础设施免受供应链攻击。

  • 维护者类型和数量以及与工具或模块关联的许可证也会影响你组织的使用。

  • 开源工具和模块可以有两个类别的许可证:宽松和 copyleft。

  • 宽松许可证允许你修改和更新模块或工具,只要你包括许可证副本及其原始版权声明。

  • copyleft 许可证允许你修改和更新模块或工具,只要你包括许可证副本、其原始版权声明并开源你的副本。

  • 在升级你的 IaC 工具之前,解耦基础设施依赖项并固定模块、插件和工具版本。

  • 使用向后兼容性开始工具更新,通过可变地重构高级资源并继续到低级资源。

  • 对于工具状态,采用蓝绿部署策略意味着创建一组具有与现有配置分离的状态的新基础设施资源。

  • 工具状态是指 IaC 工具用来检测漂移或其管理下的资源的基础设施状态副本。

  • 通过对工具状态应用蓝绿部署策略,从底层资源到高层资源,开始一个带有破坏性变更的工具更新。

  • 当用一个新的工具替换另一个工具时,使用新工具的导入功能将现有资源迁移到新工具。从底层资源开始,并将它们应用到高层资源。然后,移除旧工具的配置。

  • 如果新工具没有现有基础设施资源的导入功能,您将需要为工具状态应用蓝绿部署策略。

  • 事件驱动的基础设施即代码(IaC)在响应系统事件时运行一个最小的 IaC 模块,以自动化基础设施变更。

  • 确保事件驱动的 IaC 模块以尽可能少的资源快速部署。

附录 A. 运行示例

本书中的示例使用 Python 创建一个 JSON 配置文件,以便您可以使用 HashiCorp Terraform 运行。本附录提供了运行示例的指导。为什么要费心进行这个两步手动过程:首先使用 Python 生成 JSON 文件,然后使用 Terraform 创建资源?

首先,我想确保任何想要运行示例但无法使用 Google Cloud Platform (GCP) 的人都有机会进一步检查它们。这允许进行“本地”开发和测试,并可选择创建实时基础设施资源。

第二,JSON 文件内容很多!它们在代码列表中显得相当冗长。一个 Python 包装器允许我提供模式示例,而无需浏览 JSON 配置的行。在 Terraform JSON 语法周围添加 Python 代码提供了一些未来保障,以防我需要将示例重写为另一个工具。

注意:参考链接、库和工具语法可能会更改。请查阅 github.com/joatmon08/manning-book 以获取最新的代码。

图 A.1 使用用 Python 编写的代码列表生成 JSON 文件,并用 Terraform 运行它。

图 A.1 重复说明了运行示例所需的流程。如果您运行 python main.py,您将得到一个扩展名为 .tf.json 的 JSON 文件。在您的 CLI 中运行 terraform init 以初始化工具状态,然后运行 terraform apply 以配置资源。

我将简要讨论如何设置各种云提供商的账户。然后,我将介绍 Python 以及我在示例中引用的库,例如基础设施 API 访问和测试。最后,我将简要解释如何使用 Terraform 与 GCP 一起使用。

A.1 云提供商

本书中的示例使用 Google Cloud Platform (GCP) 作为云提供商。如果您更喜欢其他云提供商,许多示例都有侧边栏,提供了等效的实现来达到类似的架构。表 A.1 将 GCP、Amazon Web Services (AWS) 和 Microsoft Azure 中每种资源类型的近似值进行了映射。

表 A.1 云提供商间资源映射

资源 GCP AWS Azure
资源分组 GCP 项目 AWS 账户 Azure 订阅和资源组
身份和访问管理 (IAM) Google IAM AWS IAM Azure Active Directory
Linux 服务器(Ubuntu) Google 计算实例 Amazon EC2 实例 Azure Linux 虚拟机
网络 Google 虚拟私有云 (VPC) 子网 注意:具有默认网络 Amazon 虚拟私有云 (VPC) 子网 路由表 网关 注意:具有默认网络 Azure 虚拟网络 子网 路由表关联
防火墙规则 防火墙规则 安全组网络访问控制列表 网络安全组
负载均衡 Google compute forwarding rule (L4)HTTP(S) 负载均衡 (L7) AWS Elastic Load Balancing (ELB) (L4)AWS 应用程序负载均衡器 (ALB) (L7) Azure 负载均衡器 (L4)Azure 应用程序网关 (L7)
关系型数据库 (PostgreSQL) Google Cloud SQL Amazon Relational Database Service (RDS) Azure Database for PostgreSQL
容器编排器 (Kubernetes) Google Kubernetes Engine (GKE) Amazon Elastic Kubernetes Service (EKS) Azure Kubernetes Service (AKS)

在本节中,我将概述如果您选择,为每个云服务提供商需要进行的初始设置。

A.1.1 Google Cloud Platform

当您开始使用 GCP 时,创建一个新项目 (mng.bz/mOV2) 并在该项目中运行所有示例。这允许您在完成本书后删除项目及其资源。

接下来,安装 gcloud CLI (cloud.google.com/sdk/docs/install)。CLI 将帮助您进行验证,以便 Terraform 可以访问 GCP API:

$ gcloud auth application-default login

这将在您的机器上设置凭据,以便 Terraform 可以验证 GCP (mng.bz/5Qw1)。

A.1.2 Amazon Web Services

当您开始使用 AWS 时,创建一个新账户 (mng.bz/6XDD) 并在该账户中运行所有示例。这允许您在完成本书后删除账户及其资源。

接下来,在 AWS 控制台中创建一组访问密钥 (mng.bz/o21r)。您需要保存这些密钥,以便 Terraform 可以访问 AWS API。

复制访问密钥 ID 和秘密访问密钥并将其保存到环境变量中:

$ export AWS_ACCESS_KEY_ID="<Access key ID>" 
$ export AWS_SECRET_ACCESS_KEY="<Secret access key>" 

然后,设置您想要使用的 AWS 区域:

$ export AWS_REGION="us-east-1" 

这将在您的机器上设置凭据,以便 Terraform 可以验证 AWS (mng.bz/nNWg)。

A.1.3 Microsoft Azure

当您开始使用 Azure 时,创建一个新账户 (mng.bz/v6nJ)。创建新账户会默认为您提供一个订阅。这允许您在订阅内创建资源并将它们按资源组分组。完成本书后,您可以删除资源组。

接下来,安装 Azure CLI (mng.bz/44Da)。CLI 将帮助您进行验证,以便 Terraform 可以访问 Azure API。

登录到 Azure CLI:

$ az login

列出订阅,以便您可以获取默认订阅的 ID:

$ az account list

复制订阅 ID 并将其保存到环境变量中:

$ export ARM_SUBSCRIPTION_ID="<subscription ID>" 

这将在您的机器上设置凭据,以便 Terraform 可以验证 Azure (mng.bz/QvPw)。您应该为每个示例创建一个 Azure 资源组 (mng.bz/XZNG)。删除资源组以移除示例的所有基础设施资源。

A.2 Python

在您开始运行示例之前,您必须下载 Python。我在代码列表中使用 Python 3。您可以通过选择您的包管理器或 Python 下载页面(www.python.org/downloads/)来安装 Python。然而,我更喜欢使用 pyenv (github.com/pyenv/pyenv) 来下载和管理我的 Python 版本。pyenv 允许您选择所需的 Python 版本,并使用 Python 的 venv 库(docs.python.org/3/library/venv.xhtml)将其安装到虚拟环境中。

我使用虚拟环境,因为我有多个项目需要不同的 Python 版本。在同一个环境中安装每个项目的不同版本会让人困惑,并且经常导致代码损坏。因此,我想将每个项目与其依赖项和 Python 版本分开,分别放入一个开发环境中。

A.2.1 安装 Python 库

在您将 Python 3 安装到您的开发或虚拟环境之后,您需要安装一些外部库。在列表 A.1 中,我捕获了 requirements.txt 文件中的库和依赖项,这是一个包含包和版本的纯文本文件。

列表 A.1 包含本书库的 Python requirements.txt

apache-libcloud==3.3.1               ❶
google-api-python-client==2.17.0     ❷
google-cloud-billing==1.3.3          ❷
netaddr==0.8.0                       ❸
pytest==6.2.4                        ❹

❶ 安装 Apache Libcloud 库

❷ 安装 GCP 的客户端库,包括 Python 客户端和 Cloud Billing 客户端

❸ 安装 netaddr,这是一个用于解析网络信息的 Python 库

❹ 安装 pytest,这是一个 Python 测试框架

示例仓库包含一个 requirements.txt 文件,该文件冻结了您需要安装的库版本。在您的 Python 开发环境中,使用您的 CLI 通过 pip 安装库,这是 Python 的包安装程序:

$ pip install -r requirements.txt

一些示例需要更复杂的自动化或测试。它们引用了您需要单独导入的库。让我们更详细地检查需要下载的库。

Apache Libcloud

Apache Libcloud (libcloud.apache.org/) 提供了一个 Python 接口来创建、更新、读取和删除云资源。它包含一个与云服务或提供商无关的单个接口。我在本书的早期部分引用了这个库,以提供集成和端到端测试的示例。要在以下列表中使用 Apache Libcloud,您可以导入 libcloud 包并设置一个驱动程序以连接到 GCP。

列表 A.2 导入 Apache Libcloud

from libcloud.compute.types import Provider            ❶
from libcloud.compute.providers import get_driver      ❷

ComputeEngine = get_driver(Provider.GCE)               ❸
driver = ComputeEngine(                                ❹
    credentials.GOOGLE_SERVICE_ACCOUNT,                ❹
    credentials.GOOGLE_SERVICE_ACCOUNT_FILE,           ❹
    project=credentials.GOOGLE_PROJECT,                ❹
    datacenter=credentials.GOOGLE_REGION)              ❹

❶ 导入设置云提供商的对象,例如 GCP

❷ 导入初始化云提供商驱动程序的函数

❸ 设置连接到 Google Cloud 的驱动程序

❹ 将凭证传递给 Google Cloud API 以初始化驱动程序

在测试中,我使用 Apache Libcloud 而不是 Google Cloud 的客户端库,因为它提供了一个统一的 API 来访问任何云。如果我想将示例切换到 AWS 或 Azure,我只需要更改云提供商的驱动程序。测试只从云提供商读取信息,并不使用 Apache Libcloud 执行任何复杂操作。

AWS 和 Azure 等效

您需要更新 Apache Libcloud 驱动程序以使用 Amazon EC2 驱动程序 (mng.bz/yvQG) 或 Azure ARM Compute 驱动程序 (mng.bz/M5B7)。

Google Cloud 的 Python 客户端

书的后面部分包括更复杂的 IaC 和测试,这些我无法在 Apache Libcloud 中实现。Apache Libcloud 无法支持我获取 Google Cloud 资源特定信息的用例,例如定价信息!下面的列表展示了我是如何在这些用例中使用特定于 Google Cloud 的客户端库的。

列表 A.3 导入 Google Cloud 客户端库

import googleapiclient.discovery          ❶
from google.cloud import billing_v1       ❷

❶ 导入 Python 的 Google Cloud 客户端库

❷ 导入 Google Cloud Billing API 的 Python 客户端

AWS 和 Azure 等效

您可以导入 AWS SDK for Python (aws.amazon.com/sdk-for-python/) 或 Azure Python 库 (mng.bz/VMV0) 来构建 AWS 或 Azure 中的示例。

示例使用了两个由 Google Cloud 维护的库。Python 的 Google Cloud 客户端库 (mng.bz/aJ1z) 允许您访问 Google Cloud 上的许多 API 并创建、读取、更新和删除资源。然而,它不包含对 Google Cloud Billing API 的访问。

因此,对于第十二章关于成本的内容,我不得不导入由 Google Cloud 维护的另一个库来检索计费目录信息。Google Cloud Billing API 的 Python 客户端 (mng.bz/gwBl) 允许我从 Google Cloud 服务目录中读取信息。

当您有需要引用特定资源或 API(如 Apache Libcloud 中的统一 API 所不具备的)的 IaC 时,您通常需要找到一个单独的库来检索所需的信息。虽然我们希望最小化依赖,但我们必须认识到并非每个库都能满足每个用例!如果您觉得现有的库无法完成所需的自动化,请选择不同的库。

netaddr

在第五章中,我需要修改一个 IP 地址块。虽然我考虑了通过数学计算正确地址的可能性,但我决定使用库。Python 确实有一个内置的 ipaddress 库,但它不包括我需要的功能。我安装了 netaddr (netaddr.readthedocs.io/en/latest/) 来减少我需要计算 IP 地址的额外代码。

pytest

本书中的许多测试使用 pytest,一个 Python 测试框架。您也可以使用 Python 的 unittest 模块来编写和运行测试。我更喜欢 pytest,因为它提供了一个简单的接口来编写和运行测试,而不需要更复杂的测试功能。而不是深入解释 pytest,我将概述我在测试中使用的一些功能和如何运行它们。

Pytest 搜索以 test_ 为前缀的 Python 文件。此文件名表示该文件包含 Python 测试。每个测试函数也使用前缀 test_。Pytest 根据前缀选择并运行测试。

本书中的许多测试都包括测试夹具。一个 测试夹具 捕获一个已知对象,例如一个名称或常量,您可以在多个测试中进行比较。在下面的列表中,我使用夹具传递多个测试中常用的对象,如网络属性。

列表 A.4 使用 pytest 的示例

import pytest                                                              ❶

@pytest.fixture                                                            ❷
def network():                                                             ❸
   return 'my-network'                                                     ❸

def test_configuration_for_network_name(network):                          ❸
   assert network == 'my-network', 'Network name does not match expected'  ❹

❶ 导入 pytest 库

❷ 设置已知对象或测试夹具

❸ 返回已知的网络名称“my-network”并将其传递给您的第一个测试

❹ 断言网络名称与预期匹配,如果不匹配则测试失败。您还可以包含一个描述性错误消息。

测试最重要的部分是检查预期值与实际值是否匹配,或 断言。Pytest 建议每个测试使用一个 assert 语句。我遵循这个约定,因为它有助于我编写更描述性、更有帮助的测试。您的测试应该尽可能清晰地描述其意图和测试内容。

要使用 pytest 运行一系列测试,您可以传递包含测试的目录。但是,请确保您的测试目录包含通过 pytest 读取的任何文件的绝对路径!例如,第四章的测试读取外部 JSON 文件。因此,您需要将工作目录更改为章节和部分:

$ cd ch05/s02

您可以通过在命令行中将点(.)传递给 pytest 来运行目录中的所有测试:

$ pytest .

您可以通过在命令行中添加文件名来运行一个文件:

$ pytest test_network.py

本书中的许多测试使用类似的夹具和 assert 语句模式。有关其他 pytest 功能的更多信息,请参阅其文档(docs.pytest.org)。您将在 CLI 中运行 pytestpython main.py 命令来运行示例。

A.2.2 运行 Python

我将每个基础设施资源分离到一个 Python 文件中。每个目录都包含一个 main.py 文件,如列表 A.5 所示。该文件始终包含将 Python 字典写入 JSON 文件的代码。该对象需要使用 Terraform 的 JSON 配置语法来表示基础设施资源。

列表 A.5 示例 main.py 文件将字典写入 JSON 文件

import json

if __name__ == "__main__":
   server = ServerFactoryModule(name='hello-world')                      ❶
   with open('main.tf.json', 'w') as outfile:                            ❷
       json.dump(server.resources, outfile, sort_keys=True, indent=4)    ❸

❶ 为 GCP 服务器生成 Python 字典

❷ 创建一个名为“main.tf.json”的 JSON 文件,其中包含与 Terraform 兼容的 JSON 配置

❸ 将服务器字典写入 JSON 文件

您可以在终端中运行 Python 脚本:

$ python main.py

当您列出文件时,您将找到一个名为 main.tf.json 的新 JSON 文件:

$ ls
main.py      main.tf.json

许多示例要求您运行 Python 的 main.py 并生成一个名为 main.tf.json 的 JSON 文件,除非另有说明。然而,一些示例使用其他库或代码进行自动化或测试。

A.3 HashiCorp Terraform

使用 python main.py 生成 main.tf.json 文件后,您需要在 GCP 中创建资源。.tf.json 文件需要使用 HashiCorp Terraform 来创建、读取、更新和删除 GCP 中的资源。

您可以使用您选择的软件包管理器下载并安装 Terraform(www.terraform.io/downloads.xhtml)。您通过一组 CLI 命令运行它,因此您需要下载二进制文件并确保您可以在终端中运行它。Terraform 在工作目录中搜索具有 .tf 或 .tf.json 扩展名的文件,并创建、读取、更新和删除您在这些文件中定义的资源。

A.3.1 JSON 配置语法

Terraform 提供了各种接口供您创建基础设施资源。其大部分文档使用 HashiCorp 配置语言(HCL),这是一种 DSL,用于为每个云提供商定义基础设施资源。有关 Terraform 的更多信息,请参阅其文档(www.terraform.io/docs/index.xhtml)。

本书中的示例不使用 HCL。相反,它们使用 Terraform 特定的 JSON 配置语法(www.terraform.io/docs/language/syntax/json.xhtml)。这种语法使用与 HCL 相同的 DSL,只是格式化为 JSON。

Python 中的每个 main.py 文件都会将一个字典写入 JSON 文件。列表 A.6 展示了如何创建一个字典,该字典使用 JSON 配置语法定义 Terraform 资源。JSON 资源引用了 Terraform 定义的 google_compute_instance 资源(mng.bz/e71z)并设置了所有必需的属性。

列表 A.6 Terraform JSON 中服务器的 Python 字典

terraform_json = {
    'resource': [{                                  ❶
        'google_compute_instance': [{               ❷
            'my_server': [{                         ❸
               'allow_stopping_for_update': True
               'boot_disk': [{
                   'initialize_params': [{
                       'image': 'ubuntu-1804-lts'
                    }]
               }],
               'machine_type': 'e2-micro',
               'name': 'my-server',
               'zone': 'us-central1-a',
            }]
        }]
    }]
}

❶ 向 Terraform 信号,您将定义资源列表

❷ 定义了一个“google_compute_instance”,这是一个将在 GCP 中创建和配置服务器的 Terraform 资源

❸ 为服务器定义一个唯一标识符,以便 Terraform 可以跟踪它

AWS 和 Azure 的等效资源

在 AWS 中,您将使用带有默认 VPC 引用的 aws_instance Terraform 资源(mng.bz/pOPG)。

在 Azure 中,您需要创建一个虚拟网络和子网。然后,在网络上创建 azurerm_linux_virtual_machine Terraform 资源(mng.bz/Ooxn)。

当您将其写入 JSON 文件时,Python 字典变为 Terraform JSON 配置语法。Terraform 只会创建其当前工作目录中定义的资源,这些资源具有 .tf 或 .tf.json 扩展名。如果您更新代码以将配置写入没有 .tf.json 扩展名的 JSON 文件,Terraform 将不会识别文件中的资源。

A.3.2 初始化状态

在运行 Python 并创建 JSON 文件后,您需要在工作目录中初始化 Terraform。图 A.2 概述了您需要在终端中运行的命令以初始化状态并应用基础设施更改。

图 A.2 在工作目录中使用 Terraform 初始化和部署资源;完成示例后销毁资源。

在您的终端中,切换到包含 *.tf.json 文件的目录。例如,我切换到包含 2.3 节示例的目录:

$ cd ch02/s03

在您的终端中初始化 Terraform:

$ terraform init
Initializing the backend...

Initializing provider plugins...
- Reusing previous version of hashicorp/google from the dependency lock file
- Using previously-installed hashicorp/google v3.86.0

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan"
➥to see any changes that are required for your infrastructure. 
➥All Terraform commands should now work.

If you ever set or change modules or backend configuration 
➥for Terraform, rerun this command to reinitialize 
➥your working directory. If you forget, other
➥commands will detect it and remind you to do so if necessary.

Terraform 执行一个初始化步骤,创建一个名为 backend 的工具状态,并安装插件和模块。初始化会创建一系列文件,您不应从文件系统中删除这些文件。初始化 Terraform 后,您在列出目录内容时将找到一些隐藏的新文件:

$ ls -al
drwxr-xr-x  .terraform
-rw-r--r--  .terraform.lock.hcl
-rw-r--r--  main.py
-rw-r--r--  main.tf.json
-rw-r--r--  terraform.tfstate
-rw-r--r--  terraform.tfstate.backup

Terraform 将其工具状态存储在状态文件中,以便快速协调您对基础设施资源所做的任何更改。Terraform 可以引用存储在本地或服务器、工件注册库、对象存储或其他位置的状态文件。示例将工具状态存储在名为 terraform.tfstate 的本地文件中。如果您意外删除此文件,Terraform 将不再识别其管理下的资源!请确保您不要删除本地状态文件或更新示例以使用远程后端。您还可能找到一个 terraform.tfstate.backup 文件,Terraform 在进行更改之前使用该文件来备份其工具状态。

初始化还安装了一个用于 Terraform 与 Google 通信的插件。Terraform 使用插件系统来扩展其引擎并与云提供商接口。AWS 示例使用相同的 terraform init 命令为您自动下载 AWS 插件。插件或模块将被下载到 .terraform 文件夹中。

Terraform 还为您固定了插件版本,类似于 Python 的 requirements.txt 文件。您将在 .terraform.lock.hcl 中找到一个固定插件版本的列表。在示例存储库中,我将 .terraform.lock.hcl 提交到版本控制,这样 Terraform 只会安装我在生成示例时测试过的插件。

A.3.3 在您的终端中设置凭证

大多数 Terraform 插件通过使用环境变量来读取基础设施提供者 API 的凭证。我通常设置 GCP 项目环境变量,这样 Terraform 就能连接到正确的 GCP 项目:

$ export CLOUDSDK_CORE_PROJECT=<your GCP project ID>

我还通过使用 gcloud CLI 工具验证 GCP。该命令会自动为 Terraform 设置凭证以访问 GCP:

$ gcloud auth login

对于其他云提供商,我建议在您的终端中设置环境变量以验证您的 AWS 或 Azure 账户。参考 A.1 节以获取它们的配置。

A.3.4 应用 Terraform

在设置好凭证后,您可以使用 Terraform 进行干运行和部署您的基础设施资源。在您的终端中,您可以通过运行 terraform apply 来开始部署您的更改:

$ terraform apply

Terraform used the selected providers to generate 
➥the following execution plan. Resource actions 
➥are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # google_compute_instance.hello-world will be created
  + resource "google_compute_instance" "hello-world" {

... OMITTED ...

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

命令将停止并等待您在 Enter a value 处输入 yes。它等待您审查更改并确认您想要添加、更改或销毁资源。在输入 yes 之前,始终审查更改!

在您输入 yes 后,Terraform 将开始部署资源:

  Enter a value: yes

google_compute_instance.hello-world: Creating...
google_compute_instance.hello-world: 
➥Still creating... [10s elapsed]
google_compute_instance.hello-world: 
➥Creation complete after 15s [id=projects/infrastructure-as-code-book/zones
➥/us-central1-a/instances/hello-world]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

使用 terraform apply 后,您将在您的 GCP 项目中找到您的资源。

A.3.5 清理

许多示例使用重叠的名称或网络 CIDR 块。我建议您在每个章节和部分之间清理资源。Terraform 使用 terraform destroy 命令从 GCP 删除 terraform.tfstate 中列出的所有资源。在您的终端中,请确保您已验证 GCP 或您的基础设施提供商。

当您运行 terraform destroy 时,它会输出它将销毁的资源。请审查资源列表,并确保您想要删除它们!

$ terraform destroy

Terraform used the selected providers to generate 
➥the following execution plan. Resource actions 
➥are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # google_compute_instance.hello-world will be destroyed

 ... OMITTED ...

Plan: 0 to add, 0 to change, 1 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, 
  ➥as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: 

在您审查了预期要删除的资源后,在命令提示符处输入 yes。Terraform 将从 GCP 删除资源。删除将需要一些时间,因此请预计这将运行几分钟。一些示例的部署和销毁可能需要更长的时间,因为它们涉及许多资源:

  Enter a value: yes

google_compute_instance.hello-world: Destroying... elapsed]
google_compute_instance.hello-world: Still destroying...
➥[id=projects/infrastructure-as-code-book/zones
➥/us-central1-a/instances/hello-world, 2m10s elapsed]
google_compute_instance.hello-world: Destruction complete after 2m24s

Destroy complete! Resources: 1 destroyed.

在销毁资源后,如果您愿意,可以删除 terraform.tfstate、terraform .tfstate.backup 和 .terraform 文件。请记住,每次完成示例后,都要从 GCP 删除您的资源(或删除整个项目),这样您可以减少云费用!

附录 B. 练习题解答

练习题 1.1

在您的组织中选择一个基础设施脚本或配置。评估它是否遵循 IaC 原则。它是否促进可重复性、使用幂等性、帮助组合性和简化可进化性?

答案:

您可以使用以下步骤来识别您的脚本或配置是否遵循 IaC 原则:

  • 可重复性—复制并粘贴脚本或配置,与他人分享,并要求他们创建资源,而无需修改或更改配置。

  • 幂等性—运行脚本几次。它不应该改变您的基础设施。

  • 组合性—复制配置的一部分,并在其他基础设施资源之上构建它。

  • 可进化性—向配置中添加一个新的基础设施资源,并验证您是否可以在不影响其他资源的情况下进行更改。

练习题 2.1

以下配置基础设施是使用命令式还是声明式风格?

if __name__ == "__main__":
   update_packages()
   read_ssh_keys()
   update_users()
   if enable_secure_configuration:
       update_ip_tables()

答案:

代码片段使用配置基础设施的命令式风格。它定义了如何以特定顺序逐步配置服务器,而不是声明特定的目标配置。

练习题 2.2

以下哪些更改受益于不可变性的原则?(选择所有适用的选项。)

A) 减少网络以拥有更少的 IP 地址

B) 向关系数据库添加列

C) 向现有的 DNS 条目添加新的 IP 地址

D) 将服务器的软件包更新到向后不兼容的版本

E) 将基础设施资源迁移到另一个区域

答案:

正确答案是 A、D 和 E。每个更改都受益于一组实现更改的新资源。如果您尝试就地更改,可能会意外地关闭现有系统。例如,减少网络以拥有更少的 IP 地址可能会替换使用该网络的任何资源。

将软件包更新到向后不兼容的版本意味着一个损坏的包更新可能会影响服务器处理用户请求的能力。将基础设施资源迁移到另一个区域需要时间。并非所有云服务提供商的区域都支持每种类型的基础设施资源。创建新的资源有助于减轻迁移问题。您可以对答案 B 和 C 进行可变更改,可能不会影响系统。

练习题 3.1

以下 IaC 应用于哪些模块模式?(选择所有适用的选项。)

if __name__ == "__main__":
  environment = 'development'
  name = f'{environment}-hello-world'
  cidr_block = '10.0.0.0/16'

  # NetworkModule returns a subnet and network
  network = NetworkModule(name, cidr_block)

  # Tags returns a default list of tags
  tags = TagsModule()

  # ServerModule returns a single server
  server = ServerModule(name, network, tags)

A) 工厂

B) 单例

C) 原型

D) 构造器

E) 组合

答案:

基础设施即代码适用于 A、C 和 E。网络模块使用组合模式来组合子网和网络。标签模块使用原型模式来返回状态元数据。服务器模块使用工厂模式根据名称、网络和标签返回单个服务器。

代码不使用单例或构造器模式。它不创建单一的全局资源或内部逻辑来构建特定资源。

练习题 4.1

我们如何通过以下 IaC 更好地解耦数据库对网络的依赖?

class Database:
  def __init__(self, name):
    spec = {
      'name': name,
      'settings': {
        'ip_configuration': {
          'private_network': 'default'
        }
      }
    }

A) 该方法充分解耦了数据库和网络。

B) 将网络 ID 作为变量传递,而不是将其硬编码为default

C) 为所有网络属性实现并传递一个NetworkOutput对象到数据库模块。

D) 向网络模块添加一个函数,将其网络 ID 推送到数据库模块。

E) 向数据库模块添加一个函数来调用default网络 ID 的基础设施 API。

答案:

你可以实现适配器模式来输出网络属性(C)。数据库选择它使用的网络属性,例如网络 ID 或 CIDR 块。这种方法最好遵循依赖注入的原则。虽然 D 实现了依赖反转,但它没有实现控制反转。答案 E 实现了依赖注入,但继续硬编码网络 ID。

练习 6.1

你注意到负载均衡器模块的新版本破坏了你的 DNS 配置。一个队友更新了模块,使其输出私有 IP 地址而不是公共 IP 地址。你能做些什么来帮助你的团队更好地记住模块需要公共 IP 地址?

A) 为私有 IP 地址创建一个单独的负载均衡器模块。

B) 添加模块合同测试以验证模块输出私有和公共 IP 地址。

C) 在模块的文档中添加一个备注,说明它需要公共 IP 地址。

D) 在模块上运行集成测试并检查 IP 地址是否公开可访问。

答案:

而不是创建一个新的模块,你可以添加一个合同测试来帮助团队记住高级资源需要私有和公共 IP 地址(B)。你可以创建一个单独的负载均衡器模块(A)。然而,这可能不会帮助你的团队记住特定模块必须输出特定的变量。更新模块的文档(C)意味着你的团队必须记得首先阅读文档。当合同测试足够解决问题时,集成测试运行的时间和财务成本(D)可能不会帮助。

练习 6.2

你添加了一些防火墙规则以允许应用程序访问一个新的队列。对于这个变更,以下哪种测试组合对你的团队最有价值?

A) 单元和集成测试

B) 合同和端到端测试

C) 合同和集成测试

D) 单元和端到端测试

答案:

两个最有价值的测试是单元和端到端测试(D)。单元测试将帮助确保没有人会删除新的规则。端到端测试检查应用程序是否可以成功访问队列。合同测试不会提供任何帮助,因为你不需要测试防火墙规则输入和输出。

练习 7.1

选择您组织中的一个标准基础设施更改。为了自信地持续将更改交付到生产,您需要什么?关于持续部署呢?概述或绘制您的交付管道中的阶段。

答案:

在进行此练习时,请考虑以下问题:

  • 您是否有单元测试、集成测试或端到端测试?

  • 您使用哪种分支模型?

  • 您的公司是否有合规性要求?例如,在生产前必须两人批准更改。

  • 当有人需要做出更改时会发生什么?

练习 9.1

考虑以下代码:

if __name__ == "__main__":
  network.build()
  queue.build(network)
  server.build(network, queue)
  load_balancer.build(server)
  dns.build(load_balancer)

队列依赖于网络。服务器依赖于网络和队列。您将如何运行带有 SSL 升级的蓝绿部署?

答案:

您创建了一个启用 SSL 的绿色队列。然后,您创建了一个新的绿色服务器,该服务器依赖于队列。测试绿色服务器上的应用程序是否可以使用 SSL 配置访问队列。如果通过,您可以将绿色服务器添加到负载均衡器。通过使用金丝雀部署逐渐将流量发送到绿色服务器,直到您确认所有请求都成功。然后,您移除原始服务器和队列。

可选地,您可以选择省略创建绿色服务器的步骤,并将流量从原始服务器发送到 SSL。然而,这种方法可能无法运行金丝雀部署。您更新服务器的配置以直接与新的队列通信。

练习 10.1

给定以下代码,您会使用什么顺序和资源分组来重构和分解单体应用?

if __name__ == "__main__":
  zones = ['us-west1-a', 'us-west1-b', 'us-west1-c']
  project.build()
  network.build(project)
  for zone in zones:
    subnet.build(project, network, zone)
  database.build(project, network)
  for zone in zones:
    server.build(project, network, zone)
  load_balancer.build(project, network)
  dns.build()

A) DNS,负载均衡器,服务器,数据库,网络+子网,项目

B) 负载均衡器+DNS,数据库,服务器,网络+子网,项目

C) 项目,网络+子网,服务器,数据库,负载均衡器+DNS

D) 数据库,负载均衡器+DNS,服务器,网络+子网,项目

答案:

减少重构风险的顺序和分组从 DNS 开始,然后是负载均衡器、队列、数据库、服务器、网络和子网,以及项目(A)。您希望从 DNS 作为最高级依赖项开始,并独立于负载均衡器进行演变,因为负载均衡器需要项目和网络属性。以下是对下一组资源的重构:服务器、数据库、网络和项目。

您在数据库之前重构服务器,因为服务器不直接管理数据,而是依赖于数据库。如果您对重构数据库没有信心,至少您已经将服务器从单体应用中移除!您总是可以将数据库、网络和项目留在单体应用中。将大部分资源从单体应用中移除可以充分解耦系统以进行扩展。

练习 11.1

一个团队报告说,它的应用程序无法再连接到另一个应用程序。该应用程序上周工作正常,但自周一以来请求失败。团队没有对其应用程序进行任何更改,并怀疑问题可能是防火墙规则。你可以采取哪些步骤来解决这个问题?(选择所有适用的。)

A) 登录云提供商并检查应用程序的防火墙规则。

B) 将新的基础设施和应用程序部署到绿色环境进行测试。

C) 检查应用程序的 IaC 中的变化。

D) 将云提供商中的防火墙规则与 IaC 进行比较。

E) 编辑防火墙规则,允许应用程序之间的所有流量。

答案:

步骤是 A、C 和 D。你可以通过检查防火墙规则中的任何漂移来进行故障排除。如果没有发现漂移,你可以在运行应用程序的 IaC 中搜索其他差异。从故障排除的角度来看,这个问题可能不需要新的测试环境或允许应用程序之间的所有流量。

练习 12.1

给定以下代码,以下哪些陈述是正确的?(选择所有适用的。)

HOURS_IN_MONTH = 730
MONTHLY_BUDGET = 5000
DATABASE_COST_PER_HOUR = 5
NUM_DATABASES = 2
BUFFER = 0.1

def test_monthly_budget_not_exceeded():
   total = HOURS_IN_MONTH * NUM_DATABASES * DATABASE_COST_PER_HOUR
   assert total < MONTHLY_BUDGET + MONTHLY_BUDGET * BUFFER

A) 测试将通过,因为数据库的成本在预算内。

B) 测试估计数据库的月度成本。

C) 测试没有考虑不同的数据库类型。

D) 测试计算每个数据库实例的月度成本。

E) 测试包括 10%的成本超支缓冲作为软强制性政策。

答案:

答案是 B、C 和 E。数据库每月的总估计成本为 7,300 美元,而包含 10%缓冲的月度预算为 5,500 美元,这意味着测试失败。测试本身没有考虑不同的数据库类型或计算每个数据库实例的月度成本。它是基于每小时率和数据库数量进行计算的。将 10%的缓冲添加到月度预算中为预算创建了一个软强制性政策。导致轻微成本超支的小变化将减少将产品交付到生产中的摩擦,但会标记任何重大的成本变化。

练习 12.2

假设你有三个服务器。你检查它们的利用率,并注意到以下情况:

  • 你需要一个服务器来处理最小流量。

  • 你需要三个服务器来处理最大流量。

  • 服务器每天 24 小时、每周 7 天都在处理流量。

你下个月可以采取哪些措施来优化服务器的成本?

A) 安排资源在周末停止。

B) 添加autoscaling_policy以根据内存进行扩展。

C) 为所有服务器设置三小时的过期时间。

D) 将服务器更改为较小的 CPU 和内存机器类型。

E) 将应用程序迁移到容器中,并在服务器上更密集地打包应用程序。

答案:

您可以添加一个autoscaling_policy来根据内存进行扩展(B)。您不能在周末安排资源停止,因为您周末至少需要一个服务器。设置过期时间或缩减服务器规模在这种情况下并不能帮助降低成本。将应用程序迁移到容器涉及一个长期的解决方案,这不会优化下一个月的成本。

posted @ 2025-11-21 09:09  绝不原创的飞龙  阅读(21)  评论(0)    收藏  举报