The Problem: AWS Bill Shock is Real
Picture this: You spin up a few EC2 instances for a weekend project or you are trying out a new service you’ve never worked with before, forget about them, and wake up Monday to a few hundred dollar AWS bill. Sound familiar?
When I started working on AWS and trying to create projects inside the Free tier, I always got worried that I would accidentally leave some resources running and that it would drive up my AWS bill like crazy! I’ve read so many stories on Reddit about this and stories that someone’s account got hacked and they got a crazy bill at the end of the month.
But, what if we created a tool which automatically kills all resources on the account when we reach a specified budget?
The Solution: BudgetSentinel
Today, we'll build BudgetSentinel - a simple, automated cost protection system that:
- Sends email alerts when you hit 80% of your monthly budget
- Auto-stops any resources which you define when you exceed 100% of budget
- Costs $0 to run (uses AWS free tier services)
- Takes 10 minutes to deploy with Terraform
Simple, effective, and potentially saves you hundreds of dollars.
What You'll Build and Learn
By the end of this tutorial, you'll have:
- AWS Budget monitoring your monthly spending
- SNS Topic for notifications
- Email alerts at 80% and 100% thresholds
- Lambda function that automatically stops expensive resource
- Terraform infrastructure that's easy to deploy and modify
Total AWS Cost: $0 (everything runs within free tier limits)
The star of this project is Terraform - an infrastructure as code (IaC) tool that lets you define both cloud and on-prem resources in human-readable configuration files that you can version, reuse, and share.
In my previous projects, I’ve used IaC tools which are made specifically for AWS, like CloudFormation templates and AWS Cloud Development Kit. Terraform is another IaC tool, however it’s specialty is in being able to use the same tool, but for other Cloud platforms, like Azure and GCP, as well.
For example, if you learn Terraform and your first project is on AWS, you can reuse that knowledge when you are trying to deploy your other project on Azure. You’ll still need to learn Azure’s specific resources and how they’re used, but you won’t have to pick up a totally new IaC tool which is used only on Azure, like ARM templates or Bicep. This makes your transition to Azure so much smoother.
Prerequisites
Before we start, make sure you have:
- AWS CLI configured with your AWS account (
aws configure
) - Terraform installed - link to official installation instructions here
- An AWS account with basic permissions
- 10 minutes of your time
Here is a documentation link to Terraform’s official documentation for AWS services: https://registry.terraform.io/providers/hashicorp/aws/latest/docs.
The GitHub code can be found here: https://github.com/mate329/budgetsentinel
Let’s begin!
Step 1: Project Structure
Let's start by understanding what we're building. One of main concepts of Terraform are modules. A module is a collection of resources that Terraform manages together.
You may ask yourself, why are we using modules? They make the code reusable, testable, and easier to understand. Each module has a single responsibility.
Each of our modules have the following 3 files:
-
main.tf
- main file containing all resources which are going to be deployed -
variables.tf
- this file is used for configuration of the module, based on your environment -
outputs.tf
- this file defines which data you want to show inside the terminal window after deployment
Armed with this knowledge, let’s take a look how will the folder structure look for this project. Our Terraform project has three core modules:
terraform-cost-guardrails/
├── modules/
│ ├── alerts/ # Child module - SNS topic + email subscriptions
│ ├── budget/ # Child module - AWS Budget with thresholds
│ └── automation/ # Child module - Lambda function for auto-stopping
├── main.tf # Root module
├── variables.tf # Configuration options for the whole project
└── outputs.tf # What to show after deployment
In the following steps, we are going to go module-by-module first and then wrap it all up inside our main.tf
file.
In addition, here is our architecture diagram for this project:
Step 2: The Alerts Module
Let’s go over how to define the resources for our Alerts module. For this module, we need:
- SNS Topic - used as a message broker from one AWS service to another
- SNS Topic Subscription - used to subscribe our inbox to receive alert emails
To define a resource in Terraform, you can do it by defining:
resource "resource_you_need" "name_of_the_resouce" {
configuration_field = "some_value"
}
Now, let's create our notification system. The alerts module creates an SNS topic and subscribes your email to it:
# modules/alerts/main.tf
resource "aws_sns_topic" "budget_alerts" {
name = var.topic_name
tags = var.tags
}
resource "aws_sns_topic_subscription" "email" {
topic_arn = aws_sns_topic.budget_alerts.arn
protocol = "email"
endpoint = var.email
}
# Optional Slack integration
resource "aws_sns_topic_subscription" "slack" {
count = var.slack_webhook != "" ? 1 : 0
topic_arn = aws_sns_topic.budget_alerts.arn
protocol = "https"
endpoint = var.slack_webhook
}
# Topic Policy to allow AWS Budgets to publish
resource "aws_sns_topic_policy" "budget_alerts" {
arn = aws_sns_topic.budget_alerts.arn
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "budgets.amazonaws.com"
}
Action = "SNS:Publish"
Resource = aws_sns_topic.budget_alerts.arn
Condition = {
StringEquals = {
"aws:SourceAccount" = data.aws_caller_identity.current.account_id
}
}
}
]
})
}
Key Features:
- Email notifications (required)
- Optional Slack integration with the ability to implement other services if needed
- Proper IAM policies for AWS Budgets to publish messages
When this module is deployed, you will receive an email in your inbox and it’s imperative that you confirm your email so you can get the notifications. The email should look like:
Step 3: The Budget Module
Next, we create the AWS Budget that monitors spending and triggers alerts:
# modules/budget/main.tf
# AWS Budget with notifications
resource "aws_budgets_budget" "main" {
name = var.budget_name
budget_type = var.budget_type
limit_amount = var.limit
limit_unit = "USD"
time_unit = var.time_unit
time_period_start = formatdate("YYYY-MM-01_00:00", timestamp())
cost_filter {
name = "Service"
values = ["Amazon Elastic Compute Cloud - Compute", "Amazon Relational Database Service"]
}
# 80% threshold notification
notification {
comparison_operator = "GREATER_THAN"
threshold = 80
threshold_type = "PERCENTAGE"
notification_type = "ACTUAL"
subscriber_email_addresses = []
subscriber_sns_topic_arns = [var.sns_topic_arn]
}
# 100% threshold notification
notification {
comparison_operator = "GREATER_THAN"
threshold = 100
threshold_type = "PERCENTAGE"
notification_type = "ACTUAL"
subscriber_email_addresses = []
subscriber_sns_topic_arns = [var.sns_topic_arn]
}
# Forecasted 100% threshold notification
notification {
comparison_operator = "GREATER_THAN"
threshold = 100
threshold_type = "PERCENTAGE"
notification_type = "FORECASTED"
subscriber_email_addresses = []
subscriber_sns_topic_arns = [var.sns_topic_arn]
}
depends_on = [var.sns_topic_arn]
}
Why these thresholds?
- 80% gives you time to take action manually
- 100% triggers automatic protection measures
Step 4: The Automation Module
Here's where the magic happens. When your budget is exceeded, this Lambda function automatically stops your EC2 instances and RDS databases. You can add your other services to your Lambda code which will get stopped if you exceed your budget.
Since Terraform is the star of this project, we will focus on it in this blog post, however the Lambda code can be found on the mentioned GitHub repository!
Here is the Terraform code for the Automation Module:
# Create ZIP file for Lambda function
data "archive_file" "lambda_zip" {
count = var.enable_automation ? 1 : 0
type = "zip"
source_file = "${path.module}/lambda_function.py"
output_path = "${path.module}/lambda_function.zip"
}
# IAM Role for Lambda
resource "aws_iam_role" "lambda_role" {
count = var.enable_automation ? 1 : 0
name = "${var.function_name}-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
tags = var.tags
}
# IAM Policy for Lambda to stop EC2 and RDS instances
resource "aws_iam_role_policy" "lambda_policy" {
count = var.enable_automation ? 1 : 0
name = "${var.function_name}-policy"
role = aws_iam_role.lambda_role[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "arn:aws:logs:*:*:*"
},
{
Effect = "Allow"
Action = [
"ec2:DescribeInstances",
"ec2:StopInstances"
]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"rds:DescribeDBInstances",
"rds:StopDBInstance"
]
Resource = "*"
}
]
})
}
# Lambda Function
resource "aws_lambda_function" "budget_automation" {
count = var.enable_automation ? 1 : 0
filename = data.archive_file.lambda_zip[0].output_path
function_name = var.function_name
role = aws_iam_role.lambda_role[0].arn
handler = "lambda_function.lambda_handler"
runtime = "python3.9"
timeout = 300
source_code_hash = data.archive_file.lambda_zip[0].output_base64sha256
depends_on = [
aws_iam_role_policy.lambda_policy,
aws_cloudwatch_log_group.lambda_logs
]
tags = var.tags
}
# CloudWatch Log Group
resource "aws_cloudwatch_log_group" "lambda_logs" {
count = var.enable_automation ? 1 : 0
name = "/aws/lambda/${var.function_name}"
retention_in_days = 14
tags = var.tags
}
# SNS Topic Subscription for Lambda
resource "aws_sns_topic_subscription" "lambda" {
count = var.enable_automation ? 1 : 0
topic_arn = var.sns_topic_arn
protocol = "lambda"
endpoint = aws_lambda_function.budget_automation[0].arn
}
# Lambda Permission for SNS to invoke the function
resource "aws_lambda_permission" "allow_sns" {
count = var.enable_automation ? 1 : 0
statement_id = "AllowExecutionFromSNS"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.budget_automation[0].function_name
principal = "sns.amazonaws.com"
source_arn = var.sns_topic_arn
}
Safety Features:
- Only stops running resources (won't affect stopped instances)
- Skips Aurora clusters (can't be stopped individually)
- Comprehensive logging for audit trail
- Can be easily disabled with
enable_automation = false
Keep in mind that if you want to expand the scope of AWS services this tool covers, you’ll need to add additional Lambda code and edit the Lambda IAM role, so the Lambda itself can access the necessary services.
Step 5: Tying It All Together
Before we continue, let’s go over how to define the main parts of our main.tf
root module file.
We need to define the following parts:
- Required providers - here, we will define that we are going to use AWS services. Think of it as defining a library in any programming language
- Modules - we’ll need to define a path to our already created modules
Our main Terraform configuration orchestrates all three modules:
# main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
module "alerts" {
source = "./modules/alerts"
topic_name = "budget-alerts"
email = var.notification_email
slack_webhook = var.slack_webhook
}
module "budget" {
source = "./modules/budget"
budget_name = "monthly-budget"
limit = var.budget_limit
sns_topic_arn = module.alerts.sns_topic_arn
}
module "automation" {
source = "./modules/automation"
sns_topic_arn = module.alerts.sns_topic_arn
function_name = "budget-sentinel-automation"
enable_automation = var.enable_automation
}
Step 6: Configuration
Create your configuration file:
# terraform.tfvars
notification_email = "your-email@example.com" # REQUIRED
slack_webhook = "" # Optional
budget_limit = 50 # Monthly USD limit
enable_automation = true # Auto-stop resources?
aws_region = "eu-central-1" # Free tier region
Step 7: Deploy Your Cost Guardian
Now for the moment of truth - let's deploy everything with these 3 commands:
# Initialize Terraform
terraform init
# Review what will be created
terraform plan
# Deploy everything
terraform apply
Expected output:
Apply complete! Resources: 15 added, 0 changed, 0 destroyed.
Outputs:
setup_complete = "Budget: monthly-budget | Email: your-email@example.com | Auto-stop: enabled"
Don't forget: Check your email and confirm the SNS subscription!
Cost Breakdown: Why It's Free
Cleanup
When you're ready to remove everything:
terraform destroy
# Type 'yes' to confirm
# All resources deleted, $0 ongoing cost
Conclusion
In just 10 minutes, we've built a comprehensive AWS cost protection system that:
- ✅ Prevents bill shock with proactive alerts
- ✅ Automatically stops runaway costs
- ✅ Costs nothing to run (free tier)
- ✅ Uses infrastructure as code for easy management
- ✅ Scales from personal to enterprise use
The Bottom Line: This simple system can save you hundreds or thousands of dollars in AWS costs while giving you peace of mind.
The code is production-ready, well-tested, and follows AWS best practices. Whether you're a weekend warrior or managing enterprise workloads, BudgetSentinel has your back.
Ready to protect your AWS account? Get the complete code on GitHub and deploy your cost guardian today!
Top comments (0)