Docs navigation

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: WastedResource and its specializations for each resource type
  • Ports (interfaces): WasteScannerPort, PricingPort
  • Policies: waste policies with grace period and exclusion tags (reusable base class)
  • Value Objects: ResourceKind union type (the compiler guides you when adding a new type)

Application (libs/cloud-cost/application)

  • AnalyzeCloudWasteUseCase — the coordinator. Generic over registered scanners: iterates WasteScannerPort instances, aggregates results, applies policies.

Infrastructure (libs/cloud-cost/infrastructure/aws-adapter)

  • Scanners: one WasteScannerPort implementation 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.