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