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

    Terraform @ Scale - Parte 1d: Insidie e best practice in ambienti multi-tenant

    I Remote States sono uno strumento potente per condividere informazioni in modo controllato tra team e tenant. Soprattutto in ambienti cloud complessi con molteplici ambiti di responsabilità, essi creano trasparenza, riutilizzabilità e scalabilità. Allo stesso tempo, comportano dei rischi: stati errati, problemi di accesso e dipendenze non risolte possono compromettere la stabilità dell’intera infrastruttura. Questo articolo mostra come evitare queste sfide e come creare una base per un'infrastruttura affidabile e automatizzata attraverso strutture chiare e pratiche collaudate.

    State-Locking in ambienti Multi-Tenant

    In un ambiente con più team che lavorano contemporaneamente su diverse parti dell’infrastruttura, il meccanismo di state-locking è indispensabile. Senza il locking, può accadere che più applicazioni Terraform tentino di aggiornare in parallelo lo stesso state, sovrascrivendosi a vicenda. E questo finisce quasi sempre in un disastro. È quindi assolutamente necessario un meccanismo che riservi in esclusiva un file di stato a un solo utente, fino a quando questi non ha completato le operazioni di scrittura e lo rilascia per altri processi di accesso.

    Best Practice per lo State-Locking

    • Scegliere un backend con un meccanismo di locking robusto:

      Come già spiegato nell'ultima parte di questa serie di articoli, oltre all’utilizzo di un file locale in un ambiente single-user, solo l’uso di Consul come backend remoto per i file di stato, così come Terraform Cloud ed Enterprise, è supportato e ufficialmente certificato. Terraform supporta anche altri backend come HTTPS o S3, ma questi non rientrano nei contratti di supporto, il loro utilizzo avviene espressamente a proprio rischio e responsabilità, e non tutti supportano un locking robusto e privo di errori. Consul e Terraform Enterprise offrono invece meccanismi di locking affidabili a livello enterprise.

      Con l’uso di Consul, Terraform imposta automaticamente una sessione per proteggere lo state durante i processi di plan e apply.


      terraform {
        backend "consul" {
          address = "consul.example.com:8500"
          path    = "terraform/customer-a"
          lock    = true  # Attivazione esplicita del locking
        }
      }

      Questa funzione di locking non richiede quindi alcuna configurazione manuale di sessioni o altri meccanismi - Terraform se ne occupa automaticamente.

    • Riconoscere e risolvere i lock orfani:

      I lock orfani si verificano spesso quando i processi Terraform si interrompono inaspettatamente (ad es. a causa dell’interruzione di una pipeline CI/CD, di un guasto di rete o di un’interruzione da parte dell’utente).

      Per eliminare automaticamente i lock orfani, si dovrebbe implementare un processo di cleanup preventivo. Nella pratica, è sufficiente un semplice controllo prima dell'esecuzione di Terraform:


      consul lock -delete "terraform/customer-a" || true


      Questo comando rimuove i lock esistenti prima che inizi un nuovo processo Terraform - in modo efficiente e senza un inutile onere gestionale.

    • Configurare automaticamente i timeout di lock: Nella configurazione di Consul (consul.hcl) è possibile definire valori di timeout per le sessioni di lock. Questo garantisce la rimozione automatica dei lock bloccati.


      Valori consigliati per ambienti di produzione sono:


      session_ttl_min = "15m"
      session_ttl_max = "1h"

      In questo modo si evita che lock orfani blocchino a lungo le risorse.

    • Considerare i conflitti di lock nelle pipeline CI/CD:

      Negli ambienti CI/CD possono verificarsi conflitti se più pipeline cercano contemporaneamente di impostare lo stesso lock.

      Un pattern collaudato in questi casi è un meccanismo di retry con una strategia di backoff:


      for i in {1..5}; do
        terraform apply && break
        echo "Rilevato conflitto di lock. Nuovo tentativo tra $((i * 10)) secondi..."
        sleep $((i * 10))
      done

      Questo meccanismo garantisce che il deployment rimanga stabile anche in presenza di lock temporaneamente bloccati. Una spiegazione di cosa fa lo script:

      1. Esegue un ciclo cinque volte: (for i in {1..5})
      2. Ad ogni iterazione tenta di eseguire terraform apply
      3. Se terraform apply ha successo (&&), interrompe il ciclo con break
      4. Se terraform apply fallisce, attende un certo tempo (sleep) prima di tentare nuovamente
      5. Il tempo di attesa aumenta ad ogni tentativo ($((i * 10))), cioè 10, 20, 30, 40 e 50 secondi


      In questo modo il provisioning tramite Terraform viene ripetuto automaticamente nel caso si verifichino problemi temporanei come errori di rete, limiti delle API o conflitti di risorse che potrebbero causare il fallimento di un singolo tentativo.

      Un sistema di locking robusto riduce il rischio di errori e impedisce che le esecuzioni Terraform si blocchino a vicenda. Con le misure descritte, proteggete in modo affidabile il vostro ambiente da lock orfani e ritardi inutili.

    Versioning dello State

    Una solida gestione delle versioni è indispensabile. In caso di problemi, consente di tornare facilmente a versioni precedenti. Terraform Cloud/Enterprise offrono questa funzionalità di default; con Consul è invece necessario implementare meccanismi aggiuntivi di backup (snapshot).

    Gestione delle dipendenze tra State e prevenzione dei cicli

    Le dipendenze tra State possono rapidamente diventare complesse e, nel peggiore dei casi, portare a dipendenze circolari che rendono impossibile l’aggiornamento dell’infrastruttura.

    Poiché le dipendenze da Remote State in terraform_remote_state sono statiche, le configurazioni dipendenti devono essere aggiornate manualmente ogni volta che cambiano gli output rilevanti.

    Best Practice per prevenire i cicli:

    • Struttura gerarchica delle dipendenze: Risorse globaliRisorse regionaliRisorse specifiche del tenantRisorse specifiche dell'applicazione. Questa chiara dipendenza unidirezionale previene i cicli.
    • Utilizzare riferimenti indiretti: Se un riferimento diretto causa un ciclo, passare l'informazione attraverso un livello intermedio:
      # Invece di un riferimento diretto da A → C e C → A:
      # A → B → C (con B come intermediario)
      # Nella configurazione B:
      output "information_from_a" {
      value = data.terraform_remote_state.a.outputs.needed_value
      }




      # Nella configurazione C:
      data "terraform_remote_state" "b" {
      # ...
      }
      local {
      value_from_a = data.terraform_remote_state.b.outputs.information_from_a
      }

    • Utilizzare Data Sources invece di Remote State: Se possibile, preferire le Data Sources native. Queste sono più dinamiche e riducono le dipendenze tra State.

      # Invece di:
      data "terraform_remote_state" "network" {
        # ...
      }
      
      # Meglio, se possibile:
      data "oci_core_subnet" "app_subnet" {
        subnet_id = "ocid1.subnet.oc1..."
      }

      Tuttavia: utilizzare le Data Sources in modo mirato ed evitare il loro uso in for_each o altri cicli, poiché generano una chiamata API ad ogni esecuzione e quindi scalano male. L’uso di Data Sources all’interno dei moduli base non è quindi una buona pratica nella maggior parte dei casi. Ma anche i moduli richiamati dai root module non dovrebbero accedere ai file di stato. Per questo motivo, utilizzate le vostre Data Sources anche a livello di root module. 

    Minimizzazione delle informazioni nello State per migliorare le performance

    Più grande è il vostro file di stato, più tempo richiede ogni terraform plan. Terraform legge l’intero State, e in ambienti complessi ciò può rallentare sensibilmente l’esecuzione.

    Best Practice per l’ottimizzazione dello State:

    • Suddivisione granulare dello State: Una buona regola empirica: uno State non dovrebbe contenere più di 100 - 250 risorse, per garantire tempi di pianificazione accettabili. Applicate il Goldilocks-Principle (principio di Goldilocks) - ne parleremo più avanti in un altro articolo di questa serie.
    • Attenzione agli output complessi: Limitate gli output a ciò che è essenziale e necessario:

      # Da evitare:
      output "entire_vcn" {
        value = oci_core_vcn.main
      }
      
      # Meglio:
      output "vcn_essential_info" {
        value = {
          id         = oci_core_vcn.main.id
          cidr_block = oci_core_vcn.main.cidr_block
        }
      }

    • Evitare output sensibili, se non necessari: Essi aumentano la dimensione dello State e Terraform memorizza metadati aggiuntivi. Inoltre, gli output sensibili non possono essere utilizzati in altri moduli, quindi in genere è possibile evitarli.

    Debugging di dipendenze complesse nello State

    Il debugging di problemi relativi al Remote State può essere complicato. Ecco alcuni suggerimenti:

    • Attivare log dettagliati:

      export TF_LOG=DEBUG
      export TF_LOG_PATH=./terraform.log

    • Integrazione della validazione dello State nelle pipeline CI/CD:

      terraform state pull | jq '.outputs.network_config.value | has("vcn_id")'

    • Utilizzare terraform console:

      $ terraform console
      > data.terraform_remote_state.network.outputs.subnet_ids

    Provider Aliasing per scenari complessi

    Quando si lavora con più account (o con più regioni all’interno dello stesso account), il Provider Aliasing semplifica notevolmente la configurazione:


    provider "oci" {
      alias  = "global"
      region = "eu-frankfurt-1"
    }
    
    provider "oci" {
      alias  = "customer_a"
      region = "eu-amsterdam-1"
    }
    
    module "customer_a_instance" {
      source     = "./modules/instance"
      provider   = oci.customer_a
      subnet_id  = module.global.outputs.subnet_id
    }

    Conclusione

    Padroneggiare queste best practice vi aiuterà a gestire un’infrastruttura multi-tenant in modo stabile ed efficiente. Con l’esperienza, vi renderete conto che questi modelli non solo evitano problemi, ma migliorano anche la collaborazione tra i team e la qualità complessiva della vostra infrastruttura.