Deploying a Highly Available Web App on AWS Using Terraform
We’ve deployed a single web server on AWS using Terraform. If you missed it, I recommend reading Deploying Your First Web Server with Terraform before diving in today’s article
In this post, we increase the complexity by creating a highly available infrastructure.
Instead of deploying a single server, we’ll set up an autoscaling cluster using a Launch Template. This will ensure that our app can handle increased traffic seamlessly. The concepts remain similar, but the implementation gets an upgrade.
By the end of this session, you'll have a fully working web application running on AWS, including:
Auto-scaling web servers
Application Load Balancer (ALB) for traffic distribution
Secure networking using VPCs, subnets, and security groups
What Is the Significance of Highly Available Infrastructure?
Consider a spike in website traffic. If your architecture isn't well thought out, your servers may break under the strain. Building robust and scalable systems is made simple with Terraform, guaranteeing that your app will always be operational, even during periods of high traffic.
Step-by-Step Deployment
Networking
The VPC, public/private subnets, internet gateway, and route tables follow the same logic as in the single-server setup here
Configuring the Launch Template
A Launch Template serves as a blueprint for instances in your Auto-Scaling Group. Rather than hardcoding a single aws_instance, it standardizes the way servers are configured.
Below is a simple configuration:
resource "aws_launch_template" "launch_template" {
name = "web-server-launch-template"
instance_type = var.instance_type
image_id = data.aws_ami.ubuntu.id
vpc_security_group_ids = [aws_security_group.web_sg.id]
user_data = base64encode(<<-EOT
#!/bin/bash
sudo apt update -y
sudo apt install -y nginx
echo "<h1>Hello from Terraform at $(hostname)</h1>" | sudo tee /var/www/html/index.html
EOT
)
tag_specifications {
resource_type = "instance"
tags = {
Name = "${var.tag}-instance"
}
}
}
Auto-Scaling Group for Server Clusters
The Auto-Scaling Group (ASG) ensures that your app scales dynamically. Here’s how it works:
resource "aws_autoscaling_group" "web_asg" {
name = "WebServerASG"
min_size = 2
max_size = 5
desired_capacity = 3
health_check_grace_period = 300
health_check_type = "ELB"
vpc_zone_identifier = [for subnet in aws_subnet.public_subnets : subnet.id]
launch_template {
id = aws_launch_template.launch_template.id
version = "$Latest"
}
target_group_arns = [aws_lb_target_group.web_target_group.arn]
}
Application Load Balancer
The ALB is responsible for distributing traffic evenly across your servers
resource "aws_lb" "alb" {
name = "web-server-alb"
load_balancer_type = "application"
security_groups = [aws_security_group.alb_sg.id]
subnets = [for subnet in aws_subnet.public_subnets: subnet.id]
}
Here's a twist: we’re replacing direct public server access with traffic routing through an ALB. Why? To enforce security best practices and ensure scalability.
ALB Security Group:
resource "aws_security_group" "web_sg" {
name = "web_sg"
description = "Allow alb traffic inbound and outbound"
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.tag}-sg"
}
}
resource "aws_vpc_security_group_ingress_rule" "allow_http" {
for_each = { for idx, port in var.inbound_ports : idx => port }
security_group_id = aws_security_group.web_sg.id
referenced_security_group_id = aws_security_group.alb_sg.id
from_port = each.value
to_port = each.value
ip_protocol = "tcp"
}
resource "aws_security_group" "alb_sg" {
description = "Allow TLS traffic inbound and all outbound"
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.tag}-sg"
}
}
resource "aws_vpc_security_group_ingress_rule" "allow_http_alb" {
for_each = { for idx, port in var.inbound_ports : idx => port }
security_group_id = aws_security_group.alb_sg.id
cidr_ipv4 = "0.0.0.0/0"
from_port = each.value
to_port = each.value
ip_protocol = "tcp"
}
ALB Security Group (
alb_sg
):Allows inbound traffic from the public internet (port 80).
Outbound traffic is unrestricted.
Web Server Security Group (
web_sg
):Only allows traffic from the ALB.
Outbound traffic is unrestricted.
Testing the Setup
Once you apply the Terraform configuration, grab the ALB's DNS from the output:
output "alb_dns" {
value = aws_lb.alb.dns_name
}
when you now lookup the alb dns and refresh, it would load balance between the number of deployed instances