A serverless AWS cost monitoring system that automatically sends weekly and monthly cost reports to Slack, detects unused resources across multiple AWS services and regions, and supports organization-mode scanning with cost aggregation across linked AWS accounts.
Automated Cost Reporting
- Weekly AWS cost summaries (every Monday at 9:00 AM IST)
- Monthly cost forecasts and budget comparisons (1st of each month at 10:00 AM IST)
- Top spending services highlighted
- Cost anomaly detection with configurable thresholds
Unused Resource Detection
- Scans 17 AWS regions for idle or unused resources
- Detects low-CPU EC2 instances, unattached EBS volumes, orphaned snapshots
- Identifies idle RDS instances, load balancers with no traffic, unattached Elastic IPs
- Checks NAT Gateways, EFS, EKS, ECS, ElastiCache, Redshift, and OpenSearch
- Flags old/orphaned EBS snapshots, RDS snapshots, and EFS backups
Organization Mode (Multi-Account)
- Supports scanning multiple AWS accounts via STS AssumeRole
- Configurable linked accounts (comma-separated account IDs)
- Cost data grouped by linked account and service in organization mode
- Includes a ready-to-deploy CloudFormation template for the cross-account IAM role
- Falls back to single-account mode when no child accounts are configured
Slack Notifications
- Native Slack Block Kit tables for rich formatting
- Separate cost report and unused resources messages
- Budget threshold alerts
- Organization mode shows account IDs per service/resource
Serverless & Cost-Effective
- Single Lambda function handles both weekly and monthly reports
- EventBridge Scheduler for cron-based triggers
- Near-zero infrastructure cost
- No databases or persistent storage required
- Infrastructure as Code using AWS SAM
- AWS Account with Cost Explorer enabled
- Node.js 20.x or higher
- AWS SAM CLI installed (Installation Guide)
- Slack Workspace with an incoming webhook
git clone <repository-url>
cd cloud-cost-alerts
npm installCopy the example environment file:
cp example.env.json env.jsonEdit env.json with your configuration:
{
"CostReporterFunction": {
"SLACK_WEBHOOK_URL": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
"MONTHLY_BUDGET": "500",
"ANOMALY_THRESHOLD": "20"
}
}Edit samconfig.toml with your AWS details or run guided deployment:
npm run deploy:guidednpm run deploy| Variable | Description | Default |
|---|---|---|
SLACK_WEBHOOK_URL |
Slack incoming webhook URL | Required |
MONTHLY_BUDGET |
Monthly budget threshold (USD) | 500 |
ANOMALY_THRESHOLD |
Cost increase threshold to flag anomaly (%) | 20 |
TOP_SERVICES_COUNT |
Number of top services to include in report | 10 |
UNUSED_SERVICES_COUNT |
Number of unused resources to show in report | 50 |
SNAPSHOT_AGE_THRESHOLD_DAYS |
Days after which a snapshot is flagged as old | 90 |
CHILD_ACCOUNTS |
Comma-separated child account IDs | Empty (single-account) |
CROSS_ACCOUNT_ROLE_NAME |
IAM role name to assume in child accounts | CostAlertsReadRole |
ENABLE_ORGANIZATION_MODE |
Whether to run in organization mode (true/false) | true |
REGION |
AWS region | Auto-detected |
cloud-cost-alerts/
├── src/
│ ├── handlers/
│ │ └── cost-reporter.ts # Main Lambda entry point
│ ├── services/
│ │ ├── cost-explorer.ts # AWS Cost Explorer API integration
│ │ ├── slack.ts # Slack notification service
│ │ └── unused-resources.ts # Unused resource detection (multi-region, multi-account)
│ ├── utils/
│ │ ├── date-utils.ts # Date manipulation helpers
│ │ └── formatter.ts # Message formatting utilities
│ └── types/
│ └── index.ts # TypeScript type definitions
├── events/
│ ├── weekly.json # Weekly report test event
│ └── monthly.json # Monthly report test event
├── cross-account-role.yaml # CloudFormation template for child account IAM role
├── template.yaml # AWS SAM CloudFormation template
├── samconfig.toml # SAM deployment config
├── package.json # Dependencies
└── tsconfig.json # TypeScript config
# Build the project
npm run build
# Watch mode (rebuild on file changes)
npm run build:watch
# Clean build artifacts
npm run clean# Test weekly report locally
npm run test:weekly
# Test monthly report locally
npm run test:monthly# Deploy to development environment
npm run deploy
# Deploy to production environment
npm run deploy:prod
# Guided deployment (interactive setup)
npm run deploy:guidedThe system uses EventBridge Scheduler for cron-based triggers:
| Schedule | Cron Expression | Purpose |
|---|---|---|
| Weekly | cron(30 3 ? * MON *) |
Monday at 9:00 AM IST |
| Monthly | cron(30 4 1 * ? *) |
1st of month at 10:00 AM IST |
EventBridge (Scheduler)
↓
Lambda Function
↓
┌─────────────────────────────┐
│ AWS Cost Explorer API │ ← cost data & forecasts
│ CloudWatch Metrics │ ← resource utilization
│ AWS Service APIs (EC2, │ ← resource inventory
│ RDS, ELB, EKS, ECS...) │
│ STS AssumeRole (optional) │ ← cross-account access
└─────────────────────────────┘
↓
Slack Notification
Weekly Report (sent as two Slack messages):
- Cost report: total spending, top services breakdown with forecasts and week-over-week trends, anomaly detection
- Unused resources report: idle/unused resources detected across all regions
Monthly Report (sent as two Slack messages):
- Cost report: month-to-date spending, forecasted total, budget comparison, top services with daily averages
- Unused resources report: idle/unused resources detected across all regions
In organization mode, both reports include account IDs alongside each service and resource entry.
The system scans 17 AWS regions (in batches of 4 for performance) and checks the following resources using CloudWatch metrics:
| Resource Type | Detection Criteria |
|---|---|
| EC2 Instances | CPU < 5% avg or no network traffic |
| EBS Volumes | Unattached or zero I/O operations |
| EBS Snapshots | Orphaned (source volume deleted) or old (90d+) |
| RDS Instances | No connections or CPU < 5% avg |
| RDS Snapshots | Orphaned (source DB deleted) or old (90d+) |
| RDS Cluster Snapshots | Orphaned (source cluster deleted) or old |
| EFS Backups | Orphaned (source EFS deleted) or old (90d+) |
| Load Balancers | No traffic (ALB/NLB) |
| Elastic IPs | Not associated with any instance |
| NAT Gateways | No outbound traffic |
| EFS File Systems | No client connections |
| EKS Clusters | No nodes or CPU < 5% avg |
| ECS Services | No running tasks or CPU < 5% avg |
| ElastiCache | No connections or CPU < 5% avg |
| Redshift Clusters | No connections or CPU < 5% avg |
| OpenSearch Domains | No search requests or CPU < 5% avg |
To scan resources and aggregate costs across multiple AWS accounts:
Use the provided cross-account-role.yaml template:
aws cloudformation deploy \
--template-file cross-account-role.yaml \
--stack-name cost-alerts-read-role \
--parameter-overrides ManagementAccountId=<YOUR_PARENT_ACCOUNT_ID> \
--capabilities CAPABILITY_NAMED_IAM \
--region us-east-1Add the CHILD_ACCOUNTS parameter during deployment with comma-separated account IDs:
# In samconfig.toml parameter_overrides, add:
ChildAccounts="123456789012,987654321098"Or pass it via SAM deploy:
sam deploy --parameter-overrides \
ChildAccounts="123456789012,987654321098" \
SlackWebhookUrl="https://hooks.slack.com/services/..."Organization mode is automatically enabled when CHILD_ACCOUNTS is configured with at least one account ID. In this mode, cost data is grouped by linked account and service, and unused resource reports include account IDs.
Before deploying, test locally using SAM:
# Test weekly report
npm run test:weekly
# Test monthly report
npm run test:monthlyMake sure you have AWS credentials configured and env.json properly set up.
- Go to your Slack workspace settings
- Create an Incoming Webhook integration
- Copy the webhook URL
- Add it to your
env.jsonasSLACK_WEBHOOK_URL
Learn more about Slack webhooks
The Lambda function requires the following AWS IAM permissions (all configured in template.yaml):
- Cost Explorer:
ce:GetCostAndUsage,ce:GetCostForecast - STS:
sts:GetCallerIdentity,sts:AssumeRole(for organization mode cross-account access) - CloudWatch:
cloudwatch:GetMetricStatistics,cloudwatch:GetMetricData - EC2:
ec2:DescribeInstances,ec2:DescribeVolumes,ec2:DescribeRegions,ec2:DescribeAddresses,ec2:DescribeNatGateways,ec2:DescribeSnapshots - RDS:
rds:DescribeDBInstances,rds:DescribeDBSnapshots,rds:DescribeDBClusters,rds:DescribeDBClusterSnapshots - ELB:
elasticloadbalancing:DescribeLoadBalancers - EFS:
elasticfilesystem:DescribeFileSystems - EKS:
eks:ListClusters,eks:DescribeCluster - ECS:
ecs:ListClusters,ecs:ListServices,ecs:DescribeServices - ElastiCache:
elasticache:DescribeCacheClusters - Redshift:
redshift:DescribeClusters - OpenSearch:
es:ListDomainNames,es:DescribeDomain - AWS Backup:
backup:ListBackupVaults,backup:ListRecoveryPointsByBackupVault
This solution is designed to be extremely cost-effective:
- Lambda: ~$0.20/month (free tier eligible)
- EventBridge: ~$0.10/month (free tier eligible)
- Cost Explorer API: Free (included with AWS)
- Total: Essentially free on AWS free tier
- Verify IAM role has correct permissions
- Check EventBridge rules are enabled
- Review CloudWatch Logs for errors:
aws logs tail /aws/lambda/cost-reporter-dev --follow
- Verify
SLACK_WEBHOOK_URLis correct in environment variables - Check Slack workspace allows incoming webhooks
- Review Lambda logs for HTTP errors
- Ensure Cost Explorer is enabled in your AWS account
- Verify Lambda has
ce:GetCostAndUsagepermissions - Cost data takes ~24 hours to appear in Cost Explorer
- Verify the cross-account role exists in the child account
- Check that the role trust policy references the correct parent account ID
- Ensure the
CROSS_ACCOUNT_ROLE_NAMEmatches the deployed role name - Verify
CHILD_ACCOUNTSuses comma-separated account IDs (e.g.,123456789012,987654321098) - Review Lambda logs for
AssumeRoleerrors
# samconfig.toml parameter_overrides
MonthlyBudget="500" AnomalyThreshold="20" TopServicesCount="10"
# samconfig.toml parameter_overrides
MonthlyBudget="5000" AnomalyThreshold="15" TopServicesCount="10" ChildAccounts="111111111111,222222222222,333333333333"
MonthlyBudget="1000" AnomalyThreshold="10" SnapshotAgeThresholdDays="30"
# View function logs
sam logs -n CostReporterFunction
# Invoke function directly
sam local invoke CostReporterFunction -e events/weekly.json --env-vars env.json
# Delete stack
sam deleteMIT License - see LICENSE file for details.
For issues, questions, or suggestions, please open an issue in the repository.
Built by Antstack | Website