Architettura
cloudrift segue un’architettura DDD (Domain-Driven Design) con il pattern Ports & Adapters (Hexagonal Architecture). Le dipendenze puntano sempre verso l’interno, dalla CLI attraverso il layer applicativo fino al domain.
Struttura del monorepo
cloudrift/
├── apps/
│ └── cli/ # Entry point CLI (Commander.js)
├── libs/
│ └── cloud-cost/
│ ├── domain/ # Entità, ports, policies
│ ├── application/ # Use cases (AnalyzeCloudWasteUseCase)
│ └── infrastructure/
│ └── aws-adapter/ # Implementazione scanner AWS SDK v3
├── packages/ # Shared kernel, utilities
├── docs/ # Documentazione tecnica (EN + IT)
├── policy/ # Policy OPA di esempio
├── nx.json
├── pnpm-workspace.yaml
└── tsconfig.base.json
Layer
Domain (libs/cloud-cost/domain)
Contiene la logica di business pura, senza dipendenze esterne:
- Entità:
WastedResourcee le sue specializzazioni per ogni tipo di risorsa - Ports (interfacce):
WasteScannerPort,PricingPort - Policies: waste policies con grace period e tag di esclusione (base class riusabile)
- Value Objects:
ResourceKindunion type (il compilatore guida quando aggiungi un nuovo tipo)
Application (libs/cloud-cost/application)
AnalyzeCloudWasteUseCase— il coordinatore. Generico sugli scanner registrati: itera iWasteScannerPort, aggrega i risultati, applica le policy.
Infrastructure (libs/cloud-cost/infrastructure/aws-adapter)
- Scanner: un’implementazione di
WasteScannerPortper ogni tipo di risorsa (EBS, EC2, RDS, ecc.) - Usa AWS SDK v3 per le chiamate API e CloudWatch per le metriche di attività
CLI (apps/cli)
- Parsing argomenti con Commander.js
- Formatters: table, JSON, markdown, PDF
- Composition root: registra gli scanner e istanzia il use case
- Exit codes: 0 (ok), 2 (budget superato)
Perché questa architettura
- Testabilità: il domain non dipende da AWS SDK, si testa con mock
- Estensibilità: nuovi servizi = nuovi scanner, zero modifiche al use case
- Multi-cloud path: per supportare GCP/Azure, basta aggiungere nuovi adapter che implementano
WasteScannerPort - Separation of concerns: ogni layer ha responsabilità chiare e ben definite
Bounded context
Il progetto ha quattro bounded context, ciascuno con la stessa struttura libs/<context>/{domain,application,infrastructure}:
| Contesto | Comando | Cosa trova |
|---|---|---|
| cloud-cost | analyze |
Risorse sprecate con stima dei costi (44 scanner) |
| cost-analytics | cost, trend |
Confronto e trend di spesa via Cost Explorer |
| dead-resources | dead-resources |
Risorse morte/inutilizzate a costo $0 (18 check) |
| resource-security | resource-security |
Configurazioni di sicurezza a rischio (29 check) |
Condividono solo shared/kernel. L’isolamento dei layer (domain → application → infrastructure) è imposto da @nx/enforce-module-boundaries — non solo per convenzione.
Server MCP
cloudrift mcp è il secondo input adapter accanto a Commander. Espone gli stessi use case come sette tool JSON-RPC su stdio, così un agente AI (Claude Code, Kiro, VS Code Copilot Chat) può chiamare direttamente analyze_cloudrift o le versioni per-dominio più mirate (analyze_cloud_waste, analyze_dead_resources, analyze_resource_security, get_cost_trend), oltre a get_resource_types e get_required_iam_permissions. Nessun nuovo concetto di dominio — puramente un adapter di protocollo che riusa le composition root esistenti (resolveMcpScope() estrae il preambolo config/regione/account condiviso da tutti e quattro i tool di analisi).
Trend store locale
Ogni esecuzione di analyze, dead-resources e resource-security salva uno snapshot completo in un file SQLite per-account (~/.cloudrift/trends/<account-id>.db). Il comando history lo rilegge. Lo store vive in un package condiviso (shared-trend-store) che non conosce i DTO specifici di dominio — memorizza solo { domain, generatedAt, payload }. La scrittura è best-effort: un fallimento non blocca mai la scansione stessa.