DEV Community

Cover image for From Zero to AWS Infrastructure: My First Terraform Assignment
Prince Anani Selase
Prince Anani Selase

Posted on

From Zero to AWS Infrastructure: My First Terraform Assignment

Description: A hands-on walkthrough of provisioning a complete AWS network and EC2 instance using Terraform — VPC, subnets, routing, security groups, and more.


Infrastructure as Code changed the way I think about cloud resources. Instead of clicking through the AWS console, I can describe my entire environment in code, version it with Git, and reproduce it in minutes. This post walks through my first Terraform assignment — provisioning a real, working AWS environment from scratch.


What I Built

By the end of this project, Terraform had created the following resources in AWS:

Resource Name Details
VPC Terraform-vpc 10.0.0.0/16
Internet Gateway Terraform-igw Attached to the VPC
Public Route Table Terraform-public-rt Routes 0.0.0.0/0 → IGW
Public Subnet Terraform-public-subnet 10.0.1.0/24, auto-assigns public IPs
Private Subnet Terraform-private-subnet 10.0.2.0/24, no public IP
Security Group Terraform-ec2-sg Allows SSH (22) and HTTP (80) inbound
EC2 Instance Terraform-ec2 t3.micro, Amazon Linux 2, in public subnet

Project Structure

Keeping things clean and modular from day one is a habit worth building. Here's how I structured the project:

Terraform_Assignment1/
├── main.tf        # All resource definitions
├── variables.tf   # Input variables with defaults
├── outputs.tf     # Useful output values
├── PLANNING.md    # Design decisions and steps
└── README.md      # Usage documentation
Enter fullscreen mode Exit fullscreen mode

Separating variables, outputs, and resources into their own files makes the configuration readable and reusable — even at this small scale.


The Code Walkthrough

1. Provider Configuration

Every Terraform project starts with declaring which providers you need. I pinned the AWS provider to the ~> 5.0 range to avoid unexpected breaking changes.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}
Enter fullscreen mode Exit fullscreen mode

The region is driven by a variable rather than hardcoded — a small habit that pays off when you want to deploy to a different region without touching resource code.


2. The VPC and Internet Gateway

The VPC is the foundation. Everything else lives inside it. I gave it a /16 CIDR block, which provides 65,536 IP addresses — plenty of room to grow.

resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr
  tags = { Name = "Terraform-vpc" }
}

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id
  tags   = { Name = "Terraform-igw" }
}
Enter fullscreen mode Exit fullscreen mode

The Internet Gateway is what connects the VPC to the public internet. Without it, nothing inside the VPC can reach the outside world.


3. Public and Private Subnets

I created two subnets — one public, one private — both in the first available Availability Zone. Instead of hardcoding the AZ name, I used a data source to fetch it dynamically:

data "aws_availability_zones" "available" {}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnet_cidr
  map_public_ip_on_launch = true
  availability_zone       = data.aws_availability_zones.available.names[0]
  tags = { Name = "Terraform-public-subnet" }
}

resource "aws_subnet" "private" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnet_cidr
  availability_zone = data.aws_availability_zones.available.names[0]
  tags = { Name = "Terraform-private-subnet" }
}
Enter fullscreen mode Exit fullscreen mode

Key difference: map_public_ip_on_launch = true on the public subnet means any EC2 instance launched there automatically gets a public IP. The private subnet has no such setting — it's intentionally isolated, reserved for future resources like databases.


4. Routing

A subnet is only "public" if it has a route to the internet. I created a dedicated route table that sends all outbound traffic (0.0.0.0/0) through the Internet Gateway, then associated it with the public subnet.

resource "aws_route_table" "public" {
  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" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}
Enter fullscreen mode Exit fullscreen mode

The private subnet uses the VPC's default route table, which has no internet route — keeping it isolated by default.


5. Security Group

The security group acts as a virtual firewall for the EC2 instance. I opened port 22 (SSH) and port 80 (HTTP) for inbound traffic, and allowed all outbound traffic.

resource "aws_security_group" "ec2_sg" {
  name        = "Terraform-ec2-sg"
  description = "Allow SSH and HTTP inbound"
  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" }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Note: Opening SSH to 0.0.0.0/0 is fine for a learning exercise, but in production you should restrict it to your own IP address.


6. EC2 Instance

With the network in place, launching the instance is straightforward. It goes into the public subnet, gets the security group attached, and receives a public IP automatically.

resource "aws_instance" "example" {
  ami                         = var.ami_id
  instance_type               = var.instance_type
  subnet_id                   = aws_subnet.public.id
  vpc_security_group_ids      = [aws_security_group.ec2_sg.id]
  associate_public_ip_address = true
  tags = { Name = "Terraform-ec2" }
}
Enter fullscreen mode Exit fullscreen mode

7. Variables

All environment-specific values live in variables.tf with sensible defaults. This makes the entire configuration reusable — swap the region and AMI ID and you can deploy to any AWS region.

variable "aws_region"           { default = "eu-central-1" }
variable "vpc_cidr"             { default = "10.0.0.0/16" }
variable "public_subnet_cidr"   { default = "10.0.1.0/24" }
variable "private_subnet_cidr"  { default = "10.0.2.0/24" }
variable "instance_type"        { default = "t3.micro" }
variable "ami_id"               { default = "ami-051eaec1417c5d4ae" }
Enter fullscreen mode Exit fullscreen mode

8. Outputs

After terraform apply, outputs surface the key values you actually need — no digging through the console.

output "vpc_id"            { value = aws_vpc.main.id }
output "public_subnet_id"  { value = aws_subnet.public.id }
output "private_subnet_id" { value = aws_subnet.private.id }
output "ec2_instance_id"   { value = aws_instance.example.id }
output "ec2_public_ip"     { value = aws_instance.example.public_ip }
Enter fullscreen mode Exit fullscreen mode

Deploying It

terraform init     # Download the AWS provider
terraform plan     # Preview what will be created
terraform apply    # Provision everything
terraform destroy  # Tear it all down when done
Enter fullscreen mode Exit fullscreen mode

After apply, the terminal prints the outputs — including the EC2 public IP you can SSH into immediately.


Key Takeaways

  • Separate your files. main.tf, variables.tf, and outputs.tf is the minimum clean structure.
  • Use data sources over hardcoded values. Fetching the AZ dynamically means the config works across regions without changes.
  • Variables make configs reusable. Parameterise anything environment-specific from the start.
  • The private subnet is intentional. Even if nothing lives there yet, having it ready follows the principle of least privilege — internal resources should never be internet-facing by default.

This was Week 1 of my IaC journey. The goal was to understand the building blocks — VPC, subnets, routing, security groups, compute — and how Terraform ties them together declaratively. Everything from here builds on this foundation.


Have questions or feedback? Drop them in the comments below. 👇

Top comments (0)