Two-Tier AWS Network with Terraform

Deploy a public and private subnet VPC architecture using Terraform.

Introduction

30 Second Summary

Think about how buildings have public lobbies anyone can walk into, and private offices that only authorized staff can reach from inside. Cloud networks work the same way, with public-facing servers and private backend servers that are completely hidden from the outside world.

In this project, you will build a complete two-tier network architecture on AWS using Terraform. You will define all your infrastructure as code, deploy it with a single command, and prove that your private subnet is truly isolated from the internet.

What You'll Build

A production-style AWS VPC with public and private subnets, fully defined and deployed through Terraform configuration files.

By the end of this project, you'll have:

  • 5 Terraform configuration files defining a full VPC architecture with subnets, routing, and security groups.
  • A deployed two-tier network with a public EC2 instance accessible from the internet and a private EC2 instance completely isolated from direct access.
  • Hands-on proof that routing configuration (not naming) is what makes a subnet public or private.
  • Secret Mission: Add a NAT Gateway so your private instance can reach the internet without being reachable from the internet.

Are there any prerequisites?

You need an AWS Free Tier account with prior experience using S3, EC2, EBS, and IAM. You should also have VS Code and Git installed on a Windows machine.

Write the Terraform Configuration

Your tools are ready to go. Now it's time to define your entire network architecture in code.

You'll write five Terraform configuration files that together describe a complete two-tier VPC. Terraform reads all .tf files in a directory as one configuration, so splitting resources across files keeps things organized.

In this step, get ready to:
  • Write main.tf with the Terraform and provider configuration.
  • Write network.tf with the VPC, subnets, gateway, and route tables.
  • Write security.tf, compute.tf, and outputs.tf with security groups, EC2 instances, and output values.
Create a project folder and write main.tf

Every Terraform project lives in a single directory. Terraform automatically loads all .tf files in that directory when you run commands.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir two-tier-vpc
cd two-tier-vpc
  • Confirm you're in the folder by running:
pwd

You should see a path ending in two-tier-vpc.

  • Open this folder in VS Code by running:
code .

code command not recognized?

If PowerShell doesn't recognize code, open VS Code manually. Press the Windows key, type VS Code, and press Enter. Then click File > Open Folder and select the two-tier-vpc folder on your Desktop.

help me open my project folder in VS Code

  • In VS Code's Explorer sidebar, click the New File icon (the page icon with a plus sign).
  • Name the file main.tf and press Enter.

The first section of main.tf tells Terraform which provider to use and which version constraints to enforce.

  • Add the following code to main.tf:
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.92"
    }
  }

  required_version = ">= 1.2"
}

provider "aws" {
  region = "us-east-1"
}

What does this code do?

  • The terraform block declares that this project needs the AWS provider from HashiCorp's registry, version 5.92 or higher (but still in the 5.x range).
  • required_version = ">= 1.2" ensures anyone running this code has at least Terraform 1.2 installed.
  • The provider block configures the AWS provider to deploy resources in us-east-1.

Next, you need a way to reference the latest Amazon Linux AMI without hardcoding an ID that changes by region and over time. A data source queries AWS at runtime to find it.

  • Add this data source block below the provider block in main.tf:
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-2023.*-x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

What does this data source do?

A data source reads information from AWS without creating anything. This one finds the most recent Amazon Linux 2023 AMI owned by Amazon that matches the x86_64 architecture and HVM virtualization type.

Later, your EC2 instances will reference data.aws_ami.amazon_linux.id to automatically use the correct AMI ID for your region.

  • Save main.tf with Ctrl+S.

You won't see a visible change yet. The next files define the actual network resources.

✔️ Awesome, I've got everything!

Great. Double check you've saved main.tf before moving on.

ⓧ I'd like to double check the full code

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

  required_version = ">= 1.2"
}

provider "aws" {
  region = "us-east-1"
}

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-2023.*-x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}
Create network.tf with the VPC and subnets

