Automating AWS Infrastructure Using Terraform: VPC, EC2, S3, and Load Balancer Setup


As part of my DevOps learning journey, I recently worked on a project to automate a basic but complete AWS infrastructure using Terraform. This was a hands-on experience where I provisioned and configured networking, compute, storage, and load balancing resources using Infrastructure as Code (IaC). Here's a breakdown of what I built, how it works, the challenges I faced, and the lessons learned.
Project Overview
The goal was to provision the following AWS components using Terraform:
A VPC with two public subnets in different Availability Zones
An Internet Gateway and Route Table
A Security Group with HTTP and SSH access
Two EC2 instances with user-data scripts
An S3 bucket with full access granted to EC2 via IAM Role
An Application Load Balancer (ALB) with target groups and listeners
Infrastructure Details
1. VPC and Subnets
I created a custom VPC and two subnets in different AZs:
resource "aws_vpc" "myvpc" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "sub1" {
vpc_id = aws_vpc.myvpc.id
cidr_block = "10.0.0.0/24"
availability_zone = "us-east-1a"
map_public_ip_on_launch = true
}
resource "aws_subnet" "sub2" {
vpc_id = aws_vpc.myvpc.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1b"
map_public_ip_on_launch = true
}
2. Internet Gateway and Route Table
To enable internet access:
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.myvpc.id
}
resource "aws_route_table" "RT" {
vpc_id = aws_vpc.myvpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
}
resource "aws_route_table_association" "rta1" {
subnet_id = aws_subnet.sub1.id
route_table_id = aws_route_table.RT.id
}
resource "aws_route_table_association" "rta2" {
subnet_id = aws_subnet.sub2.id
route_table_id = aws_route_table.RT.id
}
3. Security Group
Allowing inbound traffic:
resource "aws_security_group" "webSg" {
name = "web"
vpc_id = aws_vpc.myvpc.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
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"]
}
}
4. IAM Role for S3 Access
Attaching an S3 access role to EC2:
resource "aws_iam_role" "ec2_role" {
name = "ec2_s3_role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Principal = { Service = "ec2.amazonaws.com" },
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "ec2_role_attachment" {
policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
role = aws_iam_role.ec2_role.name
}
resource "aws_iam_instance_profile" "ec2_profile" {
name = "ec2_s3_profile"
role = aws_iam_role.ec2_role.name
}
5. EC2 Instances
Launching two EC2 instances in each subnet:
resource "aws_instance" "webservers1" {
ami = "ami-020cba7c55df1f615"
instance_type = "t2.micro"
subnet_id = aws_subnet.sub1.id
vpc_security_group_ids = [aws_security_group.webSg.id]
user_data = base64encode(file("userdata.sh"))
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
}
resource "aws_instance" "webservers2" {
ami = "ami-020cba7c55df1f615"
instance_type = "t2.micro"
subnet_id = aws_subnet.sub2.id
vpc_security_group_ids = [aws_security_group.webSg.id]
user_data = base64encode(file("userdata1.sh"))
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
}
6. Application Load Balancer
Distributing traffic evenly:
resource "aws_lb" "myalb" {
name = "myalb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.webSg.id]
subnets = [aws_subnet.sub1.id, aws_subnet.sub2.id]
}
resource "aws_lb_target_group" "tg" {
name = "myTG"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.myvpc.id
health_check {
path = "/"
port = "traffic-port"
}
}
resource "aws_lb_target_group_attachment" "attach1" {
target_group_arn = aws_lb_target_group.tg.arn
target_id = aws_instance.webservers1.id
port = 80
}
resource "aws_lb_target_group_attachment" "attach2" {
target_group_arn = aws_lb_target_group.tg.arn
target_id = aws_instance.webservers2.id
port = 80
}
resource "aws_lb_listener" "listener" {
load_balancer_arn = aws_lb.myalb.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.tg.arn
}
}
Challenges I Faced
IAM Role Confusion: It took me time to properly link the IAM role to EC2 with the correct trust relationship and instance profile. This blog helped: Mahira Technology on IAM + EC2 + S3
ALB Configuration: Understanding the flow between ALB, listener, target groups, and EC2 was tricky at first. I referred to the Terraform AWS Load Balancer Docs
Key Takeaways
Terraform makes AWS resource provisioning scalable and repeatable
Load balancers distribute traffic and improve availability
IAM roles are critical for secure access to other AWS services like S3
Having a strong understanding of AWS networking (VPCs, Subnets, Routing) is essential
Final Thoughts
This project was an incredible learning experience and helped me gain confidence in working with Terraform and AWS. It was a practical way to understand how real infrastructure is set up in production.
If you're also exploring DevOps, Cloud, or Infrastructure as Code, I highly recommend trying something similar. Want help setting up your own? Feel free to reach out!
Subscribe to my newsletter
Read articles from Saminder Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
