DEV Community

Cover image for Building a Two-Tier Architecture with Terraform: My Hands-On Experience
Otu Udo
Otu Udo

Posted on

Building a Two-Tier Architecture with Terraform: My Hands-On Experience

Introduction

As a cloud enthusiast delving into Infrastructure as Code (IaC), I recently embarked on creating a two-tier architecture using Terraform. This project was both a challenge and a valuable learning experience, teaching me about efficient infrastructure design, optimizing Terraform modules, and securing cloud resources. In this blog, I’ll walk you through my journey, highlighting the challenges, solutions, and insights I gained along the way.


What is a Two-Tier Architecture?

A two-tier architecture separates the application layer from the database layer, enhancing scalability, security, and maintainability. For this project, I focused on creating:

  1. Application Layer: Hosted by an Auto Scaling Group (ASG) managing EC2 instances.
  2. Load Balancer Layer: An Application Load Balancer (ALB) directing incoming traffic to healthy instances.

This setup ensures high availability and scalability while maintaining a simple architecture.


Project Overview

Here’s a snapshot of my architecture:

  • S3 Backend: Used for Terraform state management.
  • VPC with Public Subnets: All resources were deployed in public subnets, secured through well-configured security groups.
  • Load Balancer: An ALB distributing traffic to application servers.
  • Auto Scaling Group: Dynamically managing application servers to ensure consistent performance.

Architectural Diagram
diagram

Initially, I incorporated four Terraform modules, including a standalone EC2 module. However, I later optimized the design by removing the EC2 module, realizing that the ASG already managed instance creation effectively.

*Key Insight: *

  • Use both EC2 and ASG modules when you need extensive customization for EC2 instances while leveraging ASG's auto-scaling.
  • Use only the ASG module for a streamlined, scalable solution when EC2 configuration is straightforward.

Step-by-Step Implementation

Step 1: Setting Up the Network
The first step was creating an s3 backend for storing states securely for this project. Kindly follow the procedue by clicking here.

Next we move to creating a VPC with two public subnets, each in a separate availability zone:

#Create VPC
resource "aws_vpc" "main" {
  cidr_block           = var.cidr_block
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = var.vpc_name
  }
}

#Create Subnets
resource "aws_subnet" "public" {
  count                   = length(var.public_subnets)
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnets[count.index]
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true
  tags = {
    Name = "${var.vpc_name}-public-${count.index}"
  }
}
Enter fullscreen mode Exit fullscreen mode

The configuration included an internet gateway and route tables to enable internet access.

Step 2: Configuring the Auto Scaling Group (ASG)

The ASG dynamically created and managed EC2 instances, simplifying scaling:

resource "aws_launch_template" "lt" {
  name_prefix   = var.name_prefix
  image_id      = var.image_id
  instance_type = var.instance_type

  network_interfaces {
    associate_public_ip_address = true
    security_groups          = var.security_groups
  }

  user_data = base64encode(var.user_data)
}
resource "aws_autoscaling_group" "asg" {
  desired_capacity     = var.desired_capacity
  min_size             = var.min_size
  max_size             = var.max_size
  vpc_zone_identifier  = var.subnets

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

  target_group_arns = [var.target_group_arn]

  tag {
    key                 = "Name"
    value               = var.name_prefix
    propagate_at_launch = true
  }
Enter fullscreen mode Exit fullscreen mode

To ensure optimal resource utilization, I incorporated lifecycle policies and auto-scaling configurations into the architecture. These measures automatically adjust the number of EC2 instances based on workload demands.

Step 3: Adding a Load Balancer

The ALB ensured efficient traffic distribution and health monitoring:

resource "aws_lb" "alb" {
  name               = var.name
  internal           = false
  load_balancer_type = "application"
  security_groups    = var.security_groups
  subnets            = var.subnets
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Securing Resources

Security groups restricted access to critical resources:

#Create Security Group for ALB
resource "aws_security_group" "alb_sg" {
  name        = "alb-sg"
  description = "Security group for the Application Load Balancer"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"] # Allow HTTP traffic from the internet
  }

  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 = "sg-alb"
  }
}

#Create Security Group for ASG
resource "aws_security_group" "asg_sg" {
  name        = "asg-sg"
  description = "Security group for EC2 instances in the ASG"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "Allow HTTP traffic from ALB"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    security_groups = [aws_security_group.alb_sg.id] # Reference ALB SG
  }

  egress {
    description = "Allow all outbound traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "asg-sg"
  }
}
Enter fullscreen mode Exit fullscreen mode

The ALB accepted HTTP traffic, while EC2 instances only allowed traffic from the ALB.

Step 5: Deploying the Infrastructure

While the above codes are just snippets.Clone the main repo to proceed with the steps below.

change directory to dev and configure the root main.tf

   cd envs/dev
Enter fullscreen mode Exit fullscreen mode

I used the following commands to deploy the setup:

  1. Initialize Terraform:
   terraform init
Enter fullscreen mode Exit fullscreen mode
  1. Plan the Infrastructure:
   terraform plan
Enter fullscreen mode Exit fullscreen mode
  1. Apply the Configuration:
   terraform apply
Enter fullscreen mode Exit fullscreen mode

The output included the ALB DNS name, which was used for testing.

DNS output

Step 6: Testing the Setup

To confirm the setup's functionality and security, I tested the ALB DNS name:
Copied the DNS name from the Terraform output.
Pasted it into a browser to verify the connection to the web server.
Security Confirmation
I also verified that the EC2 instances were only accessible via the ALB DNS. Directly attempting to connect to the EC2 instances using their public IPs resulted in denied access. This was achieved by configuring security groups to accept traffic only from the ALB.

This step reassured me that:

Traffic routing: All incoming requests were routed exclusively through the ALB.
Access control: No unauthorized traffic could directly reach the EC2 instances, ensuring a secure environment.

test


Screenshots of AWS Components

The screenshot below showcases the VPC, public subnets, route table and internet gateway created using Terraform. Each subnet in it's respective availability zone. This highlights the network's readiness for internet-facing resources.

vpc

This screenshot displays the target group attached to the ALB, showing the registered EC2 instances and their health status. The "healthy" status indicates that the ALB can successfully route traffic to these instances.

target group


Challenges and Solutions

  1. Redundant EC2 Module:

    Initially, I used a standalone EC2 module alongside the ASG, which caused redundancy. Removing it simplified the design and avoided potential conflicts.

  2. Security Concerns with Public Subnets:

    Deploying resources in public subnets required meticulous security group configurations to balance accessibility and security.


Key Learnings

  1. Optimize Module Usage: Avoid redundancy by understanding each module's functionality.
  2. Security Matters: Even in public subnets, effective security group configuration can ensure safety.
  3. Iterate and Improve: Testing and refining designs are critical for effective infrastructure management.

Conclusion

Building this two-tier architecture with Terraform was a rewarding journey that enhanced my understanding of cloud architecture and IaC best practices. I hope this blog inspires you to take on similar projects and simplifies your path to mastering Terraform.

Find the complete Terraform code in my GitHub repository. Feel free to connect on LinkedIn or leave a comment below with any questions or feedback!


Top comments (0)