DEV Community

Benjamin Tetteh
Benjamin Tetteh

Posted on

Building My First AWS VPC and EC2 Environment with Terraform: A Beginner-Friendly Guide for Career Changers

Terraform VPC setupNot long ago, terms like “VPC,” “subnet,” and “Terraform” would have made my eyes glaze over. While my background wasn’t originally rooted in cloud engineering, I’ve always been fascinated by how modern infrastructure is designed, automated, and scaled behind the scenes.

Now, partway through my DevOps journey, I’ve gone from simply reading about cloud infrastructure to actually building it, using Terraform to provision a fully functional AWS network entirely through code.

If you're reading this while sitting at a career crossroads — maybe you're a teacher, an accountant, a customer service rep, or anyone wondering "can I really break into tech?" I want this post to be your proof that yes, you absolutely can.


Before we touch any code...

Let's understand why everything exists. Tools make a lot more sense that way. Think of it like building a neighbourhood. Imagine you're a city planner given a plot of land. Your job is to:

Draw the boundary of the land — this is your VPC (Virtual Private Cloud). It defines your space in AWS. Nothing gets in or out unless you say so.

Divide the land into zones — some areas are public (like a shopping street anyone can visit) and some are private (like a gated estate — no outsiders allowed).

Build a gate to the outside world — this is the Internet Gateway (IGW). The single controlled entrance between your network and the internet.

Set the traffic rules — Route Tables tell your network traffic exactly where to go, like road signs.

Construct buildings inside the neighbourhood — these are your EC2 instances, the virtual servers that actually run applications and services.

Hire security guards for the buildings — these are your Security Groups, which act like firewalls controlling who can access your servers and through which doors (ports).


💡 Why Terraform instead of clicking around in AWS?
Clicking in the AWS console is slow, hard to repeat, and easy to mess up. Terraform lets you write your infrastructure as code — describe what you want, run one command, and it builds everything consistently every time. This is called Infrastructure as Code (IaC) and employers actively look for this skill.


The project structure

terraform-vpc/
├── main.tf       # The blueprint — everything to build
├── variables.tf  # The settings — values we can change
└── outputs.tf    # The receipt — shows what got created
Enter fullscreen mode Exit fullscreen mode

Think of main.tf as the architect's drawing, variables.tf as the customizable options, and outputs.tf as the summary report after construction is done.


Step 1: Setting up Terraform — the provider block

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

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

The first block tells Terraform: "We're working with AWS, and we want version 5 of the AWS plugin." That plugin is called a provider — think of it as an adapter that lets Terraform talk to AWS.

The second block specifies which AWS region to build in. A region is a physical location with Amazon data centres — us-east-1 is Northern Virginia, USA.

Notice var.region? That means: "look up the value of region in my variables file."


Step 2: Creating the VPC — your cloud neighbourhood

resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr_block
  tags = {
    Name = "main-vpc"
  }
}
Enter fullscreen mode Exit fullscreen mode

The cidr_block defines the total size of your network. 10.0.0.0/16 gives us room for up to 65,536 addresses — a big plot of land!

tags are just labels to help you find your resources in the AWS console. Always tag. Your future self will thank you.


Step 3: Internet Gateway — the front gate

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

Without this, your VPC is sealed off — like a neighbourhood with no road to the outside world. Notice vpc_id = aws_vpc.main.id? Terraform links resources like this. It figures out the build order automatically.


Step 4: Subnets — carving the zones

# Public subnet (the shopping street)
resource "aws_subnet" "public" {
  vpc_id     = aws_vpc.main.id
  cidr_block = var.public_subnet_cidr_block
  map_public_ip_on_launch = true # Automatically assign public IPs to instances launched in this subnet
  tags = {
    Name = "main-public-subnet"
  }
}

# Private subnet (the gated estate)
resource "aws_subnet" "private" {
  vpc_id     = aws_vpc.main.id
  cidr_block = var.private_subnet_cidr_block
  tags = {
    Name = "main-private-subnet"
  }
}
Enter fullscreen mode Exit fullscreen mode

We're carving our big VPC into zones. The public subnet (10.0.1.0/24) is for resources that need internet access, like web servers. The private subnet (10.0.2.0/24) is for sensitive things like databases — no direct internet access. The /24 gives each subnet 256 addresses.

map_public_ip_on_launch = true For resources inside a subnet to actually receive public internet access, instances launched there need public IP addresses. Without a public IP, an EC2 instance cannot be reached from the internet, even if everything else is configured correctly.


Step 5: Route tables — the traffic signs

# Public route table + association + internet route
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  tags   = { Name = "main-public-rt" }
}
resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}
resource "aws_route" "public_igw" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.main.id
}

# Private route table + association (no internet route)
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id
  tags   = { Name = "main-private-rt" }
}
resource "aws_route_table_association" "private" {
  subnet_id      = aws_subnet.private.id
  route_table_id = aws_route_table.private.id
}
Enter fullscreen mode Exit fullscreen mode

The key line is destination_cidr_block = "0.0.0.0/0" pointing to the IGW. 0.0.0.0/0 means "any address on the internet" — this is what makes the public subnet actually public.

