Gestisco un team di platform engineering. Ogni nuovo progetto di delivery inizia allo stesso modo: qualcuno copia un repo esistente, passa una giornata a rimuovere il codice applicativo vecchio, rinominare cose, aggiornare i locals di Terraform, misconfigurare i permessi IAM, dimenticare di seedare i secret, e finalmente ottenere un container che parte. Poi passa un’altra giornata sulla CI/CD.
Volevo un template che generasse tutto questo automaticamente. Un comando, qualche domanda, e un progetto deployabile. Il template doveva supportare Fargate e Lambda, esposizione opzionale tramite ALB, Aurora PostgreSQL opzionale, bucket S3 opzionali, SSO EntraID, e auto-discovery delle risorse di networking AWS dal profilo CLI.
Ho costruito tutto con Claude Code in una singola sessione estesa. Ecco come e’ andata davvero.
Come Ho Usato Claude Code#
La sessione e’ durata diverse ore e ha coperto circa 30 commit su due repository. Il pattern di interazione non era “scrivimi un template”. Era iterativo, guidato dalla produzione, e spesso avversariale. Deployavo, incontravo un errore, lo incollavo, e mi aspettavo un fix.
La Fase di Investigazione#
Ho iniziato puntando Claude Code a due repository di delivery esistenti nella nostra istanza GitLab. Ho chiesto di analizzare i pattern: struttura CI/CD, approccio Docker build, organizzazione Terraform, flusso di autenticazione, gestione dei secret.
Claude Code ha scaricato ogni file rilevante via API GitLab, li ha analizzati in parallelo usando subagent, e ha prodotto un riassunto strutturato dei pattern trovati in entrambi i repo. Ci sono voluti circa due minuti. Farlo manualmente avrebbe richiesto un’ora di lettura.
# Parafrasi del prompt reale:
"Analizza questi due repo di riferimento. Scarica .gitlab-ci.yml, Dockerfile,
Makefile, file terraform, start.sh, CLAUDE.md. Estrai i pattern per
CI/CD, Docker, auth, secret, infrastruttura."L’AI ha lanciato due agenti di ricerca in parallelo, ognuno che scaricava e analizzava 10+ file indipendentemente, e ha restituito un’analisi consolidata. Questo ha posto le basi per tutto quello che e’ seguito.
Il Loop Build-Deploy-Break-Fix#
Il pattern piu’ efficace non era la pianificazione. Era deployare su un account AWS reale e lasciare che la realta’ ci dicesse cosa non andava.
sequenceDiagram
participant Me
participant Claude
participant AWS
Me->>Claude: genera progetto dal template
Claude-->>Me: progetto creato
Me->>AWS: make tf-apply
AWS-->>Me: errore: tag duplicati
Me->>Claude: incollo errore
Claude-->>Me: fix: overlap tra provider default_tags e resource tags
Me->>AWS: make tf-apply
AWS-->>Me: errore: IAM CreateRole AccessDenied
Me->>Claude: incollo errore, verifica nomi ruoli
Claude->>AWS: aws iam list-roles --profile ...
Claude-->>Me: fix: ruoli IAM richiedono prefisso scope maiuscolo
Me->>AWS: make tf-apply
AWS-->>Me: deployato, ma ECS task fallisce
Me->>Claude: controlla log
Claude->>AWS: aws logs tail ...
Claude-->>Me: secret senza valore, seeding placeholder
Me->>AWS: task in esecuzione
Questo loop e’ successo circa 15 volte. Ogni ciclo durava 2-5 minuti. Claude Code leggeva l’errore, capiva il contesto (perche’ aveva scritto il Terraform), proponeva e applicava il fix, poi io redeployavo.
Cosa Ho Diretto vs Cosa Ha Deciso Claude#
Io davo direzioni di alto livello. Claude prendeva le decisioni implementative.
Io ho detto: “Fargate SPOT dovrebbe essere il default.”
Claude ha deciso: Impostato FARGATE_SPOT weight=100 base=0, FARGATE weight=1 base=0 come fallback. Aggiunto deployment_circuit_breaker con rollback, stopTimeout=120 per allinearsi alla finestra di reclaim SPOT di 2 minuti, deregistration_delay=30 per draining rapido.
Io ho detto: “Dovremmo sempre chiedere se vogliamo esporre con un URL standard.”
Claude ha deciso: Creato un alb.tf.jinja modulare con risorse count-based (ALB, target group, record Route53, lookup certificato ACM, redirect HTTP-to-HTTPS), incluso solo quando expose_url=true. Ha collegato CORS_ORIGIN dall’URL Route53 nell’environment del task Fargate.
Io ho detto: “Uvicorn sta leggendo quante CPU sono disponibili?”
Claude ha spiegato il problema di os.cpu_count() su Fargate (restituisce le CPU dell’host, non le unita’ allocate), poi lo ha fixato passando FARGATE_CPU come variabile d’ambiente e calcolando i worker da quella. Non ho dovuto specificare il fix.
I Momenti di Migrazione#
La sessione ha incluso due pivot architetturali significativi:
Da Cookiecutter a Copier. Ho chiesto “possiamo evitare il wrapper generate.sh se passiamo a copier?” Claude ha analizzato cosa faceva generate.sh, ha mappato ogni responsabilita’ sulle feature native di copier (domande tipizzate, prompt condizionali, post-task), e ha concluso si, il wrapper poteva essere eliminato. La migrazione ha toccato 59 file ma e’ durata circa 15 minuti.
Da repo concreto a template. Davis e’ iniziato come app hello-world Fargate, poi e’ diventato un template cookiecutter in un repo separato, poi ho detto “Davis E’ il template.” Claude ha spostato tutto in un commit, pulito i riferimenti, e l’app concreta e’ diventata il repo template copier.
Cosa Ha Fatto Concretamente Claude Code#
Auto-Discovery Risorse AWS#
Lo script post-generazione scopre le risorse di networking dal profilo AWS. Claude ha scritto la logica di discovery che:
- Chiama
aws sts get-caller-identityper l’account ID - Lista le VPC e presenta un menu se ne esistono multiple (nessuna assunzione hardcoded sui nomi delle VPC)
- Trova le subnet tramite pattern del tag Name (
*PRIVATE_1,*NAT_1) - Trova i security group tramite pattern del nome (
*-default,*-private) - Patcha i valori scoperti in
locals.tfe.env
vpcs = json.loads(vpcs_json)
if len(vpcs) == 1:
vpc = vpcs[0]
else:
print("Multiple VPCs found:")
for i, v in enumerate(vpcs):
print(f" {i+1}) {v.get('Name', 'unnamed')} ({v['VpcId']})")
choice = input("Select VPC [1]: ").strip() or "1"
vpc = vpcs[int(choice) - 1]Terraform Modulare#
Il template genera solo i file Terraform necessari:
infrastructure/terraform/
main.tf # ECR, IAM, CloudWatch (sempre)
lambda.tf # Funzione Lambda
s3.tf # Bucket S3
locals.tf # Risorse AWS scoperte
secrets.tf # Solo se Aurora selezionato
# fargate.tf -- non generato
# alb.tf -- non generato
# aurora.tf -- non generatoLo script post-generazione rimuove i file in base alle scelte dell’utente. Terraform non vede mai risorse che non dovrebbe gestire.
Assessment di Sicurezza e Fix#
Verso la fine della sessione, ho chiesto a Claude di eseguire un assessment di sicurezza base contro l’app deployata. Ha eseguito 20 test:
# CORS da origine malevola
curl -sI -H "Origin: https://evil.com" https://app.example.com/api/v1/hello
# Attacco JWT alg:none
curl -s -H "Authorization: Bearer eyJhbGciOiJub25l..." https://app.example.com/api/v1/me
# Esposizione documentazione Swagger
curl -s -o /dev/null -w "%{http_code}" https://app.example.com/api/docsHa trovato tre problemi e li ha fixati tutti:
| Finding | Fix |
|---|---|
CORS * con credenziali | Ristretto alla stessa origine, configurabile via env var CORS_ORIGIN |
| Swagger/OpenAPI docs esposti | Disabilitati in ambienti non-local (DEPLOY_ENVIRONMENT != local) |
| Versione Nginx negli header | Aggiunto server_tokens off |
Dopo il redeploy, ha rieseguito gli stessi test per verificare i fix.
Cosa Ha Funzionato Bene#
Il pattern piu’ efficace era deployare su infrastruttura reale immediatamente e usare i messaggi di errore come prompt. La context window di Claude Code manteneva tutto il Terraform che aveva scritto, quindi poteva diagnosticare gli errori istantaneamente.
Ricerca parallela con subagent
Iterazione guidata dagli errori
Derivazione scope dal nome profilo
gapmgm-italy-qual si divide in scope=gapmgm, country=italy, env=qual). Un input, tre valori. Ha gestito anche ambienti composti come dev-ca.Security testing nella stessa sessione
Cosa Non Ha Funzionato / Cosa Ho Dovuto Correggere#
Claude Code aveva bisogno di correzioni piu’ spesso su decisioni organizzative, non tecniche. Implementava le cose correttamente ma nel posto sbagliato, o con assunzioni che non corrispondevano al workflow del team.
Template come repo separato (sbagliato)
blueprints/. Ho dovuto dirgli: “Davis E’ il template.” L’esempio concreto e il template dovrebbero essere la stessa cosa. Questa era una decisione di prodotto che l’AI non poteva conoscere.'qual' hardcoded ovunque
qual hardcoded nei nomi delle variabili (aws_profile_qual), nei nomi dei file (qual.yml), e nei default. Ho dovuto insistere piu’ volte: “qual e’ estratto dal profilo, nessun riferimento hardcoded.” L’AI continuava a seguire le convenzioni del repo di riferimento.Variabili Vite build-time per config runtime
VITE_ENTRA_CLIENT_ID, che viene incorporato nel bundle JavaScript al momento del build. Il Docker build non aveva accesso a questi valori. Il fix di Claude (un endpoint /api/v1/config che restituisce la config pubblica a runtime) era buono, ma non avrebbe dovuto usare variabili build-time in primo luogo.Secret non necessario per flusso PKCE
entra_client_secret in Secrets Manager. Ho dovuto chiedere: “ci serve davvero?” La risposta era no. MSAL con PKCE e’ un flusso public client. Il backend valida gli ID token contro le chiavi JWKS pubbliche di Microsoft. Nessun secret necessario. Rimuoverlo ha eliminato completamente il problema del seeding dei secret.ECS non redeployava con nuove immagini
:latest, stesse env var), quindi terraform non vedeva diff. Il fix e’ stato iniettare BUILD_ID = null_resource.build[0].id nell’environment del container, forzando una nuova revisione della task definition ad ogni apply.Il Risultato#
In una sessione, il template e’ passato da zero a testato in produzione su due account AWS. Lo stato finale:
- Un comando genera un progetto completamente configurato:
copier copy --trust git+ssh://...davis.git ./my-app - Infrastruttura modulare: Fargate o Lambda, ALB/Route53/SSL opzionale, Aurora opzionale, S3 opzionale, SSO EntraID opzionale
- Auto-discovery AWS: VPC, subnet, security group, account ID, registry ECR. Tutto dal nome del profilo.
- Delivery branch-per-account: push sul branch
gapmgm-italy-qualdeploya su quell’account. Push sugapmgm-italy-proddeploya in produzione. Stessa pipeline, target diversi. - Testato per la sicurezza: CORS ristretto, docs nascosti in non-local, versione nginx soppressa, validazione JWT verificata contro token fake e attacchi alg:none
- Auto-scaling: target-tracking su CPU, minimo 1 task, massimo configurabile (default 5), cooldown scale-out di 15 secondi
La sessione ha prodotto circa 60 file nel template, coprendo Terraform, Python (FastAPI), TypeScript (React), Docker, nginx, GitLab CI, e script operativi.
Basandosi sull’esperienza passata, costruire un template cosi manualmente richiede circa due settimane:
- Settimana 1: Analizzare repo di riferimento, progettare la struttura del template, scrivere moduli Terraform, scrivere lo scaffolding applicativo, configurare CI/CD
- Settimana 2: Testare su diversi account, fixare permessi IAM, fixare networking, fixare gestione secret, review sicurezza, documentazione
E avrebbe comunque bisogno di iterazione dopo che il primo team prova ad usarlo.
Una sessione estesa. Deploy reali su account reali. 30+ iterazioni di fix. Test di sicurezza inclusi. Documentazione generata. Template testato con configurazioni multiple (Fargate full-stack, Lambda minimale, Fargate backend-only).
La chiave non era la velocita’ di generazione del codice. Era la velocita’ di iterazione. Deploy, rompi, fixa, deploy. Ogni ciclo durava minuti invece di ore perche’ l’AI manteneva il contesto completo di tutto quello che aveva costruito.
Lezione Chiave#
La cosa piu’ preziosa dell’usare Claude Code per lavori infrastrutturali non e’ che scrive Terraform. E’ che puo’ tenere l’intero sistema in contesto, attraverso Terraform, Docker, Python, TypeScript, nginx e CI/CD, e diagnosticare problemi trasversali istantaneamente.
Quando un task ECS falliva perche’ un secret in Secrets Manager non aveva valore, Claude non vedeva solo l’errore. Capiva che Terraform crea la risorsa secret ma non la versione del secret, che la task definition referenzia l’ARN del secret, e che il fix era aggiungere un aws_secretsmanager_secret_version con un placeholder e lifecycle { ignore_changes }. Quella catena di ragionamento attraverso tre file e due servizi AWS e’ dove sta il vero valore.
Il pattern che funziona: dare direzione di alto livello, deployare su infrastruttura reale presto, usare gli errori come prompt, e lasciare che l’AI iteri. Non cercare di renderlo perfetto in fase di pianificazione. Deployalo e lascia che la produzione ti dica cosa non va.
If you want to go deeper on any of this, I offer 1:1 coaching sessions for engineers working on AI integration, cloud architecture, and platform engineering. Book a session (50 EUR / 60 min) or reach out at manuel.fedele+website@gmail.com.