Nell'ultima parte di questa serie abbiamo mostrato come fonti dati apparentemente innocue all'interno di moduli Terraform possano trasformarsi in un serio problema di performance. Esecuzioni di terraform plan della durata di diversi minuti, pipeline instabili ed effetti di throttling delle API fuori controllo sono state le conseguenze.
Ma come si può evitare elegantemente e in modo sostenibile questa trappola di scalabilità?
In questa parte presentiamo pattern architetturali collaudati con cui centralizzare le Data Sources, iniettarle in modo efficiente dal punto di vista delle risorse e ottenere così esecuzioni Terraform rapide, stabili e prevedibili, anche con centinaia di istanze di moduli.
Inclusi: tre strategie scalabili, una guida passo-passo collaudata nella pratica e una checklist di Best Practices per moduli infrastrutturali pronti per la produzione.
Best Practice: Alternative scalabili
Soluzione 1 (scenari semplici): Variable Injection Pattern
Invece di usare Data Sources all'interno dei moduli, iniettate i dati necessari come variabili:
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 }
Soluzione 2 (scenari complessi): Structured Configuration Pattern
Per scenari più complessi utilizzate oggetti di configurazione strutturati:
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 } }
Soluzione 3 (scenari molto complessi): Data Proxy Pattern
Per scenari molto complessi create moduli "Data Proxy" dedicati:
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 }
Confronto delle performance
Un esempio concreto da un progetto cliente con provisioning di 50 istanze VM mostra la differenza drastica:
|
Dopo: Variable Injection |
|
|
150 API-Calls | 3 API-Calls |
|
$ time terraform plan real 4m23.415s |
$ time terraform plan real 0m18.732s |
Risultato: 93% in meno di tempo di pianificazione e 98% in meno di API-Calls.
Variable Injection: Guida passo-passo
Passaggio 1: Centralizzare le Data Sources
Obiettivo: Rimuovere tutte le Data Sources dai moduli e raccoglierle centralmente nel modulo root, per consolidare gli API-Calls e creare una singola fonte di verità.
Come: Spostate tutte le Data Sources utilizzate dai moduli nel modulo root. Questo garantisce che ogni informazione venga interrogata una sola volta, indipendentemente da quanti moduli ne abbiano bisogno. In questo modo si riduce il numero di API-Calls da N×M (numero di moduli × numero di Data Sources) a solo M (numero di 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 }
Passaggio 2: Elaborare i dati nei Locals
Obiettivo: Trasformare i risultati grezzi delle Data Sources in una forma consumabile, mantenendo la complessità fuori dai moduli.
Come: Usate Locals per filtrare, ordinare e convertire i risultati delle Data Sources in formati dati strutturati. Questo consente di gestire logiche complesse in modo centralizzato e fornire ai moduli dati già elaborati e puliti. Tramite for-loop ed espressioni condizionali potete implementare anche meccanismi di fallback e logiche di validazione.
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 } }
Passaggio 3: Definire variabili nei moduli
Obiettivo: Creare interfacce chiare per il passaggio dei dati ai moduli, garantendo al contempo sicurezza dei tipi e validazione.
Come: Sostituite le Data Sources nei moduli con variabili tipizzate, dotate di descrizioni significative e regole di validazione. Le definizioni di tipo assicurano coerenza e resistenza agli errori, mentre i blocchi di validazione garantiscono che solo dati validi vengano passati ai moduli. In questo modo i moduli diventano più testabili e indipendenti dalle API del cloud provider.
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." } }
Passaggio 4: Implementare moduli senza Data Sources
Obiettivo: Liberare completamente i moduli da chiamate API esterne e trasformarli in semplici contenitori di definizioni di risorse.
Come: Sostituite tutti i riferimenti a Data Sources nei moduli con riferimenti a variabili. Questo rende i moduli deterministici e prevedibili, poiché operano solo con i parametri passati e non eseguono più chiamate API inattese. Al contempo i moduli diventano testabili in modo indipendente, poiché potete iniettare dati mock tramite le variabili.
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 } }
Passaggio 5: Richiamare i moduli con dati iniettati
Obiettivo: Stabilire il collegamento tra i dati raccolti centralmente e i moduli, per definire un pattern di flusso dati pulito.
Come: Passate i dati elaborati nei locals come parametri ai moduli. In questo modo si chiude il cerchio della Variable Injection: i dati vengono raccolti una sola volta centralmente, elaborati e poi distribuiti in modo mirato ai moduli. Questo tipo di trasmissione esplicita dei dati crea dipendenze chiare, comprensibili sia per le persone che per Terraform stesso.
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 }
Checklist Best Practices
✅ Do's: Pattern scalabili
- [ ] Data Sources centrali: Definire tutte le Data Sources nel root module
- [ ] Variable Injection: Passare i dati ai moduli come variabili
- [ ] Oggetti strutturati: Organizzare dati complessi in oggetti tipizzati
- [ ] Validation Rules: Implementare validazioni per i dati iniettati
- [ ] Documentation: Scrivere descrizioni per le variabili iniettate
- [ ] Local Processing: Elaborare i dati nei locals nel root module
- [ ] Data Proxy Pattern: Usare moduli dati separati per scenari molto complessi
❌ Don'ts: Evitare gli anti-pattern
- [ ] Data Sources nei moduli: Mai usare Data Sources in moduli riutilizzabili
- [ ] Lookups ridondanti: Data Sources identiche in più moduli
- [ ] Filtri complessi: Operazioni di filtro costose in ogni modulo
- [ ] Data Sources annidate: Data Sources che dipendono da altre Data Sources
- [ ] Riferimenti dinamici: for_each su risultati di Data Sources nei moduli
- [ ] Validazione mancante: Usare dati iniettati senza validazione
Monitoring e Debugging
Per monitorare la performance delle Data Sources, potete analizzare l'output di debug di terraform plan alla ricerca di voci relative alle 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
Conclusione: Moduli performanti grazie a un'architettura consapevole
Le Data Sources sono una potente funzionalità di Terraform - ma all'interno dei moduli possono diventare una trappola per la performance. Il Variable Injection Pattern offre una soluzione elegante:
Vantaggi:
- Riduzione drastica degli API-Calls (risparmi superiori al 95%)
- Scalabilità lineare delle performance invece di degrado esponenziale
- Logica dati centralizzata per una migliore manutenibilità
- Dipendenze esplicite invece di chiamate Data Source nascoste
- Migliore testabilità grazie a dati mock iniettabili
La chiave sta nel cambio di paradigma: invece di ottenere i dati quando servono, li si ottiene una volta centralmente e li si distribuisce in modo mirato.
In ICT.technology, applicando coerentemente questi pattern, abbiamo ridotto i tempi di terraform plan da minuti a secondi - anche con centinaia di istanze di moduli.