Quick Start
Prerequisites
- Node.js 20+ — check with
node --version - AWS credentials with read-only permissions (see Required IAM permissions below)
Installation
npm install -g @cloudrift/cli
# or run it once-off, without installing:
npx @cloudrift/cli analyze
macOS/Linux via Homebrew:
brew install elleVas/cloudrift/cloudrift
From source (for contributing, or to run unreleased changes):
git clone https://github.com/elleVas/cloudrift.git
cd cloudrift
pnpm install
pnpm nx build cli # output compiled to apps/cli/dist/
The examples below use the cloudrift command (npm install). Running from source instead? Replace cloudrift with node apps/cli/dist/main.js.
AWS credentials setup
cloudrift uses the standard AWS SDK v3 credential chain. Three options, in order of preference:
Option A — AWS CLI (recommended)
aws configure
# enter: Access Key ID, Secret Access Key, default region (e.g. us-east-1), output format (json)
This creates ~/.aws/credentials with the default profile.
Option B — Edit ~/.aws/credentials manually
[default]
aws_access_key_id = AKIAIOSFODNN7EXAMPLE
aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Option C — Environment variables
export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
export AWS_DEFAULT_REGION=us-east-1
Verify: aws sts get-caller-identity should return your account ID without errors.
Required IAM permissions
The AWS user/role must have this read-only policy:
{
"Effect": "Allow",
"Action": [
"ec2:DescribeVolumes",
"ec2:DescribeAddresses",
"ec2:DescribeInstances",
"ec2:DescribeSnapshots",
"ec2:DescribeImages",
"ec2:DescribeNatGateways",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeLaunchTemplates",
"ec2:DescribeLaunchTemplateVersions",
"cloudwatch:GetMetricStatistics",
"rds:DescribeDBInstances",
"rds:DescribeDBClusters",
"rds:DescribeDBSnapshots",
"elasticloadbalancing:DescribeLoadBalancers",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:DescribeTargetHealth",
"logs:DescribeLogGroups",
"s3:ListAllMyBuckets",
"s3:GetBucketLifecycleConfiguration",
"s3:ListMultipartUploadParts",
"s3:ListBucketMultipartUploads",
"ecr:DescribeRepositories",
"ecr:DescribeImages",
"codepipeline:ListPipelines",
"codepipeline:ListPipelineExecutions",
"secretsmanager:ListSecrets",
"lambda:ListFunctions",
"elasticfilesystem:DescribeFileSystems",
"dynamodb:ListTables",
"dynamodb:DescribeTable",
"elasticache:DescribeCacheClusters",
"sagemaker:ListNotebookInstances",
"sagemaker:ListEndpoints",
"sagemaker:DescribeEndpoint",
"sagemaker:DescribeEndpointConfig",
"sagemaker:ListEndpointConfigs",
"sagemaker:ListModels",
"sagemaker:DescribeModel",
"sagemaker:ListTags",
"sqs:ListQueues",
"sqs:GetQueueAttributes",
"sqs:ListDeadLetterSourceQueues",
"sqs:ListQueueTags",
"tag:GetResources",
"eks:ListClusters",
"eks:ListNodegroups",
"eks:DescribeNodegroup",
"sts:GetCallerIdentity"
],
"Resource": "*"
}
--live-pricingadditionally requirespricing:GetProducts(AWS Pricing API). Not needed for the default static pricing.
First usage
# Scan the default region (us-east-1)
# Account ID is auto-detected via STS
cloudrift analyze
# Scan multiple regions
cloudrift analyze -r us-east-1 eu-west-1 ap-southeast-1
# Specific services only (skips the interactive picker)
cloudrift analyze --scanners ebs-volume elastic-ip
# All scanners without interactive picker
cloudrift analyze --all-services
# Disable the grace period (report resources of any age)
cloudrift analyze --min-age-days 0
The interactive picker
Running analyze in a real terminal (outside CI) shows an interactive picker — a checkbox list of every scanner, all pre-selected. Press Enter to scan everything, or deselect what you don’t need.
The picker never appears when:
stdoutis not a TTY (piped output)- The
CI=trueenvironment variable is set - You use
--silent,--scanners <kinds...>or--all-services
In those cases all scanners run automatically.
Output formats
# Console table (default)
cloudrift analyze
# JSON output (machine-readable, ideal for piping)
cloudrift analyze --format json | jq '.totalWasteMonthlyUsd'
# Filter findings with jq
cloudrift analyze --format json | jq '.findings[] | select(.category=="waste")'
# Markdown for GitHub Actions step summary
cloudrift analyze --format markdown >> "$GITHUB_STEP_SUMMARY"
In machine-readable formats (
json,markdown) all human messages are routed to stderr, so stdout carries only the report — ideal for piping.
PDF report
# PDF with auto-generated filename (cloudrift-reports/AWS_report_YYYY_MM_DD.pdf)
cloudrift analyze --pdf
# PDF with custom filename, no terminal output
cloudrift analyze --pdf ./report.pdf --silent
The PDF report contains:
- Executive summary — monthly and annual waste totals, resource count, per-type breakdown
- Top recommendations — up to 8 items sorted by impact, with estimated annual saving
- Detail pages — one table per resource type found
- Scan warnings — listed if any resource type could not be scanned
Flag order: the
--pdf=./report.pdf --silentto avoid ambiguity.
JSON file report
# Also writes a JSON file to disk (independent of --format)
cloudrift analyze --json
# With custom filename
cloudrift analyze --json ./report.json --silent
Partial failure handling
If scanning a resource type fails (e.g. missing CloudWatch permissions for NAT Gateways), the tool:
- Still returns all other results
- Shows a “Scan Warnings” section with the error details
- Marks the total as
(incomplete — see warnings above)
⚠ Scan Warnings
• NAT Gateways: Access denied to CloudWatch metrics
Total estimated waste: $56.20/month (incomplete — see warnings above)
The exit code is driven only by the cost threshold, never by scan errors.
All options
| Flag | Description | Default |
|---|---|---|
-r, --regions <regions...> |
AWS regions to scan | us-east-1 |
--format <format> |
stdout format: table, json, markdown |
table |
--config <path> |
Path to config file | auto-discovered |
--live-pricing |
Current prices from AWS Pricing API | off |
--scanners <kinds...> |
Only these services (skips picker) | — |
--all-services |
All scanners without picker | on in CI/non-TTY |
--account-id <id> |
Account ID override (auto-detected via STS) | auto |
--assume-role-arn <arn> |
Assume this IAM role for cross-account access | — |
--external-id <id> |
External ID for --assume-role-arn |
— |
--min-age-days <days> |
Grace period in days | 7 |
--ignore-tag <tag> |
Exclusion tag | cloudrift:ignore |
--pdf [filename] |
Generate PDF report | — |
--json [filename] |
Generate JSON report to disk | — |
--silent |
No stdout output | off |
--notify-slack |
Slack notification when waste exceeds costAlertThresholdUsd (reads SLACK_WEBHOOK_URL from env) |
off |
--notify-webhook |
POST a JSON summary to a webhook (reads CLOUDRIFT_WEBHOOK_URL from env) |
off |
--notify-email <address> |
Email a summary (reads CLOUDRIFT_SMTP_* from env) |
off |
-h, --help |
Show help | — |
Notifications: also available on
dead-resources,resource-security, andhistory --compare(different trigger conditions — see each command below). Best-effort: a broken webhook or misconfigured SMTP logs a warning and never fails the scan. Every credential is read from the environment, never a flag — set them in your shell profile or as CI secrets, never in a committed file.
Cross-account scanning
cloudrift can scan a different account by assuming an IAM role via STS:
# Scan a different account by assuming a role
cloudrift analyze --assume-role-arn arn:aws:iam::222222222222:role/cloudrift-scanner
# With external ID (if the trust policy requires it)
cloudrift analyze --assume-role-arn arn:aws:iam::222222222222:role/cloudrift-scanner --external-id my-secret
# Scan multiple accounts from a shell loop
for account in 111111111111 222222222222; do
cloudrift analyze \
--assume-role-arn "arn:aws:iam::${account}:role/cloudrift-scanner" \
--format json > "report-${account}.json"
done
All commands (analyze, cost, trend, dead-resources, resource-security) accept --assume-role-arn and --external-id.
Cost and trend commands
Beyond waste detection, cloudrift offers two commands to compare and track AWS spend via Cost Explorer:
# Compare this month's spend with the same days last month
cloudrift cost
# Monthly trend over the last 12 months
cloudrift trend --months 12
# Only EC2 and S3, skip billing confirmation
cloudrift trend --months 12 --services ec2 s3 --yes
# Fail in CI if spend increased by more than 20%
cloudrift cost --fail-on-increase 20 --format json
⚠️
costandtrendcall AWS Cost Explorer, which bills $0.01 per request — the only cloudrift commands that can generate an AWS cost. Both prompt for interactive confirmation before the first call (skippable with-y/--yes,--silent, or in CI). Closed billing periods are cached to disk.
Dead-resources command
Finds resources left dead or unused in the account at $0 direct AWS cost (invisible to waste detection): unused EC2 key pairs, orphaned security groups, inactive IAM users/roles, unrotated access keys, and more — 18 checks total.
# All checks, default region
cloudrift dead-resources
# Multiple regions
cloudrift dead-resources -r us-east-1 eu-west-1
# Only IAM checks
cloudrift dead-resources --scanners iam-user-inactive iam-policy-unattached
# Machine-readable output
cloudrift dead-resources --format json | jq '.findings[] | select(.severity=="warning")'
# PDF report
cloudrift dead-resources --pdf ./hygiene.pdf --silent
Findings carry a severity (info / warning / critical) instead of a $/month estimate. Requires additional IAM permissions — see the full policy with cloudrift iam-policy.
Resource-security command
Scans the account’s security posture: disabled MFA, unrotated access keys, internet-open security groups, public S3 buckets, unencrypted EBS/RDS, disabled GuardDuty/Config/Security Hub, disabled VPC Flow Logs, KMS key rotation, wildcard IAM admin policies, expiring ACM certificates, public resource policies on Lambda/SNS/SQS/ECR/Secrets Manager, and more — 29 checks total.
# Every check, default region
cloudrift resource-security
# Only IAM checks
cloudrift resource-security --scanners iam-root-mfa-disabled iam-user-mfa-disabled
# Machine-readable output
cloudrift resource-security --format json | jq '.findings[] | select(.severity=="critical")'
# PDF report
cloudrift resource-security --pdf ./security.pdf --silent
Findings also carry a severity. Requires additional IAM permissions — see the full policy with cloudrift iam-policy.
History command
View local scan history. Every run of analyze, dead-resources, and resource-security saves a snapshot to a local SQLite file (~/.cloudrift/trends/<account-id>.db) — best-effort, never blocking the scan.
# Every recorded snapshot, most recent first
cloudrift history
# Only cloud-cost history, last 10 runs
cloudrift history --domain cloud-cost --limit 10
# Machine-readable output
cloudrift history --format json | jq '.[0].payload'
# Self-contained HTML report (per-domain charts, stat tiles, linear forecast)
cloudrift history --html
# HTML report for a single domain
cloudrift history --domain cloud-cost --html
# Compare two specific snapshots
cloudrift history --compare
The --html report varies by domain: cloud-cost shows a single waste-dollar line with a linear forecast plus a “top resource type by waste” list; dead-resources/resource-security show critical/warning/info as three separate lines. The combined report opens with 3 executive stat tiles aimed at a CTO/CEO audience.
No data is ever uploaded anywhere: the file never leaves your machine.
IAM policy command
Prints the full read-only IAM policy required by cloudrift as ready-to-paste JSON:
cloudrift iam-policy
No AWS calls — useful for pasting directly into the IAM console, Terraform, or CDK.
Example output
Scanning us-east-1 (account 123456789012) for wasted cloud resources...
EBS Volumes — Unattached
┌────────────────────┬───────────┬────────┬──────┬────────────┬────────────┐
│ Volume ID │ Region │ Size │ Type │ Created │ Est. Cost │
├────────────────────┼───────────┼────────┼──────┼────────────┼────────────┤
│ vol-0abc123def456 │ us-east-1 │ 500 GB │ gp3 │ 2025-01-15 │ $40.00/mo │
└────────────────────┴───────────┴────────┴──────┴────────────┴────────────┘
Total estimated waste: $40.00/month
Per-region pricing
Prices are region-aware. Regions with specific pricing: us-east-1, us-west-2, eu-west-1, eu-central-1, ap-southeast-1, ap-northeast-1. All others fall back to us-east-1 defaults.
Every report shows prices as of with the date the price table was last verified.