The network file defines the address spaces your resources live in and the routing rules that control internet access.

  • Create a new file called network.tf in VS Code (click the New File icon in the Explorer sidebar).
  • Add the VPC and subnet definitions to network.tf:
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "devops-vpc"
  }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "us-east-1a"
  map_public_ip_on_launch = true

  tags = {
    Name = "public-subnet"
  }
}

resource "aws_subnet" "private" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "us-east-1a"

  tags = {
    Name = "private-subnet"
  }
}

What does this code do?

  • The VPC uses CIDR block 10.0.0.0/16, giving you 65,536 IP addresses to work with.
  • The public subnet (10.0.1.0/24) has map_public_ip_on_launch = true, so any instance launched here automatically gets a public IP address.
  • The private subnet (10.0.2.0/24) does NOT have that setting. Instances here will only have private IPs.

Both subnets reference the VPC with aws_vpc.main.id. Terraform automatically resolves these references and builds resources in the correct order.

Now you need routing. A subnet is only truly "public" if its route table sends internet-bound traffic to an Internet Gateway.

  • Add the Internet Gateway, route tables, and associations below your subnet blocks in network.tf:
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "devops-igw"
  }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = {
    Name = "public-rt"
  }
}

resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "private-rt"
  }
}

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "private" {
  subnet_id      = aws_subnet.private.id
  route_table_id = aws_route_table.private.id
}

What makes a subnet public vs private?

The public route table has a route sending 0.0.0.0/0 (all internet traffic) to the Internet Gateway. The private route table has NO internet route at all.

The route table associations link each subnet to its respective route table. This routing configuration is what makes a subnet public or private. The subnet name is just a label for humans.

  • Save network.tf with Ctrl+S.

You won't see a change yet. The next file defines who can access these subnets.

✔️ Awesome, I've got everything!

Great. Double check you've saved network.tf before moving on.

ⓧ I'd like to double check the full code

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "devops-vpc"
  }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "us-east-1a"
  map_public_ip_on_launch = true

  tags = {
    Name = "public-subnet"
  }
}

resource "aws_subnet" "private" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "us-east-1a"

  tags = {
    Name = "private-subnet"
  }
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "devops-igw"
  }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = {
    Name = "public-rt"
  }
}

resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "private-rt"
  }
}

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "private" {
  subnet_id      = aws_subnet.private.id
  route_table_id = aws_route_table.private.id
}
Create security.tf, compute.tf, and outputs.tf

The remaining three files define who can talk to your instances, the instances themselves, and which values Terraform should display after deployment.

  • Create a new file called security.tf and add the following security group definitions:
resource "aws_security_group" "public" {
  name        = "public-sg"
  description = "Allow SSH inbound and all outbound"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "SSH from anywhere"
    from_port   = 22
    to_port     = 22
    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 = "public-sg"
  }
}

resource "aws_security_group" "private" {
  name        = "private-sg"
  description = "Allow traffic from public subnet only"
  vpc_id      = aws_vpc.main.id

  ingress {
    description     = "All traffic from public security group"
    from_port       = 0
    to_port         = 0
    protocol        = "-1"
    security_groups = [aws_security_group.public.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "private-sg"
  }
}

What does this code do?

  • The public security group allows SSH (port 22) from anywhere and permits all outbound traffic.
  • The private security group only allows inbound traffic from instances in the public security group. It uses security_groups = [aws_security_group.public.id] instead of a CIDR block.
  • This security group chaining pattern is more secure than CIDR-based rules. If the public subnet's IP range changes, the private SG still works correctly.
  • Save security.tf.

✔️ Awesome, I've got everything!

Great. Double check you've saved security.tf before moving on.

ⓧ I'd like to double check the full code

resource "aws_security_group" "public" {
  name        = "public-sg"
  description = "Allow SSH inbound and all outbound"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "SSH from anywhere"
    from_port   = 22
    to_port     = 22
    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 = "public-sg"
  }
}

