DEV Community

Victor Robin
Victor Robin

Posted on

Deploying a Highly Available Web App on AWS Using Terraform

Welcome to Day 4 of my 30 Days of Terraform challenge. If you followed along with my first article you remember we deployed a single server into a VPC using terraform. That was a great starting point but it had a massive flaw because a single server is a single point of failure. If that availability zone goes down or the server crashes your application goes offline with it.

Today we are evolving our infrastructure from a simple hardcoded server into a fully configurable and highly available clustered deployment. We are going to build a custom Virtual Private Cloud block with public and private subnets and place our application servers in the private subnets for security while an Application Load Balancer routes internet traffic to them.

Let us dive right into the architecture and the code.

Overview

In this setup we are building a robust network foundation. We will create a VPC spanning two Availability Zones to ensure redundancy. Our architecture includes an Application Load Balancer residing in the public subnets to receive web traffic and an Auto Scaling Group managing our EC2 instances in the private subnets.

This means our actual application servers are completely hidden from the direct internet and can only be accessed through the load balancer. We are also utilizing variables and data sources extensively so the code is dynamic and reusable instead of being rigidly hardcoded.

Prerequisites

Before you start deploying this architecture you need to make sure your environment is ready.

  • Terraform installed on your local machine

  • AWS CLI installed and configured with your credentials

  • Appropriate IAM permissions configured for your AWS user account

You must ensure your IAM user or role has the permissions to create VPC resources like subnets and route tables as well as EC2 instances and Load Balancing components. A lack of these permissions will cause the deployment to fail immediately.

Step by Step Guide

1. Cloning the Repository

You can grab the complete source code for this deployment directly from my GitHub repository to follow along.

git clone https://github.com/Vivixell/Highly-Available-Web-App-on-AWS-Using-Terraform.git

cd Highly-Available-Web-App-on-AWS-Using-Terraform
Enter fullscreen mode Exit fullscreen mode

2. Running the Code

Once you have the code on your local machine the deployment process follows the standard Terraform workflow.

Initialize the working directory to download the AWS provider plugins:

terraform init
Enter fullscreen mode Exit fullscreen mode

Review the execution plan to see exactly what Terraform will create in your AWS account:

terraform plan
Enter fullscreen mode Exit fullscreen mode

Apply the configuration to build the infrastructure:

terraform apply -auto-approve
Enter fullscreen mode Exit fullscreen mode

Once the deployment finishes Terraform will output the DNS name of your new Load Balancer so you can paste it into your browser and see the live application.

3. Understanding the Code

Let us break down the most critical parts of this configuration to understand how it all pieces together.

The Power of Variables

Hardcoding values is a bad practice because it forces you to rewrite your core logic whenever a requirement changes. We moved our configurations into variables.tf using maps and objects to keep things organized.

Here is how we handle our application ports and Auto Scaling capacities dynamically.

variable "server_ports" {
  description = "A dictionary mapping application layers to their ports"
  type = map(object({
    port        = number
    description = string
  }))
  default = {
    "http" = { 
      port        = 80
      description = "Standard web traffic" 
    }
  }
}

variable "asg_capacity" {
  description = "Capacity settings for the Auto Scaling Group"
  type = object({
    min     = number
    max     = number
    desired = number
  })
  default = {
    min     = 2
    max     = 4
    desired = 2
  }
}

Enter fullscreen mode Exit fullscreen mode

The Security Groups

We operate on a zero trust model here. The load balancer security group allows public traffic on port 80 but our instance security group explicitly rejects all internet traffic and only accepts connections coming directly from the load balancer security group.

resource "aws_security_group" "instance_sg" {
  name        = "instance-sg"
  description = "Allow traffic from ALB only"
  vpc_id      = aws_vpc.my_vpc.id
}

resource "aws_vpc_security_group_ingress_rule" "instance_http" {
  security_group_id            = aws_security_group.instance_sg.id
  referenced_security_group_id = aws_security_group.alb_sg.id
  from_port                    = var.server_ports["http"].port
  to_port                      = var.server_ports["http"].port
  ip_protocol                  = "tcp"
}
Enter fullscreen mode Exit fullscreen mode

The Launch Template and Auto Scaling Group

Instead of creating standalone EC2 instances we define a blueprint using a Launch Template. We dynamically fetch the latest Ubuntu AMI and pass a startup script that installs Apache and displays the specific private IP of the server so we can visually confirm the load balancer is working across different instances.

The Auto Scaling Group then takes this template and ensures we always have our desired number of servers running across our private subnets.

resource "aws_launch_template" "my_app" {
  name_prefix   = "my-app-lt-"
  image_id      = data.aws_ami.ubuntu.id
  instance_type = var.instance_type

  vpc_security_group_ids = [aws_security_group.instance_sg.id] 

  user_data = base64encode(<<-EOF
    #!/bin/bash
    apt-get update -y
    apt-get install -y apache2
    systemctl start apache2
    systemctl enable apache2
    echo "<h1>Hello from OVR Private Subnet! My IP is: $(hostname -I)</h1>" > /var/www/html/index.html
  EOF
  )
}

resource "aws_autoscaling_group" "my_asg" {
  name             = "my-app-asg"
  desired_capacity = var.asg_capacity.desired
  max_size         = var.asg_capacity.max
  min_size         = var.asg_capacity.min

  vpc_zone_identifier = [for subnet in aws_subnet.my_private_subnet : subnet.id]
  target_group_arns   = [aws_lb_target_group.my_tg.arn]

  launch_template {
    id      = aws_launch_template.my_app.id
    version = "$Latest"
  }

  tag {
    key                 = "Name"
    value               = "my-asg-instance"
    propagate_at_launch = true
  }
}
Enter fullscreen mode Exit fullscreen mode

Challenges I Faced

Honestly the actual code writing and deployment went completely smoothly without any logic roadblocks on my end. The modular approach of mapping out the variables and understanding the flow of traffic made the process straightforward and highly predictable.

Possible Challenges You Might Have

While the code is solid you might run into environmental issues depending on your local setup.

  • Network Connectivity Drops: You might see an error like lookup sts.us-east-1.amazonaws.com: no such host when running the apply command. This simply means your local machine temporarily lost its connection to the AWS authentication servers usually due to a VPN drop or a local DNS issue.

  • IAM Permission Errors: If you are not using an Administrator account you will need to ensure your user has explicit permissions to create VPCs subnets load balancers and auto scaling groups otherwise AWS will reject the API calls.

Possible Improvements

This architecture is robust but there is always room to grow in cloud engineering. A great next step would be implementing HTTPS by requesting an SSL certificate from AWS Certificate Manager and attaching it to a secure listener on the load balancer. We could also break this monolithic file structure down further by separating the networking resources into a dedicated Terraform module to increase reusability across multiple environments.

Final Thoughts

Moving from a single instance to a load balanced auto scaling environment is one of the most important leaps you make as a cloud engineer. You are no longer just launching servers but rather designing resilient systems that can survive failures and scale with demand.

Stay tuned for the next phase of the challenge where we will dive even deeper into infrastructure automation.

Top comments (0)