Sägetstrasse 18, 3123 Belp, Switzerland +41 79 173 36 84 info@ict.technology

    Terraform @ Scale - Teil 4b: Best Practices für skalierende Data Sources

    Im letzten Teil dieser Serie haben wir gezeigt, wie harmlos wirkende Data Sources in Terraform-Modulen zu einem ernsthaften Performance-Problem werden können. Minutenlange terraform plan-Laufzeiten, instabile Pipelines und unkontrollierbare API-Throttling-Effekte waren die Folge.

    Doch wie vermeidet man diese Skalierungsfalle elegant und nachhaltig?

    In diesem Teil stellen wir bewährte Architektur-Patterns vor, mit denen Sie Data Sources zentralisieren, ressourcenschonend injizieren und so auch bei hundertfachen Modulinstanzen schnelle, stabile und vorhersehbare Terraform-Ausführungen erreichen.

    Mit dabei: drei skalierbare Lösungsstrategien, eine praxiserprobte Schritt-für-Schritt-Anleitung und eine Best Practices Checkliste für produktionsreife Infrastrukturmodule.

     

    Best Practice: Skalierende Alternativen

    Lösung 1 (einfache Szenarien): Variable Injection Pattern

    Statt Data Sources in Modulen zu verwenden, injizieren Sie die benötigten Daten als Variablen:


     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
    }
    

     

    Lösung 2 (komplexe Szenarien): Structured Configuration Pattern

    Für komplexere Szenarien verwenden Sie strukturierte Konfigurationsobjekte:


    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
      }
    }
    

    Lösung 3 (sehr komplexe Szenarien): Data Proxy Pattern

    Für sehr komplexe Szenarien erstellen Sie dedizierte "Data Proxy" Module:


     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
    }

    Performance-Vergleich

    Ein konkretes Beispiel aus einem Kundenprojekt mit Bereitstellung von 50 VM-Instanzen zeigt den dramatischen Unterschied:

     
     Vorher: Data Sources in Modulen

    Nachher: Variable Injection

    Anzahl API-Calls
    150 API-Calls 3 API-Calls
    Zeitaufwand
    $ 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

     

    Ergebnis: 93% weniger Planungszeit und 98% weniger API-Calls.

    Variable Injection: Schritt-für-Schritt Guide

    Schritt 1: Data Sources zentralisieren

    Ziel: Alle Data Sources aus den Modulen entfernen und zentral im Root-Modul sammeln, um API-Calls zu konsolidieren und eine einzige Quelle der Wahrheit zu schaffen.

    Wie: Verlegen Sie alle Data Sources, die von Modulen verwendet werden, in das Root-Modul. Dies sorgt dafür, dass jede Information nur einmal abgefragt wird, unabhängig davon, wie viele Module diese Daten benötigen. Dadurch reduzieren Sie die Anzahl der API-Calls von N×M (Anzahl Module × Anzahl Data Sources) auf nur M (Anzahl Data Sources).


     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
    }
    

     

    Schritt 2: Daten in Locals verarbeiten

    Ziel: Die rohen Data Source-Ergebnisse in eine konsumierbare Form transformieren und dabei gleichzeitig Komplexität aus den Modulen heraushalten.

    Wie: Verwenden Sie Locals, um Data Source-Ergebnisse zu filtern, zu sortieren und in strukturierte Datenformate umzuwandeln. Dies ermöglicht es, komplexe Logik zentral zu handhaben und Module mit bereits verarbeiteten, sauberen Daten zu versorgen. Durch die Verwendung von for-Schleifen und Conditional Expressions können Sie gleichzeitig Fallback-Mechanismen und Validierungslogik implementieren.


     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
      }
    }
    

     

    Schritt 3: Variablen in Modulen definieren

    Ziel: Klare Schnittstellen für die Datenübergabe an Module schaffen und dabei Typsicherheit und Validierung gewährleisten.

    Wie: Ersetzen Sie die Data Sources in Modulen durch typisierte Variablen mit aussagekräftigen Beschreibungen und Validierungsregeln. Die Typdefinitionen sorgen für Konsistenz und Fehlerresistenz, während Validierungsblöcke sicherstellen, dass nur gültige Daten an die Module weitergegeben werden. Dies macht Module testbarer und unabhängiger von der 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."
      }
    }
    

     

    Schritt 4: Module ohne Data Sources implementieren

    Ziel: Module vollständig von externen API-Calls befreien und sie zu reinen Ressourcen-Definitions-Containern machen.

    Wie: Ersetzen Sie alle Data Source-Referenzen in Modulen durch Variablen-Referenzen. Dies macht Module deterministisch und vorhersagbar, da sie nur noch mit den übergebenen Parametern arbeiten und keine unerwarteten API-Calls mehr durchführen. Gleichzeitig werden Module dadurch unabhängig testbar, da Sie Mock-Daten über die Variablen injizieren können.


     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
      }
    }
    

     

    Schritt 5: Module aufrufen mit injizierten Daten

    Ziel: Die Verbindung zwischen zentral abgerufenen Daten und Modulen herstellen, um ein sauberes Datenfluss-Pattern zu etablieren.

    Wie: Übergeben Sie die in den Locals verarbeiteten Daten als Parameter an die Module. Dies schließt den Kreis der Variable Injection: Daten werden einmal zentral abgerufen, verarbeitet und dann gezielt an die Module verteilt. Durch diese explizite Datenübertragung entstehen klare Abhängigkeiten, die sowohl für Menschen als auch für Terraform selbst nachvollziehbar sind.


     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
    }
    

     

    Best Practices Checkliste

    ✅ Do's: Skalierende Patterns

    • [ ] Zentrale Data Sources: Alle Data Sources im root module definieren
    • [ ] Variable Injection: Daten als Variablen an Module übergeben
    • [ ] Strukturierte Objekte: Komplexe Daten in typed objects organisieren
    • [ ] Validation Rules: Variable validations für injizierte Daten implementieren
    • [ ] Documentation: Variable descriptions für injizierte Daten schreiben
    • [ ] Local Processing: Datenverarbeitung in locals im root module
    • [ ] Data Proxy Pattern: Für sehr komplexe Szenarien separate Daten-Module

    ❌ Don'ts: Anti-Patterns vermeiden

    • [ ] Data Sources in Modulen: Niemals Data Sources in wiederverwendbaren Modulen
    • [ ] Redundante Lookups: Identische Data Sources in mehreren Modulen
    • [ ] Complex Filtering: Teure Filteroperationen in jedem Modul
    • [ ] Nested Data Sources: Data Sources, die von anderen Data Sources abhängen
    • [ ] Dynamic References: for_each über Data Source-Ergebnisse in Modulen
    • [ ] Missing Validation: Injizierte Daten ohne Validierung verwenden

    Monitoring und Debugging

    Um Data Source-Performance zu überwachen, können Sie den Debug-Output von terraform plan nach Einträgen der Data Sources durchsuchen und auswerten:


    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

    Fazit: Performante Module durch bewusste Architektur

    Data Sources sind ein mächtiges Terraform-Feature – aber in Modulen können sie zur Performance-Falle werden. Das Variable Injection Pattern bietet eine elegante Lösung:

    Vorteile:

    • Drastisch reduzierte API-Calls (95%+ Einsparung möglich)
    • Lineare Performance-Skalierung statt exponentieller Degradation
    • Zentralisierte Datenlogik für bessere Wartbarkeit
    • Explizite Abhängigkeiten statt versteckter Data Source-Calls
    • Bessere Testbarkeit durch injizierbare Mock-Daten

    Der Schlüssel liegt im Paradigmenwechsel: Statt Daten zu holen, wenn sie gebraucht werden, holen Sie sie einmal zentral und verteilen sie gezielt.

    Bei ICT.technology haben wir durch konsequente Anwendung dieser Patterns Terraform-Planungszeiten von Minuten auf Sekunden reduziert - selbst bei hunderten von Modulinstanzen.