Deploy a Basic VPC with Terraform

Deploy a multi-AZ VPC with public and private subnets using Terraform.

Introduction

30 Second Summary

Every app running in the cloud needs its own private network. Without one, your resources sit on shared infrastructure with no control over who can reach them.

In this project, you will deploy a production-style VPC on AWS using Terraform. You'll define subnets, internet access, and traffic routing entirely through code.

What You'll Build

A multi-AZ VPC architecture on AWS, deployed and managed entirely through Terraform configuration files.

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

  • A custom VPC with public and private subnets spread across two Availability Zones.
  • An Internet Gateway and route tables that control traffic flow between your subnets and the internet.
  • A complete Terraform project you can reuse as a networking foundation for future AWS deployments.
  • Secret Mission: Add a NAT Gateway so your private subnets can reach the internet for outbound traffic.

Are there any prerequisites?

You'll need an AWS account with CLI credentials configured and the Terraform CLI installed on your machine. Step 1 walks you through verifying both are ready to go.

Set Up Your Terraform Project

Before you can deploy any infrastructure, Terraform needs a project directory with configuration files that tell it which cloud provider to use and how to authenticate.

In this step, you will create your project folder, write the provider configuration, and run terraform init to download the AWS provider plugin.

In this step, get ready to:
  • Create a project directory with two Terraform configuration files.
  • Configure the AWS provider and verify your credentials.
  • Initialize Terraform to download the AWS provider plugin.
Create your project directory and configuration files

Every Terraform project lives in its own folder. The files inside tell Terraform what provider plugins to download and what resources to create.

  • Open your terminal.
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir vpc-terraform-project
cd vpc-terraform-project
  • Create the two configuration files by running:
touch terraform.tf main.tf
  • Confirm the files exist by running:
ls

You should see main.tf and terraform.tf in the output.

Why two files?

terraform.tf declares which provider plugins and Terraform version your project requires. main.tf is where you define the actual infrastructure resources.

Separating configuration from resources keeps your project organized as it grows.

Don't see both files?

Make sure you ran the touch command from inside the vpc-terraform-project folder. If you are in the wrong directory, run cd ~/Desktop/vpc-terraform-project and try again.

Help me find my project files.

Configure the AWS provider and verify your credentials

Terraform needs to know which cloud provider to talk to and which region to deploy resources in. The required_providers block pins the provider version so your project stays reproducible.

  • Open the vpc-terraform-project folder in VS Code or your preferred text editor.
  • Open terraform.tf and paste the following code:
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }

  required_version = ">= 1.2"
}

What does this code do?

source = "hashicorp/aws" tells Terraform to download the official AWS provider from the Terraform Registry.

version = "~> 6.0" allows any version 6.x (currently up to 6.42.0) but blocks version 7.0 or higher. This prevents breaking changes.

required_version = ">= 1.2" ensures anyone running this project has at least Terraform 1.2 installed.

  • Save terraform.tf.
  • Open main.tf and paste the following code:
provider "aws" {
  region = "us-east-1"
}

What does this code do?

The provider block configures the AWS provider with a specific region. All resources you create will be deployed to us-east-1 (N. Virginia).

Terraform uses the same credentials as your AWS CLI. If you have already run aws configure or set the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables, Terraform picks them up automatically.

  • Save main.tf.
  • Verify your AWS credentials are working by running this command in your terminal:
aws sts get-caller-identity

You should see a JSON response containing your Account, UserId, and Arn. This confirms Terraform will be able to authenticate with AWS.

Seeing an authentication error?

If you see an error about credentials not being found, make sure you have run aws configure and entered your Access Key ID and Secret Access Key.

If your credentials have expired, generate new ones in the AWS IAM console and re-run aws configure.

Help me fix my AWS credentials.

Run terraform init to download the AWS provider

The terraform init command reads your terraform.tf file and downloads the provider plugins your project needs. You run it once when setting up a new project.

  • Initialize your Terraform project by running this command:
terraform init

You should see output that ends with "Terraform has been successfully initialized" in green text.

What just happened?

Terraform downloaded the AWS provider plugin into a hidden .terraform directory inside your project folder. It also created a .terraform.lock.hcl file that locks the exact provider version for reproducibility.

  • Confirm the provider was downloaded by running:
ls -a

You should see .terraform and .terraform.lock.hcl alongside your main.tf and terraform.tf files.

Seeing a provider error?

If you see an error about the provider not being found, double check that source = "hashicorp/aws" is spelled exactly as shown in terraform.tf.

