π Day 06 β Building Your Own VPC, Deploying EC2, and Running Nginx with Terraform


Todayβs topic is one of the most exciting milestones in my Terraform journey:
π We are no longer using AWS defaults. Instead, weβre building our own Virtual Private Cloud (VPC), launching an EC2 instance inside it, and running a web server (Nginx) automatically using User Data.
This is a big step toward real-world infrastructure automation.
ποΈ What Weβre Building
A VPC with private and public subnets.
An Internet Gateway and a Route Table for connectivity.
A Security Group (like a firewall) to control traffic.
An EC2 instance that runs Nginx automatically.
Terraform outputs that give us the instanceβs public IP and a ready-to-use URL.
π Project Structure
For clean code practice, I separated resources into different files:
βββ main.tf
βββ providers.tf
βββ vpc.tf
βββ ec2.tf
βββ security-group.tf
βββ outputs.tf
This makes the project modular and easy to maintain.
βοΈ Provider Configuration
provider "aws" {
region = "ap-south-1"
}
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "6.9.0"
}
}
}
Here we specify:
AWS provider β tells Terraform weβre working with AWS.
Region β Mumbai (
ap-south-1
).Provider version β locks the AWS provider to prevent compatibility issues.
π VPC Setup
# Create VPC
resource "aws_vpc" "my_vpc" {
cidr_block = "10.0.0.0/16"
tags = { Name = "my_vpc" }
}
# Private subnet
resource "aws_subnet" "private-subnet" {
cidr_block = "10.0.1.0/24"
vpc_id = aws_vpc.my_vpc.id
tags = { Name = "private-subnet" }
}
# Public subnet
resource "aws_subnet" "public-subnet" {
cidr_block = "10.0.2.0/24"
vpc_id = aws_vpc.my_vpc.id
map_public_ip_on_launch = true
tags = { Name = "public-subnet" }
}
# Internet gateway
resource "aws_internet_gateway" "my-igw" {
vpc_id = aws_vpc.my_vpc.id
tags = { Name = "my-igw" }
}
# Routing table
resource "aws_route_table" "my-rt" {
vpc_id = aws_vpc.my_vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.my-igw.id
}
}
resource "aws_route_table_association" "public-sub" {
route_table_id = aws_route_table.my-rt.id
subnet_id = aws_subnet.public-subnet.id
}
π This builds a custom network instead of relying on AWS defaults.
CIDR block
10.0.0.0/16
β IP range of the VPC.Subnets β one private, one public.
Internet Gateway + Route Table β gives internet access to the public subnet.
π Security Groups (Your Virtual Firewall)
resource "aws_security_group" "nginx-sg" {
vpc_id = aws_vpc.my_vpc.id
# Inbound rule for HTTP
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Outbound rule
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "nginx-sg" }
}
π Explanation:
Ingress rule β allows anyone on the internet (
0.0.0.0/0
) to access port80
(HTTP).Egress rule β allows the instance to connect to the internet (needed for downloading Nginx).
π Think of a Security Group like a bouncer at a nightclub:
Ingress = who is allowed in.
Egress = who can go out.
Without it, your server is either too locked down (nobody gets in) or too open (anyone can attack).
π₯οΈ EC2 Instance with Nginx
resource "aws_instance" "nginxserver" {
ami = "ami-0144277607031eca2" # Amazon Linux 2 AMI
instance_type = "t2.micro"
subnet_id = aws_subnet.public-subnet.id
vpc_security_group_ids = [aws_security_group.nginx-sg.id]
associate_public_ip_address = true
user_data = <<-EOF
#!/bin/bash
sudo yum install nginx -y
sudo systemctl start nginx
EOF
tags = { Name = "NginxServer" }
}
π Explanation:
AMI β Amazon Linux 2 image.
t2.micro β free-tier instance type.
Subnet β launches in the public subnet.
Security Group β attaches firewall rules.
β‘ User Data (Automation at Boot)
The magic happens here:
#!/bin/bash
sudo yum install nginx -y
sudo systemctl start nginx
This script runs once when the server starts:
Installs Nginx.
Starts the web server.
π Imagine User Data as a chef who prepares the food before the restaurant opens.
When you walk in (visit the IP), the meal (website) is already ready.
This makes deployments automatic, repeatable, and scalable.
π€ Outputs
output "instance_public_ip" {
description = "The public IP address of the EC2 instance"
value = aws_instance.nginxserver.public_ip
}
output "instance_url" {
description = "The URL to access the Nginx server"
value = "http://${aws_instance.nginxserver.public_ip}"
}
After terraform apply
, you instantly see:
β
EC2 Public IP
β
Ready-to-use URL (http://<public_ip>
)
No guessing, no console copy-pasting.
π Final Result
A custom VPC with proper networking.
An EC2 instance running Nginx automatically.
Security groups managing inbound and outbound traffic.
Cleanly separated Terraform code for clarity.
Outputs giving you a working website link immediately.
This is how real-world DevOps infrastructure is built:
Secure,
Automated,
and Modular.
π Follow My Journey
π Blogs: Hashnode
π» Code: GitHub
π¦ Updates: X (Twitter)
Subscribe to my newsletter
Read articles from Abdul Raheem directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Abdul Raheem
Abdul Raheem
Cloud DevOps | AWS | Terraform | CI/CD | Obsessed with clean infrastructure. Cloud DevOps Engineer π | Automating Infrastructure & Securing Pipelines | Bridging Gaps Between Code and Cloud βοΈ Iβm on a mission to master DevOps from the ground upβbuilding scalable systems, automating workflows, and integrating security into every phase of the SDLC. Currently working with AWS, Terraform, Docker, CI/CD, and learning the art of cloud-native development.