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:
|
Nachher: Variable Injection |
|
|
150 API-Calls | 3 API-Calls |
|
$ time terraform plan real 4m23.415s |
$ time terraform plan real 0m18.732s |
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.