Terraform 的 Data Sources 是一种常用方式,用于动态地将各个云环境中真实存在的值填充到变量中。但在动态基础架构中使用它们则需要更长远的视角。比如,在某个模块中使用一个看似无害的 data.oci_identity_availability_domains 就足以让每次执行 terraform plan 的时间从几秒变为几分钟。因为 100 个模块实例就意味着 100 次 API 调用,而您的云服务提供商将开始限制请求频率。欢迎来到通过 Data Sources 意外触发 API 调用放大的世界。
本文将向您展示,为何在 Terraform 模块中使用 Data Sources 可能成为一个扩展性问题。
隐藏的扩展性问题
场景:10 秒陷阱
您编写了一个结构清晰的 Terraform 模块,用于创建 VM 实例。每个 VM 实例需要一个 Availability Domain,因此您使用了一个 Data Source:
data "oci_identity_availability_domains" "ads" { compartment_id = var.tenancy_ocid } resource "oci_core_instance" "this" { for_each = var.instances != null ? var.instances : {} availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name compartment_id = var.compartment_id shape = each.value.shape create_vnic_details { subnet_id = each.value.subnet_id } }
乍一看,一切都很正确。该模块运行良好 - 无论是针对单个 VM 实例,还是少量实例。
但随后客户将规模扩大到 50 个 VM:
module "app_servers" { for_each = var.server_configs != null ? var.server_configs : {} source = "./modules/compute-instance" instances = each.value.instances compartment_id = each.value.compartment_id }
结果: 每次执行 terraform plan 和 terraform apply 时,都会产生 50 次对 oci_identity_availability_domains 的相同 API 调用。
原本只需几秒的操作,现在变成了几分钟。对于 100 或 200 个实例,情况会变得更加严重。
在新部署的 VM 实例场景中,这种延迟在许多使用场景下也许还算可控和可接受——但如果涉及的是生命周期极短的现有资源,那后果将非常不同。请想象在为集群节点应对负载高峰而进行的资源部署中,在 Load Balancer 的后端配置中,在 DNS 记录中,或者在那些又被其他资源所依赖的资源中发生延迟。这将导致长时间中断,对客户造成外部影响,在最坏情况下甚至是不可预测的。
为何在模块中使用 Data Sources 会引发问题
Data Sources 的行为方式与资源有本质上的不同。它们会在每次执行 terraform plan 和 terraform apply 时运行,且无法被缓存。在模块中使用时,这意味着:
问题 1:API 调用放大
每个模块实例都会执行自己的 Data Source 查询,即便数据完全相同。
例如以下两个模块,它们都调用相同的子模块 ./modules/server:
module "web_servers" { for_each = var.web_configs != null ? var.web_configs : {} source = "./modules/server" compartment_id = each.value.compartment_id } module "app_servers" { for_each = var.app_configs != null ? var.app_configs : {} source = "./modules/server" compartment_id = each.value.compartment_id }
在模块 ./modules/server 中,隐藏着一个 Data Source 查询,对于 Root 模块的作者是不可见的:
data "oci_identity_availability_domains" "available" { compartment_id = var.compartment_id } resource "oci_core_instance" "this" { for_each = var.instances != null ? var.instances : {} availability_domain = data.oci_identity_availability_domains.available.availability_domains[0].name compartment_id = var.compartment_id }
如果现在例如部署 10 个 app_server 和 50 个 web_server,就会产生 60 个并行的 API 查询。没有任何云服务提供商会允许这种行为。相反,terraform apply 将会触及 API 网关的调用上限,导致部署时间变得异常漫长。
在最理想的情况下,服务器会被分批部署,例如每批 10 个实例。这种方式下,部署时间也会至少是原来的六倍。
而这不仅影响代码示例中所示的服务器实例,还会波及到 Plan 中所有其余资源。API 限制是全局性的,不只针对特定资源类型如 oci_core_instance。其他通常需要快速甚至并行访问的资源,现在也必须排队等待。这种情况在最坏的情况下甚至会导致 Race Condition 或 Timeout。
问题 2:性能退化
随着模块实例数量的增加,计划阶段的耗时将呈线性甚至指数级增长。
请看这个示例,它从 OCI 中读取现有的 OS 镜像列表,并筛选出适用于 GPU 应用的 Oracle Linux 8 特定版本:
data "oci_core_images" "compute_images" { compartment_id = var.compartment_id operating_system = "Oracle Linux" operating_system_version = "8" sort_by = "TIMECREATED" sort_order = "DESC" filter { name = "display_name" values = [".*GPU.*"] regex = true } } resource "oci_core_instance" "gpu_instances" { for_each = var.gpu_instances != null ? var.gpu_instances : {} source_details { source_id = data.oci_core_images.compute_images.images[0].id source_type = "image" } }
OS 镜像的列表非常庞大,每次针对单个 VM 实例都会被完整读取并由 Terraform Provider 进行过滤。这不仅导致大量不必要的 API 调用和冗长的响应,还会给 Terraform 自身带来负担。
问题 3:API 限制与限流
我们在本文前面已提到:云服务提供商都会设定 API 调用限制。
过多的 Data Source 调用会导致:
- HTTP 429(请求过多)错误
- 指数退避和重试循环
- 其他 Terraform 操作被阻塞
- CI/CD 流水线变得不稳定
反面教材:无法扩展的示例
示例 1:Availability Domain 困境
问题:该模块在每次实例化时都会发起两个独立的 API 调用 —— 一个用于获取 Availability Domains,一个用于获取 Subnets。在有 20 个数据库模块的环境中,会为相同的信息产生 40 次冗余 API 调用。
为何无法扩展:每个模块都会查询相同的全局 Availability Domains,尽管这些信息几乎不会发生变化。此外,每个模块实例还会执行相同的 Subnet 过滤操作,而在拥有大量子网的复杂 VPC 中,这种操作的开销尤为显著。
影响:若有 50 个数据库实例 = 100 次 API 调用加上 50 次对子网的过滤操作,而这些信息一年最多变化一次。
data "oci_identity_availability_domains" "available" { compartment_id = var.compartment_id } data "oci_core_subnets" "database" { compartment_id = var.compartment_id vcn_id = var.vcn_id filter { name = "display_name" values = ["*database*"] } } resource "oci_database_db_system" "main" { for_each = var.db_systems != null ? var.db_systems : {} availability_domain = data.oci_identity_availability_domains.available.availability_domains[0].name subnet_id = data.oci_core_subnets.database.subnets[0].id compartment_id = var.compartment_id }
示例 2:Image 查询问题
问题:该实现会在每次模块调用时执行一次开销巨大的镜像搜索操作,且带有正则过滤。无论是在 OCI 端还是 Terraform 本地,对镜像列表进行排序和处理都会消耗大量资源。
为何无法扩展:Image 查询操作特别缓慢,因为涉及到大量数据传输和复杂的过滤逻辑。每个 VM 实例都执行相同的查询,尽管查询结果对所有实例都是一样的。加之本地使用 sort() 进行排序,使问题进一步加剧。
影响:100 个 VM 实例 = 100 次高开销的 Image API 调用 + 100 次本地过滤操作 = 几分钟的计划时间,而其实完全可以一瞬完成。
data "oci_core_images" "ol8_images" { compartment_id = var.tenancy_ocid operating_system = "Oracle Linux" operating_system_version = "8" shape = var.instance_shape filter { name = "display_name" values = ["Oracle-Linux-8.8-.*"] regex = true } } locals { latest_image_id = sort([for img in data.oci_core_images.ol8_images.images : img.id])[0] } resource "oci_core_instance" "this" { for_each = var.instances != null ? var.instances : {} source_details { source_id = local.latest_image_id source_type = "image" } }
示例 3:嵌套 Data Source 问题
问题:该模块同时集成了多个扩展性问题:Cluster 查询、Worker Node 镜像查询、Data Sources 之间的复杂依赖链。每次创建 Node Pool 都会重新触发这两个 Data Source。
为何无法扩展:嵌套的 Data Sources 会形成依赖链,每个模块实例都必须完整执行这些链。与 Kubernetes 相关的镜像查询尤其缓慢,因为它们使用了复杂的 Regex 过滤模式,必须由服务器端处理。
影响:10 个 Node Pool = 20 次集群信息 API 调用 + 10 次高开销的 OKE 镜像查询 = 随着 Node Pool 数量增长而呈指数级上升的计划时间。
data "oci_containerengine_clusters" "existing" { compartment_id = var.compartment_id filter { name = "name" values = [var.cluster_name] } } data "oci_core_images" "worker_images" { compartment_id = var.compartment_id operating_system = "Oracle Linux" filter { name = "display_name" values = ["Oracle-Linux-.*-OKE-.*"] regex = true } } resource "oci_containerengine_node_pool" "workers" { for_each = var.node_pools != null ? var.node_pools : {} cluster_id = data.oci_containerengine_clusters.existing.clusters[0].id node_config_details { size = each.value.size } }
我们现在已经为您列举了多个 Data Source 的错误使用方式。但仅仅知道这些反面例子还不够。在下一篇文章中,我将展示如何通过智能架构设计与变量注入,优雅地避开这些陷阱。
因为最好的 Data Source,就是根本不运行,或只运行一次的那一个。