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.