翻译自 Structuring your Infrastructure as Code 。
如果您考虑迁移到另一个基础设施即代码工具(为什么要这样做呢?毕竟现在 IaC 领域一切都很好,对吧?!),当您开始时可能会发现自己问了一个根本性的问题:我该如何以一种能够良好扩展并经受时间考验的方式来构建结构?
没有固定的答案。每个人的做法都稍有不同,不同的工具也有不同的最佳实践。
作为 Pulumi 解决方案工程师,我在日常工作中经常回答这个问题。客户正在从其他 IaC 工具迁移,并且他们希望借此机会思考他们想要如何组织结构。
本文旨在详细介绍我对于所使用的概念和原则的高(相对高)层次的思考,以及其背后的原因。在探讨这些概念时,我将谈论我从配置管理和之前使用的众多 IaC 工具中所学到的一些经验教训。
本文中的许多概念都集中在 Pulumi 上,但很多也适用于其他工具。
我相信我的系统管理员背景正在展现出来,但我喜欢通过分层的概念来思考基础设施,类似于 OSI 模型。我在这里要概述的大多数层次紧密映射了 OSI 模型,但在创建 Git 仓库或编写一行代码之前,您可能想要将您的云基础设施分组为不同的层次。为什么会在稍后变得明显。
计费层是您注册或输入信用卡的地方。每个云提供商的做法都不同。
- AWS:组织
- Azure:帐户
- Google Cloud:帐户
根据我的经验,虽然有 API 来处理这些事情,但您可能不想用 IaC 管理此层,所以最好还是手动操作。
权限层是如何在云提供商中根本性地分隔访问的地方。同样,每个提供商都有不同的做法。
- AWS:帐户
- Azure:订阅
- Google Cloud:项目
您可能希望使用 IaC 管理此层,但您需要决定如何操作。就我个人而言,我发现该层次的 API 级别支持以及执行此操作的罕见性意味着在管理此层次时往往更易于手动操作。
现在我们开始涉及应该由 IaC 管理的层次。网络层是基础设施中一切工作的基础,其中包括 VPC、子网、NAT 网关、VPN 以及促进网络通信的任何其他内容。
- AWS:VPC、子网、路由表、Internet 网关、NAT 网关、VPN
- Azure:虚拟网络、子网、路由表、Internet 网关、NAT 网关、VPN
- Google Cloud:VPC、子网、路由表、Internet 网关、Cloud Nat、VPN
在我们建立了网络层之后,我们需要允许其他人或应用程序与云提供商的 API 进行通信。IAM 角色或服务主体位于此层。
- AWS:IAM 角色、IAM 用户、IAM 组
- Azure:服务主体、托管标识
- Google Cloud:服务帐户
数据层是您管理的资源真正开始展开的地方。这是您将找到诸如数据库、对象存储、消息队列等用于存储或传输数据的资源的地方。
- AWS:RDS、DynamoDB、S3、SQS、SNS、Kinesis、Redshift、DocumentDB、ElastiCache、DynamoDB
- Azure:SQL、CosmosDB、Blob 存储、队列存储、事件网格、事件中心、服务总线、Redis 缓存
- Google Cloud:Cloud SQL、Cloud Spanner、Cloud Storage、Cloud Pub/Sub、Cloud Datastore、Cloud Bigtable、Cloud Memorystore
计算层是您的应用程序实际运行的地方 - 这是您将找到诸如虚拟机、容器和无服务器函数等资源的地方。
- AWS:EC2、ECS、EKS、Fargate
- Azure:虚拟机、容器实例、AKS
- Google Cloud:Compute Engine、GKE
第 6 层是您将找到允许外部世界访问您的应用程序的资源的地方。
- AWS:应用程序负载均衡器、网络负载均衡器、经典负载均衡器、API 网关
- Azure:应用程序网关、负载均衡器、API 管理
- Google Cloud:负载均衡器、API 网关
在我们部署了所有支持的基础设施之后,我们现在需要实际部署应用程序本身。这是事情开始变得有点棘手的地方,完全取决于您的应用程序部署模型、技术和架构。
您可能选择根本不使用 IaC 来部署应用程序,但如果您这样做...
- AWS:Lambda、ECS 任务、Kubernetes 清单、EC2 用户数据
- Azure:Azure 函数、Kubernetes 清单
- Google Cloud:Cloud Functions、Kubernetes 清单
如果您像我一样是一个视觉学习者,您可能会发现这个可视化很有帮助:
层次 | 名称 | 示例资源 |
---|---|---|
0 | 计费 | AWS 组织 / Azure 帐户 / Google Cloud 帐户 |
1 | 权限 | AWS 帐户 / Azure 订阅 / Google Cloud 项目 |
2 | 网络 | AWS VPC / Google Cloud VPC / Azure 虚拟网络 |
3 | 权限 | AWS IAM / Azure 托管标识 / Google Cloud 服务帐户 |
4 | 数据 | AWS RDS / Azure Cosmos DB / Google Cloud SQL |
5 | 计算 | AWS EC2 / Azure 容器实例 / GKE |
6 | Ingress | AWS ELB / Azure 负载均衡器 / Google Cloud 负载均衡器 |
7 | 应用程序 | Kubernetes 清单 / Azure 函数 / ECS 任务 / Google Cloud 函数 |
在考虑这些层次时,难以量化的一个困难概念是这些资源在管理过程中的变化速率。通常,较低的层次比较不经常变化,而且除此之外,这些层次通常在变化时最具风险。
您可能会想知道为什么这很重要 - 这个问题的答案是,当构建您的 IaC 结构时,您将希望考虑如何在您选择的 IaC 封装机制中将资源分组在一起。例如,Pulumi 使用项目的概念来将资源分组在一起。
在创建和定义 Pulumi 项目中的资源时,您在添加资源时需要考虑的基本问题是“这个资源属于哪一层?”。通常情况下,您不应该在同一项目中具有来自不同层次的资源,因为这些资源的变化速率不同,更改它们的风险也不同。
注意:这个原则要归功于我的优秀同事 Ringo De Smet,他在审查这篇文章时提醒我要打破规则的重要性与生活中的所有原则一样,原则 1 并不总是适用于所有情况。
在上述层次的资源中,您可能会认为“啊!这是一个网络资源,所以我会把它放在我的网络项目中”,但资源的生命周期不一定适合作为共享资源。一个很好的例子是 AWS 安全组。
安全组通常与另一个资源相关 - 也许是您正在部署的应用程序、共享的负载均衡器,或者可能是数据层中的数据库。对于这些资源,通常最好考虑依赖资源的整体生命周期,然后再决定将其放在何处。
在这里,我的经验法则是:如果我想在不同的环境中提供此资源,或者更好的是,销毁它 - 我想同时销毁哪些其他资源?
这种情况的另一个重要考虑是权限层。在讨论权限时,我已经提到您需要将该层次视为共享权限,而特定于应用程序的权限则完全不同 - 它们确实需要直接与您的应用程序部署代码一起使用。
总之是:不要害怕打破第一个原则,但确保在这样做时考虑资源的生命周期。
单一仓库与多仓库的争论将在我们结束云计算并回到物理基础设施后持续进行,我不打算在这里解决它。我要说的是,我已经看到两者都能做得很好,也都能做得不好。
在处理 IaC 仓库时,我再次回到我们的分层系统,并根据层次做出不同的决策,关于在哪里放置用于部署仓库的代码。
对于基础设施的基础和共享方面,我通常喜欢将这些项目包含在单一的单一仓库中,回顾我的使用 Puppet 的日子,我们称之为控制仓库。我仍然喜欢使用这个术语。
如果我们回顾一下我们的层次,我通常会确保将第 1 层和第 2 层放在此共享仓库中。当我们到达第 3 层,即权限层时,我们需要决定资源本身是否是共享的。一个很好的例子是 IAM 角色,可能会用于人类用户而不是应用程序用户。这通常将在多个人和团队之间共享,因此是控制仓库的一个很好的选择。
第 4 层实际上取决于您的应用程序架构。如果您有跨多个应用程序的消息总线,则将其放在控制仓库中可能是有意义的,但如果您有一个仅由单个应用程序使用的数据库,出于多种原因,您可能不希望将其放在控制仓库中。
第 5 层再次取决于您的组织权限模型和云架构。共享共享计算资源,如跨多个应用程序的 ECS 集群或 Kubernetes 集群,不太常见,因此将其包含在应用程序仓库中可能没有太多意义。然而,如果您正在为每
个应用程序隔离计算,您几乎肯定会希望使其与应用程序特定。
第 6 层:很可能变得明显,您需要考虑应用程序架构和权限模型。如果您正在使用共享负载均衡器并通过该方式路由流量,您可能希望将其包含在控制仓库中,但如果您正在使用每个应用程序的负载均衡器,您将希望将其包含在应用程序仓库中。
至少,在使用 IaC 部署应用程序时,在应用程序仓库中有一个 deploy/ 目录是一个很好的起点。如果您使用 Pulumi,并且希望使用与您的应用程序相同的语言来进行部署,您可能会考虑在单个 package.json 或 requirements.txt 中放置所有依赖项,具体取决于您选择的语言。
在定义要分组在一起的项目时,您需要考虑变化的速率。您是否需要将数据库层和应用程序层资源分开?我会认为是的,因为您的应用程序层的变化速率可能会远远高于数据库层,但您需要根据您的组织和项目选择一个合适的决策。
做出使用单一仓库和将部署代码与应用程序一起使用的决策的主要原因是从所有权和编排的角度来考虑的。
在第 1 层、第 2 层甚至可能是第 5 层的基础架构中,这是一个操作顺序问题和工作流程编排问题。在大多数情况下,您将创建依赖于其他资源的资源,同时构建 IaC 图。
通过将资源分为不同的项目,您可以创建一个工作流程,允许您以正确的顺序部署资源。您将能够利用 Pulumi 的堆栈引用在堆栈和项目之间共享资源,但是您需要确保在依赖于第 1 层的项目中依赖于第 2 层的项目之前已经创建并解决了资源。
在单一仓库中,只需确保工作流程或 CI/CD 工具按正确的顺序运行项目即可,但在多仓库实现中,它将成为一个复杂的编排问题,可能涉及多个仓库的 Web 钩子和大量的救急胶带。
应用程序仓库在分层系统中的位置足够低,以至于运行应用程序所需的所有基础设施都将就位。将应用程序部署基础设施代码放在应用程序仓库中,可以使应用程序开发人员完全拥有其代码,从编写和功能到将其投入生产。
一旦您做出了上述基础决策,您将为构建一组定义良好的基础设施代码模式做好了准备,但最后一件需要考虑的事情是如何在控制仓库和应用程序仓库之间共享资源模式。
每个 IaC 工具都有不同的管理方式。在 Pulumi 中,您可以为单个语言创建一个组件资源,或者如果您希望支持多种语言,您可能希望创建一个 Pulumi 包,但进行此操作的原因是相同的:您希望封装一组最佳实践,您可以在多个项目之间共享。
在开始封装资源时的一个很好的考虑因素是您的组织结构和应用程序架构。如果您只有一个团队在部署一个应用程序,您可能不需要走封装任何东西的路径,但如果您是一个可能支持数十个团队部署到共享的第 5 层计算资源的平台团队,创建一个封装部署应用程序的最佳实践或创建一个封装最佳实践对象存储桶的包将为您支持的团队节省大量时间。
这些封装应该在自己独立的存储库中。您需要像为您的应用程序版本和发布一样为这些封装设置版本 - 遵循语义版本并确保为您的下游用户创建一个 API。
随着您的下游用户开始依赖这些封装,您可以引入诸如单元测试的概念,以确保在基础设施中不会破坏用户空间。
我在采用 Pulumi 时在封装层经常看到的一个常见错误是试图避免面向对象的原则,而使用我称之为“基于函数的方法”。
以此为例,您可能会尝试将某些资源封装到一个函数中。在 TypeScript 中,它看起来像这样:
export f
unction createBucket(name: string) {
return new aws.s3.Bucket(name);
}
在 Python 中是这样的:
def create_bucket(name: str) -> aws.s3.Bucket:
return aws.s3.Bucket(name)
这种抽象实现的问题是它创建了一个难以成功管理的嵌套机制。
如果您使用组件,您将获得一个更符合语言工作方式的抽象机制。在 TypeScript 中,它看起来像这样:
export class Bucket extends pulumi.ComponentResource {
public readonly bucket: aws.s3.Bucket;
constructor(name: string, args: BucketArgs, opts?: pulumi.ComponentResourceOptions) {
super("lbrlabs:index:Bucket", name, {}, opts);
this.bucket = new aws.s3.Bucket(name, args, { parent: this });
}
}
在 Python 中是这样的:
class Bucket(pulumi.ComponentResource):
def __init__(self, name: str, args: BucketArgs, opts: Optional[pulumi.ResourceOptions] = None):
super().__init__("lbrlabs:index:Bucket", name, {}, opts)
self.bucket = aws.s3.Bucket(name, args, parent=self)
尽管资源的实例化更加复杂,但随着时间的推移,其可管理性将呈指数增加。相信我,我以前曾解开过这个混乱。
一个例子胜过千言万语,因此让我们来看一下一个假设性的控制仓库和应用程序仓库。
假设我们非常原创,将仓库称为 "infrastructure"。以下是可能的结构:
├── certs
│ ├── Pulumi.development.yaml
│ ├── Pulumi.production.yaml
│ ├── Pulumi.yaml
│ ├── __main__.py
│ ├── requirements.txt
│ └── venv
├── cluster
│ ├── Pulumi.development.yaml
│ ├── Pulumi.production.yaml
│ ├── Pulumi.yaml
│ ├── README.md
│ ├── __main__.py
│ ├── requirements.txt
│ └── venv
...
在这里,我们使用 Pulumi 栈来定位不同的环境(在这种情况下是开发和生产),并为不同的层次和资源创建新的项目。
您可能还会注意到,我在每组服务的使用上非常自由。我没有将所有的网络/第2层资源组合到一个项目中,但是我遵循了分层原则,不将来自不同层次的任何资源组合到同一个项目中。
您肯定可以减少这里的项目数量(例如,您可能选择将 VPC 和 VPN 项目组合在一个网络项目中),但我通常发现项目/目录是“免费的”,减少变更的影响范围可以让人们更容易地参与到这些共享元素中。
一旦我们进入应用程序仓库,就很难作出明确的建议,但假设我们有一个名为 "example-app" 的简单 Go 应用程序。以下是可能的结构:
.
├── Dockerfile
├── Makefile
├── docker-compose.yml
├── deploy
│ ├── Pulumi.development.yaml
│ ├── Pulumi.production.yaml
│ ├── Pulumi.yaml
│ └── main.go
├── go.mod
├── go.sum
├── main.go
├── readme.md
└── README.md
希望这些都相当明显,您拥有应用程序以及用于本地开发的 Dockerfile 和 Makefile 的机制,我们可以将 Pulumi 代码放在 deploy/ 目录中。
最后,让我们来看一个封装仓库的示例。这些仓库可能会相当复杂,因此举个例子,可以看看这个封装了一些计算层次的 Pulumi 包。
我们在这里涵盖了很多内容,但希望这些能够为您提供一些有关如何构建基础设施即代码的思路。如果您正在使用 Pulumi,就像我所有的内容一样,始终愿意倾听更好的想法!