
Stop Creating an ALB for Every Service: Master Multi-Service Routing and Save Thousands with Terraform ⚡
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.
Series: Save on AWS with Terraform (21 articles)
- …
- 7Stop Creating an ALB for Every Service: Master Multi-Service Routing and Save Thousands with Terraform ⚡ This article
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):
1
2
3
4
5
10 ALBs × $16.40/month base = $164/month
LCU charges (varies) = $30-50/month
Total: ~$200/month
Annual cost: $2,400Most 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:
1
2
3
https://example.com/api/* → API service
https://example.com/admin/* → Admin service
https://example.com/blog/* → Blog serviceHost-based routing:
1
2
3
https://api.example.com → API service
https://admin.example.com → Admin service
https://blog.example.com → Blog serviceOne ALB. Multiple services. Massive savings.
📊 Cost Comparison
Before Consolidation
1
2
3
4
5
6
10 ALBs (one per service):
- 10 × $16.40/month = $164/month
- LCU charges across 10 ALBs = $50/month
- Total: $214/month
Annual cost: $2,568After Consolidation
1
2
3
4
5
6
7
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):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:
1
2
3
Priority 100-500: Specific paths/hosts
Priority 500-900: General patterns
Priority 1000+: Catch-all defaults2. Wildcard SSL Certificate
One
*.example.com cert works for all subdomains:1
2
3
4
5
6
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:
1
2
3
4
5
6
7
# 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:1
2
3
4
5
condition {
path_pattern {
values = ["/api", "/api/*"]
}
}3. Health check paths
Each target group needs its own health check:
1
2
3
4
5
# 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:
1
2
3
4
5
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
1
2
3
4
5
aws elbv2 describe-load-balancers \
--query 'LoadBalancers[?Type==`application`].[LoadBalancerName,DNSName]' \
--output table
# Count them - if > 5, you can probably consolidateStep 2: Plan consolidation
1
2
3
4
Group services by:
- Environment (prod/staging/dev)
- Network (public/private)
- Traffic pattern (high/low)Step 3: Deploy consolidated ALB
1
terraform apply -target=module.consolidated_albStep 4: Test with one service
1
2
3
# Update DNS for one service to new ALB
# Test thoroughly before migrating others
curl -I https://api.example.com/healthStep 5: Migrate remaining services
1
2
3
# Update DNS records one by one
# Wait for old ALB traffic to drain
# Delete old ALBsStep 6: Celebrate savings 🎉
📊 Real-World Example
Before consolidation:
1
2
3
4
5
6
7
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,952After consolidation:
1
2
3
4
5
6
7
8
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
✅ 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! ⚡
Series: Save on AWS with Terraform (21 articles)
- …
- 7Stop Creating an ALB for Every Service: Master Multi-Service Routing and Save Thousands with Terraform ⚡ This article
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article