Terraform 架构图:从 .tf 文件到人人都能看懂的图

Terraform 已经描述了你的基础设施,但代码和 terraform graph 都不是架构图。本文讲清一张易读的图需要什么、该删掉什么。

· 创始人兼开发者

发布于

Terraform 架构图:从 .tf 文件到人人都能看懂的图

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 输出里。

图

用户通过 HTTPS 连接到 VPC 公有子网中的负载均衡器。负载均衡器把请求转发给私有子网中的 ECS 服务,该服务使用一个 PostgreSQL 数据库和一个 S3 存储桶。一条 DNS 别名记录指向负载均衡器,一条注释列出每张卡片背后的 Terraform 资源。

10 个组件 · 5 个连接

在 Archboard 中打开
图表内容
  • 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
一个用 Terraform 定义、为人而画的 Web 应用:用户访问公有子网中的 Application Load Balancer,ECS 服务和 Multi-AZ 的 PostgreSQL 数据库位于私有子网,一条注释把每张卡片对应到它的资源地址。

这张图的读法如下:

  • 入口。 用户通过 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 中构建

  1. 使用 Google 或 GitHub 登录,并新建一张图。
  2. 想要初稿,就把你的 .tf 文件或 plan JSON 粘贴到免费的 Terraform 转图表 工具中。它会把 VPC 和子网变成框架,把资源变成卡片,把引用变成连接;根据 plan JSON 标出计划中的变更,并把目标组附加之类的胶水并入连接。选择 在 Archboard 中打开 继续编辑。
  3. 如果手动绘制,添加一个网络节点包中的 VPC 框架,里面放两个 子网 框架;在公有子网中放一个云节点包中的 云负载均衡器,在私有子网中放一个 计算实例 节点和一个 托管数据库 节点。
  4. 为存储桶添加一个云节点包中的 对象存储 节点,为用户添加一个网络节点包中的 互联网 节点,为别名记录添加一个 DNS 节点。
  5. 在连线上标注端口和协议,并添加一个基础节点包中的 Markdown 块,写明卡片与资源地址的对应关系。

这个示例共用 10 个图表节点,在 Free 套餐的限制之内。如果你的栈是用 CloudFormation 定义的,CloudFormation 转图表 工具可以对模板做同样的事。

常见问题

terraform graph 能生成架构图吗? 它生成的是 DOT 格式的依赖图,从 Terraform 1.16 起也可以是 Mermaid 格式。这对调试计划很有用,但它展示的是构建顺序而不是流量,还包含胶水资源,所以需要整理之后,人们才能把它当作架构来读。

应该从 .tf 文件还是 plan JSON 来画图? 快速查看某个模块时用 .tf 文件,评审变更时用 plan JSON,因为计划里的数量、模块和计划中的操作都已解析。

安全组应该怎么表示? 把它们写成所保护资源之间连线上的端口和协议,而不是画成单独的方框。

哪些内容应该省略? IAM 附加、路由表关联、random_id 之类的辅助资源,以及任何不会改变读者对流量、数据或暴露面理解的东西。

如何让图与代码保持同步? 当变更新增入口、数据存储或网络边界时,重新生成一份草稿,并在同一个 pull request 中更新整理过的图。

免费开始 — 1 个席位,无需信用卡。

相关文章

← 返回博客

一分钟打开画布

使用 Google 或 GitHub 登录。单席位免费,团队加入后再升级。