Build a Multi-Tier VPC on AWS

Design a three-tier VPC with public, app, and data subnets across two AZs.

Introduction

30 Second Summary

Every time you use a website or app, your data passes through carefully designed network layers that keep it secure. Think of it like a building where visitors stay in the lobby, employees work on restricted floors, and the vault sits underground with its own set of locks.

In this project, you will build a production-grade three-tier network architecture in AWS, creating separate network zones for public-facing resources, application servers, and databases.

What You'll Build

A custom VPC (Virtual Private Cloud) with six subnets across two Availability Zones, connected by gateways, route tables, and security groups that enforce least-privilege access between tiers.

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

  • A custom VPC with public, private app, and private data subnets spanning two Availability Zones.
  • Working route tables that give public resources internet access and private resources controlled outbound-only access through a NAT Gateway.
  • Least-privilege security groups enforcing traffic rules between tiers, verified by launching EC2 instances and testing connectivity end-to-end.
  • Secret Mission: Add a VPC Gateway Endpoint for S3, enabling private S3 access without traversing the NAT Gateway.

Are there any prerequisites?

You need an AWS account with console access. No local tooling is required for this project. All work is done directly in the AWS Management Console through your web browser.

Create the VPC

Every resource you deploy in AWS lives inside a network. That network is called a VPC (Virtual Private Cloud), and it gives you full control over IP addresses, routing, and security.

In this step, you will sign in to the AWS Console, navigate to the VPC service, and create a custom VPC that spans a large IP range. This VPC becomes the container for every subnet, gateway, and instance you build in the rest of the project.

In this step, get ready to:
  • Sign in to the AWS Management Console and set your region.
  • Create a custom VPC with a /16 CIDR block.
  • Enable DNS hostnames so EC2 instances receive public DNS names.
Sign in to the AWS Console and navigate to VPC

You need to be in the correct AWS region before creating any resources. All resources in this project will live in us-east-1 (US East, N. Virginia).

  • Open your web browser and go to the AWS Management Console at https://console.aws.amazon.com.
  • Sign in with your AWS account credentials.
  • In the top-right corner of the console, click the Region dropdown.
  • Select US East (N. Virginia) us-east-1.
  • In the search bar at the top of the console, type VPC and select VPC from the results.

You should see the VPC Dashboard with a summary of your default VPC resources.

What is a VPC?

A VPC is a logically isolated virtual network that you define inside AWS. It controls which IP addresses your resources use, how traffic flows between them, and what can reach the internet.

Every AWS account comes with a default VPC, but production workloads use custom VPCs so you can design the IP layout and security rules from scratch.

Create a custom VPC using the VPC only option

AWS offers a wizard that auto-creates subnets and gateways for you, but that hides how the pieces connect. You will use the VPC only option so you build each component by hand and understand how they relate.

  • On the VPC Dashboard, click Create VPC.
  • For Resources to create, select VPC only.
  • For Name tag, enter MultiTier-VPC.
  • For IPv4 CIDR block, choose IPv4 CIDR manual input and enter 10.0.0.0/16.
  • For IPv6 CIDR block, leave it as No IPv6 CIDR block.
  • For Tenancy, select Default.
  • Click Create VPC.

You should see a green success banner with your new VPC ID displayed at the top of the page.

Why a /16 CIDR block?

The /16 notation means your VPC can hold up to 65,536 IP addresses. This is the largest block AWS allows for a single VPC, giving you room to create many subnets without running out of addresses.

AWS reserves the first four and last IP address in every subnet. Planning a large range upfront avoids painful re-architecture later.

Don't see Create VPC?

Make sure you are on the VPC Dashboard page, not inside a specific VPC. Click VPC Dashboard in the left navigation pane to return to the main view.

If you see a permissions error, confirm your AWS account has the ec2:CreateVpc permission. The root account always has this by default.

Help me troubleshoot creating a VPC

Enable DNS hostnames on your VPC

By default, a custom VPC does not assign public DNS names to EC2 instances. You need to turn on DNS hostnames so that instances launched in public subnets later in this project receive a reachable hostname.

  • In the left navigation pane, click Your VPCs.
  • Select the checkbox next to MultiTier-VPC.
  • Click the Actions dropdown at the top of the page.
  • Select Edit VPC settings.
  • Under the DNS settings section, check the box for Enable DNS hostnames.
  • Click Save.

Back on the Your VPCs list, select MultiTier-VPC and look at the Details tab below. You should see both DNS hostnames and DNS resolution showing as Enabled.

Why enable DNS hostnames?

