Introduction
Cloud load balancing is a cornerstone of modern architecture to ensure high availability (HA), horizontal scalability, and application resilience. On AWS, the Application Load Balancer (ALB) excels at Layer 7 intelligent routing, advanced health checks, and seamless integration with Auto Scaling Groups (ASG).
This advanced tutorial guides you step by step through implementing a complete stack with Terraform: creating a multi-AZ VPC, public subnets, ALB with HTTP/HTTPS target groups, an EC2 launch template for a simple Nginx app, and ASG for auto-scaling. Picture it like an orchestra: the ALB is the conductor distributing requests to musicians (EC2 instances) in real time, skipping any 'off' ones via health checks.
Why this setup? It's production-ready, handles >10k req/s, supports SSL termination, and is 100% IaC for CI/CD. Estimated time: 30 min. By the end, you'll have a publicly accessible LB that's infinitely scalable. Prep your AWS account! (142 words)
Prerequisites
- Active AWS account with IAM permissions:
AmazonEC2FullAccess,AmazonVPCFullAccess,AWSElasticLoadBalancingFullAccess,AutoScalingFullAccess. - Terraform CLI v1.5+ installed (download).
- AWS CLI v2 configured (
aws configure) with admin IAM keys. - Code editor (VS Code with Terraform extension recommended).
- Advanced knowledge of AWS networking (VPC, SG, AZ) and IaC.
Initialize the Terraform Project
mkdir aws-load-balancer-terraform
cd aws-load-balancer-terraform
git init
echo 'terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.40"
}
}
required_version = ">= 1.5"
}' > versions.tf
terraform initThis script creates the project directory, initializes Git for versioning, defines Terraform/AWS provider versions in versions.tf, and runs terraform init to download providers. It lays the IaC foundations without compatibility issues. Avoid wildcard versions for production stability.
Configure Providers and Variables
Now create providers.tf and variables.tf. The AWS provider targets eu-west-1 for EU GDPR compliance. Variables enable customization (CIDR, instance type). Run terraform plan after each file to validate.
Terraform Providers and Variables
terraform {
backend "s3" {
bucket = "your-terraform-state-bucket"
key = "aws-alb/terraform.tfstate"
region = "eu-west-1"
dynamodb_table = "terraform-locks"
}
}
provider "aws" {
region = var.aws_region
}
data "aws_availability_zones" "available" {
state = "available"
}
variable "aws_region" {
description = "Région AWS"
type = string
default = "eu-west-1"
}
variable "vpc_cidr" {
description = "CIDR du VPC"
type = string
default = "10.0.0.0/16"
}
variable "instance_type" {
description = "Type d'instance EC2"
type = string
default = "t3.micro"
}This file configures the AWS provider with an S3 backend for remote state (essential for teams), an AZ data source for multi-zone setup, and default variables. The DynamoDB backend handles concurrent locks. Pitfall: Never use local backend in production—risk of state corruption.
Provision the VPC and Networking
Multi-AZ VPC: 2 public subnets for the ALB (HA). Internet Gateway (IGW) + public route table. Security Groups: ALB (HTTP/HTTPS inbound), EC2 (from ALB + SSH). Run terraform plan for a preview.
VPC and Security Group Resources
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = {
Name = "alb-vpc"
}
}
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${50 + count.index}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "public-subnet-${count.index + 1}"
}
}
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "alb-igw"
}
}
resource "aws_default_route_table" "public" {
default_route_table_id = aws_vpc.main.default_route_table_id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
}
resource "aws_security_group" "alb_sg" {
name_prefix = "alb-sg-"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "alb-sg"
}
}
resource "aws_security_group" "ec2_sg" {
name_prefix = "ec2-sg-"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb_sg.id]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "ec2-sg"
}
}Provisions VPC, 2 AZ-isolated public subnets, IGW for internet access, and restrictive SGs (principle of least privilege). The EC2 SG allows traffic only from the ALB on port 80. Pitfall: Without map_public_ip_on_launch, targets lack public IPs; test with plan.
Application Load Balancer and Target Group
resource "aws_lb" "main" {
name = "alb-loadbalancer"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb_sg.id]
subnets = aws_subnet.public[*].id
tags = {
Environment = "prod"
}
}
resource "aws_lb_target_group" "app" {
name = "app-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id
health_check {
enabled = true
healthy_threshold = 2
interval = 30
matcher = "200"
path = "/"
port = "traffic-port"
protocol = "HTTP"
timeout = 5
unhealthy_threshold = 2
}
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08"
certificate_arn = data.aws_acm_certificate.cert.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}Creates an internet-facing ALB in public subnets, target group with strict health check (root path on HTTP returning 200), and HTTP/HTTPS listeners (adjust ACM cert). Forwards all traffic to the TG. Pitfall: Health check path must match your app (Nginx root works); otherwise, instances show as unhealthy.
Deploy EC2 with Launch Template and ASG
Launch Template: Amazon Linux 2023 AMI, user-data installs Nginx + health endpoint. ASG: Min 2 instances, scale on CPU >70%, attaches to TG. Run terraform apply to deploy! Access the ALB DNS afterward.
Launch Template, ASG, and Outputs
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64-kernel-*"]
}
}
resource "aws_launch_template" "app" {
name_prefix = "app-lt-"
image_id = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
vpc_security_group_ids = [aws_security_group.ec2_sg.id]
user_data = base64encode(
templatefile("${path.module}/user-data.sh", {
health_path = "/"
})
)
tag_specifications {
resource_type = "instance"
tags = {
Name = "alb-app"
}
}
}
resource "aws_autoscaling_group" "app" {
desired_capacity = 2
max_size = 4
min_size = 2
vpc_zone_identifier = aws_subnet.public[*].id
target_group_arns = [aws_lb_target_group.app.arn]
health_check_grace_period = 300
health_check_type = "ELB"
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
tag {
key = "Name"
value = "alb-asg"
propagate_at_launch = true
}
}
resource "aws_autoscaling_policy" "scale_up" {
name = "scale-up"
scaling_adjustment = 2
adjustment_type = "ChangeInCapacity"
cooldown = 300
autoscaling_group_name = aws_autoscaling_group.app.name
}
resource "aws_autoscaling_policy" "scale_down" {
name = "scale-down"
scaling_adjustment = -1
adjustment_type = "ChangeInCapacity"
cooldown = 300
autoscaling_group_name = aws_autoscaling_group.app.name
}
output "alb_dns_name" {
description = "DNS de l'ALB"
value = aws_lb.main.dns_name
}
output "target_group_arn" {
value = aws_lb_target_group.app.arn
}Launch template with auto-resolved AMI, Nginx user-data (separate file below), multi-AZ ASG with ELB health checks, and CPU-based scaling policies. Outputs for CI/CD integration. Pitfall: Too-short grace period causes false unhealthy flags; user-data must expose the health path.
User-Data Script for Nginx
#!/bin/bash
yum update -y
yum install -y nginx
systemctl enable nginx
systemctl start nginx
cat > /usr/share/nginx/html/index.html <<EOF
<!DOCTYPE html>
<html>
<head><title>AWS ALB Test</title></head>
<body>
<h1>Load Balanced by AWS ALB! ✅</h1>
<p>Instance ID: $(curl -s http://169.254.169.254/latest/meta-data/instance-id)</p>
</body>
</html>
EOF
systemctl reload nginxUser-data bootstrap script: updates packages, installs Nginx, and creates a custom page with Instance ID to verify routing. Health check on / returns 200. Pitfall: Without reload, changes aren't applied; test locally with bash user-data.sh.
Apply and Test the Deployment
terraform validate
tfenv vars set aws_region eu-west-1 # si tfenv
terraform plan -var="vpc_cidr=10.1.0.0/16"
terraform apply -auto-approve
terraform output alb_dns_name
curl $(terraform output -raw alb_dns_name)
echo 'DNS ALB: $(terraform output -raw alb_dns_name)' > alb-url.txt
# Cleanup
t# erraform destroy -auto-approveValidates, plans (optional var override), applies, outputs DNS, tests with curl (should show Nginx HTML). Cleanup with destroy. Pitfall: Without -var-file, defaults are used; monitor costs (~$0.025/h for ALB + EC2).
Best Practices
- Multi-AZ required: At least 2 subnets in different AZs for 99.99% SLA.
- Strict health checks: App-specific path, 2-3 threshold, 30s interval.
- ALB SSL termination: Offload HTTPS, free/auto-renew ACM certs.
- CloudWatch monitoring: Alarms for CPU>70%, connections, latency; integrate with Slack.
- Advanced IaC: Reusable Terraform modules, S3 remote state + locks.
Common Errors to Avoid
- Overly permissive SGs: Always specify sources (ALB ID for EC2); audit with GuardDuty.
- No ASG grace period: Instances killed prematurely; set min 300s.
- Health check failures: Check Nginx logs (
/var/log/nginx/error.log) and path matcher (200 only). - Lost Terraform state: Use S3 backend from the start, never local in production.
Next Steps
- Official docs: AWS ALB, Terraform AWS.
- Advanced: WAF integration, Lambda targets, EKS Ingress.
- Training: AWS & Terraform DevOps at Learni.
- Example GitHub repo: fork & contribute!