CUDly is a comprehensive CLI tool for managing cloud cost commitments across AWS, Azure, and GCP. It helps organizations optimize cloud spending by automating the discovery, analysis, and purchase of multiple Reserved Instances, Savings Plans, and Committed Use Discounts by running a single command.
Full flag documentation, examples, and subcommand reference: docs/cli/README.md
Topic pages:
- Filtering - account, region, engine, instance-type, SP-type, and threshold filters
- Purchase Safety - dry-run, audit log, idempotency window, and guardrails
- Cloud Setup -
configure-azureandconfigure-gcpself-hosted credential bootstrap
- Multi-Cloud Support - Unified interface for AWS (production), Azure (experimental), and GCP (experimental)
- Intelligent Recommendations - Fetches and analyzes commitment recommendations from cloud provider APIs
- Safe Purchase Automation - Execute purchases with built-in safety controls (dry-run by default)
- Flexible Coverage Control - Purchase only a percentage of recommendations for gradual adoption
- CSV Workflow - Generate recommendations, review offline, then execute purchases
- Advanced Filtering - Filter by region, instance type, engine, and account
- Comprehensive Reporting - Detailed cost estimates, savings calculations, and audit trails
| Service | Commitment Type | Description |
|---|---|---|
| Amazon RDS | Reserved Instances | MySQL, PostgreSQL, MariaDB, Oracle, SQL Server, Aurora |
| Amazon ElastiCache | Reserved Nodes | Redis, Memcached |
| Amazon EC2 | Reserved Instances | All instance families |
| Amazon OpenSearch | Reserved Instances | Search domain instances |
| Amazon Redshift | Reserved Nodes | DC2 and RA3 node types |
| Amazon MemoryDB | Reserved Nodes | Memory-optimized nodes |
| Savings Plans | Hourly Commitments | Compute, EC2 Instance, SageMaker, Database |
| Service | Commitment Type |
|---|---|
| Azure SQL Database | Reserved Capacity |
| Azure Virtual Machines | Reserved Instances |
| Azure Cache for Redis | Reserved Capacity |
| Azure Cosmos DB | Reserved Capacity |
| Azure Cognitive Search | Reserved Capacity |
| Azure Managed Redis | Reserved Capacity |
| Azure Savings Plans | Hourly Commitments |
| Azure Synapse Analytics | Reserved Capacity |
| Service | Commitment Type |
|---|---|
| Compute Engine | Committed Use Discounts |
| Cloud SQL | Committed Use Discounts |
| Memorystore | Committed Use Discounts |
Tested means the service has been exercised end-to-end with real AWS accounts and validated in production workloads. Experimental means the implementation exists and is functional, but needs real-world validation -- contributions and testers are very welcome.
| AWS Service | CLI Flag | Status |
|---|---|---|
| Amazon RDS | rds |
Tested |
| Amazon ElastiCache | elasticache |
Tested |
| Amazon EC2 (Reserved Instances) | ec2 |
Experimental (seeking testers) |
| Amazon OpenSearch | opensearch |
Experimental (seeking testers) |
| Amazon Redshift | redshift |
Experimental (seeking testers) |
| Amazon MemoryDB | memorydb |
Experimental (seeking testers) |
| Savings Plans (Compute, EC2 Instance, SageMaker, Database) | savingsplans |
Experimental (seeking testers) |
git clone https://github.com/LeanerCloud/CUDly.git
cd CUDly
go build -o cudly cmd/*.gogo install github.com/LeanerCloud/CUDly/cmd@latest# Get RDS recommendations with default settings (3-year, no-upfront, 80% coverage)
./cudly --services rds
# Get recommendations for multiple services
./cudly --services rds,elasticache,ec2
# Get recommendations for all supported services
./cudly --all-services# Apply filters to narrow down recommendations
./cudly --services rds \
--include-regions us-east-1,eu-west-1 \
--exclude-instance-types db.t2.micro \
--coverage 50# Purchase from generated CSV (requires explicit --purchase flag)
./cudly --input-csv cudly-dryrun-*.csv --purchase
# Skip confirmation prompt
./cudly --input-csv cudly-dryrun-*.csv --purchase --yes| Flag | Description | Default |
|---|---|---|
-s, --services |
Comma-separated service list. Per-RI services: rds, elasticache, ec2, opensearch, redshift, memorydb. Per-plan-type Savings Plans: savings-plans-compute, savings-plans-ec2instance, savings-plans-sagemaker, savings-plans-database. Fan-out aliases: savingsplans, savings-plans, and sp expand to all four SP plan types. |
rds |
--all-services |
Process all supported services | false |
| Flag | Description | Default |
|---|---|---|
-p, --payment |
Payment option: all-upfront, partial-upfront, no-upfront |
no-upfront |
-t, --term |
Term in years: 1 or 3 |
3 |
-c, --coverage |
Coverage percentage (0-100) — % of each recommendation's instance count to purchase | 80 |
-u, --target-coverage |
Target % (0-100) of historical demand to cover with commitments; the rest spills to on-demand. Sizes counts so projected coverage approximates target, projected utilization stays near 100%. Overrides --coverage. |
0 (disabled) |
--max-instances |
Maximum instances to purchase (0 = unlimited) | 0 |
--override-count |
Override recommended count with specific value | 0 |
--coveragevs--target-coverage: two related but distinct sizing levers.--coveragescales each AWS recommendation's instance count by a fixed fraction (rec.Count * coverage/100).--target-coveragesizes against historical average hourly usage instead (floor(avg * target/100)), so the resulting count reflects real demand rather than AWS's recommended count. Both lean the same direction (higher value = more RIs, lower value = fewer), but--target-coverageis the right lever when the historical-usage signal is what you want to size by and you're explicitly leaving on-demand headroom for growth or bursts.
| Flag | Description | Default |
|---|---|---|
--purchase |
Execute actual purchases (dry-run by default) | false |
--yes |
Skip confirmation prompts | false |
-i, --input-csv |
Input CSV file with recommendations | - |
-o, --output |
Output CSV file path | auto-generated |
| Flag | Description |
|---|---|
--include-regions |
Only include these regions |
--exclude-regions |
Exclude these regions |
--include-instance-types |
Only include these instance types |
--exclude-instance-types |
Exclude these instance types |
--include-engines |
Only include these database engines |
--exclude-engines |
Exclude these database engines |
--include-accounts |
Only include these account names |
--exclude-accounts |
Exclude these account names |
--include-extended-support |
Include instances on extended support engine versions (see below) |
--include-sp-types |
Only include these Savings Plan types (Compute, EC2Instance, SageMaker, Database) |
--exclude-sp-types |
Exclude these Savings Plan types |
By default, CUDly excludes instances running on database engine versions that are in AWS Extended Support. This is because Extended Support incurs additional per-vCPU-hour charges that may offset RI savings.
For example, MySQL 5.7 and PostgreSQL 11 are in Extended Support. Instances running these versions are automatically excluded from RI recommendations.
Note: This feature requires the --validation-profile flag to specify an AWS profile with permissions to describe RDS instances across all member accounts in your organization.
# Extended support filtering with validation profile
./cudly --services rds --validation-profile my-org-reader-profile
# Include extended support instances (skip filtering)
./cudly --services rds --include-extended-supportThis is useful if you plan to upgrade the database version before the RI term ends, or if the Extended Support charges are acceptable for your use case.
CUDly automatically checks for Reserved Instances purchased within the last 24 hours and adjusts recommendations to avoid duplicate purchases. This is useful when running the tool multiple times in quick succession or when recovering from partial purchase failures.
For example, if you purchase 5 db.r6g.large RIs and run CUDly again within 24 hours, those 5 instances will be subtracted from the recommendation count to prevent double-purchasing.
| Flag | Description |
|---|---|
--profile |
AWS profile to use |
--validation-profile |
AWS profile for instance type validation |
Purchase 50% of 1-year partial-upfront RDS recommendations:
./cudly --services rds \
--payment partial-upfront \
--term 1 \
--coverage 50Apply different coverage percentages per service:
./cudly \
--services rds,elasticache,ec2 \
--rds-coverage 50 \
--elasticache-coverage 80 \
--ec2-coverage 100 \
--payment no-upfront \
--term 3Only process specific regions with instance limits:
./cudly --services ec2 \
--include-regions us-east-1,us-west-2 \
--max-instances 50 \
--payment all-upfront \
--term 3# Step 1: Generate recommendations
./cudly --all-services --output recommendations.csv
# Step 2: Review CSV file externally
# Step 3: Purchase with filters
./cudly \
--input-csv recommendations.csv \
--include-regions us-east-1 \
--exclude-instance-types db.t2.micro,cache.t2.micro \
--coverage 75 \
--purchase./cudly --services rds,elasticache \
--exclude-instance-types db.t2.micro,db.t2.small,db.t3.micro,cache.t2.micro \
--payment partial-upfront \
--term 3# Get only Database Savings Plans recommendations using the per-plan-type slug
./cudly --services savings-plans-database \
--term 1 \
--coverage 80# Pick exactly the SP plan-types you want by listing per-plan-type slugs
./cudly --services savings-plans-compute,savings-plans-ec2instance \
--term 3 \
--coverage 80# `savingsplans` (and `savings-plans`, `sp`) is shorthand that fans out to every SP
# plan type -- equivalent to listing all four per-plan-type slugs.
./cudly --services savingsplans \
--term 3 \
--coverage 80Per-plan-type vs alias: prefer the explicit per-plan-type slugs (
savings-plans-compute,savings-plans-ec2instance,savings-plans-sagemaker,savings-plans-database) when you want precise scope. Use thesavingsplans/savings-plans/spalias only when you intentionally want all four SP plan types together.
The coverage percentage controls what portion of recommendations to act on:
| Coverage | Description | Use Case |
|---|---|---|
| 100% | All recommended instances | Maximum savings, stable workloads |
| 75% | Three-quarters of recommendations | Balanced approach |
| 50% | Half of recommendations | Conservative adoption |
| 25% | Quarter of recommendations | Testing/validation |
| 0% | Skip service entirely | Exclude from processing |
Per-account service overrides let you tweak the global Settings → Purchasing defaults (term, payment, coverage) on a per-account, per-service basis.
Use overrides when an account's purchasing policy differs from the rest of your fleet:
- Dev/staging accounts: prefer 1-year, no-upfront for RDS to keep flexibility cheap, while production uses the global 3-year all-upfront.
- Workload-shape outliers: an account that runs a steady ElastiCache cluster can override to 3-year all-upfront for ElastiCache only, while the same account inherits the global default for everything else.
- Pilot rollouts: enable a new SP plan-type on one account first, leave it disabled globally, then promote when proven.
If every account should get the same change, edit the global Settings -> Purchasing card instead; that propagates without per-account work.
- Open the dashboard → Settings → Accounts.
- Find the account row → click the row to expand it → click Service overrides.
- The override modal opens. Pick the (provider, service) pair you want
to override and fill in any of
term,payment,coverage,enabled. Fields you leave blank inherit the global default (see "What 'Inherit' means" below). - Click Save. The override row appears under the account; the recommendation engine reads it on the next refresh.
All providers supported: the override modal lists services for the account's provider (AWS, Azure, GCP). The UI and the backend both accept overrides for any provider.
- All override rows support inline edit of Term, Payment, Coverage, and Enabled directly on the row. Each field persists immediately on change.
- Delete removes the override entirely; the account falls back to the global default for that (provider, service) pair.
A blank field on an override is not stored as a sentinel value. The
PUT request omits the field, the row stays sparse, and the recommendation
engine reads the global default at evaluation time. So if you set the
global default from 3yr no-upfront to 1yr no-upfront, every override
that left term blank starts producing 1-year recommendations
automatically. Overrides that explicitly set term: 3yr keep that.
The override modal targets the same endpoint as scripted setups:
PUT /api/accounts/{id}/service-overrides/{provider}/{service}. Existing
automation continues to work without change; the UI and the API write to
the same account_service_overrides row.
CUDly includes multiple safety mechanisms to prevent unintended purchases:
- Dry-run by default - No purchases without explicit
--purchaseflag - Interactive confirmation - Prompts before actual purchases (unless
--yes) - CSV workflow - Review recommendations before purchasing
- Coverage control - Purchase only what you need
- Instance limits - Cap total purchases with
--max-instances - Duplicate prevention - Checks for existing commitments
- Instance type validation - Validates against known types
- Detailed logging - Full audit trail of operations
- CSV exports - Permanent record of all recommendations and purchases
CUDly uses the standard AWS SDK credential chain:
- Environment variables (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY) - Shared credentials file (
~/.aws/credentials) - AWS config file (
~/.aws/config) - IAM instance role (EC2/ECS)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CostExplorer",
"Effect": "Allow",
"Action": [
"ce:GetReservationPurchaseRecommendation",
"ce:GetReservationUtilization",
"ce:GetReservationCoverage",
"ce:GetSavingsPlansPurchaseRecommendation"
],
"Resource": "*"
},
{
"Sid": "ReservedInstanceOperations",
"Effect": "Allow",
"Action": [
"rds:DescribeReservedDBInstancesOfferings",
"rds:DescribeReservedDBInstances",
"rds:PurchaseReservedDBInstancesOffering",
"elasticache:DescribeReservedCacheNodesOfferings",
"elasticache:DescribeReservedCacheNodes",
"elasticache:PurchaseReservedCacheNodesOffering",
"ec2:DescribeReservedInstancesOfferings",
"ec2:DescribeReservedInstances",
"ec2:PurchaseReservedInstancesOffering",
"es:DescribeReservedInstanceOfferings",
"es:DescribeReservedInstances",
"es:PurchaseReservedInstanceOffering",
"redshift:DescribeReservedNodeOfferings",
"redshift:DescribeReservedNodes",
"redshift:PurchaseReservedNodeOffering",
"memorydb:DescribeReservedNodesOfferings",
"memorydb:DescribeReservedNodes",
"memorydb:PurchaseReservedNodesOffering",
"savingsplans:DescribeSavingsPlans",
"savingsplans:CreateSavingsPlan"
],
"Resource": "*"
},
{
"Sid": "RegionDiscovery",
"Effect": "Allow",
"Action": [
"ec2:DescribeRegions",
"ec2:DescribeInstanceTypeOfferings"
],
"Resource": "*"
},
{
"Sid": "AccountDiscovery",
"Effect": "Allow",
"Action": [
"sts:GetCallerIdentity",
"organizations:ListAccounts"
],
"Resource": "*"
}
]
}Uses Azure SDK DefaultAzureCredential:
- Azure CLI (
az login) - Environment variables (
AZURE_TENANT_ID,AZURE_CLIENT_ID,AZURE_CLIENT_SECRET) - Managed Identity (Azure VM)
Uses Google Cloud SDK credential chain:
- Service account JSON (
GOOGLE_APPLICATION_CREDENTIALS) - Application Default Credentials
- gcloud CLI authentication
CUDly generates CSV files with comprehensive details:
Timestamp,Status,Service,Provider,Account,Region,ResourceType,Count,Term,PaymentOption,UpfrontCost,RecurringCost,TotalCost,EstimatedSavings,PurchaseID- Dry run:
cudly-dryrun-YYYYMMDD-HHMMSS.csv - Purchase:
cudly-purchase-YYYYMMDD-HHMMSS.csv
CUDly/
├── cmd/ # CLI entry point and orchestration
├── pkg/ # Shared multi-cloud packages
│ ├── common/ # Cloud-agnostic types and interfaces
│ └── provider/ # Provider abstraction layer
└── providers/ # Cloud-specific implementations
├── aws/ # AWS provider (production)
│ ├── services/ # Service clients (RDS, EC2, etc.)
│ └── recommendations/ # Cost Explorer integration
├── azure/ # Azure provider (experimental)
│ └── services/ # Azure service clients
└── gcp/ # GCP provider (experimental)
└── services/ # GCP service clients
- Interface-driven - All implementations follow defined interfaces for testability
- Multi-cloud abstraction - Unified types and behaviors across providers
- Plugin architecture - Services registered and discovered at runtime
- Safety-first - Multiple layers of protection against unintended purchases
Note: The web GUI is experimental. It is under active development and has not been validated at scale. Use the CLI for production workloads.
In addition to the CLI, this branch ships a browser-based dashboard. The same
Go binary that runs the CLI also acts as the application server: it serves the
pre-built TypeScript/Webpack frontend as static files (controlled by the
STATIC_DIR environment variable) and exposes a REST API at /api/. There is
no separate web server process.
| Area | What you can do |
|---|---|
| Dashboard | Summary of active commitments, upcoming expirations, and savings trends |
| Recommendations | Browse and refresh commitment recommendations; trigger purchases from the UI |
| Purchase plans | Create, approve, pause, resume, and delete planned-purchase workflows; view execution history |
| History | Full purchase history with analytics and cost-breakdown views |
| Inventory & Coverage | List active commitments across accounts; view per-provider, per-service coverage breakdown |
| RI Exchange | AWS Convertible RI exchange: reshape recommendations, quote, and execute exchanges |
| Settings | Application configuration, cloud account credentials, user/group management, API keys |
The web interface is included in main. The dashboard is operational for AWS workloads; Azure
and GCP support in the web UI follows the same maturity as the CLI providers
(both are experimental). Specifically:
- AWS: recommendations, purchases, RI exchange, inventory, and coverage views are all wired and backed by real AWS APIs (Cost Explorer, EC2, RDS, etc.). This is the primary tested path.
- Azure: reservation recommendations and purchases are implemented in the
API handlers (see
internal/api/handler_recommendations.go,providers/azure/), but Azure support is experimental. The RI Exchange feature covers Azure Convertible RIs as a distinct code path. - GCP: GCP commitment recommendations and purchases are experimental. The handler routing exists, but end-to-end coverage is limited compared to AWS.
- The RI Exchange feature currently targets AWS Convertible EC2 Reserved Instances only.
- Multi-account support (AWS Organizations) is implemented; Azure/GCP multi-account federation is in progress.
CUDly is self-hosted only. You deploy it into your own cloud account using
the Terraform configurations under terraform/environments/. The Terraform
modules build and push a Docker container image, provision the database,
secrets, and networking, and deploy the application to one of the supported
runtimes.
| Cloud | Runtime | Terraform environment |
|---|---|---|
| AWS | Lambda (default) or Fargate (ECS) | terraform/environments/aws/ |
| GCP | Cloud Run | terraform/environments/gcp/ |
| Azure | Container Apps | terraform/environments/azure/ |
- Terraform >= 1.6.0
- Docker with buildx
- Go 1.25+
- Cloud CLI authenticated:
aws,gcloud, oraz
# AWS dev
./scripts/tf-deploy.sh aws dev
# GCP dev
./scripts/tf-deploy.sh gcp dev
# Azure dev
./scripts/tf-deploy.sh azure devcd terraform/environments/aws
cp dev.tfvars.example dev.tfvars # edit with your values
terraform init -backend-config=backends/dev.tfbackend
terraform plan -var-file=dev.tfvars
terraform apply -var-file=dev.tfvarsSee docs/DEPLOYMENT.md for the full deployment guide,
including Azure and GCP details, CDN/CloudFront configuration, remote state
backends, and CI/CD integration.
Key tfvars fields
| Variable | Purpose |
|---|---|
admin_email |
Email address for the initial administrator account |
admin_password |
Initial admin password (leave unset to auto-generate and store in Secrets Manager) |
compute_platform |
AWS only: "lambda" (default, scale-to-zero) or "fargate" (always-warm ECS) |
The Terraform apply also handles Docker image build/push and database migrations automatically on each apply.
After terraform apply completes, retrieve the application URL from the
Terraform outputs:
# AWS Lambda
terraform -chdir=terraform/environments/aws output lambda_function_url
# AWS Fargate (ALB)
terraform -chdir=terraform/environments/aws output fargate_api_url
# GCP Cloud Run
terraform -chdir=terraform/environments/gcp output cloud_run_service_url
# Azure Container Apps
terraform -chdir=terraform/environments/azure output container_app_urlOpen that URL in your browser. On a fresh deployment the login page includes a
one-time "Set up admin" step. Provide the admin_email you configured in
tfvars and either the password you set or the one retrieved from Secrets
Manager:
# Retrieve the auto-generated admin password (AWS)
aws secretsmanager get-secret-value \
--secret-id "$(terraform -chdir=terraform/environments/aws output -raw admin_password_secret_name)" \
--query SecretString --output textAfter the admin account is created, log in with that email and password. You can then add more users, configure cloud account credentials, and begin using the dashboard.
See docs/DEVELOPMENT.md for the full development
guide, including the local Docker environment, database migrations, hot
reload, and debugging workflows.
- Go 1.23 or later
- AWS/Azure/GCP credentials for integration testing
# Build binary
go build -o cudly cmd/*.go
# Run tests
go test ./...
# Run tests with coverage
go test -cover ./...
# Run specific package tests
go test ./providers/aws/...| Directory | Purpose |
|---|---|
cmd/ |
CLI implementation, flag parsing, orchestration |
pkg/common/ |
Cloud-agnostic types (Provider, Service, Commitment) |
pkg/provider/ |
Provider interface, registry, factory |
providers/aws/ |
AWS implementation with 8 service clients |
providers/azure/ |
Azure implementation (experimental) |
providers/gcp/ |
GCP implementation (experimental) |
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
- Additional commitment-eligible services (any provider service that offers reserved capacity, savings plans, or committed-use discounts)
- Azure and GCP service implementations
- Enhanced reporting and analytics
- Web UI dashboard
This project is licensed under the Open Software License 3.0 (OSL-3.0). See the LICENSE file for details.
The OSL-3.0 is an OSI-approved open source license that:
- Allows commercial use, modification, and distribution
- Requires attribution and license preservation
- Includes a patent grant
- Requires derivative works to be licensed under OSL-3.0
This tool can make actual cloud commitment purchases when used with the --purchase flag.
- Always verify recommendations before purchasing
- Test thoroughly in dry-run mode first
- Start with low coverage percentages
- Use instance limits for safety
- The authors are not responsible for unintended purchases or financial commitments
- Issues: GitHub Issues
- Discussions: GitHub Discussions
This tool is brought to you by LeanerCloud. We help companies reduce their cloud costs using a mix of services and tools such as AutoSpotting.
Running at significant scale on AWS and looking for cost optimization help? We can help you avoid committing to suboptimal resources by rightsizing and other optimizations before purchasing commitments. Contact us.