Enterprise Multi-Cloud FinOps Data Platform
CloudCost CLI is a provider-neutral, open-source FinOps data platform designed to collect, normalize, enrich, analyze, and govern cloud financial and infrastructure data across AWS, Azure, GCP, OCI, and Alibaba Cloud.
- Declarative YAML Pipelines: Define your extraction, transformation, and loading in a single
cloudcost.ymlfile. - Apache Arrow Data Plane: Lightning-fast, in-memory data movement.
- Bring-Your-Own Warehouse: Load normalized billing data directly into DuckDB, PostgreSQL, or Google BigQuery.
- SQL-First Governance Engine: Write FinOps policies (like identifying unallocated costs or zero utilization resources) using standard SQL, producing structured findings.
- Live pricing, not hardcoded tables: every check that needs a price fetches it from the provider's own live pricing API (Azure Retail Prices API today) at run time, not a rate card baked into the code that goes stale.
- Native Cloud Providers: Direct integration with
boto3,azure-storage-blob,oci, andoss2.
Every check below was built the same way: create the actual cloud resource, run cloudcost sync + policy run + findings list, confirm the real dollar amount, then tear the resource down. No synthetic fixtures, no invented prices. See CONTRIBUTING.md for the exact pattern and how to add your own.
Compute
- Stopped-but-not-deallocated VMs (still billing, live per-size pricing) —
stopped_not_deallocated_vms - VM rightsizing via CPU/network/disk metrics + live pricing —
vm_sizing_recommendation,rightsizing_utilization - Idle VM Scale Set (fixed instance count, low CPU) —
idle_vmss - Idle AKS node pool (control plane is free; the node VMs aren't) —
aks_idle_nodepool - Idle Container Instance (restartPolicy=Always, near-zero CPU) —
idle_container_instances - Idle Container Apps Dedicated workload profile (reserved nodes, zero apps) —
idle_container_apps_dedicated - Idle Batch pool (dedicated nodes, zero active jobs) —
idle_batch_pool - Idle Premium SSD v2 IOPS/throughput overage (unattached) —
idle_premiumv2_disk_overage - Zero-utilization resources (general) —
zero_utilization
Networking
- Unattached managed disks —
unattached_disks - Unassociated public IPs —
unassociated_public_ips - Idle load balancers / NAT gateways / Application Gateways —
idle_load_balancers,idle_nat_gateways,idle_app_gateways - Idle Azure Firewall —
idle_firewall - Idle Azure Bastion (zero sessions) —
idle_bastion - Idle Private Endpoints (zero traffic) —
idle_private_endpoints - Orphaned NSGs —
orphaned_nsgs - Idle Front Door profile —
idle_front_door - Idle Traffic Manager endpoint monitoring —
idle_traffic_manager - Idle/orphaned DNS zones —
idle_dns_zone
Data & storage
- Old/orphaned disk snapshots —
old_snapshots - Premium disk downsize opportunities —
premium_disk_downsize - Empty storage accounts —
empty_storage_accounts - Log Analytics Commitment Tier over-reservation —
log_analytics_idle_commitment
Databases & messaging
- Azure SQL DTU underutilization —
sql_dtu_underutilized - Idle Cosmos DB provisioned throughput (SQL, MongoDB, and Cassandra APIs) —
cosmosdb_idle_ru,cosmosdb_mongo_idle_ru,cosmosdb_cassandra_idle_ru - Idle PostgreSQL / MySQL Flexible Server —
postgres_idle_flexible,mysql_idle_flexible - Idle Redis Cache (live per-tier/SKU pricing) —
redis_idle - Idle Event Hubs Namespace (Standard) —
idle_eventhub - Idle Service Bus Namespace (Premium) —
idle_servicebus_premium
App platform
- Idle App Service Plans —
idle_app_service_plans - Idle Static Web App (Standard) —
idle_static_web_app - Idle App Configuration store —
idle_app_configuration - Idle SignalR Service —
idle_signalr - Idle Managed Grafana —
idle_managed_grafana - Idle Container Registries —
idle_container_registries
Governance
- Untagged resources —
untagged_resources - Unallocated costs —
unallocated_costs
Beyond cloud infra
- GitHub Actions wasted CI minutes (failed/cancelled workflow runs) —
github_wasted_actions_minutes
Standalone binaries — no Python, no pip, no uv, no dependency management at all. Each is built and verified (--help runs clean) on its own native CI runner (macOS 14 arm64, ubuntu-latest, windows-latest via GitHub Actions).
# macOS (Apple Silicon) — Homebrew, handles download/chmod/PATH for you
brew install raphgm/tap/cloudcost
cloudcost --help# macOS (Apple Silicon) — or download the binary directly
curl -L https://github.com/raphgm/cloudcost-cli/releases/download/v0.1.0/cloudcost-macos-arm64 -o cloudcost
chmod +x cloudcost
./cloudcost --help# Linux (x86_64)
curl -L https://github.com/raphgm/cloudcost-cli/releases/download/v0.1.0/cloudcost-linux-x86_64 -o cloudcost
chmod +x cloudcost
./cloudcost --help# Windows (x86_64) — PowerShell
Invoke-WebRequest https://github.com/raphgm/cloudcost-cli/releases/download/v0.1.0/cloudcost-windows-x86_64.exe -OutFile cloudcost.exe
.\cloudcost.exe --helpIntel Mac isn't built yet. New binaries build automatically via .github/workflows/build-release.yml on every GitHub release, or can be triggered manually via workflow_dispatch.
For other platforms, or if you're contributing. Requires Python 3.13+ and uv:
uv venv
uv pip install -e .
source .venv/bin/activate- Initialize a new project:
cloudcost init- Validate your pipeline configuration:
cloudcost validate cloudcost.yml- Sync data across your clouds to your warehouse:
cloudcost sync cloudcost.yml- Run your SQL governance policies:
cloudcost policy run cloudcost.yml- View your optimization findings:
cloudcost findings listfocus.normalize doesn't guess your source's column names — declare the mapping explicitly in the transform's config, since every provider's raw export uses different names (Azure Cost Management's azure.cost_export source, for example, produces ServiceName/PreTaxCost, not the FOCUS-style service_name/billed_cost the bundled policies expect):
transforms:
- name: normalize_costs
type: focus.normalize
input: azure_billing
config:
provider: azure
column_map:
ServiceName: service_name
PreTaxCost: billed_cost
InstanceId: resource_id
ResourceGroup: resource_group
UsageDateTime: usage_dateThis was verified end-to-end against a real Azure Cost Management export (215 real cost line items, cloudcost sync + cloudcost policy run both succeeding and producing real findings) — see cloudcost.azure-real.yml for the full working example. AWS/OCI/Alibaba each need their own column_map matched to their real export schema; none of those have been verified against live data yet.
Independently re-verified from a completely fresh clone (new git clone, new uv venv, new uv pip install -e ., no leftover state) against the same live export, confirming the fix isn't an artifact of the environment it was written in:
Extracting from source: azure_billing
-> Extracted 215 rows
Transforming data: normalize_costs
-> Transformed 215 rows
Loading data to destination: local_duckdb
-> Loaded 215 rows into local_duckdb
Evaluating policy: unallocated-costs
-> Generated 0 findings.
Evaluating policy: zero-utilization
-> Generated 6 findings.
Real breakdown from that data: $50.50 total across the resource group's lifetime, with Azure Bastion alone at $26.02 — 52% of total spend — the same always-on-cost-concentration pattern flagged in the Open Cloud Cost Intelligence project, now caught independently by this tool's zero-utilization policy against Virtual Machines (6 findings, $0.21–$1.00 each).
Every check's real-resource verification (see above) proves it works against live cloud data. Separately, tests/ covers the pipeline engine and policy SQL logic with fast, offline unit tests (in-memory DuckDB, no cloud access needed):
pytest tests/ -vWant to add a check for AWS, GCP, or another Azure service? See CONTRIBUTING.md for the 4-file pattern every check follows, and the open issues for good starting points (AWS/GCP parity, safe auto-remediation, PyPI packaging).
CloudCost utilizes a layered plugin architecture. The CLI is built on Typer and Rich. Data is extracted via provider plugins directly into PyArrow tables, transformed in-memory, and synced to DuckDB or other data warehouses. Policies are executed directly against the warehouse to generate evidence-backed FinOps findings.
cloudcost policy run reads its DuckDB path from the pipeline's own destinations config (the first type: duckdb entry) rather than a hardcoded default — this was a real bug until 2026-09-20 (policy run would fail with "database does not exist" against any pipeline that didn't name its file exactly data/cloudcost.duckdb), found and fixed while testing this tool against a real Azure account.
