Ingegneria Difensiva: Sistemi che Resistono alle Catastrofi
Scopri i pattern di ingegneria difensiva che prevengono i disastri in produzione: molly guard, dry-run, soft delete e perché i dialog di conferma non funzionano.

Un ingegnere junior di un'azienda fintech di medie dimensioni ha eseguito uno script di migrazione del database un venerdì pomeriggio. Lo script doveva ripulire i record orfani in un ambiente di staging. Invece si è connesso al database di produzione e ha cancellato 4,2 milioni di record delle transazioni dei clienti. Il backup? Vecchio di tre giorni. L'azienda ha passato le 72 ore successive in piena gestione dell'incidente, riconciliando a mano i record partendo dai log del processore di pagamenti. Il costo totale ha superato i 400.000 dollari tra ore di ingegneria, crediti ai clienti e segnalazioni regolatorie.
Lo script non aveva alcun controllo sull'ambiente. Nessun flag di dry-run. Nessun messaggio di conferma che mostrasse quale database fosse collegato. La stringa di connessione arrivava da una variabile d'ambiente che, per caso, sul portatile dell'ingegnere era impostata su produzione a causa di una sessione di debug di due settimane prima. Ognuno di questi errori era evitabile.
Perché i dialog "Sei sicuro?" non funzionano
Il meccanismo di sicurezza più diffuso nel software è il dialog di conferma. È anche il più inutile. Gli studi sulla fatica da conferma mostrano che, dopo averne incontrati più di qualche volta, gli utenti cliccano su "Sei sicuro?" quasi sempre, senza eccezioni. Il messaggio diventa invisibile: solo un clic in più sulla strada verso ciò che avevi già deciso di fare.
Il problema non è che le conferme siano un'idea sbagliata. È che le conferme generiche non trasmettono alcuna informazione. "Sei sicuro di voler procedere?" non ti dice cosa stai per fare. Confrontalo con: "Stai per eliminare 4.217.893 righe dalla tabella transactions in prod-us-east-1. Digita il nome del database per confermare." La seconda versione ti obbliga a leggere davvero cosa sta succedendo. È la differenza tra un dosso e una molly guard.
Cos'è una molly guard e perché conta
Il termine "molly guard" deriva da una copertura fisica di plastica posta sul Big Red Button dei mainframe per evitare spegnimenti accidentali. Pare prenda il nome dalla figlia piccola di un programmatore, Molly, che continuava a premerlo. In ambito software, una molly guard è qualsiasi meccanismo che rende le azioni distruttive difficili da compiere per sbaglio, lasciandole però possibili quando sono intenzionali.
Il pacchetto Linux molly-guard fa esattamente questo. Intercetta i comandi shutdown, reboot e halt nelle sessioni SSH e ti chiede di digitare l'hostname della macchina che stai per spegnere. Non puoi procedere in automatico: devi dimostrare di sapere su quale macchina ti trovi.
$ sudo reboot
Good grief! You're on a remote SSH session to prod-web-03.
Please type the hostname to confirm: prod-web-03
# Compare with the useless version:
$ sudo reboot
Are you sure? (y/n): y
# *types y without reading, muscle memory*
Il principio è semplice: la conferma deve richiedere un'informazione che dimostri che l'operatore ha capito l'azione. Digitare "y" non dimostra nulla. Digitare l'hostname, il nome della tabella o il numero di record coinvolti dimostra che hai letto l'avviso.
Dry-run: prima mostra, poi agisci
Ogni operazione distruttiva dovrebbe avere una modalità dry-run. Non "dovrebbe" nel senso di best practice, ma nel senso che prima o poi te ne pentirai se non l'hai. Un dry run esegue tutta la logica dell'operazione, registra esattamente cosa accadrebbe e poi si ferma. Nessun effetto collaterale, piena visibilità.
Il pattern è semplice da implementare. Ecco uno script di migrazione con dry-run integrato:
#!/bin/bash
set -euo pipefail
DRY_RUN=${DRY_RUN:-true} # Default to dry-run!
DB_HOST=$(get_db_host)
DB_NAME=$(get_db_name)
echo "Target: $DB_HOST / $DB_NAME"
echo "Mode: $([ "$DRY_RUN" = true ] && echo 'DRY RUN' || echo 'LIVE')"
ROWS=$(psql -h "$DB_HOST" -d "$DB_NAME" -t -c \
"SELECT count(*) FROM orphaned_records WHERE created_at < now() - interval '90 days'")
echo "Records to delete: $ROWS"
if [ "$DRY_RUN" = true ]; then
echo "Dry run complete. Set DRY_RUN=false to execute."
exit 0
fi
# Require explicit confirmation for live runs
read -p "Type '$DB_NAME' to confirm deletion: " CONFIRM
if [ "$CONFIRM" != "$DB_NAME" ]; then
echo "Aborted."
exit 1
fi
psql -h "$DB_HOST" -d "$DB_NAME" -c \
"DELETE FROM orphaned_records WHERE created_at < now() - interval '90 days'"
echo "Deleted $ROWS records."
Nota tre cose: il default è dry-run (devi scegliere di distruggere, non scegliere di non farlo), lo script mostra il database di destinazione e il numero di righe prima di fare qualsiasi cosa, e anche in modalità live richiede di digitare il nome del database. Tre livelli di difesa. Uno qualsiasi di questi avrebbe impedito l'incidente che ho descritto all'inizio.
Le reti di sicurezza per le migrazioni del database
Le migrazioni del database sono una delle operazioni a più alto rischio in qualsiasi pipeline di deploy. Spesso sono irreversibili, operano su uno stato condiviso e una migrazione sbagliata può mandare giù l'intera applicazione. Eppure la maggior parte dei team le tratta come un passaggio di deploy qualunque.
Ecco una gerarchia dei meccanismi di sicurezza, dal base al blindato:
- Controlli sull'ambiente — Lo script di migrazione verifica di puntare all'ambiente previsto prima di eseguire. Sembra ovvio. Non immagini quanto spesso manchi.
- Snapshot pre-migrazione — Crea automaticamente uno snapshot del database prima di ogni migrazione. Se la migrazione fallisce o causa problemi, puoi ripristinare in minuti, non in ore.
- Guardie sul numero di righe — Se una migrazione dovesse modificare più di N righe, richiedi una conferma esplicita. Una migrazione che tocca all'improvviso milioni di righe è quasi sempre un bug.
- Timeout sulle istruzioni — Imposta timeout aggressivi sulle query di migrazione. Una migrazione che gira per 45 minuti sta bloccando le tabelle e degradando le prestazioni. Fallisci in fretta.
- Solo migrazioni retrocompatibili — Imponi che ogni migrazione sia retrocompatibile con il codice attualmente in produzione. Questo significa niente rinomina di colonne, niente aggiunta di NOT NULL senza default, niente eliminazione di colonne ancora referenziate.
# Rails example using strong_migrations gem
class AddPhoneToUsers < ActiveRecord::Migration[7.1]
def change
# This will raise an error with a safe alternative:
# add_column :users, :phone, :string, null: false
#
# Instead, do it in two steps:
add_column :users, :phone, :string
# Then in a separate migration after backfill:
# change_column_null :users, :phone, false
end
end
Strumenti come strong_migrations per Rails, squawk per PostgreSQL e skeema per MySQL individuano i pattern pericolosi prima che arrivino in produzione. Se non usi nulla di simile, ti affidi alla code review per cogliere problemi sottili nelle migrazioni, e i reviewer spesso se li lasciano sfuggire.
Soft delete e finestre di annullamento
Le hard delete sono un impegno permanente preso in un momento di certezza. Il problema è che la certezza spesso sbaglia. I soft delete, cioè contrassegnare i record come eliminati senza rimuoverli davvero, ti danno una finestra per recuperare dagli errori.
-- Hard delete: gone forever
DELETE FROM projects WHERE id = 4872;
-- Soft delete: recoverable
UPDATE projects
SET deleted_at = now(),
deleted_by = 'user:priya'
WHERE id = 4872;
-- Recovery is trivial
UPDATE projects
SET deleted_at = NULL,
deleted_by = NULL
WHERE id = 4872;
Il compromesso è reale: i soft delete complicano le query (serve WHERE deleted_at IS NULL ovunque), aumentano lo storage e possono generare confusione sullo stato reale dei dati. Ma per qualsiasi dato visibile all'utente in cui una cancellazione accidentale è possibile, il compromesso vale la pena. GitHub non elimina davvero il tuo repository per 90 giorni. Slack conserva i messaggi cancellati per motivi di conformità. Il cestino di Gmail si svuota dopo 30 giorni. Non sono incidenti: sono decisioni ingegneristiche deliberate.
Lo stesso principio vale per l'infrastruttura. Invece di terminare un'istanza EC2, fermala prima. Invece di eliminare un database, rinominalo in mydb_deleted_20260315 e imposta un promemoria in agenda per eliminarlo davvero tra due settimane. Il costo di tenere un'istanza ferma o un database rinominato per qualche giorno è trascurabile rispetto al costo di un ripristino da backup.
Sicurezza nei deploy: canary, circuit breaker e rollback
I deploy sono un'altra categoria di azioni distruttive che la maggior parte dei team non tratta con sufficiente cautela. Un deploy sbagliato può mandare giù la produzione con la stessa efficacia di un database eliminato, e succede molto più spesso.
Il minimo indispensabile per la sicurezza dei deploy include:
- Deploy canary — Instrada dall'1 al 5% del traffico verso la nuova versione. Se i tassi di errore salgono, esegui il rollback automatico prima che il raggio d'impatto cresca.
- Trigger di rollback automatico — Definisci soglie per tassi di errore, latenza e fallimenti dei health check che attivino il rollback automatico. Non affidarti a un essere umano che se ne accorga alle 2 di notte.
- Blocco dei deploy durante gli incidenti — Se c'è un incidente in corso, blocca tutti i deploy. L'ultima cosa che ti serve durante lo spegnimento di un incendio è qualcuno che rilascia modifiche non correlate.
- Rollback con un clic — Tornare indietro dovrebbe essere più facile che andare avanti. Se il tuo processo di rollback prevede di collegarti via SSH ai server ed eseguire comandi a mano, non hai un vero processo di rollback.
# Kubernetes progressive delivery with Argo Rollouts
apiVersion: argoproj.io/v1alpha1
kind: Rollout
spec:
strategy:
canary:
steps:
- setWeight: 5
- pause: { duration: 5m }
- analysis:
templates:
- templateName: error-rate-check
- setWeight: 25
- pause: { duration: 5m }
- setWeight: 50
- pause: { duration: 10m }
- setWeight: 100
# Auto-rollback if error rate exceeds 1%
abortScaleDownDelaySeconds: 30
Il filo conduttore è l'impegno progressivo. Non passi dallo 0 al 100% in un solo colpo. Fai piccoli passi, verifichi ciascuno e mantieni la possibilità di tornare indietro in ogni fase. È più lento che fare yolo-deploy su tutte le istanze in una volta sola, ma la prima volta che intercetta un deploy difettoso prima che raggiunga tutti gli utenti, sarai grato per ogni minuto extra.
Prevenzione architetturale: rendere impossibile la cosa sbagliata
I migliori meccanismi di sicurezza non ti chiedono di stare attento. Rendono strutturalmente impossibile fare la cosa sbagliata. È la differenza tra un guardrail e un cartello di avvertimento.
- Infrastruttura immutabile — Se non puoi collegarti via SSH ai server di produzione, non puoi eseguire comandi su di essi per errore. Se i deploy sono sempre nuove istanze da un'immagine nota, non puoi avere deriva di configurazione.
- Accesso con privilegi minimi — Gli ingegneri non dovrebbero avere credenziali di produzione per il database sul proprio portatile. Punto. Usa strumenti di accesso just-in-time che concedano credenziali temporanee con tracciamento degli accessi.
- Credenziali separate per ambiente — Se staging e produzione usano archivi di credenziali diversi, non puoi letteralmente collegarti per errore alla produzione con gli strumenti di staging.
- Protezione dalla cancellazione — AWS consente di attivare la protezione da terminazione sulle istanze EC2 e la protezione dalla cancellazione sui database RDS. Attivale per tutto ciò che conta. È una modifica di configurazione da cinque secondi che previene un'intera classe di errori catastrofici.
Se qualcuno può distruggere per sbaglio la produzione con un solo comando, il problema non è la persona: è il sistema che ha permesso a un solo comando di distruggere la produzione.
Ho visto team rispondere agli incidenti in produzione aggiungendo più documentazione, più checklist, più formazione. Aiutano, ma si basano tutti sul presupposto che gli esseri umani siano perfetti. Non lo sono. La risposta migliore è cambiare il sistema in modo che l'errore non possa avvenire in primo luogo, o, se avviene, che il raggio d'impatto sia contenuto e il ripristino rapido.
Costruire una cultura ingegneristica che mette la sicurezza al primo posto
Strumenti e architettura contano, ma è la cultura a determinare se vengano davvero implementati. I team che considerano i meccanismi di sicurezza un peso burocratico o un overhead li salteranno sotto la pressione delle scadenze, e la pressione delle scadenze è permanente.
La pratica più efficace che ho visto è trattare i meccanismi di sicurezza come un requisito ingegneristico di prima classe, non come un optional. Ogni operazione distruttiva dovrebbe passare una design review che chieda esplicitamente: cosa succede se viene eseguita sul target sbagliato? Cosa succede se viene eseguita due volte? Cosa succede se usa dati obsoleti? Come la annulliamo?
I post-mortem senza colpevoli sono ormai il minimo sindacale. Ma la pratica meno diffusa, che secondo me conta di più, è il pre-mortem. Prima di lanciare una modifica rischiosa, riunisci il team e chiedi: "Supponiamo che sia andata malissimo. Cosa è successo?" Le persone sono sorprendentemente brave a individuare le modalità di guasto quando la domanda è formulata come immaginazione anziché come previsione. I guasti che individuano diventano i meccanismi di sicurezza che costruisci.
Quell'ingegnere junior che ha eliminato il database di produzione? È ancora in azienda. Oggi è uno dei più convinti sostenitori dell'ingegneria difensiva nel team. L'incidente non è stata colpa sua: è stato un fallimento di sistema. E il sistema, adesso, è molto più difficile da rompere.


