Each AWS ALB costs $16-22/month. Most teams create way too many. Here’s how to consolidate using host-based and path-based routing with Terraform.
Let me guess: You have one Application Load Balancer (ALB) per service.
api-prod-albweb-prod-albadmin-prod-albblog-prod-albworkers-prod-alb- … and 5 more
That’s 10 ALBs × $16/month = $160/month = $1,920/year just sitting there.
Here’s the kicker: You probably only need 1-2 ALBs total.
Let me show you how to consolidate with Terraform and cut your load balancer bill by 70%.
💸 The ALB Cost Breakdown
Each ALB costs:
- $0.0225/hour (~$16.40/month) base charge
- $0.008/LCU-hour for traffic (Load Balancer Capacity Units)
Typical setup (10 microservices):
10 ALBs ×...
Each AWS ALB costs $16-22/month. Most teams create way too many. Here’s how to consolidate using host-based and path-based routing with Terraform.
Let me guess: You have one Application Load Balancer (ALB) per service.
api-prod-albweb-prod-albadmin-prod-albblog-prod-albworkers-prod-alb- … and 5 more
That’s 10 ALBs × $16/month = $160/month = $1,920/year just sitting there.
Here’s the kicker: You probably only need 1-2 ALBs total.
Let me show you how to consolidate with Terraform and cut your load balancer bill by 70%.
💸 The ALB Cost Breakdown
Each ALB costs:
- $0.0225/hour (~$16.40/month) base charge
- $0.008/LCU-hour for traffic (Load Balancer Capacity Units)
Typical setup (10 microservices):
10 ALBs × $16.40/month base = $164/month
LCU charges (varies) = $30-50/month
Total: ~$200/month
Annual cost: $2,400
Most of these ALBs are barely used. Your staging blog-alb might serve 100 requests/day but still costs $16/month.
🎯 The Solution: Path-Based & Host-Based Routing
ALBs support routing rules that let one ALB serve multiple services:
Path-based routing:
https://example.com/api/* → API service
https://example.com/admin/* → Admin service
https://example.com/blog/* → Blog service
Host-based routing:
https://api.example.com → API service
https://admin.example.com → Admin service
https://blog.example.com → Blog service
One ALB. Multiple services. Massive savings.
📊 Cost Comparison
Before Consolidation
10 ALBs (one per service):
- 10 × $16.40/month = $164/month
- LCU charges across 10 ALBs = $50/month
- Total: $214/month
Annual cost: $2,568
After Consolidation
2 ALBs (production + staging):
- 2 × $16.40/month = $32.80/month
- LCU charges (consolidated) = $35/month
- Total: $67.80/month
Annual cost: $813.60
Savings: $1,754.40/year (68% reduction!) 🎉
🛠️ Terraform Implementation
Path-Based Routing (Single Domain)
Perfect when all services are under one domain (e.g., example.com):
# modules/consolidated-alb/main.tf
resource "aws_lb" "main" {
name = "consolidated-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.public_subnet_ids
enable_deletion_protection = true
enable_http2 = true
tags = {
Name = "consolidated-alb"
}
}
# HTTPS listener with path-based routing
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = var.certificate_arn
# Default action - return 404
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "Not Found"
status_code = "404"
}
}
}
# HTTP listener - redirect to HTTPS
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = "80"
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
# API service (path: /api/*)
resource "aws_lb_target_group" "api" {
name = "api-tg"
port = 3000
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
path = "/api/health"
healthy_threshold = 2
unhealthy_threshold = 3
timeout = 5
interval = 30
}
}
resource "aws_lb_listener_rule" "api" {
listener_arn = aws_lb_listener.https.arn
priority = 100
action {
type = "forward"
target_group_arn = aws_lb_target_group.api.arn
}
condition {
path_pattern {
values = ["/api/*"]
}
}
}
# Admin service (path: /admin/*)
resource "aws_lb_target_group" "admin" {
name = "admin-tg"
port = 4000
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
path = "/admin/health"
}
}
resource "aws_lb_listener_rule" "admin" {
listener_arn = aws_lb_listener.https.arn
priority = 200
action {
type = "forward"
target_group_arn = aws_lb_target_group.admin.arn
}
condition {
path_pattern {
values = ["/admin/*"]
}
}
}
# Blog service (path: /blog/*)
resource "aws_lb_target_group" "blog" {
name = "blog-tg"
port = 8080
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
path = "/blog/health"
}
}
resource "aws_lb_listener_rule" "blog" {
listener_arn = aws_lb_listener.https.arn
priority = 300
action {
type = "forward"
target_group_arn = aws_lb_target_group.blog.arn
}
condition {
path_pattern {
values = ["/blog/*"]
}
}
}
# Web frontend (default - root path)
resource "aws_lb_target_group" "web" {
name = "web-tg"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
path = "/health"
}
}
resource "aws_lb_listener_rule" "web" {
listener_arn = aws_lb_listener.https.arn
priority = 1000 # Lower priority = later evaluation
action {
type = "forward"
target_group_arn = aws_lb_target_group.web.arn
}
condition {
path_pattern {
values = ["/*"] # Catch-all for everything else
}
}
}
Host-Based Routing (Multiple Subdomains)
Better for services with their own subdomains:
# host-based-alb.tf
resource "aws_lb" "multi_host" {
name = "multi-host-alb"
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.public_subnet_ids
tags = { Name = "multi-host-alb" }
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.multi_host.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = var.wildcard_certificate_arn # *.example.com
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "Not Found"
status_code = "404"
}
}
}
# api.example.com
resource "aws_lb_listener_rule" "api" {
listener_arn = aws_lb_listener.https.arn
priority = 100
action {
type = "forward"
target_group_arn = aws_lb_target_group.api.arn
}
condition {
host_header {
values = ["api.example.com"]
}
}
}
# admin.example.com
resource "aws_lb_listener_rule" "admin" {
listener_arn = aws_lb_listener.https.arn
priority = 200
action {
type = "forward"
target_group_arn = aws_lb_target_group.admin.arn
}
condition {
host_header {
values = ["admin.example.com"]
}
}
}
# blog.example.com
resource "aws_lb_listener_rule" "blog" {
listener_arn = aws_lb_listener.https.arn
priority = 300
action {
type = "forward"
target_group_arn = aws_lb_target_group.blog.arn
}
condition {
host_header {
values = ["blog.example.com"]
}
}
}
# www.example.com (main site)
resource "aws_lb_listener_rule" "web" {
listener_arn = aws_lb_listener.https.arn
priority = 400
action {
type = "forward"
target_group_arn = aws_lb_target_group.web.arn
}
condition {
host_header {
values = ["www.example.com", "example.com"]
}
}
}
Combined: Host + Path Routing (Most Flexible)
Mix both approaches for maximum flexibility:
# Combined routing - host + path
resource "aws_lb_listener_rule" "api_v2" {
listener_arn = aws_lb_listener.https.arn
priority = 150
action {
type = "forward"
target_group_arn = aws_lb_target_group.api_v2.arn
}
condition {
host_header {
values = ["api.example.com"]
}
}
condition {
path_pattern {
values = ["/v2/*"]
}
}
}
🎓 Consolidation Strategy
What to Consolidate
✅ Consolidate these:
- Dev/staging/QA environments (low traffic)
- Internal tools and dashboards
- Microservices in the same application
- Services with similar traffic patterns
❌ Keep separate ALBs for:
- Production vs non-production (isolation)
- Internet-facing vs internal services
- Services with wildly different traffic (1M req/day vs 100 req/day)
- Compliance-separated environments
Recommended Setup
For most teams:
1. Production ALB (internet-facing)
- api.example.com
- www.example.com
- admin.example.com
2. Internal ALB (private subnets)
- monitoring.internal
- logs.internal
3. Non-prod ALB (dev/staging/qa)
- dev.example.com
- staging.example.com
Total: 3 ALBs instead of 15+
Savings: 80% on ALB costs
💡 Pro Tips
1. Use Priority Wisely
Lower priority = evaluated first. Structure your rules:
Priority 100-500: Specific paths/hosts
Priority 500-900: General patterns
Priority 1000+: Catch-all defaults
2. Wildcard SSL Certificate
One *.example.com cert works for all subdomains:
resource "aws_acm_certificate" "wildcard" {
domain_name = "*.example.com"
validation_method = "DNS"
subject_alternative_names = ["example.com"] # Include root domain
}
Saves money vs per-service certificates.
3. Use Target Group Attributes
Fine-tune performance per service:
resource "aws_lb_target_group" "api" {
# ... other config ...
deregistration_delay = 30 # Faster deploys
stickiness {
type = "lb_cookie"
cookie_duration = 86400
enabled = true
}
load_balancing_algorithm_type = "least_outstanding_requests"
}
4. Monitor Per-Target Group
Even with one ALB, you can track each service separately:
resource "aws_cloudwatch_metric_alarm" "api_5xx" {
alarm_name = "api-high-5xx-errors"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "HTTPCode_Target_5XX_Count"
namespace = "AWS/ApplicationELB"
period = 60
statistic = "Sum"
threshold = 10
dimensions = {
TargetGroup = aws_lb_target_group.api.arn_suffix
LoadBalancer = aws_lb.main.arn_suffix
}
}
⚠️ Common Gotchas
1. Path order matters
More specific paths MUST have lower priority numbers:
# WRONG - catch-all evaluated first
priority = 100: /*
priority = 200: /api/*
# RIGHT - specific first
priority = 100: /api/*
priority = 200: /*
2. Trailing slashes
/api ≠ /api/ in path patterns. Use both:
condition {
path_pattern {
values = ["/api", "/api/*"]
}
}
3. Health check paths
Each target group needs its own health check:
# DON'T use same path for all services
health_check { path = "/health" } # ❌
# DO use service-specific paths
health_check { path = "/api/health" } # ✅
4. Target registration
Targets register to target groups, not ALBs:
resource "aws_lb_target_group_attachment" "api" {
target_group_arn = aws_lb_target_group.api.arn
target_id = aws_instance.api.id
port = 3000
}
🚀 Migration Checklist
Step 1: Audit existing ALBs
aws elbv2 describe-load-balancers \
--query 'LoadBalancers[?Type==`application`].[LoadBalancerName,DNSName]' \
--output table
# Count them - if > 5, you can probably consolidate
Step 2: Plan consolidation
Group services by:
- Environment (prod/staging/dev)
- Network (public/private)
- Traffic pattern (high/low)
Step 3: Deploy consolidated ALB
terraform apply -target=module.consolidated_alb
Step 4: Test with one service
# Update DNS for one service to new ALB
# Test thoroughly before migrating others
curl -I https://api.example.com/health
Step 5: Migrate remaining services
# Update DNS records one by one
# Wait for old ALB traffic to drain
# Delete old ALBs
Step 6: Celebrate savings 🎉
📊 Real-World Example
Before consolidation:
15 ALBs across environments:
- 5 production services = 5 ALBs
- 5 staging services = 5 ALBs
- 5 dev services = 5 ALBs
Cost: 15 × $16.40 = $246/month
Annual: $2,952
After consolidation:
3 ALBs total:
- 1 production (multi-service) = $16.40
- 1 staging (multi-service) = $16.40
- 1 dev (multi-service) = $16.40
Cost: 3 × $16.40 = $49.20/month
Annual: $590.40
Savings: $2,361.60/year (80% reduction!) 💰
🎯 Summary
| Setup | Monthly Cost | Annual Cost |
|---|---|---|
| 10 ALBs (1 per service) | $214 | $2,568 |
| 2 ALBs (consolidated) | $68 | $816 |
| Savings | $146 | $1,752 |
Key takeaways:
✅ Most teams over-provision ALBs (one per service)
✅ Path-based and host-based routing consolidate multiple services
✅ Typical savings: 60-80% on ALB costs
✅ Implementation time: 2-4 hours
✅ Zero performance impact
✅ Better resource utilization
Stop creating an ALB for every service. Consolidate with Terraform and watch your AWS bill drop. 🚀
Consolidated your ALBs? How many did you eliminate? Share in the comments! 💬
Follow for more AWS cost optimization with Terraform! ⚡