The Problem
A startup's AWS bill grew from $8K to $32K/month in a year. Nobody knows what's costing what. Engineers spin up instances for testing and forget to terminate them. Dev environments run 24/7. Three load balancers have zero targets. An RDS instance for a discontinued feature has been running for 14 months. The CFO asks "can we cut cloud costs?" and engineering says "we'll look into it" — then never does.
Step 1: Resource Scanner
typescript
// src/scanner/aws-scanner.ts
import { EC2Client, DescribeInstancesCommand, DescribeVolumesCommand } from '@aws-sdk/client-ec2';
import { RDSClient, DescribeDBInstancesCommand } from '@aws-sdk/client-rds';
import { ElasticLoadBalancingV2Client, DescribeLoadBalancersCommand, DescribeTargetGroupsCommand, DescribeTargetHealthCommand } from '@aws-sdk/client-elastic-load-balancing-v2';
import { CloudWatchClient, GetMetricStatisticsCommand } from '@aws-sdk/client-cloudwatch';
import { z } from 'zod';
const ec2 = new EC2Client({});
const rds = new RDSClient({});
const elb = new ElasticLoadBalancingV2Client({});
const cw = new CloudWatchClient({});
const WasteItem = z.object({
resourceType: z.string(),
resourceId: z.string(),
region: z.string(),
issue: z.string(),
monthlyCostEstimate: z.number(),
recommendation: z.string(),
confidence: z.enum(['high', 'medium', 'low']),
tags: z.record(z.string(), z.string()).default({}),
});
type WasteItem = z.infer<typeof WasteItem>;
export async function scanForWaste(): Promise<WasteItem[]> {
const waste: WasteItem[] = [];
// 1. Idle EC2 instances (CPU < 5% for 7 days)
const instances = await ec2.send(new DescribeInstancesCommand({ Filters: [{ Name: 'instance-state-name', Values: ['running'] }] }));
for (const reservation of instances.Reservations ?? []) {
for (const instance of reservation.Instances ?? []) {
const avgCpu = await getAvgMetric(instance.InstanceId!, 'AWS/EC2', 'CPUUtilization', 7);
if (avgCpu < 5) {
const cost = estimateEC2Cost(instance.InstanceType!);
waste.push({
resourceType: 'EC2 Instance',
resourceId: instance.InstanceId!,
region: process.env.AWS_REGION ?? 'us-east-1',
issue: `Idle: ${avgCpu.toFixed(1)}% avg CPU over 7 days`,
monthlyCostEstimate: cost,
recommendation: avgCpu < 1 ? 'Terminate (likely unused)' : `Downsize from ${instance.InstanceType} to smaller`,
confidence: avgCpu < 1 ? 'high' : 'medium',
tags: Object.fromEntries((instance.Tags ?? []).map(t => [t.Key!, t.Value!])),
});
}
}
}
// 2. Unattached EBS volumes
const volumes = await ec2.send(new DescribeVolumesCommand({ Filters: [{ Name: 'status', Values: ['available'] }] }));
for (const vol of volumes.Volumes ?? []) {
const cost = (vol.Size ?? 0) * 0.08; // $0.08/GB/month for gp2
waste.push({
resourceType: 'EBS Volume',
resourceId: vol.VolumeId!,
region: process.env.AWS_REGION ?? 'us-east-1',
issue: `Unattached volume (${vol.Size}GB ${vol.VolumeType})`,
monthlyCostEstimate: cost,
recommendation: 'Snapshot and delete (or delete if unneeded)',
confidence: 'high',
tags: Object.fromEntries((vol.Tags ?? []).map(t => [t.Key!, t.Value!])),
});
}
// 3. Load balancers with no targets
const lbs = await elb.send(new DescribeLoadBalancersCommand({}));
for (const lb of lbs.LoadBalancers ?? []) {
const tgs = await elb.send(new DescribeTargetGroupsCommand({ LoadBalancerArn: lb.LoadBalancerArn }));
let hasHealthyTargets = false;
for (const tg of tgs.TargetGroups ?? []) {
const health = await elb.send(new DescribeTargetHealthCommand({ TargetGroupArn: tg.TargetGroupArn }));
if ((health.TargetHealthDescriptions ?? []).length > 0) hasHealthyTargets = true;
}
if (!hasHealthyTargets) {
waste.push({
resourceType: 'Load Balancer',
resourceId: lb.LoadBalancerName!,
region: process.env.AWS_REGION ?? 'us-east-1',
issue: 'No healthy targets registered',
monthlyCostEstimate: 22, // ~$22/month for ALB
recommendation: 'Delete if no longer needed',
confidence: 'high',
tags: {},
});
}
}
// 4. Oversized RDS instances
const rdsInstances = await rds.send(new DescribeDBInstancesCommand({}));
for (const db of rdsInstances.DBInstances ?? []) {
const avgCpu = await getAvgMetric(db.DBInstanceIdentifier!, 'AWS/RDS', 'CPUUtilization', 14);
const connections = await getAvgMetric(db.DBInstanceIdentifier!, 'AWS/RDS', 'DatabaseConnections', 7);
if (avgCpu < 10 && connections < 5) {
waste.push({
resourceType: 'RDS Instance',
resourceId: db.DBInstanceIdentifier!,
region: process.env.AWS_REGION ?? 'us-east-1',
issue: `Oversized: ${avgCpu.toFixed(1)}% CPU, ${connections.toFixed(0)} avg connections`,
monthlyCostEstimate: estimateRDSCost(db.DBInstanceClass!),
recommendation: connections < 1 ? 'Terminate (likely unused)' : 'Downsize instance class',
confidence: connections < 1 ? 'high' : 'medium',
tags: {},
});
}
}
return waste;
}
async function getAvgMetric(resourceId: string, namespace: string, metric: string, days: number): Promise<number> {
const end = new Date();
const start = new Date(end.getTime() - days * 86400000);
const result = await cw.send(new GetMetricStatisticsCommand({
Namespace: namespace,
MetricName: metric,
Dimensions: [{ Name: 'InstanceIdentifier', Value: resourceId }],
StartTime: start,
EndTime: end,
Period: 86400,
Statistics: ['Average'],
}));
const points = result.Datapoints ?? [];
return points.length > 0 ? points.reduce((s, p) => s + (p.Average ?? 0), 0) / points.length : 0;
}
function estimateEC2Cost(instanceType: string): number {
const costs: Record<string, number> = {
't3.micro': 7.59, 't3.small': 15.18, 't3.medium': 30.37,
'm5.large': 69.12, 'm5.xlarge': 138.24, 'r5.large': 91.10,
};
return costs[instanceType] ?? 50;
}
function estimateRDSCost(instanceClass: string): number {
const costs: Record<string, number> = {
'db.t3.micro': 12.41, 'db.t3.small': 24.82, 'db.t3.medium': 49.64,
'db.r5.large': 131.40, 'db.r5.xlarge': 262.80,
};
return costs[instanceClass] ?? 100;
}
Step 2: CLI Output
typescript
// src/cli/index.ts
import { Command } from 'commander';
import { scanForWaste } from '../scanner/aws-scanner';
const program = new Command();
program
.command('scan')
.description('Scan for cloud waste and savings opportunities')
.option('--min-savings <dollars>', 'Minimum monthly savings to report', '5')
.option('--json', 'Output as JSON')
.action(async (opts) => {
console.log('🔍 Scanning AWS resources...\n');
const waste = await scanForWaste();
const filtered = waste.filter(w => w.monthlyCostEstimate >= parseInt(opts.minSavings));
const total = filtered.reduce((s, w) => s + w.monthlyCostEstimate, 0);
if (opts.json) {
console.log(JSON.stringify({ items: filtered, totalMonthlySavings: total }, null, 2));
return;
}
console.log(`Found ${filtered.length} optimization opportunities:\n`);
for (const item of filtered.sort((a, b) => b.monthlyCostEstimate - a.monthlyCostEstimate)) {
const icon = item.confidence === 'high' ? '🔴' : item.confidence === 'medium' ? '🟡' : '🟢';
console.log(`${icon} ${item.resourceType}: ${item.resourceId}`);
console.log(` Issue: ${item.issue}`);
console.log(` Savings: $${item.monthlyCostEstimate.toFixed(0)}/month`);
console.log(` Action: ${item.recommendation}\n`);
}
console.log(`\n💰 Total potential savings: $${total.toFixed(0)}/month ($${(total * 12).toFixed(0)}/year)`);
});
program.parse();
Results
- First scan savings: $4.2K/month found ($50.4K/year)
- 14-month-old RDS: terminated, saving $262/month
- 23 unattached EBS volumes: deleted, saving $180/month
- 3 empty load balancers: removed, saving $66/month
- 7 idle EC2 instances: 3 terminated, 4 downsized, saving $890/month
- Dev environments: now auto-stop after hours, saving $2.8K/month
- Monthly scan: scheduled via CI, prevents waste from accumulating