By Vivian Chiamaka Okose
I previously talked about how I learned how to provision a virtual machine on Azure. This present task taught me how networking actually works.
That might sound like an exaggeration, but it is not. When you have to build every single layer of a cloud network by hand -- in code -- and watch each piece slot into place before the next one can exist, something clicks that no amount of reading diagrams ever produces.
This is the story of my second Terraform deployment: an AWS EC2 instance inside a custom VPC, accessible via SSH, running Nginx. Eight resources, one configuration file, and a few lessons I will carry into every cloud project I work on.
What I Built
- A custom VPC with CIDR
10.0.0.0/16and DNS support enabled - A public subnet (
10.0.1.0/24) inaf-south-1awith auto-assign public IP - A private subnet (
10.0.2.0/24) inaf-south-1bfor future backend resources - An Internet Gateway attached to the VPC
- A Route Table routing all outbound traffic through the IGW
- A Route Table Association linking the public subnet to that route table
- A Security Group allowing SSH (port 22) and HTTP (port 80)
- An EC2 instance running Ubuntu 22.04, accessible via SSH with Nginx installed
Eight resources. All defined in Terraform. All deployed in under two minutes.
The Four-Piece AWS Networking Puzzle
This is the single most important thing I learned in this assignment.
In AWS, internet connectivity for a resource requires four things working together:
1. A VPC -- your isolated private network. Think of it as your building.
2. An Internet Gateway (IGW) -- the physical door connecting your building to the outside world. Without this, nothing gets in or out, regardless of what IP addresses you assign.
3. A Route Table with an internet route -- the signpost that says "all traffic going outside? Use the front door (IGW)." The route 0.0.0.0/0 -> igw is that signpost.
4. A Route Table Association -- the act of putting that signpost specifically in your public subnet. Without this, the signpost exists but your subnet is not reading it.
Miss any one of these four and your EC2 has no internet access even with a public IP assigned. The public IP is just a label -- the routing infrastructure is what makes it actually reachable.
# The door
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
}
# The signpost
resource "aws_route_table" "public_rt" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
}
# Putting the signpost in the right subnet
resource "aws_route_table_association" "public_assoc" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public_rt.id
}
Once this pattern clicked, I could see it everywhere in cloud architecture diagrams. It is one of those fundamentals that everything else builds on.
The Complete main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "af-south-1"
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = { Name = "terraform-vpc" }
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = true
availability_zone = "af-south-1a"
tags = { Name = "terraform-public-subnet" }
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "af-south-1b"
tags = { Name = "terraform-private-subnet" }
}
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = { Name = "terraform-igw" }
}
resource "aws_route_table" "public_rt" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
tags = { Name = "terraform-public-rt" }
}
resource "aws_route_table_association" "public_assoc" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public_rt.id
}
resource "aws_security_group" "ec2_sg" {
name = "terraform-ec2-sg"
description = "Allow SSH and HTTP"
vpc_id = aws_vpc.main.id
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTP"
from_port = 80
to_port = 80
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 = "terraform-ec2-sg" }
}
resource "aws_instance" "ec2" {
ami = "ami-0f256846cac23da94"
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.ec2_sg.id]
associate_public_ip_address = true
key_name = "terraform-ec2-key"
tags = { Name = "terraform-ec2" }
}
output "ec2_public_ip" {
description = "Public IP of the EC2 instance"
value = aws_instance.ec2.public_ip
}
SSH Key Pairs and Immutable Infrastructure
AWS EC2 uses SSH key pairs for authentication rather than passwords. I created the key pair before applying the configuration:
mkdir -p ~/.ssh
aws ec2 create-key-pair \
--key-name terraform-ec2-key \
--region af-south-1 \
--query "KeyMaterial" \
--output text > ~/.ssh/terraform-ec2-key.pem
chmod 400 ~/.ssh/terraform-ec2-key.pem
Something interesting: key_name is an immutable attribute on EC2 instances. You cannot attach a key pair to an already-running instance -- Terraform has to destroy and recreate it. This is immutable infrastructure in action. Some changes are too fundamental to apply in place. In production, this means a planned maintenance window. Understanding the difference between mutable and immutable attributes is an essential Terraform operations skill.
Deployment and Verification
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
Outputs:
ec2_public_ip = "13.246.221.152"
SSH into the instance:
ssh -i ~/.ssh/terraform-ec2-key.pem ubuntu@13.246.221.152
# Welcome to Ubuntu 22.04.4 LTS
# ubuntu@ip-10-0-1-134:~$
The private IP 10.0.1.134 confirmed the instance was sitting exactly where I placed it -- inside the 10.0.1.0/24 public subnet.
After installing Nginx and visiting http://13.246.221.152, the welcome page loaded. Infrastructure I built with code, serving traffic over the public internet.
AWS vs Azure: A Quick Comparison
Having completed both assignments back to back, the contrast is clear. Azure handles some connectivity implicitly. AWS is explicit about everything -- every routing layer is your responsibility to declare. This is more verbose but builds genuine understanding because nothing is hidden from you.
Neither is better in absolute terms. But the AWS approach forces you to understand networking deeply, which makes you a better cloud engineer on any platform.
I am documenting my full DevOps learning journey in public. Follow along for Terraform, AWS, Azure, and real-world infrastructure lessons.
GitHub: vivianokose











Top comments (0)