If you see a version constraint error, make sure the version = "~> 6.0" line uses a tilde-arrow (~>) and not a plain greater-than sign.

Help me fix terraform init.

✔️ Awesome, I've got everything!

Great. Double check you have saved both files.

ⓧ I'd like to double check the full code

Here is the full content of each file. Make sure yours matches exactly.

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

  required_version = ">= 1.2"
}
provider "aws" {
  region = "us-east-1"
}

Your Terraform project is initialized and ready to go. Next up, you will define your first real cloud resource: the VPC itself.

Create the VPC and Understand Your CIDR Block

Every AWS resource you deploy needs a network to live in. A VPC (Virtual Private Cloud) is your own isolated section of the AWS cloud where you control the IP addresses, subnets, and traffic routing.

In this step, you'll define your VPC's address space using a CIDR block and create the VPC resource in Terraform. Then you'll preview what Terraform plans to build before deploying anything.

In this step, get ready to:
  • Define local variables for consistent project naming.
  • Create a VPC resource with a /16 CIDR block and DNS support.
  • Run terraform plan to preview the infrastructure Terraform will create.
Define local variables and the VPC resource

Before creating the VPC, you'll set up local variables that keep your project organized. Locals let you define values once and reference them across multiple resources.

What is a CIDR block?

A CIDR block defines a range of IP addresses for your network. The notation 10.0.0.0/16 means your VPC owns all addresses from 10.0.0.0 to 10.0.255.255. That gives you 65,536 IP addresses to carve into smaller subnets later.

The number after the slash (/16) tells you how many bits are locked. A smaller number means more IPs. A /24 would only give you 256 addresses.

  • In main.tf, add the following code below your provider "aws" block:
locals {
  project_name = "my-vpc-project"
  azs          = ["us-east-1a", "us-east-1b"]
}

# --- VPC ---

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

  tags = {
    Name = "${local.project_name}-vpc"
  }
}

What does this code do?

  • locals defines reusable values. project_name keeps your resource names consistent. azs lists two Availability Zones you'll spread your subnets across later.
  • cidr_block = "10.0.0.0/16" gives your VPC a private address range of 65,536 IPs.
  • enable_dns_support and enable_dns_hostnames allow resources inside the VPC to resolve DNS names. This is required for services like RDS or ECS.
  • The tags block uses string interpolation (${local.project_name}) to name your VPC based on the local variable you defined above.
  • Save main.tf.
  • You won't see a visible change yet. The next section runs terraform plan to confirm Terraform understands your code.

Seeing a syntax error?

  • Make sure the locals block is outside the provider block, not nested inside it.
  • Check that the azs list uses square brackets and quotes around each zone name.
  • Verify that the closing brace } for the resource block is present. Missing braces are the most common HCL error.

Help me fix a syntax error in my Terraform VPC configuration

✔️ 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

Here's the complete main.tf as it should look now:

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

locals {
  project_name = "my-vpc-project"
  azs          = ["us-east-1a", "us-east-1b"]
}

# --- VPC ---

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

  tags = {
    Name = "${local.project_name}-vpc"
  }
}
Run terraform plan to preview your infrastructure

Terraform separates planning from execution. You always preview what will happen before committing real changes to your AWS account.

  • Preview the resources Terraform will create by running this command:
terraform plan

Terraform reads your main.tf and shows what it will do. You should see a + symbol next to the VPC resource, indicating it will be created.

At the bottom, look for the summary line that says Plan: 1 to add, 0 to change, 0 to destroy.

What does terraform plan do?

The plan command compares your code to the current state of your infrastructure. Since you haven't deployed anything yet, it sees one new resource to create.

This is a dry run. Nothing is actually created in AWS until you run terraform apply in a later step.

Plan failed with an error?

  • If you see a credentials error, check that your AWS CLI is configured. Run aws sts get-caller-identity to verify your credentials work.
  • If you see a provider error, make sure you ran terraform init in the previous step. The provider plugin must be downloaded first.
  • If you see a syntax error, scroll to the top of the output. Terraform shows the exact line number and file where it found the problem.

Help me fix a terraform plan error for my VPC project

Your VPC definition is ready and Terraform has confirmed it can build it. Next up, you'll create public subnets and an Internet Gateway so resources in your VPC can reach the internet.

Build Public Subnets and Internet Access

Your VPC exists, but it's completely isolated. Nothing inside it can reach the internet, and nothing from the internet can reach it.

