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.