When DNS hostnames is enabled, any EC2 instance with a public IP automatically receives a DNS name like ec2-203-0-113-25.compute-1.amazonaws.com. Without this setting, you can only reach the instance by raw IP address.

This is required for EC2 Instance Connect (which you will use later to connect to your bastion host) and is a common exam topic on the SAA-C03.

Don't see the DNS hostnames option?

Make sure you clicked Edit VPC settings (not Edit CIDRs or another option). The DNS settings only appear on the VPC settings page.

Help me find the DNS hostnames setting

Your custom VPC is live and ready to hold subnets. Next up, you will slice this large IP range into six subnets spread across two Availability Zones to build the three-tier architecture.

Create Subnets Across Two Availability Zones

Your VPC is ready, but right now it is one big empty network. To organize resources into tiers, you need subnets that carve the VPC's address space into smaller, purpose-specific segments.

In this step, you will create six subnets across two Availability Zones. Spreading subnets across multiple Availability Zones protects your application from the failure of a single data center.

In this step, get ready to:
  • Create 6 subnets (public, private-app, private-data) in each of two Availability Zones.
  • Enable auto-assign public IPv4 on the two public subnets.
Create the six subnets

Each tier of your architecture gets its own subnet in each Availability Zone. Here is the plan:

  • us-east-1a: Public-1a (10.0.1.0/24), Private-App-1a (10.0.2.0/24), Private-Data-1a (10.0.3.0/24).
  • us-east-1b: Public-1b (10.0.4.0/24), Private-App-1b (10.0.5.0/24), Private-Data-1b (10.0.6.0/24).

Why /24 for each subnet?

A /24 CIDR block gives you 256 IP addresses per subnet. AWS reserves the first four and the last IP address in every subnet, leaving you 251 usable addresses.

Since your VPC uses a /16 block (65,536 addresses total), you have plenty of room for six /24 subnets without overlapping.

You will create the first subnet in detail, then repeat for the remaining five.

  • In the VPC console left navigation pane, click Subnets.
  • Click Create subnet.
  • Under VPC ID, select your MultiTier-VPC from the dropdown.
  • Under Subnet name, enter Public-1a.
  • For Availability Zone, select US East (N. Virginia) / us-east-1a.
  • For IPv4 CIDR block, select Manual input and enter 10.0.1.0/24.
  • Click Add new subnet to add a second subnet on the same page.
  • Fill in the second subnet: name Private-App-1a, Availability Zone us-east-1a, CIDR 10.0.2.0/24.
  • Click Add new subnet again and fill in: name Private-Data-1a, Availability Zone us-east-1a, CIDR 10.0.3.0/24.
  • Click Add new subnet again and fill in: name Public-1b, Availability Zone us-east-1b, CIDR 10.0.4.0/24.
  • Click Add new subnet again and fill in: name Private-App-1b, Availability Zone us-east-1b, CIDR 10.0.5.0/24.
  • Click Add new subnet one more time and fill in: name Private-Data-1b, Availability Zone us-east-1b, CIDR 10.0.6.0/24.
  • Click Create subnet at the bottom of the page.

You should see a success banner confirming all six subnets were created.

Why two Availability Zones?

Each Availability Zone is a physically separate data center within the same AWS Region. If one zone goes down, resources in the other zone keep running.

This multi-AZ pattern is a core principle of the AWS Well-Architected Framework and appears frequently on the SAA-C03 exam.

Don't see your VPC in the dropdown?

Make sure your console region is still set to US East (N. Virginia) in the top-right corner. VPCs are region-specific, so switching regions hides resources created in another region.

help me find my VPC in the subnet creation dropdown

Enable auto-assign public IPv4 on public subnets

Instances launched in a public subnet need a public IP address so they can communicate with the internet. By default, subnets in a custom VPC do not assign public IPs automatically.

  • In the Subnets list, select the checkbox next to Public-1a.
  • Click Actions, then click Edit subnet settings.
  • Check the box labelled Enable auto-assign public IPv4 address.
  • Click Save.
  • Return to the Subnets list and repeat the same steps for Public-1b: select it, Actions > Edit subnet settings > enable Enable auto-assign public IPv4 address > Save.

Why only public subnets get auto-assign?

Private subnets should never expose instances directly to the internet. Keeping auto-assign disabled on private subnets means those instances only get private IP addresses, enforcing the network boundary by design.

To confirm your settings, select each public subnet and look at the Details tab. You should see Auto-assign public IPv4 address set to Yes for both Public-1a and Public-1b.

Auto-assign still shows No?

Make sure you clicked Save after checking the box. If you navigated away without saving, the setting reverts. Select the subnet again and repeat the edit.