In this step, you'll build the public networking layer. You'll create an Internet Gateway, carve out two public subnets across separate Availability Zones, and wire them together with a route table.

In this step, get ready to:
  • Create an internet gateway to connect your VPC to the public internet.
  • Define two public subnets in separate availability zones.
  • Create a route table that directs internet-bound traffic through the gateway.
Create an internet gateway

An Internet Gateway is the doorway between your VPC and the public internet. Without one, no resource in your VPC can send or receive traffic from the outside world.

  • In main.tf, add the following code below the closing brace of your aws_vpc resource:
# --- Internet Gateway ---

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

  tags = {
    Name = "${local.project_name}-igw"
  }
}

What does this code do?

  • aws_internet_gateway "main" creates a gateway resource and gives it the label main so you can reference it later in your route table.
  • vpc_id = aws_vpc.main.id attaches the gateway to your VPC. Terraform resolves this reference automatically.
  • The Name tag uses your local.project_name variable so all resources share a consistent naming pattern.
  • Save main.tf.

Seeing a syntax error on save?

Make sure you placed the new code after the closing } of the aws_vpc block, not inside it. Each resource block needs its own opening and closing braces.

help me fix a syntax error in my Terraform internet gateway resource

Create two public subnets

Subnets carve your VPC's large IP range into smaller segments. Public subnets are where you place resources that need direct internet access, like web servers or load balancers.

You'll create two subnets in different availability zones. This gives your architecture redundancy so that if one data center has an issue, the other keeps running.

  • Add the following code below your internet gateway resource in main.tf:
# --- Public Subnets ---

resource "aws_subnet" "public_1" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = local.azs[0]
  map_public_ip_on_launch = true

  tags = {
    Name = "${local.project_name}-public-1"
  }
}

resource "aws_subnet" "public_2" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.2.0/24"
  availability_zone       = local.azs[1]
  map_public_ip_on_launch = true

  tags = {
    Name = "${local.project_name}-public-2"
  }
}

What does this code do?

  • Each subnet gets a /24 CIDR block, which provides 256 IP addresses. These are carved from your VPC's larger 10.0.0.0/16 range.
  • availability_zone uses local.azs[0] and local.azs[1] to place each subnet in a different data center (us-east-1a and us-east-1b).
  • map_public_ip_on_launch = true means any resource launched in these subnets automatically gets a public IP address. This is what makes them "public" subnets.

Why two subnets instead of one?

Spreading resources across multiple availability zones is a core AWS best practice. If one availability zone goes down, resources in the other zone keep serving traffic. Many AWS services (like load balancers) require subnets in at least two zones.

  • Save main.tf.

Seeing an error about overlapping CIDR blocks?

Double-check that public_1 uses 10.0.1.0/24 and public_2 uses 10.0.2.0/24. Each subnet must have a unique, non-overlapping range within the VPC.

help me fix overlapping CIDR blocks in my Terraform subnets

Create a public route table and associate your subnets

Subnets alone don't know where to send internet-bound traffic. A route table provides the directions.

By pointing 0.0.0.0/0 (all non-local traffic) at the Internet Gateway, you're telling AWS to send anything destined for the internet through your gateway.

  • Add the following code below your public subnets in main.tf:
# --- Public Route Table ---

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 = "${local.project_name}-public-rt"
  }
}

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

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

What does this code do?

  • The route block inside aws_route_table creates a rule: any traffic headed to 0.0.0.0/0 (the entire internet) gets forwarded to your Internet Gateway.
  • The two aws_route_table_association resources link each public subnet to this route table. Without these associations, the subnets would use the VPC's default route table (which has no internet route).
  • Save main.tf.
  • Verify your configuration is valid by running:
terraform plan

You should see output ending with a line like Plan: 6 to add, 0 to change, 0 to destroy. The 6 resources are your internet gateway, two subnets, one route table, and two route table associations.

Seeing fewer than 6 resources in the plan?

Make sure all three code sections are present in your file: the internet gateway, both public subnets, the route table, and both associations. Also confirm the VPC resource from the previous step is still in main.tf.

help me figure out why terraform plan shows the wrong number of resources

✔️ Awesome, I've got everything!

Great. Double check you've saved main.tf and move on to the next step.

ⓧ I'd like to double check the full code

Here's what your main.tf should look like at this point:

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

locals {
  project_name = "my-vpc-project"
  azs          = ["us-east-1a", "us-east-1b"]
}

# --- VPC ---

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

  tags = {
    Name = "${local.project_name}-vpc"
  }
}

