DEV Community

Cover image for Deploy Web Servers with Terraform: EC2 + Load Balancer Tutorial

Deploy Web Servers with Terraform: EC2 + Load Balancer Tutorial

πŸ‘‹ Hey there, tech enthusiasts!

I'm Sarvar, a Cloud Architect with a passion for transforming complex technological challenges into elegant solutions. With extensive experience spanning Cloud Operations (AWS & Azure), Data Operations, Analytics, DevOps, and Generative AI, I've had the privilege of architecting solutions for global enterprises that drive real business impact. Through this article series, I'm excited to share practical insights, best practices, and hands-on experiences from my journey in the tech world. Whether you're a seasoned professional or just starting out, I aim to break down complex concepts into digestible pieces that you can apply in your projects.


"A single server is a single point of failure. A load balancer is your insurance policy."

🎯 Welcome Back!

Remember in Article 6 when you built your first VPC with public and private subnets? You created the network foundation, but it was emptyβ€”no servers, no applications, nothing running.

Here's the reality: A VPC without compute resources is like building a highway with no cars. You need:

  • Web servers to run your applications
  • A way to handle traffic spikes
  • Protection against server failures
  • Zero-downtime deployments

That's where EC2 instances and Load Balancers come in.

By the end of this article, you'll:

  • βœ… Deploy EC2 instances with Terraform
  • βœ… Configure security groups for web servers
  • βœ… Use user data to automate server setup
  • βœ… Create an Application Load Balancer (ALB)
  • βœ… Implement health checks and target groups
  • βœ… Build high-availability web infrastructure

Time Required: 45 minutes (20 min read + 25 min practice)

Cost: ~$33/month (~$16 with free tier for ALB)

Difficulty: Intermediate

Let's deploy some real infrastructure! πŸš€


πŸ’” The Problem: Single Server Syndrome

The Nightmare Scenario

It's 2 AM. Your phone rings.

Monitoring Alert: Website Down
Status: 503 Service Unavailable
Cause: EC2 instance crashed
Impact: 100% of users affected
Revenue Loss: $500/minute
Enter fullscreen mode Exit fullscreen mode

You scramble to:

  1. SSH into the server (if you can)
  2. Restart the application
  3. Hope it comes back up
  4. Watch users leave your site

The root cause? You had one server running everything.

Common Single-Server Problems:

❌ Traffic Spike: Black Friday hits, server crashes from load

❌ Hardware Failure: AWS instance dies, site goes down

❌ Deployment Risk: Update breaks something, entire site offline

❌ No Redundancy: One point of failure = business risk

❌ Manual Recovery: You're the human load balancer at 2 AM

❌ Poor User Experience: Slow response times, timeouts, errors

Sound familiar? Let's fix this with load balancing.


🌟 What is a Load Balancer? (Quick Theory)

Simple Definition

Load Balancer = Traffic cop for your servers. It:

  • Distributes incoming requests across multiple servers
  • Monitors server health automatically
  • Routes traffic only to healthy servers
  • Enables zero-downtime deployments

Think of it like this:

Without Load Balancer:

All Users β†’ Single Server β†’ πŸ’₯ Overloaded/Crashed
Enter fullscreen mode Exit fullscreen mode

With Load Balancer:

Users β†’ Load Balancer β†’ Server 1 (healthy) βœ…
                     β†’ Server 2 (healthy) βœ…
                     β†’ Server 3 (unhealthy) ❌ (no traffic)
Enter fullscreen mode Exit fullscreen mode

Why You Need This

Scenario 1: Traffic Spike

  • Normal: 100 requests/sec β†’ 2 servers handle it easily
  • Black Friday: 10,000 requests/sec β†’ Load balancer distributes across all servers
  • Result: Site stays up, users happy

Scenario 2: Server Failure

  • Server 1 crashes at 2 AM
  • Load balancer detects failure in 30 seconds
  • Automatically stops sending traffic to Server 1
  • Server 2 handles all traffic
  • Result: You sleep through the night