help me enable auto-assign public IPv4 on my subnet

Your network is now segmented into six purpose-built lanes. Next up, you will wire these subnets to the internet and to each other by creating gateways and route tables.

Configure Internet Gateway, NAT Gateway, and Route Tables

Your six subnets are in place, neatly divided across two Availability Zones. But right now, none of them can reach the internet or route traffic beyond the VPC.

In this step, you'll build the routing layer that connects your network to the outside world. You'll create an Internet Gateway for public-facing traffic, a NAT Gateway for controlled outbound access from private subnets, and route tables that direct traffic to the right gateway.

In this step, get ready to:
  • Create and attach an Internet Gateway and NAT Gateway to your VPC.
  • Build three route tables with tier-specific routing rules.
  • Associate each route table with its corresponding subnets.
Create and attach an Internet Gateway and NAT Gateway

An Internet Gateway is the door between your VPC and the public internet. Without it, nothing in your VPC can send or receive traffic from outside.

  • In the VPC console left sidebar, click Internet gateways.
  • Click Create internet gateway.
  • Enter MultiTier-IGW in the Name tag field.
  • Click Create internet gateway.

Your Internet Gateway exists but is not connected to anything yet. You need to attach it to your VPC.

  • On the Internet Gateway detail page, click Actions, then Attach to VPC.
  • Select MultiTier-VPC from the Available VPCs dropdown.
  • Click Attach internet gateway.

The state should change to Attached. Your VPC now has a path to the public internet.

What is an Internet Gateway?

An Internet Gateway allows resources in your public subnets to communicate with the internet. It performs network address translation for instances that have a public IP address.

Each VPC can have only one Internet Gateway attached at a time. It is horizontally scaled, redundant, and highly available by default.

Now you'll create the NAT Gateway. This allows instances in private subnets to reach the internet for things like software updates, without exposing them to inbound traffic.

  • In the left sidebar, click NAT gateways.
  • Click Create NAT gateway.
  • Enter MultiTier-NAT in the Name field.
  • In the Subnet dropdown, select Public-1a.
  • Under Elastic IP allocation ID, click Allocate Elastic IP.
  • Click Create NAT gateway.

The NAT Gateway will take 1-2 minutes to move from Pending to Available. Wait for the state to show Available before proceeding.

Why does the NAT Gateway go in a public subnet?

The NAT Gateway needs a public IP address (the Elastic IP you just allocated) and a route to the Internet Gateway. Only public subnets have that route. Private instances send outbound traffic to the NAT Gateway, which forwards it to the internet on their behalf.

This means private instances can download updates and call APIs, but nothing on the internet can initiate a connection back to them.

What does the NAT Gateway cost?

A NAT Gateway in us-east-1 costs $0.045 per hour while provisioned, plus $0.045 per GB of data processed. For this project, you'll tear it down in the Before You Go section to avoid ongoing charges.

NAT Gateway stuck in Pending?

It can take up to 2 minutes for the state to change. Refresh the page if the status does not update automatically.

If it moves to Failed, check that you selected a public subnet (Public-1a) and that the Elastic IP was allocated successfully.

Help me troubleshoot my NAT Gateway creation

Create three route tables with tier-specific routes

Route tables are the traffic rules of your VPC. Each subnet looks at its associated route table to decide where to send outbound traffic.

You'll create three route tables: one that sends traffic to the Internet Gateway (for public subnets), one that sends traffic to the NAT Gateway (for app-tier subnets), and one with no internet route at all (for data-tier subnets).

  • In the left sidebar, click Route tables.
  • Click Create route table.
  • Enter Public-RT in the Name field.
  • Select MultiTier-VPC from the VPC dropdown.
  • Click Create route table.

Now add the internet route to this table.

  • On the route table detail page, click the Routes tab.
  • Click Edit routes.
  • Click Add route.
  • Enter 0.0.0.0/0 in the Destination field.
  • In the Target dropdown, select Internet Gateway, then select MultiTier-IGW.
  • Click Save changes.

What does 0.0.0.0/0 mean?

The destination 0.0.0.0/0 means "all traffic that doesn't match a more specific route." It acts as a default route. When a subnet's route table has this pointing to the Internet Gateway, any traffic destined for the public internet flows there.

Next, create the route table for the private app tier.

  • Click Route tables in the left sidebar.
  • Click Create route table.
  • Enter Private-App-RT in the Name field.
  • Select MultiTier-VPC from the VPC dropdown.
  • Click Create route table.
  • Click the Routes tab, then Edit routes.
  • Click Add route.
  • Enter 0.0.0.0/0 as the Destination.
  • In the Target dropdown, select NAT Gateway, then select your NAT Gateway (MultiTier-NAT).
  • Click Save changes.