# --- Internet Gateway ---

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

  tags = {
    Name = "${local.project_name}-igw"
  }
}

# --- Public Subnets ---

resource "aws_subnet" "public_1" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = local.azs[0]
  map_public_ip_on_launch = true

  tags = {
    Name = "${local.project_name}-public-1"
  }
}

resource "aws_subnet" "public_2" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.2.0/24"
  availability_zone       = local.azs[1]
  map_public_ip_on_launch = true

  tags = {
    Name = "${local.project_name}-public-2"
  }
}

# --- Public Route Table ---

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 = "${local.project_name}-public-rt"
  }
}

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

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

Your public networking layer is complete. Next up, you'll create the private subnets where you'll place resources that should never be directly exposed to the internet.

Build Private Subnets

Your public subnets are live and connected to the internet through the Internet Gateway. Now it's time to build the other half of your network architecture.

Private subnets hold resources that should never be directly reachable from the internet, like databases or internal APIs. In this step, you'll create two private subnets and a route table that keeps their traffic contained within the VPC.

In this step, get ready to:
  • Create two private subnets in separate Availability Zones.
  • Define a private route table with local-only traffic.
  • Associate both private subnets with the private route table.
Create two private subnets in separate Availability Zones

Private subnets work just like public subnets in terms of structure. The key difference is that they do NOT have map_public_ip_on_launch enabled, so instances inside them won't receive public IP addresses.

  • In main.tf, find the closing brace of your aws_route_table_association "public_2" resource (the last resource in the file).
  • Add the following private subnet resources below it:
# --- Private Subnets ---

resource "aws_subnet" "private_1" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.3.0/24"
  availability_zone = local.azs[0]

  tags = {
    Name = "${local.project_name}-private-1"
  }
}

resource "aws_subnet" "private_2" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.4.0/24"
  availability_zone = local.azs[1]

  tags = {
    Name = "${local.project_name}-private-2"
  }
}

What does this code do?

Each private subnet carves out a /24 block (256 IP addresses) from your VPC's larger /16 range.

  • 10.0.3.0/24 and 10.0.4.0/24 are the next two blocks after your public subnets (10.0.1.0/24 and 10.0.2.0/24).
  • Notice there is no map_public_ip_on_launch argument. It defaults to false, meaning resources in these subnets won't get public IPs.
  • Each subnet sits in a different Availability Zone for redundancy, matching the same AZ layout as your public subnets.
  • Save main.tf.
  • You won't see a change yet. The next chunk adds the route table that controls where traffic from these subnets can go.
Create a private route table with no internet route

Your public route table has a 0.0.0.0/0 route pointing to the Internet Gateway. The private route table intentionally omits that route, keeping traffic local within the VPC.

  • In main.tf, add the following code below the private subnets you just created:
# --- Private Route Table ---

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

  tags = {
    Name = "${local.project_name}-private-rt"
  }
}

Why is there no route block?

Every route table in AWS automatically includes a local route that allows traffic within the VPC's CIDR block. You don't need to define it in Terraform.

By NOT adding a 0.0.0.0/0 route here, any resource in the private subnets can talk to other resources inside the VPC but cannot reach the public internet.

  • Save main.tf.
  • You won't see a change yet. The next chunk associates your private subnets with this route table.
Associate both private subnets with the private route table

A route table does nothing until subnets are explicitly associated with it. Without an association, your private subnets would fall back to the VPC's default main route table.

  • In main.tf, add the following code below your private route table resource:
resource "aws_route_table_association" "private_1" {
  subnet_id      = aws_subnet.private_1.id
  route_table_id = aws_route_table.private.id
}

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

What does this code do?

  • Each aws_route_table_association links one subnet to one route table.
  • private_1 connects the first private subnet to the private route table. private_2 does the same for the second.
  • Once associated, traffic from these subnets follows the private route table's rules (VPC-local only, no internet).
  • Save main.tf.
  • Verify your configuration is valid by running:
terraform plan

You should see Terraform planning to add 5 new resources (2 private subnets, 1 route table, 2 route table associations) marked with a + symbol.

Seeing an error in terraform plan?

  • Check for mismatched braces. Every opening { must have a corresponding }.
  • Make sure the resource names (private_1, private_2) in your associations match the names in your subnet resource blocks exactly.
  • Verify that aws_route_table.private.id in your associations matches the route table resource name "private".

Help me debug my terraform plan error for private subnets

✔️ Awesome, I've got everything!

Great. Double check you've saved main.tf and your terraform plan shows the 5 new resources.

