Been meaning to do a proper Terraform project for a while. This weekend I finally did it.
The goal was simple: use Terraform to provision AWS infrastructure from scratch and have a running Nginx server at the end of it.
What I Built
Here's the architecture at a glance:

A browser hits an Internet Gateway, which routes traffic into a VPC containing a public subnet. Inside that subnet lives an EC2 instance running Nginx in a Docker container, exposed on port 8080.
Project Structure
.
├── main.tf
├── terraform.tfvars
└── entrypoint.sh
Step 1: Variables
Instead of hardcoding values, I declared variables and supplied them via terraform.tfvars. This keeps things reusable and environment-aware.
terraform.tfvars
vpc_cidr_block = "10.0.0.0/16"
subnet_1_cidr_block = "10.0.10.0/24"
env_prefix = "dev"
avail_zone = "ap-south-1a"
my_ip = "YOUR_IP/32" # replaced with my actual IP
instance_type = "t2.micro"
⚠️ Never commit your real IP or credentials to a public repo. Add
terraform.tfvarsto.gitignore.
Step 2: Provider
provider "aws" {
region = "ap-south-1"
}
This tells Terraform to use AWS and deploy everything into the Mumbai region (ap-south-1).
Step 3: Networking
VPC
resource "aws_vpc" "myapp-vpc" {
cidr_block = var.vpc_cidr_block
tags = {
Name = "${var.env_prefix}-vpc"
}
}
This creates an isolated virtual network with the CIDR 10.0.0.0/16, providing me 65,536 private IP addresses to work with.
Subnet
resource "aws_subnet" "myapp-subnet-1" {
vpc_id = aws_vpc.myapp-vpc.id
cidr_block = var.subnet_1_cidr_block
availability_zone = var.avail_zone
tags = {
Name = "${var.env_prefix}-subnet-1"
}
}
This carves out a /24 subnet (10.0.10.0/24) — 256 addresses — inside the VPC, pinned to ap-south-1a.
Internet Gateway
resource "aws_internet_gateway" "myapp-igw" {
vpc_id = aws_vpc.myapp-vpc.id
tags = {
Name = "${var.env_prefix}-internet-gateway"
}
}
By default, a VPC is completely isolated. The Internet Gateway is what allows it to communicate with the outside world.
Route Table
resource "aws_default_route_table" "main-rtb" {
default_route_table_id = aws_vpc.myapp-vpc.default_route_table_id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.myapp-igw.id
}
tags = {
Name = "${var.env_prefix}-main-rtb"
}
}
Rather than creating a new route table and manually associating it with the subnet, I modified the default route table that AWS automatically creates with the VPC. The 0.0.0.0/0 route sends all outbound traffic through the Internet Gateway.
Note: I initially wrote a custom
aws_route_tableresource, but switched toaws_default_route_tableto keep things simpler — it's automatically associated with all subnets that don't have an explicit association.
Step 4: Security Group
resource "aws_default_security_group" "default-sg" {
vpc_id = aws_vpc.myapp-vpc.id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.my_ip] # SSH only from your machine
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # HTTP open to the world
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
prefix_list_ids = []
}
tags = {
Name = "${var.env_prefix}-default-sg"
}
}
Two ingress rules:
- Port 22 (SSH) — locked down to your IP only. No random internet scanners getting in.
-
Port 80 (HTTP) — open to everyone so the web server is publicly accessible.
All outbound traffic is allowed (
protocol = "-1"means all protocols).
Step 5: Fetching the Latest Ubuntu AMI Dynamically
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
output "ami_id" {
value = data.aws_ami.ubuntu.id
}
Instead of hardcoding an AMI ID (which is region-specific and goes stale), I used a data source to dynamically fetch the most recent Ubuntu HVM AMI. The output block prints the resolved AMI ID after terraform apply, which is handy for debugging.
Step 6: EC2 Instance
resource "aws_instance" "myapp-instance" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = aws_subnet.myapp-subnet-1.id
vpc_security_group_ids = [aws_default_security_group.default-sg.id]
availability_zone = var.avail_zone
user_data = file("entrypoint.sh")
tags = {
Name = "${var.env_prefix}-server"
}
}
The instance is a t2.micro (free-tier eligible), placed in my subnet with the security group I defined. The user_data field passes entrypoint.sh as a bootstrap script that runs once on first boot.
Step 7: The Bootstrap Script
entrypoint.sh
#!/bin/bash
sudo apt update -y
sudo apt install docker.io -y
sudo systemctl start docker
sudo usermod -aG docker ubuntu
sudo systemctl restart docker
sudo docker run -d -p 8080:80 nginx
This script:
- Updates the package index
- Installs Docker
- Starts the Docker daemon
- Adds the
ubuntuuser to thedockergroup - Pulls and runs the official Nginx image, mapping host port
8080→ container port80
Deploying
# Initialize — downloads the AWS provider plugin
terraform init
# Preview what will be created
terraform plan
# Apply — this actually creates the resources
terraform apply
Then visit http://<public-ip>:8080 in your browser — you should see the Nginx welcome page.
All the code for this project is available on github
Top comments (0)