resource "aws_security_group" "private" {
  name        = "private-sg"
  description = "Allow traffic from public subnet only"
  vpc_id      = aws_vpc.main.id

  ingress {
    description     = "All traffic from public security group"
    from_port       = 0
    to_port         = 0
    protocol        = "-1"
    security_groups = [aws_security_group.public.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "private-sg"
  }
}
  • Create a new file called compute.tf and add the EC2 instance definitions:
resource "aws_key_pair" "main" {
  key_name   = "devops-key"
  public_key = file("~/.ssh/devops-key.pub")
}

resource "aws_instance" "public" {
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.public.id]
  key_name               = aws_key_pair.main.key_name

  tags = {
    Name = "public-instance"
  }
}

resource "aws_instance" "private" {
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.private.id
  vpc_security_group_ids = [aws_security_group.private.id]
  key_name               = aws_key_pair.main.key_name

  tags = {
    Name = "private-instance"
  }
}

What does this code do?

  • The aws_key_pair resource uploads your public SSH key (generated in Step 1) to AWS so your instances accept your private key for login.
  • Both instances use t2.micro (Free Tier eligible) and the AMI from your data source.
  • The public instance goes into the public subnet with the public security group. The private instance goes into the private subnet with the private security group.
  • file("~/.ssh/devops-key.pub") tells Terraform to read your local public key file at deploy time.
  • Save compute.tf.

✔️ Awesome, I've got everything!

Great. Double check you've saved compute.tf before moving on.

ⓧ I'd like to double check the full code

resource "aws_key_pair" "main" {
  key_name   = "devops-key"
  public_key = file("~/.ssh/devops-key.pub")
}

resource "aws_instance" "public" {
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.public.id]
  key_name               = aws_key_pair.main.key_name

  tags = {
    Name = "public-instance"
  }
}

resource "aws_instance" "private" {
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.private.id
  vpc_security_group_ids = [aws_security_group.private.id]
  key_name               = aws_key_pair.main.key_name

  tags = {
    Name = "private-instance"
  }
}
  • Create a new file called outputs.tf and add the output definitions:
output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.main.id
}

output "public_instance_public_ip" {
  description = "Public IP of the public instance"
  value       = aws_instance.public.public_ip
}

output "public_instance_private_ip" {
  description = "Private IP of the public instance"
  value       = aws_instance.public.private_ip
}

output "private_instance_private_ip" {
  description = "Private IP of the private instance"
  value       = aws_instance.private.private_ip
}

What do outputs do?

Outputs display important values after Terraform finishes deploying. You'll need the public IP to SSH in and the private IP to test connectivity between instances.