ⓧ I'd like to double check the full code

Here is the full main.tf as it should look at the end of this step:

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

locals {
  project_name = "my-vpc-project"
  azs          = ["us-east-1a", "us-east-1b"]
}

# --- VPC ---

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

  tags = {
    Name = "${local.project_name}-vpc"
  }
}

# --- Internet Gateway ---

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

  tags = {
    Name = "${local.project_name}-igw"
  }
}

# --- Public Subnets ---

resource "aws_subnet" "public_1" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = local.azs[0]
  map_public_ip_on_launch = true

  tags = {
    Name = "${local.project_name}-public-1"
  }
}

resource "aws_subnet" "public_2" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.2.0/24"
  availability_zone       = local.azs[1]
  map_public_ip_on_launch = true

  tags = {
    Name = "${local.project_name}-public-2"
  }
}

# --- Private Subnets ---

resource "aws_subnet" "private_1" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.3.0/24"
  availability_zone = local.azs[0]

  tags = {
    Name = "${local.project_name}-private-1"
  }
}

resource "aws_subnet" "private_2" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.4.0/24"
  availability_zone = local.azs[1]

  tags = {
    Name = "${local.project_name}-private-2"
  }
}

# --- Public Route Table ---

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 = "${local.project_name}-public-rt"
  }
}

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

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

# --- Private Route Table ---

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

  tags = {
    Name = "${local.project_name}-private-rt"
  }
}

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

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

Why do private subnets matter?

In a production environment, you'd place databases, application servers, and internal services in private subnets. Only your load balancers or bastion hosts sit in public subnets, acting as controlled entry points.

This separation reduces your attack surface. Even if someone discovers your private subnet's IP range, there's no route from the internet to reach it.

Your VPC network architecture is complete. You have public subnets for internet-facing resources and private subnets for isolated workloads. Next up, you'll add outputs and deploy the entire infrastructure with terraform apply.

Add Outputs and Deploy

Your VPC architecture is fully defined in code. Public subnets can reach the internet, private subnets stay isolated, and route tables control the traffic flow between them.

Now it's time to bring this infrastructure to life. In this step, you'll add Terraform outputs to surface key resource IDs, then deploy everything to AWS with a single command.

In this step, get ready to:
  • Define Terraform outputs for your VPC and subnet IDs.
  • Deploy the entire infrastructure with terraform apply.
  • Verify the deployed resources in the AWS Console.
Define outputs for your VPC infrastructure

Terraform outputs let you extract key values from your deployed infrastructure. After a successful terraform apply, outputs print the values you care about directly to the terminal.

  • Create a new file called outputs.tf in your project directory by running:
touch outputs.tf
  • Confirm the file exists by running:
ls

You should see main.tf, terraform.tf, outputs.tf, and the .terraform directory in the output.

  • Open outputs.tf in your editor and add the following output definitions:
output "vpc_id" {
  description = "The ID of the VPC"
  value       = aws_vpc.main.id
}

output "public_subnet_ids" {
  description = "IDs of the public subnets"
  value       = [aws_subnet.public_1.id, aws_subnet.public_2.id]
}

output "private_subnet_ids" {
  description = "IDs of the private subnets"
  value       = [aws_subnet.private_1.id, aws_subnet.private_2.id]
}

output "internet_gateway_id" {
  description = "The ID of the Internet Gateway"
  value       = aws_internet_gateway.main.id
}

What does this code do?

Each output block declares a value that Terraform prints after deployment completes.

  • vpc_id surfaces the unique identifier AWS assigns to your VPC.
  • public_subnet_ids and private_subnet_ids collect the IDs of each subnet pair into a list.
  • internet_gateway_id captures the gateway ID so you can reference it in future projects.
  • Save outputs.tf.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

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

output "public_subnet_ids" {
  description = "IDs of the public subnets"
  value       = [aws_subnet.public_1.id, aws_subnet.public_2.id]
}

output "private_subnet_ids" {
  description = "IDs of the private subnets"
  value       = [aws_subnet.private_1.id, aws_subnet.private_2.id]
}

output "internet_gateway_id" {
  description = "The ID of the Internet Gateway"
  value       = aws_internet_gateway.main.id
}
Deploy your infrastructure with terraform apply

With all your configuration files in place, you can now deploy the entire VPC to AWS. Terraform will create every resource you've defined across main.tf and track their state locally.

  • Deploy your infrastructure by running:
terraform apply

What happens when you run this?

