1. The Voluntary Cloud Tax
Watching an AWS bill speedrun your IT budget is pure panic. Stop paying the voluntary cloud tax for dev servers that nobody turned off on Friday.
2. The Idle Math
Let's look at the math: there are 168 hours in a week, and your developers are allegedly working 40 of them. That means for almost 130 hours a week, your non-production servers are just sitting there. They are active for maybe a quarter of the time.
The other 76%, they're completely idle, doing absolutely nothing, just quietly burning company cash in a data center somewhere.
3. The Baseline Architecture
Here is a baseline setup: we've got a standard Application Load Balancer sitting in front of an Auto Scaling Group with three T3 mediums. If we run a cost breakdown right now, it wants to charge us $108 a month. For a development server, that's a bit too much.
Here is the complete Terraform configuration used to provision the baseline architecture, including the Launch Template, Auto Scaling Group, and networking components.
View Full Baseline Terraform Code (main.tf)
provider "aws" {
region = "us-east-1"
}
# 1. Fetch the latest Amazon Linux OS (Only declared ONCE here)
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-2023.*-x86_64"]
}
}
# 2. Minimal Networking for the Load Balancer
resource "aws_vpc" "dev_vpc" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "dev_subnet_a" {
vpc_id = aws_vpc.dev_vpc.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"
map_public_ip_on_launch = true
}
resource "aws_subnet" "dev_subnet_b" {
vpc_id = aws_vpc.dev_vpc.id
cidr_block = "10.0.2.0/24"
availability_zone = "us-east-1b"
map_public_ip_on_launch = true
}
resource "aws_security_group" "dev_sg" {
name = "dev-sg"
vpc_id = aws_vpc.dev_vpc.id
# Allow inbound HTTP traffic
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Allow all outbound traffic (so the servers can download updates)
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_internet_gateway" "dev_igw" {
vpc_id = aws_vpc.dev_vpc.id
}
resource "aws_route_table" "dev_public_rt" {
vpc_id = aws_vpc.dev_vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.dev_igw.id
}
}
resource "aws_route_table_association" "a" {
subnet_id = aws_subnet.dev_subnet_a.id
route_table_id = aws_route_table.dev_public_rt.id
}
resource "aws_route_table_association" "b" {
subnet_id = aws_subnet.dev_subnet_b.id
route_table_id = aws_route_table.dev_public_rt.id
}
# 3. The Launch Template
resource "aws_launch_template" "dev_web" {
name_prefix = "dev-web-template"
image_id = data.aws_ami.amazon_linux.id
instance_type = "t3.medium"
vpc_security_group_ids = [aws_security_group.dev_sg.id]
# This script automatically installs a web server when the instance boots!
user_data = base64encode(<<-EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
# Grab the private IP of the specific EC2 instance
INSTANCE_IP=$(hostname -I | awk '{print $1}')
# Build the webpage
echo "" > /var/www/html/index.html
echo "Hello from the Dev Environment!
" >> /var/www/html/index.html
echo "Traffic is currently being handled by Server IP: $INSTANCE_IP
" >> /var/www/html/index.html
echo "" >> /var/www/html/index.html
EOF
)
}
# 4. The Auto Scaling Group (Deploys 3 Instances)
resource "aws_autoscaling_group" "dev_web_asg" {
name = "dev-web-asg"
vpc_zone_identifier = [aws_subnet.dev_subnet_a.id, aws_subnet.dev_subnet_b.id]
desired_capacity = 3
min_size = 3
max_size = 3
launch_template {
id = aws_launch_template.dev_web.id
version = "$Latest"
}
target_group_arns = [aws_lb_target_group.dev_web_tg.arn]
}
# 5. The Load Balancer
resource "aws_lb" "dev_alb" {
name = "dev-web-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.dev_sg.id]
subnets = [aws_subnet.dev_subnet_a.id, aws_subnet.dev_subnet_b.id]
}
resource "aws_lb_target_group" "dev_web_tg" {
name = "dev-web-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.dev_vpc.id
}
# The Listener (Tells the ALB where to send traffic)
resource "aws_lb_listener" "dev_listener" {
load_balancer_arn = aws_lb.dev_alb.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.dev_web_tg.arn
}
}
Let's run terraform apply and deploy our setup. Terraform does its thing, we check the AWS console, and
the instances are running. If we hit the load balancer URL, we get our test page. Refresh it a
couple of times, the IPs change, so traffic is routing perfectly. The architecture is solid, but the
billing department is still crying.
4. Implementing Scheduled Scaling
Now, let's cut the AWS bill by treating the servers like office lights, automatically turning them off when people go home using a couple of lines of Terraform.
Here's the magic trick: scheduled scaling. We drop in two aws_autoscaling_schedule
resources into our configuration.
# --- SCHEDULED SCALING (The Cost Saver) ---
resource "aws_autoscaling_schedule" "scale_down" {
scheduled_action_name = "scale-down-evening"
min_size = 0
max_size = 0
desired_capacity = 0
recurrence = "0 18 * * 1-5" # 6 PM Mon-Fri
autoscaling_group_name = aws_autoscaling_group.dev_web_asg.name
}
resource "aws_autoscaling_schedule" "scale_up" {
scheduled_action_name = "scale-up-morning"
min_size = 3
max_size = 3
desired_capacity = 3
recurrence = "0 8 * * 1-5" # 8 AM Mon-Fri
autoscaling_group_name = aws_autoscaling_group.dev_web_asg.name
}
The first block tells the group to scale down to zero instances at 6 PM every weekday. The second block tells it to wake back up and spin up three instances at 8 AM every weekday.
We run a quick terraform apply to push the update. Check the AWS console again, and look under the
autoscaling group, and our instances officially have a bedtime.
5. The Cost Savings
Let's actually look at the math to see if it was worth it. To calculate our baseline cost without the schedule, we run a standard Infracost breakdown:
infracost breakdown --path .
This command returns our $108/month estimate based on instances running 24/7. But what happens when
we
apply our bedtime? We can feed Infracost a custom usage file (infracost-usage.yml) that
simulates our 40-hour work week instead of the default 730-hour month.
# infracost-usage.yml
version: 0.1
resource_usage:
aws_autoscaling_group.dev_web_asg:
instances: 1 # This simulates 40 hours/week out of the standard 730-hour month
We run the breakdown again, this time passing in the custom usage file:
infracost breakdown --path . --usage-file infracost-usage.yml
Our estimated bill drops from the $108 to $47. It took 5 minutes of code to cut the bill in half.