Finally, create the route table for the data tier. This one gets no internet route at all.

  • Click Route tables in the left sidebar.
  • Click Create route table.
  • Enter Private-Data-RT in the Name field.
  • Select MultiTier-VPC from the VPC dropdown.
  • Click Create route table.

Why no internet route for the data tier?

Your data tier holds databases and other sensitive resources. By giving it no route to the internet, even outbound, you ensure that data can never leave this tier except through the app tier. This is the principle of network segmentation in action.

Associate route tables with subnets

A route table only takes effect when it's associated with a subnet. Right now, your three new route tables exist but aren't connected to anything.

  • In the route tables list, select Public-RT.
  • Click the Subnet associations tab.
  • Click Edit subnet associations.
  • Check the boxes for Public-1a and Public-1b.
  • Click Save associations.

Repeat for the private app route table.

  • Select Private-App-RT from the route tables list.
  • Click the Subnet associations tab.
  • Click Edit subnet associations.
  • Check the boxes for Private-App-1a and Private-App-1b.
  • Click Save associations.

Finally, associate the data route table.

  • Select Private-Data-RT from the route tables list.
  • Click the Subnet associations tab.
  • Click Edit subnet associations.
  • Check the boxes for Private-Data-1a and Private-Data-1b.
  • Click Save associations.

How does traffic flow now?

Public subnets route 0.0.0.0/0 to the Internet Gateway. Instances there can send and receive internet traffic directly.

Private app subnets route 0.0.0.0/0 to the NAT Gateway. Instances there can reach the internet for outbound requests, but nothing on the internet can reach them directly.

Private data subnets have no internet route. They can only communicate within the VPC.

Subnet not appearing in the association list?

Each subnet can only be explicitly associated with one route table at a time. If a subnet is missing from the list, it may already be associated with another route table. Check the other route tables to see if it was accidentally associated there.

Help me fix my route table subnet associations

Your routing layer is fully wired. Traffic from public subnets flows directly to the Internet Gateway, app-tier traffic goes through the NAT Gateway, and data-tier subnets are completely isolated from the internet. Next up, you'll lock things down further with security groups that control exactly which resources can talk to each other.

Create Security Groups with Least-Privilege Rules

Your network's routing is in place. Traffic from public subnets can reach the internet, private app subnets can make outbound connections through the NAT Gateway, and private data subnets are fully isolated.

Routing alone does not control which resources can talk to each other within the VPC. That is where security groups come in. Security groups act as virtual firewalls attached to individual resources, filtering traffic based on rules you define. In this step, you will create three security groups that enforce a strict chain: only your IP can reach the bastion, only the bastion can reach the app tier, and only the app tier can reach the data tier.

In this step, get ready to:
  • Create three security groups with inbound rules that reference each other by security group ID.
  • Verify the least-privilege chain from your IP through the bastion, app tier, and data tier.
Create the Bastion-SG security group

The bastion host is the only resource accessible directly from the internet. Its security group must allow SSH access from your IP address only.

  • In the VPC console left navigation pane, click Security groups.
  • Click Create security group.
  • In the Security group name field, enter Bastion-SG.
  • In the Description field, enter Allow SSH from my IP only.
  • In the VPC dropdown, remove the default VPC and select your MultiTier-VPC.

Now add the inbound rule that allows SSH from your IP address only.

  • In the Inbound rules section, click Add rule.
  • Set Type to SSH. The protocol and port range auto-fill to TCP and 22.
  • Set Source to My IP. The console auto-fills your current public IP address.
  • Leave the Outbound rules section untouched. The default rule allows all outbound traffic.
  • Click Create security group.

You should see a success banner and the Bastion-SG detail page with your inbound rule listed.

Why My IP instead of Anywhere?

Selecting My IP restricts SSH access to only your current public IP address. If you chose Anywhere-IPv4 (0.0.0.0/0), anyone on the internet could attempt to SSH into your bastion.

This is a core least-privilege principle. Grant only the minimum access needed for the task.

Create the AppTier-SG security group

The app tier should only accept traffic from the bastion. Instead of hardcoding an IP address, you will reference the Bastion-SG security group ID as the source. This means any resource attached to Bastion-SG is automatically trusted.

  • Navigate back to the Security groups list in the left navigation pane.
  • Click Create security group.
  • In the Security group name field, enter AppTier-SG.
  • In the Description field, enter Allow SSH and HTTP from Bastion-SG.
  • In the VPC dropdown, remove the default VPC and select MultiTier-VPC.

