Architecture
cloudrift follows a DDD (Domain-Driven Design) architecture with the Ports & Adapters (Hexagonal Architecture) pattern. Dependencies always point inward, from the CLI through the application layer to the domain.
Monorepo structure
cloudrift/
├── apps/
│ └── cli/ # CLI entry point (Commander.js)
├── libs/
│ └── cloud-cost/
│ ├── domain/ # Entities, ports, policies
│ ├── application/ # Use cases (AnalyzeCloudWasteUseCase)
│ └── infrastructure/
│ └── aws-adapter/ # AWS SDK v3 scanner implementation
├── packages/ # Shared kernel, utilities
├── docs/ # Technical documentation (EN + IT)
├── policy/ # Example OPA policies
├── nx.json
├── pnpm-workspace.yaml
└── tsconfig.base.json
Layers
Domain (libs/cloud-cost/domain)
Contains pure business logic with no external dependencies:
- Entities:
WastedResourceand its specializations for each resource type - Ports (interfaces):
WasteScannerPort,PricingPort - Policies: waste policies with grace period and exclusion tags (reusable base class)
- Value Objects:
ResourceKindunion type (the compiler guides you when adding a new type)
Application (libs/cloud-cost/application)
AnalyzeCloudWasteUseCase— the coordinator. Generic over registered scanners: iteratesWasteScannerPortinstances, aggregates results, applies policies.
Infrastructure (libs/cloud-cost/infrastructure/aws-adapter)
- Scanners: one
WasteScannerPortimplementation per resource type (EBS, EC2, RDS, etc.) - Uses AWS SDK v3 for API calls and CloudWatch for activity metrics
CLI (apps/cli)
- Argument parsing with Commander.js
- Formatters: table, JSON, markdown, PDF
- Composition root: registers scanners and instantiates the use case
- Exit codes: 0 (ok), 2 (budget exceeded)
Why this architecture
- Testability: domain doesn’t depend on AWS SDK, testable with mocks
- Extensibility: new services = new scanners, zero changes to the use case
- Multi-cloud path: to support GCP/Azure, just add new adapters implementing
WasteScannerPort - Separation of concerns: each layer has clear, well-defined responsibilities
Bounded contexts
The project has four bounded contexts, each following the same libs/<context>/{domain,application,infrastructure} layout:
| Context | Command | What it finds |
|---|---|---|
| cloud-cost | analyze |
Wasted resources with cost estimates (44 scanners) |
| cost-analytics | cost, trend |
Spend comparison and monthly trends via Cost Explorer |
| dead-resources | dead-resources |
Dead/unused resources at $0 AWS cost (18 checks) |
| resource-security | resource-security |
Security posture misconfigurations (29 checks) |
They share only shared/kernel. Layer isolation (domain → application → infrastructure) is enforced by @nx/enforce-module-boundaries lint rules — not just convention.
MCP server
cloudrift mcp is the second input adapter alongside Commander. It exposes the same use cases as seven JSON-RPC tools over stdio, so an AI agent (Claude Code, Kiro, VS Code Copilot Chat) can call analyze_cloudrift or the narrower per-domain versions (analyze_cloud_waste, analyze_dead_resources, analyze_resource_security, get_cost_trend), plus get_resource_types and get_required_iam_permissions, directly. No new domain concepts — purely a protocol adapter reusing the existing composition roots (resolveMcpScope() extracts the config/region/account preamble shared by all four analysis tools).
Local trend store
Every run of analyze, dead-resources, and resource-security persists a full snapshot to a per-account SQLite file (~/.cloudrift/trends/<account-id>.db). The history command reads it back. The store lives in a shared package (shared-trend-store) that knows nothing about domain-specific DTOs — it just stores { domain, generatedAt, payload }. Writing is best-effort: a failure never blocks the scan itself.