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

    Terraform @ Scale - Parte 4a: le Data Sources sono pericolose!

    Le Data Sources di Terraform sono uno strumento molto usato per popolare dinamicamente le variabili con valori reali dell'ambiente cloud in uso. Tuttavia, il loro utilizzo in infrastrutture dinamiche richiede una certa lungimiranza. Basta ad esempio un innocuo data.oci_identity_availability_domains all'interno di un modulo - e all’improvviso ogni terraform plan impiega minuti anziché secondi. Perché 100 istanze del modulo significano 100 chiamate API, e il suo provider cloud inizia a limitare il traffico (throttling). Benvenuti nel mondo dell’amplificazione API indesiderata attraverso le Data Sources.

    In questo articolo Le mostrerò perché le Data Sources nei moduli Terraform possono rappresentare un problema di scalabilità. 

     

    Il problema nascosto della scalabilità

    Scenario: la trappola dei 10 secondi

    Ha scritto un modulo Terraform pulito per istanze VM. Per ogni VM ha bisogno di una Availability Domain, quindi utilizza una 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
      }
    }

    A prima vista è tutto corretto. Il modulo funziona perfettamente - per una singola istanza VM, e anche per qualche istanza in più.

    Ma poi il Suo cliente scala a 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
    }

     Il risultato: 50 chiamate API identiche a oci_identity_availability_domains ad ogni terraform plan e terraform apply.

    Ciò che prima durava pochi secondi ora richiede minuti. Con 100 o 200 istanze diventa davvero doloroso.

    Nel caso di nuove istanze VM, questo può ancora essere tollerabile e pianificabile in molti scenari d’uso - ma cosa succede se si tratta di risorse esistenti con cicli di vita brevi? Immagini tempi d’attesa più lunghi durante il provisioning di nodi di cluster per gestire picchi di carico, di backend di Load Balancer, di record DNS o persino di risorse da cui dipendono altre risorse. In tal caso si rischiano lunghi disservizi con un impatto visibile per i Suoi clienti, che nel peggiore dei casi possono risultare persino imprevedibili.

    Perché le Data Sources nei moduli possono essere problematiche

    Le Data Sources si comportano in modo fondamentalmente diverso rispetto alle risorse. Vengono eseguite a ogni terraform plan e terraform apply e non possono essere messe in cache. In un modulo, questo significa:

    Problema 1: Amplificazione delle API

    Ogni istanza del modulo esegue le proprie interrogazioni Data Source, anche se i dati sono identici.

    Ad esempio, i seguenti due moduli che richiamano entrambi lo stesso modulo figlio ./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
    }

    Nel modulo ./modules/server è nascosta, invisibile per l’autore del modulo root, l’interrogazione di una Data Source:


    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
    }

     Se adesso, per esempio, vengono deployate 10 app_server e 50 web_server, questo si traduce in 60 interrogazioni API parallele(!). Nessun Cloud Provider lo consente. Invece, il Suo terraform apply raggiunge i limiti delle API del gateway e il provisioning richiede un’eternità.

    Nel migliore dei casi, i server vengono provisionati in pacchetti più piccoli, ad esempio in gruppi da 10 istanze. In questo caso, il provisioning richiede già almeno sei volte il tempo.

    E ciò non riguarda solo le istanze server mostrate nell’esempio di codice, ma anche tutte le altre risorse ancora presenti nel piano. Il limite API vale per tutto, non solo per un determinato Resource Type come oci_core_instance. Anche altre risorse, normalmente accessibili in tempi brevi o persino in parallelo, devono ora attendere in coda. Questo può portare, nel peggiore dei casi, a Race Conditions o Timeouts.

    Problema 2: Degrado delle prestazioni

    All’aumentare del numero di istanze di modulo, il tempo di pianificazione cresce in modo lineare o addirittura esponenziale. 

    Osservi questo esempio, che legge la lista degli OS-Images disponibili su OCI e filtra una versione specifica ottimizzata per GPU di 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"
      }
    }

     La lista degli OS-Images disponibili è estremamente lunga e viene letta integralmente a ogni istanza VM, per poi essere filtrata dal provider Terraform.  Ciò comporta non solo accessi API non necessari con risposte molto voluminose, ma sovraccarica anche Terraform stesso. 

    Problema 3: Limiti API e Throttling

    L’abbiamo già accennato sopra in questo articolo: i Cloud Provider impongono dei limiti API.

    Troppe chiamate Data Source comportano quindi:

    • Errori HTTP 429 (Too Many Requests)
    • Cicli di Exponential Backoff e Retry
    • Blocco di altre operazioni Terraform
    • Pipelines CI/CD instabili

     

    Anti-Pattern: Esempi che non scalano

    Esempio 1: Il dilemma delle Availability Domain

    Problema: Questo modulo esegue due chiamate API separate a ogni istanza - una per le Availability Domain e una per i Subnet. In un ambiente con 20 moduli database, si generano 40 chiamate API ridondanti per informazioni identiche.

    Perché non scala: Ogni modulo interroga le stesse Availability Domain globali, sebbene queste cambino molto raramente. Inoltre, ogni istanza del modulo esegue lo stesso filtro sui Subnet, che può diventare particolarmente costoso in VPC complessi con molti Subnet.

    Effetto: Con 50 istanze database = 100 chiamate API più 50 filtraggi per dati che si modificano al massimo una volta all'anno.


    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
    }

     

    Esempio 2: Il problema della ricerca immagini

    Problema: Questa implementazione esegue una costosa ricerca immagini con filtro Regex a ogni invocazione del modulo. L'ordinamento e l'elaborazione locale delle liste immagini sono onerosi sia su OCI che localmente in Terraform.

    Perché non scala: Le ricerche immagini sono particolarmente lente, poiché trasferiscono grandi quantità di dati e applicano filtri complessi. Ogni VM esegue la stessa ricerca, sebbene il risultato sia identico per tutte le istanze. L’ordinamento locale con sort() aggrava ulteriormente il problema.

    Effetto: Con 100 istanze VM = 100 costose chiamate API per immagini + 100 operazioni di filtro locali = diversi minuti di tempo di pianificazione per qualcosa che si potrebbe fare in un attimo.


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

     

    Esempio 3: Il problema delle Data Source annidate

    Problema: Questo modulo combina diversi problemi di scalabilità: ricerche di cluster, ricerche di immagini per i nodi worker e catene complesse di dipendenze tra Data Source. Ogni creazione di Node Pool riattiva entrambe le Data Source.

    Perché non scala: Le Data Source annidate generano catene di dipendenza che devono essere attraversate per intero a ogni istanza del modulo. Le ricerche di immagini specifiche per Kubernetes sono particolarmente lente, poiché utilizzano filtri molto specifici con Regex Pattern che devono essere elaborati lato server.

    Effetto: Con 10 Node Pool = 20 chiamate API per informazioni cluster + 10 costose ricerche immagini OKE = tempi di pianificazione che crescono in modo esponenziale con l’aumento del numero di 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
      }
    }

     

    Ora Le abbiamo mostrato diversi esempi di come NON utilizzare le Data Source. Ma questo da solo non basta. Nel prossimo articolo Le mostrerò come evitare elegantemente queste trappole grazie a un'architettura intelligente e all'uso mirato della Variable Injection.

    Perché la migliore Data Source è quella che non viene eseguita affatto o solo una volta.