Now add two inbound rules. The first allows SSH access from the bastion, and the second allows HTTP traffic from the bastion.

  • In the Inbound rules section, click Add rule.
  • Set Type to SSH.
  • Set Source to Custom. In the search field that appears, type Bastion-SG. Select the Bastion-SG security group from the dropdown results.
  • Click Add rule again to add a second rule.
  • Set Type to HTTP. The port range auto-fills to 80.
  • Set Source to Custom. Search for and select Bastion-SG again.
  • Leave the Outbound rules section with the default all-traffic rule.
  • Click Create security group.

You should see the AppTier-SG detail page with both inbound rules showing the Bastion-SG security group ID as their source.

Why reference a security group ID instead of an IP?

When you use a security group ID as the source, AWS automatically allows traffic from any resource attached to that security group. If the bastion's IP changes, or you add a second bastion, the rule still works without updates.

This SG-to-SG reference pattern is an AWS best practice and a frequently tested concept on the SAA-C03 exam.

Create the DataTier-SG security group

The data tier is the most restricted layer. It should only accept database connections from the app tier.

  • Navigate back to the Security groups list.
  • Click Create security group.
  • In the Security group name field, enter DataTier-SG.
  • In the Description field, enter Allow MySQL from AppTier-SG only.
  • In the VPC dropdown, remove the default VPC and select MultiTier-VPC.

Add the single inbound rule for database traffic.

  • In the Inbound rules section, click Add rule.
  • Set Type to MYSQL/Aurora. The port range auto-fills to 3306.
  • Set Source to Custom. Search for and select AppTier-SG.
  • Leave the Outbound rules section with the default all-traffic rule.
  • Click Create security group.

You should see the DataTier-SG detail page with the MySQL/Aurora inbound rule referencing the AppTier-SG security group ID.

Can't find your security group in the Source dropdown?

Make sure you selected MultiTier-VPC as the VPC for the new security group. The source dropdown only shows security groups that belong to the same VPC.

If the VPC field still shows the default VPC, clear it and re-select MultiTier-VPC before adding inbound rules.

Still stuck? Help me troubleshoot why my security group isn't appearing in the Source dropdown.

Review the security group chain

With all three security groups created, confirm that the least-privilege chain is correctly configured. Each tier should only accept traffic from the tier directly above it.

  • In the Security groups list, click on Bastion-SG.
  • Click the Inbound rules tab. Confirm you see SSH (port 22) with your IP address as the source.
  • Click the Outbound rules tab. Confirm the default rule allows all traffic to 0.0.0.0/0.
  • Go back to the Security groups list and click on AppTier-SG.
  • In the Inbound rules tab, confirm you see SSH (port 22) and HTTP (port 80), both with the Bastion-SG security group ID as their source.
  • Go back and click on DataTier-SG.
  • In the Inbound rules tab, confirm you see MYSQL/Aurora (port 3306) with the AppTier-SG security group ID as its source.

What is the traffic chain?

Your security groups now enforce this chain: Your IP can reach the bastion (SSH). The bastion can reach the app tier (SSH and HTTP). The app tier can reach the data tier (MySQL on port 3306).

No tier can be accessed by anything other than the tier directly above it. This is the security group chaining pattern recommended by the AWS Well-Architected Framework.

Your network is now locked down with least-privilege access controls. Next up, you will launch EC2 instances in each tier and prove that the routing and security groups actually work end-to-end.

Launch EC2 Instances and Verify Connectivity

Your VPC has the network plumbing and security rules in place. Now it is time to prove it all works by launching actual servers and testing traffic flow between them.

In this step, you will launch three EC2 instances across the tiers and verify that traffic flows exactly as your architecture intends. The bastion can reach the app tier, the app tier can reach the internet (via NAT), and the data tier accepts connections only from the app tier.

In this step, get ready to:
  • Launch a bastion host in the public subnet with a new key pair.
  • Launch app-tier and data-tier instances in private subnets.
  • Verify connectivity between all three tiers using EC2 Instance Connect and SSH.
Launch a bastion host in Public-1a