Scenario 3: Deployment

  • Deploy new code to Server 1
  • Load balancer keeps sending traffic to Server 2
  • Test Server 1, then switch traffic
  • Result: Zero downtime deployment

πŸ“‹ Prerequisites

Before starting, make sure you have:

  • βœ… Completed Article 6: Building Your First AWS VPC
  • βœ… Terraform installed (v1.0+)
  • βœ… AWS CLI configured
  • βœ… Basic understanding of VPC and networking
  • βœ… An SSH key pair in AWS (or we'll create one)

πŸ—οΈ What We're Building

Internet
    ↓
Application Load Balancer (ALB)
    ↓
    β”œβ”€β†’ EC2 Instance 1 (Apache)
    └─→ EC2 Instance 2 (Apache)
Enter fullscreen mode Exit fullscreen mode

Architecture Components:

  1. VPC - Our isolated network
  2. Public Subnet - Where our resources live
  3. Internet Gateway - Internet access
  4. Security Groups - Firewall rules
  5. EC2 Instances - Web servers running Apache
  6. Application Load Balancer - Traffic distributor
  7. Target Group - Manages server health

πŸ“ Project Structure

Create this folder structure:

07-ec2-load-balancer/
β”œβ”€β”€ main.tf           # Main infrastructure code
β”œβ”€β”€ variables.tf      # Input variables
β”œβ”€β”€ outputs.tf        # Output values
└── terraform.tfvars  # Variable values
Enter fullscreen mode Exit fullscreen mode


πŸ”§ Step 1: Define Variables

Create variables.tf:

# AWS Region
variable "aws_region" {
  description = "AWS region where resources will be created"
  type        = string
  default     = "us-east-1"
}

# Project Name
variable "project_name" {
  description = "Project name for resource naming"
  type        = string
  default     = "terraform-web"
}

# Environment
variable "environment" {
  description = "Environment name"
  type        = string
  default     = "dev"
}

# VPC CIDR
variable "vpc_cidr" {
  description = "CIDR block for VPC"
  type        = string
  default     = "10.0.0.0/16"
}

# Public Subnet CIDR
variable "public_subnet_cidr" {
  description = "CIDR block for public subnet"
  type        = string
  default     = "10.0.1.0/24"
}

# Instance Type
variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t2.micro"
}

# Instance Count
variable "instance_count" {
  description = "Number of EC2 instances"
  type        = number
  default     = 2
}

# SSH Key Name
variable "key_name" {
  description = "SSH key pair name"
  type        = string
  default     = "my-key"
}

# My IP for SSH Access
variable "my_ip" {
  description = "Your IP address for SSH access (CIDR format)"
  type        = string
  default     = "0.0.0.0/0"  # Change this to your IP for security
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • instance_count = 2 - We'll create 2 web servers
  • instance_type = "t2.micro" - Free tier eligible
  • my_ip - Restrict SSH access (change from 0.0.0.0/0 in production!)

🌐 Step 2: Create VPC and Networking

Add to main.tf:

# Terraform Configuration
terraform {
  required_version = ">= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Provider Configuration
provider "aws" {
  region = var.aws_region
}

# Data source: Get latest Amazon Linux 2023 AMI
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# Data source: Get available availability zones
data "aws_availability_zones" "available" {
  state = "available"
}

# VPC
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name        = "${var.project_name}-vpc"
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}

# Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name        = "${var.project_name}-igw"
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}

# Public Subnet
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnet_cidr
  availability_zone       = data.aws_availability_zones.available.names[0]
  map_public_ip_on_launch = true

  tags = {
    Name        = "${var.project_name}-public-subnet"
    Environment = var.environment
    Type        = "Public"
    ManagedBy   = "Terraform"
  }
}

# Public Route Table

# Public Subnet 2 (Different AZ for ALB)
resource "aws_subnet" "public_2" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.2.0/24"
  availability_zone       = data.aws_availability_zones.available.names[1]
  map_public_ip_on_launch = true

  tags = {
    Name        = "${var.project_name}-public-subnet-2"
    Environment = var.environment
    Type        = "Public"
    ManagedBy   = "Terraform"
  }
}
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = {
    Name        = "${var.project_name}-public-rt"
    Environment = var.environment
    Type        = "Public"
    ManagedBy   = "Terraform"
  }
}