The private subnet gets its own route table but no internet route — isolated by design. The associations are the connectors that glue each table to its subnet.


Step 6: Security groups - for the public subnet

resource "aws_security_group" "public_sg" {
  name        = "public-sg"
  description = "Allow SSH and HTTP access"
  vpc_id      = aws_vpc.main.id

  # Allow SSH access from anywhere
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # Allow HTTP access from anywhere
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # Allow all outbound traffic
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "main-public-sg"
  }
}
Enter fullscreen mode Exit fullscreen mode

A Security Group is basically a firewall attached to your EC2 instance. It decides who can enter, which ports are open, what type of traffic is allowed.

This Security Group allows:

  • SSH access (port 22)
  • HTTP web traffic (port 80)

cidr_blocks = ["0.0.0.0/0"] means “Allow traffic from anywhere on the internet.”


Step 7: Create EC2 instance in the public subnet

resource "aws_instance" "public_ec2" {
  ami             = var.ami_id
  instance_type   = var.instance_type
  subnet_id       = aws_subnet.public.id # This places the EC2 inside the public subnet
  security_groups = [aws_security_group.public_sg.id]
  tags = {
    Name = "terraform-public-ec2"
  }
}
Enter fullscreen mode Exit fullscreen mode

ami = var.ami_id An AMI (Amazon Machine Image) is essentially a template for the operating system. Think of it like installing Windows or Linux onto a new computer. I used an Amazon Linux AMI for this project.

instance_type = var.instance_type This defines the size and power of the virtual server. I used t3.micro (referenced in the variables.tf file) which is lightweight and great for learning purposes.

subnet_id = aws_subnet.public.id This places the EC2 instance inside the public subnet we created earlier.

Then security_groups = [aws_security_group.public_sg.id] attaches the firewall rules to the server.


Step 8: Variables — the settings file

variable "region" {
  type        = string
  description = "AWS region to deploy resources in"
  default     = "us-east-1"
}

variable "vpc_cidr_block" {
  type        = string
  description = "CIDR block for the VPC"
  default     = "10.0.0.0/16"
}

variable "public_subnet_cidr_block" {
  type        = string
  description = "CIDR block for the public subnet"
  default     = "10.0.1.0/24"
}

variable "private_subnet_cidr_block" {
  type        = string
  description = "CIDR block for the private subnet"
  default     = "10.0.2.0/24"
}

variable "ami_id" {
  type        = string
  description = "AMI ID for the EC2 instance"
  default     = "ami-0c94855ba95c71c99"
}

variable "instance_type" {
  type        = string
  description = "Instance type for the EC2 instance"
  default     = "t3.micro"
}
Enter fullscreen mode Exit fullscreen mode

Instead of hardcoding values everywhere, I used variables for region, subnet CIDRs, AMI ID, instance type. This makes the infrastructure easier to modify and reuse later.

For instance, instead of repeating "us-east-1" everywhere, we define it once in variables. Want to deploy to a different region? Change it in one place, not everywhere. Clean, reusable, professional.


Step 9: Outputs — the receipt

# output the VPC id
output "vpc_id" {
  value = aws_vpc.main.id
}

# output the public subnet id
output "public_subnet_id" {
  value = aws_subnet.public.id
}

# output the private subnet id
output "private_subnet_id" {
  value = aws_subnet.private.id
}

# EC2 Public IP
output "ec2_public_ip" {
  value = aws_instance.public_ec2.public_ip
}

# EC2 Instance ID
output "ec2_instance_id" {
  value = aws_instance.public_ec2.id
}
Enter fullscreen mode Exit fullscreen mode

After Terraform finishes, outputs are printed in your terminal — like a receipt. Instead of logging into AWS to find your VPC ID, Public IP, EC2 Instance ID, Terraform just hands it to you. That means you can immediately locate the server, connect to it and verify the deployment worked, without manually digging through the AWS console.


Deploying the infrastructure

Once everything was configured, I used the standard Terraform workflow:

terraform init     # Download the AWS provider and initialize Terraform
terraform fmt      # Format the code neatly
terraform validate # Check for configuration errors
terraform plan     # Preview what Terraform will create
terraform apply    # Provision the infrastructure
Enter fullscreen mode Exit fullscreen mode

And when you're done experimenting:

terraform destroy  # Tear everything down to avoid unnecessary AWS charges
Enter fullscreen mode Exit fullscreen mode

The full architecture


Challenges I faced

Like every beginner, I made mistakes:

  • Using quotes incorrectly around variables inside main.tf which broke my configuration
  • Confusion around route tables and associations
  • Understanding how IGW actually connects to subnets
  • Debugging Terraform errors for the first time

But each error helped me understand Terraform and AWS networking better.


What's next?

I plan to explore:

  • Connecting to the EC2 instance via SSH
  • Installing NGINX and hosting a simple webpage
  • Terraform modules
  • Remote state management with S3
  • CI/CD pipelines with GitHub Actions
  • Docker and container deployment I'm still learning and still building. Follow along!

Top comments (1)

Collapse
 
devjp profile image
Justin

An excellent introductory post with good security posture, very clean examples too. I look forward to reading your next posts on populating your new TF-managed VPC.