Without outputs, you'd have to dig through the AWS Console to find these values every time.

  • Save outputs.tf.
  • Confirm all five files exist by running this in the VS Code terminal (Ctrl+` to open it):
ls *.tf

You should see compute.tf, main.tf, network.tf, outputs.tf, and security.tf listed.

  • Verify your formatting is correct by running:
terraform fmt

What does terraform fmt do?

This command rewrites your .tf files to match Terraform's canonical style (consistent indentation, aligned equals signs). If it prints any filenames, those files were reformatted.

If you see no output, your files were already perfectly formatted.

Seeing an error from terraform fmt?

If you see a syntax error, it means there's a typo in one of your .tf files (usually a missing brace, missing quote, or extra comma). The error message will name the file and line number. Open that file and compare it against the full code tabs above.

help me fix a terraform fmt syntax error

✔️ Awesome, I've got everything!

Great. Double check you've saved outputs.tf before moving on.

ⓧ I'd like to double check the full code

output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.main.id
}

output "public_instance_public_ip" {
  description = "Public IP of the public instance"
  value       = aws_instance.public.public_ip
}

output "public_instance_private_ip" {
  description = "Private IP of the public instance"
  value       = aws_instance.public.private_ip
}

output "private_instance_private_ip" {
  description = "Private IP of the private instance"
  value       = aws_instance.private.private_ip
}

Your entire network architecture is defined in code. Next up, you'll deploy all of these resources to AWS with a single command.

Deploy the Infrastructure

Your five configuration files define the entire network architecture. But right now, those files are just blueprints sitting on your machine.

In this step, you will use the Terraform workflow to turn those blueprints into real AWS resources. The workflow has three stages: initialize (download providers), plan (preview changes), and apply (create resources).

In this step, get ready to:
  • Initialize your Terraform project and download the AWS provider plugin.
  • Preview the deployment plan to see exactly what Terraform will create.
  • Apply the configuration to deploy all 12 AWS resources.
Initialize the Terraform project

Before Terraform can create any resources, it needs to download the provider plugin specified in your main.tf file. The init command sets up your working directory and fetches the hashicorp/aws provider.

  • In your PowerShell terminal, make sure you are inside your project folder (the one containing your .tf files).
  • Download the AWS provider plugin by running this command:
terraform init
  • You should see Installed hashicorp/aws in the output, followed by a version number and "signed by HashiCorp".

What does this command do?

The terraform init command reads your provider requirements from main.tf and downloads the matching plugin. It creates two things in your project folder:

  • A .terraform directory containing the downloaded provider binary.
  • A .terraform.lock.hcl file that locks the exact provider version for reproducible builds.

Seeing an error about credentials?

If you see a message about AWS credentials not being configured, your aws configure settings from Step 1 may not have saved correctly. Run aws configure again and re-enter your Access Key ID, Secret Access Key, region (us-east-1), and output format (json).

If the error mentions a network timeout, check your internet connection and try again.

help me troubleshoot my terraform init error

Preview the deployment plan

The plan command is your safety net. It shows you exactly what Terraform will create, modify, or destroy before any real changes happen.

  • Preview what Terraform will create by running this command:
terraform plan
  • Review the output. You should see approximately 12 resources listed with a + symbol next to each one, indicating they will be created.

What should I see in the plan?

The plan lists every resource Terraform will create. Look for these key items:

  • A VPC with CIDR block 10.0.0.0/16.
  • Two subnets (public at 10.0.1.0/24 and private at 10.0.2.0/24).
  • An Internet Gateway, two route tables, and two route table associations.
  • Two security groups controlling inbound and outbound traffic.
  • Two EC2 instances (t2.micro) and one key pair.

At the bottom, you will see a summary line showing the total number of resources to add. No changes happen until you run terraform apply.

Seeing errors in your plan?

If the plan shows errors, the most common causes are:

  • A typo in one of your .tf files. Check the error message for the file name and line number.
  • A missing reference (e.g., using aws_vpc.main.id before defining aws_vpc.main). Make sure all five files are saved.

help me fix my terraform plan errors

Create the AWS resources

Now that the plan looks correct, it is time to deploy. The apply command creates all the resources in your AWS account.

  • Deploy all your infrastructure by running this command:
terraform apply

What happens next?

Terraform shows you the same plan output and asks for confirmation. Type yes and press Enter to proceed.

Terraform then creates each resource in dependency order. The VPC is created first because subnets depend on it. Instances are created last because they depend on subnets and security groups. This takes approximately 2-3 minutes.

  • Type yes when Terraform asks "Do you want to perform these actions?" and press Enter.
  • Wait for Terraform to finish creating all resources. You should see Apply complete! followed by the count of resources added.
  • Note the outputs displayed at the bottom. You will see public_instance_public_ip, public_instance_private_ip, private_instance_private_ip, and vpc_id. Copy the public_instance_public_ip value somewhere handy. You will need it in the next step.

Why does the private instance have no public IP?

Notice that outputs.tf does not include a public IP for the private instance. That is because the private subnet does not have map_public_ip_on_launch set to true. The instance only gets a private IP within the VPC, making it unreachable from the internet.

Apply failed or timed out?

Common issues during apply:

  • "Error creating Key Pair: InvalidKeyPair.Duplicate" means a key pair named devops-key already exists in your AWS account. Delete it from the EC2 console under **Network & Security** > **Key Pairs**, then run terraform apply again.
  • "Error launching instance: VcpuLimitExceeded" means your account has hit the EC2 instance limit. Check the AWS Service Quotas console for your us-east-1 region.
  • If the command hangs for more than 5 minutes, check your internet connection. Terraform needs to communicate with the AWS API throughout the process.

help me fix my terraform apply error

Your entire network is now live in AWS, deployed from code in under 3 minutes. Next up, you will SSH into your instances and prove that the routing isolation actually works.

Verify Your Network Architecture

Your infrastructure is live. You have a VPC with two subnets, two EC2 instances, and routing rules that should isolate the private subnet from the internet.

But does it actually work? In this step, you will connect to your public instance, prove it can reach the internet, confirm it can communicate with the private instance, and then verify that the private instance has no direct path to the internet.

In this step, get ready to:
  • SSH into the public instance and confirm internet access.
  • Ping the private instance to prove intra-VPC routing works.
  • Verify the private route table has no internet route.
SSH into the public instance and test internet access

The public instance sits in a subnet whose route table sends 0.0.0.0/0 traffic to the Internet Gateway. If your routing is correct, you can reach this instance from your local machine over SSH and the instance can reach the internet.

  • Connect to your public instance by running this command in PowerShell:
ssh -i $env:USERPROFILE\.ssh\devops-key ec2-user@[[PUBLIC_IP="your public instance IP from terraform output"]]

What does this command do?

  • -i $env:USERPROFILE\.ssh\devops-key tells SSH to authenticate using the private key you generated earlier.
  • ec2-user is the default username on Amazon Linux instances.
  • The IP address comes from the public_instance_public_ip output that Terraform printed after terraform apply.
  • When prompted to trust the host fingerprint, type yes and press Enter.
  • Once connected, test internet access from the instance by running:
curl http://checkip.amazonaws.com
  • You should see a public IP address printed in the terminal. This confirms the public instance can reach the internet through the Internet Gateway.

Connection timed out or refused?

  • Make sure you are using the public IP from terraform output, not the private IP.
  • Check that your security group allows SSH (port 22) inbound from 0.0.0.0/0. Run terraform state show aws_security_group.public to verify.
  • If you see a permissions error about the key, make sure you are pointing to the correct path. The key file is at $env:USERPROFILE\.ssh\devops-key.

Why can't I SSH into my public instance?

Ping the private instance from the public instance

You are now inside the public instance. The private instance lives in the same VPC, so traffic between them uses internal routing. The private security group allows all inbound traffic from instances using the public security group.

  • From the public instance, ping the private instance by running:
ping -c 4 [[PRIVATE_IP="your private instance IP from terraform output"]]

Why does this ping succeed?

Both subnets are in the same VPC (10.0.0.0/16). The VPC's local route handles traffic between 10.0.1.0/24 and 10.0.2.0/24 without needing an Internet Gateway.

The private security group allows all inbound traffic from the public security group. This security group chaining pattern is how production environments grant access between tiers without opening ports to the internet.

  • You should see 4 packets transmitted and 4 packets received. This proves intra-VPC communication works between your public and private subnets.

Seeing 100% packet loss?

  • Double-check you are using the private IP of the private instance (starts with 10.0.2.), not the public IP.
  • Confirm the private security group's ingress references the public security group. Run terraform state show aws_security_group.private in a separate PowerShell window to check.

Why is my ping failing between instances in the same VPC?

Confirm the private subnet has no internet route

You have proven the public instance reaches the internet and can talk to the private instance. The final piece is confirming that the private instance is truly isolated from direct internet access.

  • Exit the SSH session by running:
exit
  • You are back in PowerShell on your local machine.
  • Inspect the private route table by running:
terraform state show aws_route_table.private

What should I see?

The output lists the attributes of your private route table resource. Look at the route section. You should see only the local VPC route (10.0.0.0/16). There is no 0.0.0.0/0 entry, which means instances in this subnet have no path to the internet.

This is the proof that routing configuration determines whether a subnet is public or private. The subnet itself is not labeled "private" anywhere in AWS. It is private because its route table has no internet route.

  • Also confirm the private instance has no public IP by running:
terraform output private_instance_private_ip
  • You will see only a private IP (starting with 10.0.2.). There is no public_instance_public_ip equivalent for the private instance because map_public_ip_on_launch is not set on the private subnet.

Why routing matters more than naming

A common misconception is that calling a subnet "private" makes it private. In reality, a subnet is public only if its route table has a route to an Internet Gateway. Without that route, no traffic can leave or enter from the internet regardless of what you name the subnet.

Your private instance also has no public IP address. Even if you added an internet route later, the instance would need a public IP (or a NAT Gateway) to actually send traffic out.

Don't see the route table output?

  • Make sure you ran exit first. You need to be in your local PowerShell (inside your project directory), not still SSH'd into the EC2 instance.
  • Confirm you are in the correct folder where your .tf files and terraform.tfstate live.

Why is terraform state show not finding my resource?

Your network architecture is verified. The public instance reaches the internet and communicates with the private instance. The private instance is completely isolated from direct internet access. Next up, you will tear down all these resources cleanly with a single Terraform command.

Secret mission

Add a NAT Gateway

Your private instance is fully isolated from the internet. But what if it needs to download security patches or call an external API? In production, private subnets use a NAT Gateway to make outbound connections without being reachable from the outside. Add one to your network and prove it works.

Clean Up Your Resources

Clean Up Your Resources

All AWS cloud resources from this project have already been destroyed via terraform destroy. There are no ongoing costs.

Decide whether to keep your local project files and tools for future Terraform projects, or remove them entirely.

Resources you used:

  • Terraform project folder on your Desktop (containing .tf files, .terraform directory, and state files).
  • SSH key pair at ~/.ssh/devops-key and ~/.ssh/devops-key.pub.
  • AWS CLI (installed via MSI).
  • AWS credentials stored in %UserProfile%\.aws.

Keep everything running

No action needed. Choose this if you plan to build more Terraform projects or continue working with AWS.

  • Your Terraform project folder stays on your Desktop as a reference for future projects.
  • Terraform and the AWS CLI remain installed and ready to use.
  • Your SSH key pair remains available for connecting to future EC2 instances.
  • Your AWS credentials stay configured for future CLI usage.

Pause - I'll come back to this later

All cloud resources are already destroyed. There is nothing running that costs money. Your local files stay on disk until you decide to clean them up.

  • No action required. You can pick up where you left off by running terraform apply in your project folder to redeploy the infrastructure.

Delete - I don't want to use this again

Remove all project files, SSH keys, and optionally uninstall the AWS CLI.

  • Delete your Terraform project folder by running this in PowerShell:
Remove-Item -Recurse -Force ~\Desktop\[[PROJECT_FOLDER="name of your terraform project folder"]]
  • Delete the SSH key pair you generated for this project:
Remove-Item ~\.ssh\devops-key, ~\.ssh\devops-key.pub
  • Uninstall the AWS CLI through Windows settings:
  • Open Settings.
  • Select Apps, then Installed apps.
  • Search for AWS Command Line Interface.
  • Click the three-dot menu and select Uninstall.
  • (Optional) Remove your stored AWS credentials and configuration:
Remove-Item -Recurse -Force ~\.aws

What about Terraform?

Terraform is a standalone binary you placed in a folder on your PATH. To remove it, delete the terraform.exe file from the folder where you extracted it (e.g., C:\terraform\), then remove that folder from your system PATH environment variable.

Nice Work!

Nice Work!

You just designed, deployed, and verified a production-style two-tier network architecture entirely through code. That is the exact intersection of cloud networking and Infrastructure as Code that cloud and DevOps roles demand.

You've learned how to:

  • Built a complete two-tier VPC with public and private subnets, proving that routing configuration (not subnet naming) is what determines public vs private behavior.
  • Written 5 Terraform configuration files that can recreate your entire network architecture in minutes.
  • Mastered the complete Terraform lifecycle of init, plan, apply, and destroy to deploy and tear down cloud infrastructure on demand.
  • Secret Mission: Added a NAT Gateway to give private instances outbound internet access while maintaining inbound isolation.

Ready to quiz yourself?