A bastion host is the single entry point into your private network. It sits in the public subnet so you can reach it from the internet, then acts as a jump box to reach private instances.

  • In the AWS Console, navigate to EC2 using the search bar at the top.
  • In the left navigation pane, click Instances.
  • Click Launch instances in the top right.
  • For Name, enter Bastion-Host.
  • Under Application and OS Images (Amazon Machine Image), confirm Amazon Linux 2023 AMI is selected. It should be the default and show Free tier eligible.
  • Under Instance type, select t2.micro (Free tier eligible).
  • Under Key pair (login), click Create new key pair.
  • In the dialog, enter multitier-key for the name.
  • Set Key pair type to RSA.
  • Set Private key file format to .pem.
  • Click Create key pair. The .pem file downloads automatically. Save it somewhere safe.
  • Under Network settings, click Edit.
  • For VPC, select MultiTier-VPC.
  • For Subnet, select Public-1a.
  • For Auto-assign public IP, confirm it shows Enable. This inherits from the subnet setting you configured earlier.
  • Under Firewall (security groups), select Select existing security group.
  • Choose Bastion-SG from the dropdown.
  • Leave all other settings at their defaults and click Launch instance.

What is a bastion host?

A bastion host (also called a jump box) is a hardened server that acts as the only entry point from the public internet into your private network. You connect to the bastion first, then hop from it to private instances.

This pattern keeps your app and data servers completely hidden from the internet while still allowing administrators to manage them.

Wait for the instance state to show Running and the status check to show 2/2 checks passed. This takes 1-2 minutes.

Launch app-tier and data-tier instances

Now you will launch two more instances in the private subnets. These instances have no public IP, so they are invisible from the internet.

  • Click Launch instances again.
  • For Name, enter App-Tier.
  • Confirm Amazon Linux 2023 AMI is selected.
  • Select t2.micro for the instance type.
  • Under Key pair (login), select the existing multitier-key pair.
  • Under Network settings, click Edit.
  • For VPC, select MultiTier-VPC.
  • For Subnet, select Private-App-1a.
  • For Auto-assign public IP, confirm it shows Disable.
  • Under Firewall (security groups), select Select existing security group.
  • Choose AppTier-SG from the dropdown.
  • Click Launch instance.

Now launch the data-tier instance.

  • Click Launch instances one more time.
  • For Name, enter Data-Tier.
  • Confirm Amazon Linux 2023 AMI and t2.micro are selected.
  • Under Key pair (login), select multitier-key.
  • Under Network settings, click Edit.
  • For VPC, select MultiTier-VPC.
  • For Subnet, select Private-Data-1a.
  • For Auto-assign public IP, confirm it shows Disable.
  • Under Firewall (security groups), select Select existing security group.
  • Choose DataTier-SG from the dropdown.
  • Click Launch instance.
  • Return to the Instances list. Wait for all three instances to show Running state.
  • Confirm the Bastion-Host has a public IP address in the Public IPv4 address column.
  • Confirm the App-Tier and Data-Tier instances have no public IP.

Why no public IPs on private instances?

Instances in private subnets intentionally have no public IP address. They can still reach the internet for outbound requests (through the NAT Gateway), but nothing on the internet can initiate a connection to them. This is the core security benefit of a multi-tier architecture.

Instance stuck in Pending state?

Give it up to 2 minutes. If it stays in Pending, check that you selected the correct subnet and VPC. An instance in the wrong VPC will not find the subnet and may fail to launch.

help me troubleshoot my EC2 instance that is stuck in Pending state

Verify connectivity between tiers

This is where you prove the architecture works. You will connect to the bastion, hop to the app tier, test internet access through the NAT Gateway, and confirm the data tier is reachable from the app tier.

  • In the Instances list, select the Bastion-Host instance by clicking its checkbox.
  • Click Connect at the top of the page.
  • Select the EC2 Instance Connect tab.
  • Verify the Username field shows ec2-user.
  • Click Connect. A browser-based terminal opens.

What is EC2 Instance Connect?

EC2 Instance Connect lets you SSH into an instance directly from your browser. AWS pushes a temporary SSH key to the instance that expires after 60 seconds. You do not need to manage SSH keys or install any client software.

This works for instances with a public IP. For the private instances, you will SSH from the bastion using the key pair you downloaded.

You are now logged into the bastion host. Next, you will SSH from the bastion to the app-tier instance using its private IP.

  • Before you can SSH to the app tier, you need your private key on the bastion. Open the multitier-key.pem file you downloaded earlier in a text editor on your local machine.
  • Copy the entire contents (including the -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY----- lines).
  • In the bastion's browser terminal, create a key file by running:
nano multitier-key.pem
  • Paste the key contents into the editor.
  • Press Ctrl+X, then Y, then Enter to save and exit.
  • Set the correct permissions on the key file by running:
chmod 400 multitier-key.pem

Why chmod 400?

SSH refuses to use a private key if the file permissions are too open. The chmod 400 command restricts the file so only the owner can read it. Without this, SSH will reject the key with a "permissions too open" error.

