Terraform 已经精确地描述了你的基础设施,而且处于版本控制之下。那为什么没人能从代码看懂这个系统?因为 .tf 文件回答的是“有什么、怎么配置的”,而读架构图的人问的是别的问题:流量从哪里进来,谁和谁通信,数据存放在哪里,什么暴露在外。
本文介绍如何从 Terraform 得到一张人能看懂的图:为什么 terraform graph 不是这张图,如何把资源映射为框架和卡片,什么时候用 plan JSON,哪些内容可以省略,以及如何在 pull request 评审中使用结果。
为什么代码和 terraform graph 都不是这张图
一个小型生产栈很容易就有上百个资源,分散在多个文件和模块里,其中很多是“胶水”:IAM 策略附加、路由表关联、安全组规则、目标组附加。“互联网能不能访问数据库?”的答案就在其中,却分散在十几个引用里。
terraform graph 也解决不了这个问题。它用 DOT 语言输出一张图;从 Terraform 1.7 起,默认输出是一张简化图,按照命令参考的说法,它只描述资源之间的依赖顺序。-type=plan 之类的选项可以显示 Terraform 生成计划时所用的更完整图的近似版本;从 Terraform 1.16 起,-format=mermaid 可以输出 Mermaid 而不是 DOT。
依赖图回答的是“什么必须先于什么存在”。ECS 服务依赖一个 IAM 角色、一个目标组和一个任务定义,所以箭头从服务指向它的前置条件,这往往与流量的方向相反,而且胶水资源占的位置和数据库一样大。它适合用来调试计划,但不是架构图。
示例栈
AWS 上一个典型的 Web 应用:一个在两个可用区中都有公有子网和私有子网的 VPC、一个 Application Load Balancer、一个运行在 Fargate 上的 ECS 服务、一个 RDS 上的 PostgreSQL 数据库,以及一个存放上传文件的 S3 存储桶。下面是配置的节选:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "private_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.11.0/24"
availability_zone = "eu-west-1a"
}
resource "aws_lb" "web" {
name = "web"
load_balancer_type = "application"
subnets = [aws_subnet.public_a.id, aws_subnet.public_b.id]
security_groups = [aws_security_group.alb.id]
}
resource "aws_ecs_service" "app" {
name = "app"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
launch_type = "FARGATE"
desired_count = 2
network_configuration {
subnets = [aws_subnet.private_a.id, aws_subnet.private_b.id]
security_groups = [aws_security_group.app.id]
}
load_balancer {
target_group_arn = aws_lb_target_group.app.arn
container_name = "app"
container_port = 8080
}
}
resource "aws_db_instance" "main" {
identifier = "app-db"
engine = "postgres"
instance_class = "db.t4g.medium"
allocated_storage = 50
username = "app"
manage_master_user_password = true
multi_az = true
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.db.id]
}
把 Terraform 映射到图上
大部分工作是决定哪些资源成为图上的元素,哪些成为其他元素的细节:
| 在 Terraform 中 | 在图上 |
|---|---|
aws_vpc、aws_subnet,以及 vpc_id / subnet_id 引用 |
框架:VPC 包住公有子网和私有子网 |
带监听器和目标组的 aws_lb |
一张负载均衡器卡片;监听端口写在连线上 |
带集群和任务定义的 aws_ecs_service |
一张计算卡片,注明期望数量 |
带子网组的 aws_db_instance |
私有子网中的一张数据库卡片,注明 Multi-AZ |
带版本控制和访问阻止的 aws_s3_bucket |
一张对象存储卡片 |
| 安全组规则 | 连线上的端口和协议 |
| 资源之间的引用 | 连接,按流量流动的方向画 |
depends_on |
如果它传达了读者需要的信息,就画成虚线 |
IAM 角色、策略、附加;random_*、time_*、null_resource |
省略,或在卡片上写一条简短注释 |
用 .tf 文件还是 plan JSON
你可以直接从配置出发,也可以从计划出发:
terraform plan -out plan.out
terraform show -json plan.out > plan.json
.tf 文件来得快,不需要运行任何东西,但 count 和 for_each 不会展开,变量是未知的,远程模块也看不到内部。plan JSON 把这些全部解析好了,它的 resource_changes 列表说明计划会对每个资源做什么:["create"]、["update"]、["delete"],或者表示替换的 ["delete", "create"]。所以评审变更时,它是更好的输入。处理这个文件要小心:terraform show 文档提醒,state 中的敏感值会以明文出现在 JSON 输出里。
图
图表内容
- Users
- app.example.com
- VPC
- Public subnets · 2 AZs
- Application LB
- Private subnets · 2 AZs
- ECS service ×2
- PostgreSQL, Multi-AZ
- Uploads bucket
- HTTPS 443
- alias record
- HTTP 8080
- PostgreSQL 5432
- HTTPS 443, uploads
这张图的读法如下:
- 入口。 用户通过 443 端口的 HTTPS 访问 Application Load Balancer。DNS 别名记录指向它。
- 网络边界。 VPC 里有两个框架:放负载均衡器的公有子网,以及放其他所有东西的私有子网。私有子网中的任何资源都无法从互联网访问。
- 路径。 负载均衡器通过 8080 端口把请求转发给 ECS 服务。服务通过 5432 端口访问 PostgreSQL,并通过 HTTPS 把上传文件写入 S3。
- 注释。 它把每张卡片对应到背后的资源地址,并列出有意省略的内容,方便读者找到代码。
先删减,再补上 Terraform 不知道的东西
大胆删减。IAM 胶水、路由表关联、日志组以及按规则拆开的安全组资源,留在代码里就好。对大多数读者来说,每个可部署单元或数据存储一张卡片是合适的粒度。
然后补上 Terraform 无法告诉你的信息:
- 谁在使用这个系统,以及流量从哪里进入。
- 流向。 引用从资源指向它需要的东西;流量常常是反方向的。
- 端口和协议,从安全组规则中取出,写在连线上。
- 外部服务,即应用会调用、但不在这份配置中管理的服务。
- 事故中重要的事实:Multi-AZ、备份、期望数量。
在 pull request 评审中使用
由 pull request 的计划生成的图,能让评审者看到变更的形状,而不是一长串 diff。尤其要留意以下几点:
- 新的公开入口,例如负载均衡器、公有子网,或对 0.0.0.0/0 开放的安全组。
- 有状态资源被替换。 数据库或存储桶上出现
["delete", "create"],值得再找一位评审者。 - 在子网之间移动的资源,或离开私有网络的资源。
- 新的数据存储,它们需要负责人、备份,并在你的数据地图中登记。
把生成的图和精心整理的架构图分开。生成的图是评审某一次变更的辅助工具;整理过的图则是解释系统的文档。
保持更新
像对待代码一样对待整理过的图:指定负责人,在模块的 README 里放上链接,并在变更新增入口、数据存储或网络边界时更新它。不会过时的架构图介绍了专门为此设计的工作流程。同一个栈的网络部分,请参阅 AWS VPC 架构图指南;要在两个区域运行它,请参阅 AWS 多区域架构图。
在 Archboard 中构建
- 使用 Google 或 GitHub 登录,并新建一张图。
- 想要初稿,就把你的
.tf文件或 plan JSON 粘贴到免费的 Terraform 转图表 工具中。它会把 VPC 和子网变成框架,把资源变成卡片,把引用变成连接;根据 plan JSON 标出计划中的变更,并把目标组附加之类的胶水并入连接。选择 在 Archboard 中打开 继续编辑。 - 如果手动绘制,添加一个网络节点包中的 VPC 框架,里面放两个 子网 框架;在公有子网中放一个云节点包中的 云负载均衡器,在私有子网中放一个 计算实例 节点和一个 托管数据库 节点。
- 为存储桶添加一个云节点包中的 对象存储 节点,为用户添加一个网络节点包中的 互联网 节点,为别名记录添加一个 DNS 节点。
- 在连线上标注端口和协议,并添加一个基础节点包中的 Markdown 块,写明卡片与资源地址的对应关系。
这个示例共用 10 个图表节点,在 Free 套餐的限制之内。如果你的栈是用 CloudFormation 定义的,CloudFormation 转图表 工具可以对模板做同样的事。
常见问题
terraform graph 能生成架构图吗? 它生成的是 DOT 格式的依赖图,从 Terraform 1.16 起也可以是 Mermaid 格式。这对调试计划很有用,但它展示的是构建顺序而不是流量,还包含胶水资源,所以需要整理之后,人们才能把它当作架构来读。
应该从 .tf 文件还是 plan JSON 来画图?
快速查看某个模块时用 .tf 文件,评审变更时用 plan JSON,因为计划里的数量、模块和计划中的操作都已解析。
安全组应该怎么表示? 把它们写成所保护资源之间连线上的端口和协议,而不是画成单独的方框。
哪些内容应该省略?
IAM 附加、路由表关联、random_id 之类的辅助资源,以及任何不会改变读者对流量、数据或暴露面理解的东西。
如何让图与代码保持同步? 当变更新增入口、数据存储或网络边界时,重新生成一份草稿,并在同一个 pull request 中更新整理过的图。