🔥 Un solo terraform destroy - e all’improvviso 15 sistemi clienti sono offline 🔥
Il "distruttore del venerdì pomeriggio" ha colpito di nuovo.
In questo articolo in due parti analizziamo uno dei più grandi problemi strutturali dell’infrastruttura, ma anche uno dei rischi più sottovalutati dell’Infrastructure-as-Code dal punto di vista del management. Infatti aiutiamo le aziende a ridurre sistematicamente i rischi legati al Blast Radius.
Perché la migliore esplosione è quella che non avviene affatto.
L’anatomia di una catastrofe Terraform
Scenario: Il distruttore del venerdì pomeriggio
Sono le 16:30 di venerdì. Uno sviluppatore vuole ripulire rapidamente un ambiente di test ed esegue terraform destroy. Quello che non sa: la VPC da eliminare è referenziata tramite Remote State da altri tre state che gestiscono carichi di lavoro in produzione per diversi clienti.
⚠️ Il risultato:
- 15 sistemi clienti si guastano simultaneamente
- Le connessioni al database si interrompono
- I load balancer perdono i loro target
- I sistemi di monitoraggio segnalano un’interruzione totale
- Il weekend è rovinato
Questo scenario non è inventato - accade nella realtà più spesso di quanto si pensi. La causa risiede solitamente in una combinazione di:
- State monolitici: Troppe risorse in un singolo state creano dipendenze ingestibili
- Responsabilità non chiare: Nessuno sa esattamente chi è responsabile di quali risorse
- Mancanza di guardrail: Nessuna protezione tecnica contro eliminazioni accidentali
L’Infrastructure-as-Code promette controllo, riproducibilità ed efficienza. Ma se gli state di Terraform sono mal strutturati, un innocuo terraform destroy o terraform apply può trasformarsi in una catastrofe. Il cosiddetto Blast Radius - il raggio degli effetti indesiderati - può rapidamente superare ogni aspettativa.
Dopo il nostro ultimo articolo sul principio Goldilocks per la dimensione ottimale degli state, ora ci dedichiamo a uno degli aspetti più critici nello scalare Terraform: la gestione del Blast Radius. Perché la domanda non è se qualcosa andrà storto, ma quanto sarà coinvolto quando accadrà.
Cos'è il Blast-Radius in Terraform?
Il termine Blast Radius proviene originariamente dalla tecnologia degli esplosivi e descrive il raggio entro cui un'esplosione causa danni. Nel mondo di Terraform, indica l’ambito dell’infrastruttura che può essere interessato da modifiche, errori o guasti.
La connessione tra le singole risorse è costituita dalla dipendenza reciproca tra esse. Terraform crea internamente un Dependency Graph dinamico, che non è sempre completamente visibile o intuitivamente corretto per l’ingegnere.
Alcune dipendenze sono ovvie. Per esempio, un server virtuale necessita anche di una rete in cui essere collocato. Questa rete viene solitamente collegata nella risorsa di calcolo relativa al server da parte dell’ingegnere, quindi questa dipendenza non è solo logica, ma anche documentata nel codice del modulo Terraform.
Ma cosa succede con le dipendenze indirette e generate dinamicamente, che emergono solo a seguito dei calcoli interni di Terraform? Per esempio, l’indirizzo IP di un servizio web non è assegnato direttamente al server virtuale, bensì a una risorsa di indirizzi IP, che a sua volta è collegata a una risorsa VNIC, che forse - o forse no - è assegnata a uno o più server virtuali, o a un load balancer, o magari a un firewall. A questo punto diventa già più difficile riconoscere al volo come si ripercuote la modifica di una subnet mask nel sottorete da cui proviene l’indirizzo IP assegnato.
Le infrastrutture IT, soprattutto a causa di una buona dose di crescita storica e di modifiche nel tempo, diventano rapidamente più complesse di quanto previsto in fase di progettazione. Ci si ritrova quindi a dipendere dal fatto che ogni ingegnere e l’automazione nella pipeline leggano effettivamente il piano di esecuzione generato da terraform plan che scorre nella finestra della shell, lo comprendano completamente e lo verifichino per correttezza - cosa che, a causa della mancanza di una visione globale, di tempo e motivazione, quasi nessun operatore umano fa, e la pipeline media di CI/CD ancor meno.
Visualizzazione del Blast-Radius:
- State piccoli = poche dipendenze dirette e indirette = Blast-Radius ridotto: un errore coinvolge solo poche risorse
- State grandi = più dipendenze dirette e indirette = Blast-Radius esteso: un errore può avere effetti ampi e imprevedibili.
Nella pratica, un Blast-Radius fuori controllo si manifesta volentieri in vari modi:
- Cancellazioni indesiderate: Un terraform destroy rimuove non solo le risorse desiderate, ma anche componenti dipendenti in altri sistemi. In altre parole, si interviene su un angolo del paesaggio infrastrutturale, e scoppia tutto in un’altra area completamente inaspettata. Questo accade principalmente negli state grandi e monolitici.
- Guasti a cascata: Una modifica a una risorsa centrale causa malfunzionamenti in servizi o risorse apparentemente indipendenti. Ciò può accadere non solo a causa di dipendenze, ma anche per l’organizzazione errata delle risorse in tipi di dati sbagliati, cioè errori nel codice. Una volta, per esempio, avevo un cliente con alcune centinaia di record DNS definiti ciascuno come risorsa propria utilizzando count invece di for_each in una lista. In seguito a un terraform apply, uno dei primi elementi di questa lista è stato eliminato ... e diverse centinaia di record DNS successivi pure, perché l’indice è cambiato e Terraform, in questo caso, elimina e ricrea la risorsa. La situazione è peggiorata perché il fornitore della Public Cloud, a causa di rigidi limiti sulle API, ha permesso la creazione dei nuovi record DNS solo molto lentamente e in blocchi da massimo 5 per volta, invece che rapidamente e in un’unica operazione. Così quasi tutti i servizi non coinvolti del cliente sono rimasti inattivi per oltre un’ora. Come si spiega un simile incidente al Change Management e al responsabile della comunicazione?
- Dipendenze cross-state: Le referenze a Remote State causano effetti collaterali inaspettati durante apply o destroy. Ciò significa che un’istanza Terraform lavora con dati provenienti da un file di state diverso dal proprio. "Ops, in quella rete c’era ancora un Bare Metal in produzione di un altro cliente …"
- Sovrapposizioni tra tenant: Le modifiche per un cliente coinvolgono accidentalmente anche altri tenant. Si pensi, per esempio, a regole firewall gestite dinamicamente in un ambiente condiviso.
- Il caos assoluto: Le cose si fanno davvero interessanti quando si verificano contemporaneamente più degli scenari qui descritti. Se siete fortunati, l’intero sito è stato costruito in modo ridondante altrove.
Tipici scenari di Blast-Radius
1. Il disastro del Remote State
# State A: Network Foundation
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
output "vpc_id" {
value = aws_vpc.main.id
}
# State B: Application (fa riferimento a State A)
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "terraform-states"
key = "network/terraform.tfstate"
}
}
resource "aws_instance" "app" {
[...]
subnet_id = data.terraform_remote_state.network.outputs.private_subnet_ids[0] [...]
}
⚠️ Il problema: Se la VPC in State A viene distrutta, State B perde le sue referenze. Questo porta a stati inconsistenti e richiede un intervento manuale con terraform import o terraform state rm per la riparazione. In molte aziende ciò comporta anche problemi organizzativi, poiché le reti e le istanze server sono spesso gestite in silos separati e la comunicazione costruttiva e tempestiva in caso di incidente non è sempre garantita.
2. Il caos Multi-Tenant
Uno scenario particolarmente critico si verifica quando più tenant (clienti) condividono risorse nello stesso state:
Esempio: Shared Infrastructure State
├── shared-infrastructure/ │ ├── customer-a-resources.tf │ ├── customer-b-resources.tf │ ├── customer-c-resources.tf │ └── shared-services.tf
Una modifica per Customer A può influenzare involontariamente anche Customer B e C. Ancora peggio: un terraform destroy potrebbe coinvolgere tutti i clienti contemporaneamente.
Per questo è molto importante isolare completamente tutti i tenant l’uno dall’altro. Sì, inizialmente può costare di più. Ma un’interruzione costa di più.
3. L’inferno delle dipendenze
Catene complesse di dipendenze tra risorse possono portare a effetti a cascata imprevedibili:
Catena di dipendenze:
Una modifica all’inizio della catena può avere ripercussioni fino alla fine. Questo comporta effetti collaterali inaspettati, principalmente a causa di dipendenze invisibili nel codice, calcolate da Terraform.
Qui la responsabilità ricade in realtà sui rispettivi provider di Terraform. Le dipendenze richiedono infatti che il provider non inoltri semplicemente alla API del cloud provider i valori di input senza verificarli o correlarli, ma che implementi anche un minimo di logica e buon senso. E credetemi: molti provider, da questo punto di vista, sono davvero, davvero scarsi.
Minimizzazione del Blast Radius: strategie e best practice
Probabilmente ora vi ho fatto venire molte rughe di preoccupazione sulla fronte. Quindi parliamo ora di come evitare questi problemi.
Approccio 1: Segmentazione dello State in base al Blast Radius
Il metodo più efficace per controllare il Blast Radius è una segmentazione intelligente dello state. Nell’architettura di una soluzione di automazione ci orientiamo ai seguenti principi:
Isolamento secondo livello di impatto:
- Critical States: Infrastruttura core con alto Blast Radius (Network, IAM, DNS)
- Service States: Risorse specifiche per l’applicazione con Blast Radius medio
- Ephemeral States: Risorse temporanee con Blast Radius minimo
# Esempio di una struttura ottimizzata per il blast-radius in AWS
├── foundation/ # Infrastruttura critica │ ├── network-core/ # VPC, Transit Gateways (alto Blast Radius) │ ├── security-baseline/ # IAM, KMS Keys (alto Blast Radius) │ └── dns-zones/ # Route53 Zones (medio Blast Radius) │ ├── platform/ # Platform Services │ ├── kubernetes-cluster/ # EKS/OKE Cluster (medio Blast Radius) │ ├── databases/ # RDS, DocumentDB (medio Blast Radius) │ └── monitoring/ # CloudWatch, Grafana (basso Blast Radius) │ └── applications/ # Livello applicativo ├── frontend-dev/ # Development Environment (basso Blast Radius) ├── frontend-prod/ # Production Environment (medio Blast Radius) └── batch-jobs/ # Batch Processing (basso Blast Radius)
Importante: è fondamentale considerare anche la Rate of Change ove possibile. Questo significa evitare che gli state soggetti a modifiche frequenti siano referenziati da altri state che, invece, non devono cambiare spesso. Se una risorsa si modifica regolarmente ogni qualche settimana o mese, forse non dovrebbe ricoprire un ruolo critico nell’infrastruttura e dovrebbe piuttosto essere spostata in uno Ephemeral State separato.
Un’alta frequenza di modifica e l’infrastruttura critica si escludono a vicenda - ma stabilire i confini è una vostra decisione, e questa decisione va presa e documentata il prima possibile.
Parleremo della Rate of Change e di come tenerla sotto controllo in un articolo successivo di questa serie.
Approccio 2: Dependency Inversion con Remote State
Invece di creare dipendenze dirette, si può mitigare la situazione mediante Data Sources e Lifecycle Preconditions. Lasciate che vi spieghi con un esempio.
Il problema: dipendenze dirette (Anti-Pattern)
# Anti-Pattern: Dipendenza diretta resource "aws_instance" "app" { subnet_id = aws_subnet.main.id }
Analizziamo questo punto più nel dettaglio:
- L’istanza EC2 fa riferimento direttamente a una risorsa Subnet nello stesso state. Significa che entrambe le risorse sono collegate nello stesso file terraform.tfstate
- Un terraform destroy sulla subnet elimina entrambe le risorse contemporaneamente. Questo può accadere anche con un terraform apply, se una modifica impone l’eliminazione temporanea e la ricreazione della subnet, Terraform distruggerà e ricreerà anche l’istanza EC2.
- Ne consegue che anche le modifiche alla subnet possono influenzare involontariamente l’istanza EC2.
Questo è pericoloso perché:
- Alto Blast Radius: Una modifica alla rete può distruggere l’applicazione, nel peggiore dei casi anche i dati memorizzati localmente sull’istanza EC2.
- Cicli di vita accoppiati: La subnet e l’istanza EC2 devono sempre essere gestite insieme. Una modifica alla rete implica automaticamente una change request anche per l’applicazione.
- Conflitti di responsabilità: Il team di rete e il team applicativo rischiano di bloccarsi a vicenda.
- Problemi di rollback: In caso di errore, non è più possibile ripristinare l’istanza EC2 dal backup, poiché la configurazione della rete salvata potrebbe non essere più compatibile con la situazione attuale.
Una soluzione parziale: Dependency Inversion (Best Practice)
Contro le modifiche (ovvero le mutazioni) si è ancora in gran parte impotenti. Ma si può proteggere la propria EC2-Instanz da un destroy accidentale tramite la Dependency Inversion, scollegandola dalle reti.
# App State (consuma gli output del Network) data "terraform_remote_state" "network" { backend = "s3" config = { bucket = "terraform-states" key = "foundation/network/terraform.tfstate" } }
Cosa cambia adesso?
- La VPC e la subnet si trovano in uno state separato (foundation/network/).
- L’app non fa più riferimento diretto alla subnet, ma utilizza la Remote State Data Source.
- Quindi non esiste più una dipendenza diretta da risorsa a risorsa tra l’istanza EC2 e la subnet.
L’app non dipende più direttamente dalla risorsa Subnet, ma da un output astratto del Network State. Indirettamente però ancora sì.
Le dipendenze da Remote State non sono una protezione completa contro il Blast Radius. Rappresentano piuttosto un "Circuit Breaker" che vi concede tempo per reagire, ma alla fine le dipendenze devono comunque essere gestite.
Per questo motivo, questo è solo il primo passo. Infatti, se la risorsa di rete nel Remote State cambia, ciò ha ancora effetti sulla nostra istanza EC2. Perciò facciamo un passo in più, implementiamo una validazione e, se necessario, tiriamo il freno di emergenza.
La validazione e la programmazione difensiva
Nel file locals.tf definiamo:
locals { vpc_id = try(data.terraform_remote_state.network.outputs.vpc_id, null) subnet_ids = try(data.terraform_remote_state.network.outputs.subnet_ids, []) }
Cosa succede qui?
Implementiamo un comportamento difensivo: la funzione try() previene errori se il Remote State non esiste. In tal caso, vpc_id riceverà il valore null e subnet_ids una lista vuota (una VPC può contenere più subnet, quindi è una lista).
Ora esistono diversi possibili approcci.
Opzione 1: Detect-Only Pattern
Dichiariamo ora la nostra istanza EC2 con un lifecycle:
resource "aws_instance" "app" {
lifecycle { precondition { condition = can(data.terraform_remote_state.network.outputs.vpc_id) error_message = "WARNING: Network state not available - using fallback configuration" } }
subnet_id = try( data.terraform_remote_state.network.outputs.private_subnet_ids[0], "subnet-fallback-12345" # Fallback su un ID di subnet noto e stabile ) }
L'esempio è fortemente semplificato per chiarire il principio di base. Nella pratica può certamente essere migliorato e raffinato. Ma il procedimento resta simile:
- Se nel Remote State non è più definita una VPC (local.vpc_id diventa null), terraform plan si interrompe con il messaggio di avviso come errore.
- Se la VPC esiste ma la Subnet ID no, viene effettuato un fallback su un’altra Subnet ID. Questa può essere hardcodata (brutta soluzione) oppure provenire da un’altra fonte – per semplicità mostro qui la prima.
Nel caso concreto, però, ciò comporterebbe uno spostamento del server in un’altra subnet, cioè un'operazione di destroy seguita da una nuova creazione. Questo non ci aiuta molto, almeno non per questo tipo di risorsa. Potrebbe quindi essere opportuno definire anche per la Subnet ID una lifecycle precondition e forzare un'interruzione durante la fase di plan. Oppure, in alternativa, si può semplicemente vietare a Terraform di distruggere l’istanza EC2 – ed è proprio ciò che vedremo adesso.
Opzione 2: Prevent Destroy Pattern
Così si impedisce a Terraform di distruggere una risorsa, facendo invece fallire l’operazione con un messaggio di errore:
resource "aws_instance" "app" { lifecycle { prevent_destroy = true # Impedisce l'eliminazione accidentale precondition { condition = can(data.terraform_remote_state.network.outputs.vpc_id) error_message = "ATTENZIONE: stato della rete non disponibile - utilizzo della configurazione di fallback" } } subnet_id = try( data.terraform_remote_state.network.outputs.private_subnet_ids[0], "subnet-fallback-12345" # Fallback su un ID di subnet noto e stabile ) }
L’unica riga aggiunta qui è prevent_destroy = true, il resto dell’esempio è identico a quello precedente.
Esiste anche una terza variante che può essere combinata con questa soluzione.
Opzione 3: Explicit Confirmation Pattern
Qui si imposta una variabile booleana su true per autorizzare esplicitamente la distruzione e ricreazione automatica della risorsa.
Avvertenza: Questo non protegge dalla distruzione dell’istanza EC2 in caso di modifiche alle reti! Questa espressione nel ciclo for_each serve solo ad autoproteggersi: tramite var.confirm_network_dependency_removal dichiarate esplicitamente di essere consapevoli di questo rischio. Seguite questo approccio solo se una pipeline CI/CD stabile è per voi più importante dell’esistenza dell’istanza del server, ad esempio in ambienti di sviluppo o dove vige una rigida politica di Immutability.
variable "confirm_network_dependency_removal" { type = bool default = false } data "terraform_remote_state" "network" { backend = "s3" config = { bucket = "terraform-states" key = "foundation/network/terraform.tfstate" region = "eu-central-1" } } locals { vpc_ok = can(data.terraform_remote_state.network.outputs.vpc_id) subnet_ok = can(data.terraform_remote_state.network.outputs.private_subnet_ids[0]) deploy_app = ( local.vpc_ok || var.confirm_network_dependency_removal ) && local.subnet_ok app_instances = local.deploy_app ? { "main" = true } : {} } resource "aws_instance" "app" { for_each = local.app_instances ami = "ami-12345678" instance_type = "t3.micro" subnet_id = data.terraform_remote_state.network.outputs.private_subnet_ids[0] lifecycle { prevent_destroy = true precondition { condition = local.vpc_ok error_message = "VPC mancante – deployment negato" } precondition { condition = local.subnet_ok error_message = "Subnet mancante – deployment negato" } } tags = { Name = "BlastRadiusProtected" } }
Approccio 3: Guardrail del Blast-Radius con Lifecycle Rules
Come già accennato nel secondo approccio: Terraform offre diversi meccanismi per impedire cancellazioni accidentali.
Opzione 1: prevent_destroy per risorse critiche
Lo abbiamo già illustrato più sopra, ma vale la pena ricordarlo:
# Prevent Destroy per risorse critiche resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" lifecycle { prevent_destroy = true } tags = { Name = "Production-VPC" Environment = "production" BlastRadius = "high" } }
Notate bene cosa stiamo facendo qui: proteggiamo le risorse dipendenti come subnet ed EC2 dichiarando la VPC una risorsa critica e impedendone l’eliminazione. Questo è naturalmente possibile solo se la VPC è gestita da noi e non proviene da un Remote State.
Opzione 2: create_before_destroy per risorse stateful
Quando un provider definisce che non è possibile aggiornare alcuni argomenti di una risorsa in modo dinamico, essa deve essere distrutta e ricreata. Poiché molte risorse in un’infrastruttura possono esistere una sola volta (come gli indirizzi IP), Terraform tende a distruggere prima e ricreare poi.
È però possibile consentire la distruzione solo a condizione che la nuova risorsa venga prima creata:
resource "aws_db_instance" "main" { lifecycle { create_before_destroy = true ignore_changes = [ password, # Ignora modifiche alla password per evitare sostituzioni inutili ] } [...] }
Tuttavia, è responsabilità vostra garantire che non vi siano conflitti. La vecchia e la nuova risorsa devono poter esistere contemporaneamente; nel caso di un indirizzo IP, che deve essere unico nella rete, potrebbe avere senso creare la nuova risorsa senza IP, e solo in un secondo momento trasferire l’IP dalla vecchia alla nuova. Questo aumenta la complessità e va applicato in modo selettivo.
Opzione 3: Conditional Destroy con preconditions
La terza opzione consiste nell’usare ancora una lifecycle precondition per interrogare un flag che regola se una risorsa possa essere eliminata o meno, causando l’interruzione del terraform plan con un messaggio di errore:
variable "confirmed_destroy" { type = bool default = false description = "Consente esplicitamente la distruzione delle risorse se impostato su `true`." } resource "aws_instance" "app" { lifecycle { precondition { condition = var.environment != "production" || var.confirmed_destroy == true error_message = "Le risorse in produzione richiedono conferma esplicita per essere distrutte." } } [...] }
Approccio 4: Strategia professionale con Policy-as-Code (Terraform Enterprise)
I metodi precedenti hanno un tratto comune: sono tutti dei workaround parziali e richiedono compromessi. Se cercate una soluzione professionale, allora è necessario ricorrere a Terraform Enterprise. Per ambienti enterprise, Terraform Enterprise offre funzionalità avanzate di Policy-as-Code tramite Sentinel:
# Sentinel Policy: Controllo del Blast Radius import "tfplan/v2" as tfplan # Impedisce la cancellazione di risorse con alto Blast Radius high_blast_radius_resources = [ "aws_vpc", "aws_route53_zone", "aws_iam_role" ] main = rule { all tfplan.resource_changes as _, resource { resource.type not in high_blast_radius_resources || resource.change.actions not contains "delete" } }
In questo esempio, innanzitutto VPC, IAM Role e Route 53 DNS Zones vengono classificate come infrastruttura critica ad alto Blast Radius.
Successivamente, qualsiasi tentativo di cancellare risorse di questi tipi viene intercettato da una regola. Se tale regola viene attivata, Terraform interrompe l’esecuzione subito dopo la fase di plan e rifiuta il terraform apply.
Conclusione (provvisoria)
Il Blast Radius in Terraform va preso in considerazione fin dall’inizio, altrimenti si incorre in rischi rilevanti. Il rischio può essere mitigato parzialmente con la versione gratuita di Terraform attraverso alcuni accorgimenti e workaround nei moduli, ma non può essere eliminato in modo affidabile.
Se cercate una soluzione a livello enterprise, che possa essere applicata e auditata a livello aziendale, dovreste considerare Terraform Enterprise con modulo Sentinel. Se la vostra gestione del rischio è in grado di quantificare costi e impatto di un’interruzione, allora la licenza Terraform Enterprise può essere vista come un’assicurazione, il cui costo è confrontabile con il beneficio ottenibile.
Prospettive
Abbiamo ora parlato in dettaglio di ciò che possiamo fare per ridurre il rischio di un Blast Radius eccessivo.
Ma cosa facciamo se accade comunque? Quali sono le possibilità per contenere i danni e riportare l’infrastruttura in uno stato operativo?
Lo vedremo nella prossima parte di questa serie di articoli.