Android Map | Article Map
Terraform @ Scale - 第 4b 部分:可扩展 Data Sources 的最佳实践

Color logo   no background

    Terraform @ Scale - 第 4b 部分:可扩展 Data Sources 的最佳实践

    在本系列的上一部分中,我们展示了看似无害的 Terraform 模块中 Data Sources 如何演变成严重的性能问题。结果包括长达数分钟的 terraform plan 执行时间、不稳定的流水线以及无法控制的 API 节流效应。

    那么,如何优雅且可持续地避免这一扩展陷阱?

    在本部分中,我们将介绍一些经过验证的架构模式,帮助您实现 Data Sources 的集中化管理,以资源友好的方式进行注入,从而即便在存在数百个模块实例的情况下,也能实现快速、稳定且可预测的 Terraform 执行。

    内容包括:三种可扩展的解决策略、一份经过实战验证的逐步指南以及一份面向生产环境的基础设施模块最佳实践检查清单。

     

    最佳实践:可扩展的替代方案

    解决方案 1(适用于简单场景):变量注入模式(Variable Injection Pattern)

    与其在模块中使用 Data Sources,不如将所需数据作为变量注入:


    data "oci_identity_availability_domains" "available" {
      compartment_id = var.tenancy_ocid
    }
    
    data "oci_core_subnets" "database" {
      compartment_id = var.compartment_id
      vcn_id         = var.vcn_id
      
      filter {
        name   = "display_name"
        values = ["*database*"]
      }
    }
    
    locals {
      availability_domains = data.oci_identity_availability_domains.available.availability_domains
      database_subnets     = data.oci_core_subnets.database.subnets
    }
    
    module "databases" {
      for_each = var.database_configs != null ? var.database_configs : {}
      
      source = "./modules/database"
      
      availability_domains = local.availability_domains
      subnet_ids          = [for subnet in local.database_subnets : subnet.id]
      
      name = each.key
      size = each.value.size
    }

     


    variable "availability_domains" {
      type = list(object({
        name = string
        id   = string
      }))
      description = "Available ADs for database placement"
    }
    
    variable "subnet_ids" {
      type        = list(string)
      description = "Database subnet IDs"
    }
    
    resource "oci_database_db_system" "main" {
      for_each = var.db_systems != null ? var.db_systems : {}
      
      availability_domain = var.availability_domains[0].name
      subnet_id          = var.subnet_ids[0]
      compartment_id     = var.compartment_id
    }

     

    解决方案 2(适用于复杂场景):结构化配置模式(Structured Configuration Pattern)

    对于更复杂的场景,请使用结构化配置对象:


    data "oci_core_images" "ol8" {
      compartment_id           = var.tenancy_ocid
      operating_system         = "Oracle Linux"
      operating_system_version = "8"
    }
    
    locals {
      compute_images = {
        "VM.Standard.E4.Flex" = {
          image_id         = [for img in data.oci_core_images.ol8.images : img.id if can(regex(".*E4.*", img.display_name))][0]
          boot_volume_size = 50
        }
        "BM.Standard3.64" = {
          image_id         = [for img in data.oci_core_images.ol8.images : img.id if can(regex(".*Standard.*", img.display_name))][0]
          boot_volume_size = 100
        }
      }
      
      network_config = {
        availability_domains = data.oci_identity_availability_domains.ads.availability_domains
        vcn_id              = data.oci_core_vcn.main.id
      }
    }
    
    module "compute_instances" {
      for_each = var.instance_configs != null ? var.instance_configs : {}
      
      source = "./modules/compute-instance"
      
      compute_config = local.compute_images[each.value.shape]
      network_config = local.network_config
    }

     


    variable "compute_config" {
      type = object({
        image_id         = string
        boot_volume_size = number
      })
      description = "Pre-resolved compute configuration"
    }
    
    variable "network_config" {
      type = object({
        availability_domains = list(object({
          name = string
          id   = string
        }))
        vcn_id = string
      })
      description = "Pre-resolved network configuration"
    }
    
    resource "oci_core_instance" "this" {
      for_each = var.instances != null ? var.instances : {}
      
      availability_domain = var.network_config.availability_domains[0].name
      compartment_id      = var.compartment_id
      
      source_details {
        source_id               = var.compute_config.image_id
        source_type            = "image"
        boot_volume_size_in_gbs = var.compute_config.boot_volume_size
      }
    }

    解决方案 3(适用于非常复杂的场景):数据代理模式(Data Proxy Pattern)

    对于非常复杂的场景,请创建专用的 “Data Proxy” 模块:


    data "oci_core_images" "oracle_linux" {
      compartment_id           = var.tenancy_ocid
      operating_system         = "Oracle Linux"
      operating_system_version = "8"
    }
    
    data "oci_core_vcn" "main" {
      vcn_id = var.vcn_id
    }
    
    data "oci_core_security_lists" "web" {
      compartment_id = var.compartment_id
      vcn_id         = var.vcn_id
      
      filter {
        name   = "display_name"
        values = ["*web*"]
      }
    }
    
    output "platform_data" {
      value = {
        image_id = data.oci_core_images.oracle_linux.images[0].id
        vcn_id   = data.oci_core_vcn.main.id
        
        instance_shapes = {
          small  = "VM.Standard.E3.Flex"
          medium = "VM.Standard.E4.Flex"
          large  = "VM.Standard3.Flex"
        }
      }
    }

     


    module "platform_data" {
      source = "./modules/data-proxy"
      
      tenancy_ocid   = var.tenancy_ocid
      compartment_id = var.compartment_id
      vcn_id         = var.vcn_id
    }
    
    module "web_servers" {
      for_each = var.web_server_configs != null ? var.web_server_configs : {}
      
      source = "./modules/oci-instance"
      
      platform_data = module.platform_data.platform_data
      
      name          = each.key
      instance_type = each.value.size
    }

    性能对比

    以下是一个来自客户项目的具体示例,在该项目中部署了 50 个 VM 实例,展示了显著的差异:

     
     之前:模块中使用 Data Sources

    之后:变量注入

    API 调用次数
    150 次 API 调用 3 次 API 调用
    耗时
    $ time terraform plan
    ...
    Plan: 50 to add, 0 to change, 0 to destroy.
    real 4m23.415s
    user 0m12.484s
    sys 0m2.108s
    $ time terraform plan
    ...
    Plan: 50 to add, 0 to change, 0 to destroy.
    real 0m18.732s
    user 0m8.234s
    sys 0m1.456s

     

    结果: 计划时间减少 93%,API 调用减少 98%。

    变量注入:逐步指南

    步骤 1:集中管理 Data Sources

    目标: 将所有 Data Sources 从模块中移除,集中放置在根模块中,以整合 API 调用并建立单一真实来源(Single Source of Truth)。

    操作方法: 将所有被模块使用的 Data Sources 移动到根模块。这样,每一类信息只会被查询一次,无论有多少模块需要这些数据。这样可以将 API 调用次数从 N×M(模块数量 × Data Source 数量)减少到 M(Data Source 数量)。


    data "oci_identity_availability_domains" "ads" {
      compartment_id = var.tenancy_ocid
    }
    
    data "oci_core_images" "ol8" {
      compartment_id           = var.tenancy_ocid
      operating_system         = "Oracle Linux"  
      operating_system_version = "8"
    }
    
    data "oci_core_vcn" "main" {
      vcn_id = var.vcn_id
    }

     

    步骤 2:在 Locals 中处理数据

    目标: 将原始 Data Source 结果转换为可消费的数据格式,并从模块中移除复杂性。

    操作方法: 使用 Locals 对 Data Source 结果进行过滤、排序,并转换为结构化的数据格式。这使得复杂逻辑可以集中处理,而模块只需接收已经清洗过的结构化数据。通过使用 for 循环与条件表达式,您还可以实现回退机制和验证逻辑。


    locals {
      availability_domains = [
        for ad in data.oci_identity_availability_domains.ads.availability_domains : ad.name
      ]
      
      compute_images = {
        standard = [
          for img in data.oci_core_images.ol8.images :
          img.id if can(regex(".*Standard.*", img.display_name))
        ][0]
        
        gpu = [
          for img in data.oci_core_images.ol8.images :
          img.id if can(regex(".*GPU.*", img.display_name))  
        ][0]
      }
      
      network_config = {
        vcn_id               = data.oci_core_vcn.main.id
        vcn_cidr            = data.oci_core_vcn.main.cidr_block
        availability_domains = local.availability_domains
      }
    }

     

    步骤 3:在模块中定义变量

    目标: 为模块的数据传递建立清晰的接口,并确保类型安全与验证机制。

    操作方法: 将模块中的 Data Sources 替换为带有明确描述与验证规则的类型化变量定义。类型定义确保一致性和容错性,而验证块确保只有有效数据会传递到模块中。这提升了模块的可测试性,并使其脱离对 Cloud Provider API 的直接依赖。


    variable "availability_domains" {
      type        = list(string)
      description = "List of available availability domains"
      
      validation {
        condition     = length(var.availability_domains) > 0
        error_message = "At least one availability domain must be provided."
      }
    }
    
    variable "compute_images" {
      type        = map(string)
      description = "Map of compute images by type"
      
      validation {
        condition = alltrue([
          for image_id in values(var.compute_images) :
          can(regex("^ocid1\\.image\\.", image_id))
        ])
        error_message = "All image IDs must be valid OCI OCIDs."
      }
    }

     

    步骤 4:实现不依赖 Data Sources 的模块

    目标: 使模块完全不依赖外部 API 调用,并将其转变为纯粹的资源定义容器。

    操作方法: 将模块中所有对 Data Source 的引用替换为变量引用。这使得模块具有确定性和可预测性,只基于传入参数运行,不再产生意外的 API 调用。同时,这也让模块可以独立测试,因为您可以通过变量注入模拟数据。


    resource "oci_core_instance" "this" {
      for_each = var.instances != null ? var.instances : {}
      
      availability_domain = var.availability_domains[each.value.ad_index]
      compartment_id      = var.compartment_id
      shape              = each.value.shape
      
      create_vnic_details {
        subnet_id = each.value.subnet_id
      }
      
      source_details {
        source_id   = var.compute_images[each.value.image_type]
        source_type = "image"
      }
      
      metadata = {
        ssh_authorized_keys = var.ssh_public_key
      }
    }

     

    步骤 5:调用模块并注入数据

    目标: 建立集中数据与模块之间的连接,形成清晰的数据流模式。

    操作方法: 将在 Locals 中处理的数据作为参数传入模块中。这样就形成了完整的变量注入流程:数据在中央被拉取和转换,然后明确地传递给各个模块。通过这种显式传递,依赖关系变得清晰可见,无论是对人类读者还是 Terraform 本身来说都易于理解。


    module "web_servers" {
      for_each = var.web_server_configs != null ? var.web_server_configs : {}
      
      source = "./modules/compute"
      
      availability_domains = local.availability_domains
      compute_images      = local.compute_images  
      network_config      = local.network_config
      
      instances      = each.value.instances
      compartment_id = each.value.compartment_id
      ssh_public_key = var.ssh_public_key
    }

     

    最佳实践检查清单

    ✅ 应该做的:可扩展模式

    • [ ] 集中管理 Data Sources:在 root module 中定义所有 Data Sources
    • [ ] 变量注入:将数据作为变量传递给模块
    • [ ] 结构化对象:将复杂数据组织为 typed objects
    • [ ] 验证规则:为注入的数据实现变量验证
    • [ ] 文档说明:为注入的数据撰写变量描述
    • [ ] 本地处理:在 root module 中使用 locals 处理数据
    • [ ] 数据代理模式(Data Proxy Pattern):对于非常复杂的场景使用独立的数据模块

    ❌ 不该做的:避免反模式

    • [ ] 模块中使用 Data Sources:切勿在可复用模块中使用 Data Sources
    • [ ] 冗余查找:在多个模块中使用相同的 Data Sources
    • [ ] 复杂筛选:在每个模块中执行高开销的过滤操作
    • [ ] 嵌套的 Data Sources:依赖于其他 Data Sources 的 Data Sources
    • [ ] 动态引用:在模块中对 Data Source 结果使用 for_each
    • [ ] 缺乏验证:使用未验证的注入数据

    监控与调试

    要监控 Data Source 的性能,您可以在 terraform plan 的调试输出中搜索和分析 Data Sources 的相关条目:


    export TF_LOG=DEBUG
    export TF_LOG_PATH=./terraform.log
    
    terraform plan 2>&1 | grep -E "(data\.|GET|POST)" | wc -l
    
    terraform plan 2>&1 | grep -E "data\." | awk '{print $2}' | sort | uniq -c

    总结:通过架构决策实现高性能模块

    Data Sources 是 Terraform 的强大特性——但在模块中使用时可能成为性能陷阱。变量注入模式(Variable Injection Pattern)提供了一种优雅的解决方案:

    优势:

    • 显著减少 API 调用(可节省 95% 以上)
    • 线性性能扩展,避免指数级退化
    • 集中式数据逻辑,更易于维护
    • 显式依赖关系,不再隐藏 Data Source 调用
    • 更高的可测试性,可通过注入模拟数据进行测试

    关键在于思维方式的转变: 与其在需要时再获取数据,不如集中获取一次并有目的地进行分发。

    ICT.technology,我们通过持续应用这些模式,将 Terraform 的计划时间从数分钟缩短到几秒钟——即使是在包含数百个模块实例的场景中也是如此。