# Route Table Association
resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

# Route Table Association for Public Subnet 2
resource "aws_route_table_association" "public_2" {
  subnet_id      = aws_subnet.public_2.id
  route_table_id = aws_route_table.public.id
}
Enter fullscreen mode Exit fullscreen mode

New Concept: Data Sources

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]
  # ...
}
Enter fullscreen mode Exit fullscreen mode

Data sources query existing AWS resources. Here we're finding the latest Amazon Linux AMI automatically - no need to hardcode AMI IDs!


πŸ”’ Step 3: Configure Security Groups

Security groups are like firewalls. Add to main.tf:

# Security Group for ALB
resource "aws_security_group" "alb" {
  name        = "${var.project_name}-alb-sg"
  description = "Security group for Application Load Balancer"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "HTTP from anywhere"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

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

  tags = {
    Name        = "${var.project_name}-alb-sg"
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}

# Security Group for EC2 Instances
resource "aws_security_group" "instance" {
  name        = "${var.project_name}-instance-sg"
  description = "Security group for EC2 instances"
  vpc_id      = aws_vpc.main.id

  ingress {
    description     = "HTTP from ALB"
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }

  ingress {
    description = "SSH from my IP"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.my_ip]
  }

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

  tags = {
    Name        = "${var.project_name}-instance-sg"
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}
Enter fullscreen mode Exit fullscreen mode

Security Architecture:

Internet β†’ ALB (Port 80) β†’ EC2 Instances (Port 80)
                            EC2 Instances (Port 22 from your IP)
Enter fullscreen mode Exit fullscreen mode

Key Security Practices:

  1. ALB accepts HTTP (80) from anywhere
  2. EC2 instances only accept HTTP from ALB (not directly from internet!)
  3. SSH (22) only from your IP address
  4. This is called "defense in depth"

πŸ–₯️ Step 4: Deploy EC2 Instances

Now for the exciting part - creating web servers! Add to main.tf:

# EC2 Instances
resource "aws_instance" "web" {
  count = var.instance_count

  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type
  key_name      = var.key_name
  subnet_id     = aws_subnet.public.id

  vpc_security_group_ids = [aws_security_group.instance.id]

  user_data = base64encode(<<-EOF
    #!/bin/bash
    dnf update -y
    dnf install -y httpd
    systemctl start httpd
    systemctl enable httpd
    echo "<h1>Terraform Web Server - Instance: $(ec2-metadata --instance-id | cut -d' ' -f2)</h1><p>AZ: $(ec2-metadata --availability-zone | cut -d' ' -f2)</p>" > /var/www/html/index.html
    EOF
  )

  tags = {
    Name        = "${var.project_name}-web-${count.index + 1}"
    Environment = var.environment
    ManagedBy   = "Terraform"
    Server      = "web-${count.index + 1}"
  }
}
Enter fullscreen mode Exit fullscreen mode

Breaking Down the Code:

1. Count Meta-Argument:

count = var.instance_count
Enter fullscreen mode Exit fullscreen mode

Creates multiple instances. With instance_count = 2, we get 2 servers!

2. User Data - The Magic:

user_data = <<-EOF
  #!/bin/bash
  yum update -y
  yum install -y httpd
  # ...
EOF
Enter fullscreen mode Exit fullscreen mode

User data runs automatically when the instance starts. It:

  • Updates the system
  • Installs Apache web server
  • Starts Apache
  • Creates a custom HTML page

3. Dynamic Naming:

Name = "${var.project_name}-web-${count.index + 1}"
Enter fullscreen mode Exit fullscreen mode
  • First instance: terraform-web-web-1
  • Second instance: terraform-web-web-2

βš–οΈ Step 5: Create Application Load Balancer

The load balancer distributes traffic. Add to main.tf:

# Application Load Balancer
resource "aws_lb" "main" {
  name               = "${var.project_name}-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = [aws_subnet.public.id, aws_subnet.public_2.id]

  enable_deletion_protection = false

  tags = {
    Name        = "${var.project_name}-alb"
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}

# Target Group
resource "aws_lb_target_group" "main" {
  name     = "${var.project_name}-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = aws_vpc.main.id

  health_check {
    enabled             = true
    healthy_threshold   = 2
    unhealthy_threshold = 2
    timeout             = 5
    interval            = 30
    path                = "/"
    protocol            = "HTTP"
    matcher             = "200"
  }

  tags = {
    Name        = "${var.project_name}-tg"
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}

# Target Group Attachment
resource "aws_lb_target_group_attachment" "main" {
  count = var.instance_count

  target_group_arn = aws_lb_target_group.main.arn
  target_id        = aws_instance.web[count.index].id
  port             = 80
}

# ALB Listener
resource "aws_lb_listener" "main" {
  load_balancer_arn = aws_lb.main.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.main.arn
  }

  tags = {
    Name        = "${var.project_name}-listener"
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}
Enter fullscreen mode Exit fullscreen mode

Understanding Load Balancer Components:

1. Application Load Balancer (ALB):

  • The main load balancer resource
  • internal = false - Internet-facing
  • load_balancer_type = "application" - Layer 7 (HTTP/HTTPS)

2. Target Group:

  • Manages the backend servers
  • Performs health checks every 30 seconds
  • Marks servers healthy after 2 successful checks
  • Marks servers unhealthy after 2 failed checks

3. Target Group Attachment:

count = var.instance_count
target_id = aws_instance.web[count.index].id
Enter fullscreen mode Exit fullscreen mode

Registers each EC2 instance with the target group.

4. Listener:

  • Listens on port 80
  • Forwards traffic to the target group

Health Check Flow:

ALB β†’ Checks "/" every 30s β†’ Expects HTTP 200 β†’ 
  βœ… Healthy (2 successes) β†’ Receives traffic
  ❌ Unhealthy (2 failures) β†’ No traffic
Enter fullscreen mode Exit fullscreen mode

πŸ“€ Step 6: Define Outputs

Create outputs.tf:

# VPC Outputs
output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.main.id
}

# Subnet Output
output "public_subnet_id" {
  description = "ID of public subnet"
  value       = aws_subnet.public.id
}

# EC2 Instance Outputs
output "instance_ids" {
  description = "IDs of EC2 instances"
  value       = aws_instance.web[*].id
}

output "instance_public_ips" {
  description = "Public IPs of EC2 instances"
  value       = aws_instance.web[*].public_ip
}

# Load Balancer Outputs
output "alb_dns_name" {
  description = "DNS name of the Application Load Balancer"
  value       = aws_lb.main.dns_name
}

output "alb_url" {
  description = "URL to access the load balancer"
  value       = "http://${aws_lb.main.dns_name}"
}

# Security Group Outputs
output "alb_security_group_id" {
  description = "ID of ALB security group"
  value       = aws_security_group.alb.id
}

output "instance_security_group_id" {
  description = "ID of instance security group"
  value       = aws_security_group.instance.id
}
Enter fullscreen mode Exit fullscreen mode

Splat Expression:

aws_instance.web[*].id
Enter fullscreen mode Exit fullscreen mode

The [*] gets ALL instance IDs as a list. Super useful with count!


βš™οΈ Step 7: Set Variable Values

Create terraform.tfvars:

aws_region = "us-east-1"
project_name = "terraform-web"
environment = "dev"

# Network Configuration
vpc_cidr = "10.0.0.0/16"
public_subnet_cidr = "10.0.1.0/24"

# EC2 Configuration
instance_type = "t2.micro"
instance_count = 2
key_name = "my-key"

# Security - Change this to your IP address
my_ip = "0.0.0.0/0"
Enter fullscreen mode Exit fullscreen mode

⚠️ Important: Change my_ip to your actual IP address for security!

Find your IP:

curl ifconfig.me
Enter fullscreen mode Exit fullscreen mode

Then update:

my_ip = "203.0.113.0/32"  # Your IP
Enter fullscreen mode Exit fullscreen mode

πŸš€ Step 8: Deploy the Infrastructure

1. Create SSH Key Pair (if you don't have one)

# Create key pair in AWS
aws ec2 create-key-pair \
  --key-name my-key \
  --query 'KeyMaterial' \
  --output text > my-key.pem

![ ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ttadkcyqnuxbsnpwdqs7.png)


# Set permissions
chmod 400 my-key.pem
Enter fullscreen mode Exit fullscreen mode

2. Initialize Terraform

terraform init
Enter fullscreen mode Exit fullscreen mode

Expected output:

Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.100.0...

Terraform has been successfully initialized!
Enter fullscreen mode Exit fullscreen mode

3. Review the Plan

terraform plan
Enter fullscreen mode Exit fullscreen mode

You should see:

Plan: 14 to add, 0 to change, 0 to destroy.
Enter fullscreen mode Exit fullscreen mode

Resources being created:

  • 1 VPC
  • 1 Internet Gateway
  • 1 Subnet
  • 1 Route Table + Association
  • 2 Security Groups
  • 2 EC2 Instances
  • 1 Application Load Balancer
  • 1 Target Group
  • 2 Target Group Attachments
  • 1 ALB Listener

4. Apply the Configuration

terraform apply
Enter fullscreen mode Exit fullscreen mode

Type yes when prompted.

This will take 3-5 minutes because:

  • EC2 instances need to boot
  • User data script runs
  • ALB needs to provision
  • Health checks need to pass


βœ… Step 9: Test Your Infrastructure

1. Get the Load Balancer URL

terraform output alb_url
Enter fullscreen mode Exit fullscreen mode

Output:

http://terraform-web-alb-1740709428.us-east-1.elb.amazonaws.com
Enter fullscreen mode Exit fullscreen mode

2. Test in Browser

Open the URL in your browser. You should see:

πŸš€ Terraform Web Server
Deployed with Infrastructure as Code

Instance ID: i-09305fc7d4638360a
Availability Zone: us-east-1a
Server: 1
Enter fullscreen mode Exit fullscreen mode

3. Test Load Balancing

Refresh the page multiple times. You'll see the Instance ID and Server number change - that's the load balancer distributing traffic!

First request  β†’ Server: 1
Second request β†’ Server: 2
Third request  β†’ Server: 1
Enter fullscreen mode Exit fullscreen mode

4. Test Individual Instances

# Get instance IPs
terraform output instance_public_ips

![ ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jbibepofm64vdjwyk9kj.png)


# Test directly (should work)
curl http://<instance-ip>
Enter fullscreen mode Exit fullscreen mode

πŸ” Understanding What Happened

Traffic Flow

User Request
    ↓
ALB DNS (terraform-web-alb-xxx.elb.amazonaws.com)
    ↓
ALB Listener (Port 80)
    ↓
Target Group (Health Check: βœ…)
    ↓
Round-Robin Distribution
    β”œβ”€β†’ EC2 Instance 1 (Apache) β†’ Response
    └─→ EC2 Instance 2 (Apache) β†’ Response
Enter fullscreen mode Exit fullscreen mode

Health Check Process

Every 30 seconds:
ALB β†’ GET / β†’ EC2 Instance
    ↓
HTTP 200 OK?
    β”œβ”€ Yes (2 times) β†’ Mark Healthy β†’ Send Traffic
    └─ No (2 times)  β†’ Mark Unhealthy β†’ Stop Traffic
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ Advanced Testing

Test High Availability

Scenario: What if one server fails?

# Get instance IDs
terraform output instance_ids

# Stop one instance
aws ec2 stop-instances --instance-ids i-xxxxx

# Wait 1 minute for health check to fail

# Test the ALB URL - still works!
curl http://<alb-dns>
Enter fullscreen mode Exit fullscreen mode

The ALB automatically stops sending traffic to the unhealthy instance!

Monitor Health Status

# Check target health
aws elbv2 describe-target-health \
  --target-group-arn <target-group-arn>
Enter fullscreen mode Exit fullscreen mode

Output:

{
  "TargetHealthDescriptions": [
    {
      "Target": {
        "Id": "i-xxxxx",
        "Port": 80
      },
      "HealthCheckPort": "80",
      "TargetHealth": {
        "State": "healthy"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

πŸ› Troubleshooting Common Issues

Issue 1: "InvalidKeyPair.NotFound"

Error:

Error: creating EC2 Instance: InvalidKeyPair.NotFound
Enter fullscreen mode Exit fullscreen mode

Solution:

# List your key pairs
aws ec2 describe-key-pairs

# Create if missing
aws ec2 create-key-pair --key-name my-key \
  --query 'KeyMaterial' --output text > my-key.pem
chmod 400 my-key.pem

# Update terraform.tfvars with correct key name
Enter fullscreen mode Exit fullscreen mode

Issue 2: ALB Shows "503 Service Unavailable"

Cause: Instances are unhealthy or still booting.

Solution:

# Wait 2-3 minutes for:
# 1. Instances to boot
# 2. User data to complete
# 3. Health checks to pass

# Check target health
aws elbv2 describe-target-health \
  --target-group-arn $(terraform output -raw target_group_arn)
Enter fullscreen mode Exit fullscreen mode

Issue 3: Can't SSH to Instances

Cause: Security group blocks your IP.

Solution:

# Get your current IP
curl ifconfig.me

# Update terraform.tfvars
my_ip = "YOUR_IP/32"

# Apply changes
terraform apply
Enter fullscreen mode Exit fullscreen mode

Issue 4: "Subnet must have at least 2 availability zones"

Error with ALB:

Error: creating ELBv2 Load Balancer: ValidationError
Enter fullscreen mode Exit fullscreen mode

Solution: ALBs require at least 2 subnets in different AZs. Update main.tf:

# Add second subnet
resource "aws_subnet" "public_2" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.2.0/24"
  availability_zone       = data.aws_availability_zones.available.names[1]
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project_name}-public-subnet-2"
  }
}

# Update ALB subnets
resource "aws_lb" "main" {
  # ...
  subnets = [aws_subnet.public.id, aws_subnet.public_2.id]
}
Enter fullscreen mode Exit fullscreen mode

πŸ’° Cost Breakdown

Monthly Costs (us-east-1):

Resource Quantity Cost/Month Total
t2.micro EC2 2 $8.50 $17.00
Application Load Balancer 1 $16.20 $16.20
Data Transfer (1GB) - $0.09 $0.09
Total ~$33.29

Free Tier Benefits:

  • First 750 hours/month of t2.micro (covers both instances!)
  • First 15 GB data transfer out
  • Actual cost with free tier: ~$16.20/month (just the ALB)

Cost Optimization Tips:

  1. Use t2.micro (free tier eligible)
  2. Delete resources when not in use
  3. Use Network Load Balancer ($10.95/month) if you don't need Layer 7 features
  4. Consider using Auto Scaling (covered in future articles)

🧹 Cleanup

Important: Don't forget to destroy resources to avoid charges!

# Destroy everything
terraform destroy

# Type 'yes' when prompted
Enter fullscreen mode Exit fullscreen mode

This will delete:

  • βœ… Load Balancer
  • βœ… Target Group
  • βœ… EC2 Instances
  • βœ… Security Groups
  • βœ… VPC and networking

Verify deletion:

# Check EC2 instances
aws ec2 describe-instances --filters "Name=tag:ManagedBy,Values=Terraform"

# Check load balancers
aws elbv2 describe-load-balancers
Enter fullscreen mode Exit fullscreen mode

πŸ“š Key Concepts Learned

1. Count Meta-Argument

count = 2
Enter fullscreen mode Exit fullscreen mode

Creates multiple identical resources. Access with [count.index].

2. Data Sources

data "aws_ami" "amazon_linux" { }
Enter fullscreen mode Exit fullscreen mode

Query existing AWS resources instead of hardcoding values.

3. User Data

user_data = <<-EOF
  #!/bin/bash
  # Bootstrap script
EOF
Enter fullscreen mode Exit fullscreen mode

Automate instance configuration at launch.

4. Security Group References

security_groups = [aws_security_group.alb.id]
Enter fullscreen mode Exit fullscreen mode

Reference one security group from another for layered security.

5. Splat Expressions

aws_instance.web[*].id
Enter fullscreen mode Exit fullscreen mode

Get all values from resources created with count.

6. Health Checks

health_check {
  path     = "/"
  interval = 30
}
Enter fullscreen mode Exit fullscreen mode

Automatic monitoring and traffic routing.


🎯 Real-World Applications

This architecture is used for:

1. Web Applications

  • WordPress sites
  • E-commerce platforms
  • SaaS applications

2. API Backends

  • REST APIs
  • GraphQL servers
  • Microservices

3. Content Delivery

  • Static websites
  • Media streaming
  • File downloads

4. Development Environments

  • Staging servers
  • Testing environments
  • Demo applications

πŸš€ What's Next?

In the next article, we'll add:

  • RDS Database - Persistent data storage
  • AWS Secrets Manager - Secure credential management
  • Private Subnets - Enhanced security
  • Database Connection - Connect EC2 to RDS

Coming Up: Article 8: Secure Database Deployment: RDS + Secrets Manager with Terraform


πŸ“– Additional Resources

Official Documentation:

Related Articles:


πŸ’¬ Questions?

Common Questions:

Q: Can I use different instance types for each server?

A: Yes! Use for_each instead of count for more flexibility (covered in Article 13).

Q: How do I add HTTPS/SSL?

A: You'll need an ACM certificate and update the listener to port 443. We'll cover this in a future article.

Q: Can I deploy to multiple regions?

A: Yes! Use Terraform workspaces or separate state files per region.

Q: How do I add auto-scaling?

A: Replace EC2 instances with an Auto Scaling Group. We'll cover this in Article 15.


βœ… Summary

Today you learned how to:

  • βœ… Deploy multiple EC2 instances with Terraform
  • βœ… Use count to create multiple resources
  • βœ… Configure security groups for layered security
  • βœ… Automate server setup with user data
  • βœ… Create an Application Load Balancer
  • βœ… Implement health checks and high availability
  • βœ… Test load balancing in action

You now have production-ready web infrastructure! πŸŽ‰


πŸ“Œ Wrapping Up

Thank you for reading. I hope this article provided practical insights and a clearer understanding of the topic.

If you found this useful:

  • ❀️ Like if it added value
  • πŸ¦„ Unicorn if you’re applying it today
  • πŸ’Ύ Save it for your next optimization session
  • πŸ”„ Share it with your team

πŸ’‘ What’s Next

More deep dives are coming soon on:

  • Cloud Operations
  • GenAI & Agentic AI
  • DevOps Automation
  • Data & Platform Engineering

Follow along for weekly insights and hands-on guides.


🌐 Portfolio & Work

You can explore my full body of work, certifications, architecture projects, and technical articles here:

πŸ‘‰ Visit My Website


πŸ› οΈ Services I Offer

If you're looking for hands-on guidance or collaboration, I provide:

  • Cloud Architecture Consulting (AWS / Azure)
  • DevSecOps & Automation Design
  • FinOps Optimization Reviews
  • Technical Writing (Cloud, DevOps, GenAI)
  • Product & Architecture Reviews
  • Mentorship & 1:1 Technical Guidance

🀝 Let’s Connect

I’d love to hear your thoughts. Feel free to drop a comment or connect with me on:

πŸ”— LinkedIn

For collaborations, consulting, or technical discussions, reach out at:

πŸ“§ simplynadaf@gmail.com


Found this helpful? Share it with your team.

⭐ Star the repo β€’ πŸ“– Follow the series β€’ πŸ’¬ Ask questions

Made by Sarvar Nadaf

🌐 https://sarvarnadaf.com


Top comments (0)