Now you need the private IP of your app-tier instance.

  • Switch back to the EC2 Instances page in another browser tab.
  • Click on the App-Tier instance name.
  • In the Details tab, find and copy the Private IPv4 address (it will look something like 10.0.2.x).
  • Return to the bastion's browser terminal and SSH to the app-tier instance by running:
ssh -i multitier-key.pem ec2-user@[[APP_TIER_PRIVATE_IP="your App-Tier private IPv4 address (e.g. 10.0.2.x)"]]
  • When prompted "Are you sure you want to continue connecting?", type yes and press Enter.

You are now on the app-tier instance. The prompt changes to show you are on a different host.

  • Test outbound internet access through the NAT Gateway by running:
ping -c 3 google.com

You should see three successful ping replies. This confirms your app-tier instance can reach the internet through the NAT Gateway, even though it has no public IP.

How does a private instance reach the internet?

Your Private-App-RT route table sends all internet-bound traffic (0.0.0.0/0) to the NAT Gateway. The NAT Gateway translates the private IP to its own public Elastic IP, forwards the request to the internet, and routes the response back. The outside world only sees the NAT Gateway's IP, never the private instance's.

Now verify the data tier is reachable from the app tier. The security group only allows port 3306 (MySQL) from AppTier-SG, not SSH (port 22). So you will test with a TCP connection on port 3306.

  • Switch back to the EC2 console tab and find the Private IPv4 address of the Data-Tier instance (it will look like 10.0.3.x).
  • Return to the app-tier terminal session and test connectivity to the data tier on port 3306 by running:
timeout 5 bash -c 'echo > /dev/tcp/[[DATA_TIER_PRIVATE_IP="your Data-Tier private IPv4 address (e.g. 10.0.3.x)"]]/3306' && echo "Port 3306 reachable" || echo "Port 3306 not reachable"

What should I see?

You will likely see "Port 3306 not reachable" because no MySQL service is actually running on the data-tier instance. However, the key difference is that the connection attempt does not time out immediately. The packets reach the instance (the security group allows them through from AppTier-SG), but nothing is listening on port 3306 to accept the connection.

If you were to try the same command on port 22 (SSH), it would time out because the security group blocks that port from the app tier entirely. This proves your security groups are working correctly.

  • Optionally, verify that SSH (port 22) is blocked from the app tier to the data tier by running:
timeout 5 bash -c 'echo > /dev/tcp/[[DATA_TIER_PRIVATE_IP="your Data-Tier private IPv4 address (e.g. 10.0.3.x)"]]/22' && echo "Port 22 reachable" || echo "Port 22 not reachable"

This command will time out and print "Port 22 not reachable". The security group is silently dropping the packets because DataTier-SG does not allow SSH from AppTier-SG.

Security groups drop traffic silently

When a security group blocks traffic, it does not send a rejection message. The connection simply times out. This is why you used the timeout 5 prefix. Without it, you would wait indefinitely for a response that never comes.

EC2 Instance Connect fails to connect?

Ensure your bastion instance is in a public subnet with a public IP. Check that Bastion-SG allows inbound SSH (port 22). Also confirm the instance status shows 2/2 checks passed before attempting to connect.

If SSH from bastion to app-tier times out, verify you are using the correct private IP and that AppTier-SG allows SSH from the Bastion-SG security group ID (not from an IP address).

help me troubleshoot EC2 Instance Connect or SSH connectivity in my multi-tier VPC

Your three-tier architecture is now proven. Traffic flows exactly as designed: internet to bastion, bastion to app tier, app tier outbound through NAT, and data tier accessible only from the app tier on the correct port. You have built and verified a production-grade network architecture from scratch.

Secret mission

Add a VPC Gateway Endpoint for S3

Your private app-tier instances currently route all S3 traffic through the NAT Gateway, costing $0.045 per GB processed. In this bonus mission, you will create a VPC Gateway Endpoint that sends S3 traffic directly over the AWS backbone network for free. This is one of the most commonly tested cost-optimization patterns on the SAA-C03 exam.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses a NAT Gateway that costs $0.045/hour while provisioned, so leaving it running will incur charges.

Cost warning

Your NAT Gateway costs approximately $1.08/day while it exists, even if no traffic passes through it. The Elastic IP also costs $0.005/hour if it becomes unassociated from a resource.

If you are not actively experimenting, choose Pause or Delete to avoid unexpected charges on your AWS bill.

