Salta al contenuto principale

Ho Costruito un Template Infrastrutturale di Produzione con Claude Code in Una Sessione

Indice dei contenuti
In una singola sessione, ho usato Claude Code per costruire, deployare, rompere, fixare, testare la sicurezza e iterare su un template infrastrutturale production-grade che genera progetti AWS Fargate e Lambda completamente configurati. Questa non e’ una storia di AI che scrive boilerplate. E’ una storia di AI usata come partner ingegneristico attraverso 30+ iterazioni di infrastruttura reale su account AWS reali.

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.

Cosa ho chiesto
# 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:

  1. 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.

  2. 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-identity per 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.tf e .env
VPC discovery (da post-generate.py)
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:

Lambda + S3, no ALB, no Aurora
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 generato

Lo 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:

Esempi di test VAPT
# 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/docs

Ha trovato tre problemi e li ha fixati tutti:

FindingFix
CORS * con credenzialiRistretto alla stessa origine, configurabile via env var CORS_ORIGIN
Swagger/OpenAPI docs espostiDisabilitati in ambienti non-local (DEPLOY_ENVIRONMENT != local)
Versione Nginx negli headerAggiunto server_tokens off

Dopo il redeploy, ha rieseguito gli stessi test per verificare i fix.

Cosa Ha Funzionato Bene
#

Suggerimento

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
Lanciare due agenti di ricerca per analizzare i repo di riferimento simultaneamente ha tagliato il tempo di investigazione da un’ora a due minuti. Ogni agente scaricava e analizzava 10+ file indipendentemente.
Iterazione guidata dagli errori
Incollare i messaggi di errore AWS direttamente nella conversazione era il feedback loop piu’ veloce. Claude aveva scritto il Terraform, quindi capiva il contesto senza rileggere i file. Fix, commit, push, deploy, ripeti.
Derivazione scope dal nome profilo
Invece di chiedere agli utenti scope, environment e account AWS separatamente, Claude derivava tutto dal nome del profilo AWS (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
Avere la stessa AI che ha scritto il codice anche ad attaccarlo significava che sapeva esattamente dove guardare. Ha testato la propria validazione JWT, la propria configurazione CORS, il proprio setup nginx. E sapeva come fixare quello che trovava.

Cosa Non Ha Funzionato / Cosa Ho Dovuto Correggere
#

Nota

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)
Claude ha creato il template come repo separato sotto 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
La prima versione aveva 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
L’ID client EntraID era impostato via 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
Claude ha incluso un 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
Il problema piu’ persistente. Le immagini Docker venivano buildare e pushate, ma ECS continuava a eseguire la versione vecchia. La causa: la task definition non cambiava (stesso tag :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-qual deploya su quell’account. Push su gapmgm-italy-prod deploya 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.

Articoli correlati