Terraform first runs a plan (just like terraform plan) showing every resource it will create. It then prompts you with Do you want to perform these actions? and waits for confirmation.

Type yes and press Enter to proceed. Terraform will create each resource and display progress messages as it goes.

  • When prompted, type yes and press Enter.

After a few seconds, you should see a message ending with Apply complete! followed by a count of resources added. Your four outputs will also print to the terminal with real AWS resource IDs.

Seeing an error?

  • If you see an authentication error, make sure your AWS credentials are configured. Run aws sts get-caller-identity to verify.
  • If you see a syntax error, check that your outputs.tf and main.tf files are saved with no stray characters.
  • If Terraform reports a resource conflict, someone may have already created resources with the same name in your account. Run terraform destroy first, then try again.

Help me debug my terraform apply error

Verify your resources in the AWS Console

The terminal output confirms your resources exist. Now verify them visually in the AWS Console to see exactly what Terraform created.

  • Open your browser and go to the AWS Console.
  • In the search bar at the top, type VPC and select VPC from the results.
  • Click Your VPCs in the left sidebar.
  • Look for the VPC named my-vpc-project-vpc with a CIDR of 10.0.0.0/16.
  • Click Subnets in the left sidebar.
  • Confirm you see four subnets: my-vpc-project-public-1, my-vpc-project-public-2, my-vpc-project-private-1, and my-vpc-project-private-2.

What should I see?

The VPC dashboard shows your VPC with its CIDR block, the four subnets spread across two Availability Zones, and the Internet Gateway attached. Each resource has the tag name you defined in your Terraform code.

Secret mission

Add a NAT Gateway for Private Subnet Internet Access

In production, private subnets often need outbound internet access for software updates and API calls. Can you add a NAT Gateway to enable this without exposing your private resources to the public internet?

Clean Up Your Resources

Clean Up Your Resources

The VPC, subnets, Internet Gateway, and route tables you created are completely free AWS resources with no ongoing charges. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Cost warning

If you completed the Secret Mission, the NAT Gateway and Elastic IP DO incur charges. The NAT Gateway costs $0.045/hour (~$32/month) and the unused Elastic IP costs $0.005/hour.

If you are not actively using these resources, choose the Pause or Delete option below to avoid unexpected charges on your AWS bill.

Resources you used:

  • AWS VPC with 4 subnets, Internet Gateway, and route tables (managed by Terraform).
  • Local project directory with Terraform configuration files and state.
  • NAT Gateway and Elastic IP (Secret Mission only).

Keep everything running

No action needed. Choose this if you plan to use this VPC as a foundation for future projects.

  • Your VPC, subnets, Internet Gateway, and route tables cost nothing to keep running.
  • Your local Terraform files and state remain in the project directory on your Desktop.
  • If you completed the Secret Mission, your NAT Gateway and Elastic IP are still incurring charges. Consider the Pause option to remove just those resources.

Pause - I'll come back to this later

Remove the costly NAT Gateway resources while keeping your free VPC infrastructure intact.

  • If you did NOT complete the Secret Mission, there is nothing to pause. Your VPC resources are free.
  • If you completed the Secret Mission, remove the NAT Gateway and Elastic IP resources from your Terraform configuration, then run the following command to apply the changes:
terraform apply

This removes the NAT Gateway and Elastic IP from AWS while keeping your free VPC, subnets, and route tables in place.

Delete - I don't want to use this again

Remove all project resources from AWS and delete local files to start completely fresh.

  • Destroy all AWS resources managed by Terraform by running this command from your project directory:
terraform destroy
  • When prompted, type yes and press Enter to confirm.
  • Terraform removes the VPC, all subnets, the Internet Gateway, route tables, and any Secret Mission resources (NAT Gateway, Elastic IP).
  • Delete your local project files by running:

macOS

cd ~/Desktop
rm -rf my-vpc-terraform

Windows

cd ~\Desktop
rmdir /s /q my-vpc-terraform

Your AWS account and Terraform CLI installation remain on your machine for future projects.

Nice Work!

Nice Work!

Nicely done. You just deployed a production-grade VPC architecture entirely through code.

Here's what you learned:

  • Built a custom VPC with a /16 CIDR block spanning 65,536 IP addresses across two Availability Zones.
  • Created 4 subnets (2 public, 2 private) with an Internet Gateway and route tables controlling traffic flow between them.
  • Managed the entire infrastructure lifecycle with Terraform using plan, apply, and destroy.
  • Secret Mission: Added a NAT Gateway for outbound internet access from private subnets.

Ready to quiz yourself?