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.

Let me guess: You have one Application Load Balancer (ALB) per service.

  • api-prod-alb
  • web-prod-alb
  • admin-prod-alb
  • blog-prod-alb
  • workers-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! ⚡

Leave a Reply