Resources you used:

  • 3 EC2 instances (Bastion Host, App-Tier, Data-Tier) in MultiTier-VPC.
  • NAT Gateway with an associated Elastic IP.
  • VPC Gateway Endpoint (S3-Gateway-Endpoint).
  • 3 security groups (Bastion-SG, AppTier-SG, DataTier-SG).
  • 6 subnets across two Availability Zones.
  • Internet Gateway (MultiTier-IGW).
  • 3 route tables (Public-RT, Private-App-RT, Private-Data-RT).
  • MultiTier-VPC.
  • Key pair (multitier-key) and the downloaded .pem file on your machine.

Keep everything running

No action needed. Choose this if you are still actively experimenting with your VPC architecture.

  • Your NAT Gateway will continue to cost approximately $1.08/day.
  • Your EC2 instances are free-tier eligible (t2.micro, 750 hours/month for 12 months from account creation), but all three running together consume free-tier hours 3x faster.
  • Stop EC2 instances when not in use to preserve free-tier hours. Select each instance in the EC2 console, click Instance state, then Stop instance.
  • The VPC Gateway Endpoint for S3 has no hourly or data processing charges, so it costs nothing to keep.

Pause - I'll come back to this later

Shut down running processes to stop hourly charges while keeping your VPC infrastructure in place.

  • Stop all 3 EC2 instances. In the EC2 console, select each instance, click Instance state, then Stop instance. Stopped instances have no compute charges, but EBS storage continues at a minimal cost.
  • Delete the NAT Gateway. Navigate to VPC > NAT gateways, select your NAT Gateway, click Actions, then Delete NAT gateway. Type delete to confirm.
  • Wait 2 minutes for the NAT Gateway to finish deleting, then release the Elastic IP. Navigate to VPC > Elastic IP addresses, select the Elastic IP, click Actions, then Release Elastic IP addresses.
  • Your VPC, subnets, route tables, security groups, Internet Gateway, and VPC Gateway Endpoint remain in place at no cost. You can recreate the NAT Gateway and Elastic IP later to resume.

Delete - I don't want to use this again

Remove all project resources completely. Follow this order to avoid dependency errors.

  • Terminate all 3 EC2 instances. In the EC2 console, select all three instances (Bastion Host, App-Tier, Data-Tier), click Instance state, then Terminate instance. Wait for all instances to show Terminated state.
  • Delete the NAT Gateway. Navigate to VPC > NAT gateways, select your NAT Gateway, click Actions, then Delete NAT gateway. Type delete to confirm.
  • Wait 2 minutes for the NAT Gateway to finish deleting. Then release the Elastic IP. Navigate to VPC > Elastic IP addresses, select the Elastic IP, click Actions, then Release Elastic IP addresses.
  • Delete the VPC Gateway Endpoint. Navigate to VPC > Endpoints, select S3-Gateway-Endpoint, click Actions, then Delete VPC endpoints.
  • Delete all 3 security groups. Navigate to VPC > Security groups, select Bastion-SG, AppTier-SG, and DataTier-SG, click Actions, then Delete security groups. Do not delete the default security group.
  • Delete all 6 subnets. Navigate to VPC > Subnets, filter by your MultiTier-VPC, select all 6 subnets, click Actions, then Delete subnet.
  • Detach and delete the Internet Gateway. Navigate to VPC > Internet gateways, select MultiTier-IGW, click Actions, then Detach from VPC. Then select it again, click Actions, then Delete internet gateway.
  • Delete the 3 route tables (Public-RT, Private-App-RT, Private-Data-RT). Navigate to VPC > Route tables, select each custom route table, click Actions, then Delete route table. Do not delete the Main route table.
  • Delete the VPC. Navigate to VPC > Your VPCs, select MultiTier-VPC, click Actions, then Delete VPC.
  • Delete the key pair. Navigate to EC2 > Key Pairs, select multitier-key, click Actions, then Delete. Also delete the multitier-key.pem file from your local machine.

Nice Work!

Nice Work!

You just built a production-grade, three-tier network architecture on AWS from scratch. This is the same pattern companies use to run secure web applications in the cloud.

You've learned how to:

  • Design and build a custom VPC with 6 subnets across 2 Availability Zones, implementing a three-tier architecture that segments public, application, and data layers.
  • Configure Internet Gateways, NAT Gateways, and route tables to control traffic flow between public and private tiers.
  • Implement least-privilege security groups that reference each other by security group ID, and verify end-to-end connectivity by launching EC2 instances in each tier.
  • Secret Mission: Added a VPC Gateway Endpoint for S3, learning the cost-optimization pattern that eliminates NAT data processing charges.

Ready to quiz yourself?