DEV Community

Cover image for My Second Terraform Deployment: Building a Complete AWS Network from Scratch (And What I Learned That Surprised Me)
Vivian Chiamaka Okose
Vivian Chiamaka Okose

Posted on

My Second Terraform Deployment: Building a Complete AWS Network from Scratch (And What I Learned That Surprised Me)

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/16 and DNS support enabled
  • A public subnet (10.0.1.0/24) in af-south-1a with auto-assign public IP
  • A private subnet (10.0.2.0/24) in af-south-1b for 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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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:~$
Enter fullscreen mode Exit fullscreen mode

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.


init

terraform plan

Terraform apply

EC2 running

aws vpc

SSH connection

Nginx status

nginx on browser

terraform destroy

EC2 terminated

main.tf code snippet


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)