Troubleshoot a VPC-Hosted Web Server
Build a VPC from scratch and diagnose three real-world network failures.
Introduction
30 Second Summary
When a website goes down, someone has to figure out why. In cloud environments, the problem is often buried in the network layer, hidden behind routing tables, firewall rules, and access controls.
In this project, you will build a custom VPC on AWS, deploy an Nginx web server on an EC2 instance, and then intentionally break three different networking components to practice diagnosing and fixing them from the command line.
What You'll Build
A custom VPC environment hosting a web server, monitored by CloudWatch, with three hands-on troubleshooting scenarios covering route tables, Network ACLs, and security groups.
By the end of this project, you'll have:
- A custom VPC with public and private subnets, an internet gateway, route tables, and security groups built entirely via the AWS CLI.
- A running EC2 web server monitored by a CloudWatch CPU utilization alarm.
- Hands-on experience diagnosing and fixing three common VPC networking failures: broken routes, NACL blocks, and security group misconfigurations.
- Secret Mission: Deploy a second instance in the private subnet with a NAT gateway, and prove it can reach the internet but cannot be reached from outside.
Are there any prerequisites?
You need an AWS account (Free Tier works) with the AWS CLI already installed and configured. All resources in this project are created during the walkthrough.
Verify Your AWS CLI Setup
Every resource in this project gets created through the AWS CLI.
Before you start building, you need to confirm that your CLI is authenticated and pointing at the right region. In this step, you'll verify your identity, set your default region, and check that your account qualifies for free tier EC2 instances.
In this step, get ready to:
- Confirm your AWS CLI is authenticated and your default region is set.
- Verify that t3.micro is free-tier eligible on your account.
- List your existing VPCs so you know what's already in your account.
Verify your identity and CLI configuration
The AWS CLI needs valid credentials and a default region before it can create any resources. If either is missing, every command in this project will fail.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Confirm your CLI is authenticated by running this command:
aws sts get-caller-identity
What should I see?
You should see a JSON response with three fields: UserId, Account, and Arn. This confirms your CLI can talk to AWS.
Seeing an error?
- If you see Unable to locate credentials, your CLI is not configured yet. Run aws configure and enter your access key ID, secret access key, and default region.
- If you see command not found, the AWS CLI is not installed. Follow the AWS CLI installation guide for your operating system.
- help me debug my AWS CLI configuration
- Check your default region by running:
aws configure get region
✔️ I see us-east-1
Your region is already set. Move on to the next substep.
ⓧ I see a different region or nothing
This project uses us-east-1 for all resources. A consistent region avoids cross-region errors in later steps.
- Set your default region to us-east-1 by running:
aws configure set region us-east-1
- Confirm the change by running:
aws configure get region
You should now see us-east-1 in the output.
Confirm free tier eligibility
You'll launch a t3.micro instance later in this project. Before spending any money, confirm it's covered by your account's free tier.
- List all free-tier eligible instance types by running:
aws ec2 describe-instance-types --filters Name=free-tier-eligible,Values=true --query "InstanceTypes[*].[InstanceType]" --output text
Scan the output for t3.micro. It should appear in the list.
What does this command do?
The --filters flag narrows results to only free-tier eligible types. The --query flag uses JMESPath to extract just the instance type names. The --output text flag prints them as plain text instead of JSON.
Why do the eligible types vary by account?
AWS updated the free tier on July 15, 2025. Accounts created after that date get a broader set of eligible types (including t3.micro, t3.small, and others) with a $100 sign-up credit for 6 months.
Accounts created before that date have a smaller list (t2.micro, t3.micro) with a 12-month duration. Either way, t3.micro should appear in your list.
Don't see t3.micro in the list?
- Make sure your region is set correctly. Free tier eligibility is tied to your account, but the command queries against the current region's available types.
- If you're using an older AWS account that has already exhausted its free tier period, the instance will still work. It just won't be free. The cost for a t3.micro is about $0.01/hr.
- help me figure out why t3.micro isn't showing as free-tier eligible
List your existing VPCs
Every AWS account comes with a default VPC in each region. You'll create a brand new custom VPC alongside it, so it helps to know what's already there.
- List all VPCs in your account by running:
aws ec2 describe-vpcs --query "Vpcs[*].[VpcId,CidrBlock,Tags[?Key=='Name'].Value|[0]]" --output table
What should I see?
You should see a table with at least one row for your default VPC. It will have a VPC ID (like vpc-0abc123...), a CIDR block (usually 172.31.0.0/16), and possibly a None for the Name tag.
The custom VPC you build in the next step will use a completely different CIDR block, so there's no conflict with the default one.
Seeing an empty table?
- Some regions may not have a default VPC if it was previously deleted. This is fine. You're about to create a new one.
- Double-check your region is us-east-1 by running aws configure get region.
- help me troubleshoot my VPC listing
Your CLI is verified and your account is ready. Next up, you'll build a custom VPC from scratch with public and private subnets.
Create the VPC and Subnets
Your CLI is verified and ready to go. Now it's time to build the actual network your web server will live in.
Every EC2 instance needs a network to operate in. In this step, you'll create a custom VPC with two subnets and an Internet Gateway to connect it to the public internet.
In this step, get ready to:
- Create a custom VPC with DNS hostnames enabled.
- Create a public subnet and a private subnet inside the VPC.
- Create and attach an internet gateway for public internet access.
Create a VPC and enable DNS hostnames
A VPC is your own isolated section of the AWS cloud. You define the IP address range it uses with a CIDR block, and everything you build in this project lives inside it.
- Create your custom VPC by running this command:
aws ec2 create-vpc --cidr-block 10.0.0.0/16 --tag-specifications "ResourceType=vpc,Tags=[{Key=Name,Value=TroubleshootVPC}]"
What does this command do?
This creates a VPC with the CIDR block 10.0.0.0/16, which gives you 65,536 IP addresses to work with. The --tag-specifications flag names the VPC TroubleshootVPC so you can easily identify it later.
The command outputs a JSON object describing your new VPC.
- Find the VpcId value in the output (it looks like vpc-0abc123def456...). Copy it and save it somewhere, like a text file or notepad. You will use this ID in almost every command from here on.
By default, new VPCs don't assign DNS hostnames to instances launched inside them. You need DNS hostnames enabled so your EC2 instance gets a public DNS name.
- Enable DNS hostnames on your VPC by running this command:
aws ec2 modify-vpc-attribute --vpc-id [[VPC_ID="your VPC ID"]] --enable-dns-hostnames
- Verify your VPC was created correctly by running:
aws ec2 describe-vpcs --vpc-ids [[VPC_ID="your VPC ID"]] --query "Vpcs[0].[VpcId,CidrBlock,State]" --output table
You should see a table showing your VPC ID, the CIDR block 10.0.0.0/16, and the state available.
Seeing an error when creating the VPC?
- If you see an access denied error, confirm your IAM user has EC2 permissions. Run aws sts get-caller-identity to verify you're using the right account.
- On Windows PowerShell, make sure you're using double quotes around the --tag-specifications value. Single quotes can cause parsing errors in PowerShell.
- If the modify-vpc-attribute command fails, double check that the VPC ID you pasted matches the VpcId from the create-vpc output exactly.
Still stuck? Get help here.
Create a public subnet and a private subnet
Subnets divide your VPC into smaller network segments. A public subnet is one that has a route to the internet. A private subnet does not.
You'll place your web server in the public subnet so users can reach it. The private subnet will be used later in the Secret Mission.
- Create the public subnet by running this command:
aws ec2 create-subnet --vpc-id [[VPC_ID="your VPC ID"]] --cidr-block 10.0.0.0/24 --availability-zone us-east-1a --tag-specifications "ResourceType=subnet,Tags=[{Key=Name,Value=PublicSubnet}]"
What does this command do?
This creates a subnet with 256 IP addresses (10.0.0.0/24) inside your VPC. The --availability-zone us-east-1a places it in a specific Availability Zone, which is a physically separate data center within the region.
- Find the SubnetId in the output and save it as your public subnet ID.
- Create the private subnet by running this command:
aws ec2 create-subnet --vpc-id [[VPC_ID="your VPC ID"]] --cidr-block 10.0.1.0/24 --availability-zone us-east-1a --tag-specifications "ResourceType=subnet,Tags=[{Key=Name,Value=PrivateSubnet}]"
- Save the SubnetId from this output as your private subnet ID.
Why two subnets?
Separating resources into public and private subnets is a core AWS networking pattern. Public subnets hold resources that need to be reachable from the internet, like web servers. Private subnets hold resources that should only be reachable from within the VPC, like databases.
Both subnets use the same Availability Zone (us-east-1a) so resources in each subnet can communicate with low latency.
For the public subnet to actually assign public IP addresses to instances launched inside it, you need to enable auto-assign public IPv4.
- Enable auto-assign public IPv4 on the public subnet by running:
aws ec2 modify-subnet-attribute --subnet-id [[PUBLIC_SUBNET_ID="your public subnet ID"]] --map-public-ip-on-launch
- Verify both subnets exist by running:
aws ec2 describe-subnets --filters "Name=vpc-id,Values=[[VPC_ID=\"your VPC ID\"]]" --query "Subnets[*].[SubnetId,CidrBlock,Tags[0].Value]" --output table
You should see a table with two rows. One row shows PublicSubnet with CIDR 10.0.0.0/24, and the other shows PrivateSubnet with CIDR 10.0.1.0/24.
Only seeing one subnet, or seeing an error?
- Make sure you used the correct VPC ID in both create-subnet commands. If you accidentally used a different VPC ID, the subnet exists but won't appear when you filter by your TroubleshootVPC ID.
- If you see a CIDR conflict error, it means the CIDR block overlaps with an existing subnet in the same VPC. Check that you used 10.0.0.0/24 for the public subnet and 10.0.1.0/24 for the private subnet.
Still stuck? Get help here.
Create and attach an internet gateway
Your VPC is completely isolated from the internet by default. An internet gateway is the component that connects your VPC to the outside world.
- Create an internet gateway by running this command:
aws ec2 create-internet-gateway --tag-specifications "ResourceType=internet-gateway,Tags=[{Key=Name,Value=TroubleshootIGW}]"
- Find the InternetGatewayId in the output (it looks like igw-0abc123def456...) and save it.
- Attach the internet gateway to your VPC by running:
aws ec2 attach-internet-gateway --internet-gateway-id [[IGW_ID="your internet gateway ID"]] --vpc-id [[VPC_ID="your VPC ID"]]
What is an internet gateway?
An internet gateway is a VPC component that allows communication between your VPC and the public internet. Without it, nothing inside your VPC can send or receive traffic from outside AWS.
Creating the gateway is only half the job. You also need a route table rule pointing to it, which you'll set up in the next step.
- Verify the internet gateway is attached to your VPC by running:
aws ec2 describe-internet-gateways --internet-gateway-ids [[IGW_ID="your internet gateway ID"]] --query "InternetGateways[0].Attachments" --output table
You should see a table with your VPC ID and the state available. This confirms the gateway is connected to TroubleshootVPC.
Seeing 'not found' or 'attachment failed'?
- If you see InvalidInternetGatewayID.NotFound, double check that the internet gateway ID you pasted matches the InternetGatewayId from the create output exactly.
- Each VPC can only have one internet gateway attached. If the attach command fails, check whether your VPC already has a gateway attached from a previous attempt.
Still stuck? Get help here.
Keep your IDs handy
By now you should have three IDs saved: your VPC ID, public subnet ID, and private subnet ID (plus the internet gateway ID). Keep these in an easy-to-reach spot. You'll reference them throughout the rest of the project.
Your VPC, subnets, and internet gateway are all in place. Next up, you'll configure routing and security rules so traffic can actually flow through this network.
Configure Routing and Security
Your VPC now has a public subnet, a private subnet, and an internet gateway. But the internet gateway on its own does not give your subnets internet access.
Traffic needs to know where to go. In this step, you will create a route table to direct traffic through the internet gateway and a security group to control which ports are open on your future web server.
In this step, get ready to:
- Create a route table and route traffic to the internet gateway.
- Create a security group allowing SSH and HTTP access.
- Verify the full network configuration as a known-good baseline.
Create a public route table and configure routing
A route table contains rules that determine where network traffic is directed. Without one, packets from your public subnet have no path to the Internet Gateway.
- Create a route table in your VPC by running this command:
aws ec2 create-route-table --vpc-id [[VPC_ID="your VPC ID"]] --tag-specifications "ResourceType=route-table,Tags=[{Key=Name,Value=PublicRouteTable}]"
- Find the RouteTableId in the output and save it. You will need it for the next two commands.
What is a route table?
Think of a route table as a set of directions for network traffic. Each rule says: if a packet is headed to this destination, send it through this gateway.
Every VPC comes with a default route table that only handles traffic within the VPC itself (the local route). You are creating a custom route table that adds a route for internet-bound traffic.
- Add a default route that sends all internet-bound traffic (0.0.0.0/0) to the internet gateway by running:
aws ec2 create-route --route-table-id [[RTB_ID="your route table ID"]] --destination-cidr-block 0.0.0.0/0 --gateway-id [[IGW_ID="your internet gateway ID"]]
You should see "Return": true in the output, confirming the route was added.
- Associate the route table with your public subnet by running:
aws ec2 associate-route-table --route-table-id [[RTB_ID="your route table ID"]] --subnet-id [[PUBLIC_SUBNET_ID="your public subnet ID"]]
What did these three commands do?
- create-route-table created a new route table inside your VPC.
- create-route added a rule that sends all non-local traffic (0.0.0.0/0) to the internet gateway.
- associate-route-table linked the route table to your public subnet. Only subnets associated with this route table use its rules.
Seeing an error on any of these commands?
- Double-check that you pasted the correct IDs. The route table ID starts with rtb-, the internet gateway ID starts with igw-, and the subnet ID starts with subnet-.
- If you see InvalidRouteTableID.NotFound, confirm the route table ID matches the output from the first command.
- help me fix a route table creation error
Create a security group with SSH and HTTP access
A security group acts as a virtual firewall for your EC2 instance. It controls which inbound and outbound traffic is allowed. You need to open two ports: SSH (port 22) for connecting to the instance and HTTP (port 80) for serving web traffic.
- Create a security group named WebServerSG in your VPC by running:
aws ec2 create-security-group --group-name WebServerSG --description "Web server security group" --vpc-id [[VPC_ID="your VPC ID"]]
- Find the GroupId in the output and save it. You will use it for the next commands.
- Allow inbound SSH traffic on port 22 by running:
aws ec2 authorize-security-group-ingress --group-id [[SG_ID="your security group ID"]] --protocol tcp --port 22 --cidr 0.0.0.0/0
- Allow inbound HTTP traffic on port 80 by running:
aws ec2 authorize-security-group-ingress --group-id [[SG_ID="your security group ID"]] --protocol tcp --port 80 --cidr 0.0.0.0/0
What do these rules allow?
- Port 22 (SSH) lets you connect to the instance remotely to run commands.
- Port 80 (HTTP) lets browsers access the web server you will install later.
- 0.0.0.0/0 means traffic from any IP address is allowed. In production, you would restrict SSH to your own IP. For this lab, allowing all sources makes troubleshooting simpler.
Why are security groups called stateful?
Security groups remember connections. When you allow inbound HTTP on port 80, the response traffic going back out is automatically allowed. You do not need a separate outbound rule for it.
This becomes important later when you compare security groups to Network ACLs, which are stateless and require rules in both directions.
Seeing a duplicate group name error?
- If you see InvalidGroup.Duplicate, a security group named WebServerSG already exists in this VPC. Run aws ec2 describe-security-groups --filters "Name=group-name,Values=WebServerSG" to find its ID and use that instead.
- help me fix a security group creation error
Verify the network configuration
Before moving on, you need to confirm everything is wired up correctly. This verified state becomes your known-good baseline. When you intentionally break things later, you will compare against this baseline to diagnose problems.
- Check your route table by running:
aws ec2 describe-route-tables --route-table-ids [[RTB_ID="your route table ID"]]
You should see two routes in the output. The first is a local route for 10.0.0.0/16 (traffic within the VPC). The second is your 0.0.0.0/0 route pointing to the internet gateway.
- Check your security group rules by running:
aws ec2 describe-security-groups --group-ids [[SG_ID="your security group ID"]]
In the IpPermissions section, you should see two inbound rules. One allows TCP port 22 (SSH) from 0.0.0.0/0. The other allows TCP port 80 (HTTP) from 0.0.0.0/0.
Why document a known-good baseline?
In a real support scenario, the first thing you do when something breaks is compare the current state against what you know used to work. Having a verified baseline saves time because you can immediately spot what changed.
In Step 6, you will intentionally break three different networking components. These verification outputs are the reference point for diagnosing each failure.
Missing a route or security group rule?
- If the route table only shows the local route, the create-route command did not succeed. Re-run it with the correct route table and internet gateway IDs.
- If the security group shows zero inbound rules, the authorize-security-group-ingress commands did not succeed. Re-run them with the correct security group ID.
- help me verify my VPC route table and security group configuration
Your VPC networking is fully configured. Traffic from the public subnet can reach the internet through the internet gateway, and your security group is ready to protect the web server. Next up, you will launch an EC2 instance and install Nginx to serve a real web page.
Launch and Configure the Web Server
Your VPC network is fully wired up with routing and security rules. Now it's time to put that infrastructure to work by launching a real web server inside it.
In this step, you'll launch an EC2 instance in your public subnet and install Nginx to serve a web page. When you see the Nginx welcome page in your browser, that confirms every layer of your network is working correctly.
In this step, get ready to:
- Look up the latest Amazon Linux 2023 AMI and launch an EC2 instance.
- Connect to the instance using EC2 Instance Connect in the browser.
- Install and start Nginx, then verify it's accessible from the internet.
Look up the latest Amazon Linux 2023 AMI and launch an instance
Every EC2 instance starts from an AMI (Amazon Machine Image), which is a template containing the operating system and pre-installed software. You'll use the latest Amazon Linux 2023 AMI.
- Find the latest Amazon Linux 2023 AMI ID by running this command:
aws ec2 describe-images --owners amazon --filters "Name=name,Values=al2023-ami-2023*-x86_64" "Name=state,Values=available" --query "Images | sort_by(@, &CreationDate) | [-1].ImageId" --output text
What does this command do?
This searches Amazon's public AMI catalog for Amazon Linux 2023 images. The --query sorts them by creation date and grabs the newest one.
The output is a single AMI ID like ami-0abcdef1234567890. Copy this value. You'll need it for the next command.
- Copy the AMI ID from the output and save it somewhere you can easily paste from.
No output or an error?
If the command returns nothing, make sure your default region is set. Run aws configure get region to check. If it's blank, set it to us-east-1 and try again.
Help me troubleshoot this AMI lookup error
Now you have the AMI ID. Time to launch the instance.
- Launch a t3.micro instance in your public subnet by running this command:
macOS
aws ec2 run-instances \
--image-id [[AMI_ID="your AMI ID from the previous command"]] \
--instance-type t3.micro \
--subnet-id [[PUBLIC_SUBNET_ID="your PublicSubnet ID"]] \
--security-group-ids [[SG_ID="your WebServerSG ID"]] \
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=WebServer}]"
Windows
aws ec2 run-instances `
--image-id [[AMI_ID="your AMI ID from the previous command"]] `
--instance-type t3.micro `
--subnet-id [[PUBLIC_SUBNET_ID="your PublicSubnet ID"]] `
--security-group-ids [[SG_ID="your WebServerSG ID"]] `
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=WebServer}]"
What does this command do?
- --image-id tells AWS which operating system template to use (the Amazon Linux 2023 AMI you just found).
- --instance-type t3.micro selects a small instance size that's covered by the Free Tier.
- --subnet-id places the instance in your public subnet, where it gets a public IP address.
- --security-group-ids attaches your WebServerSG, which allows SSH and HTTP traffic.
- --tag-specifications names the instance WebServer so you can find it easily in the console.
- The command outputs a large JSON block. Find the InstanceId field near the top of the output and copy its value (it starts with i-). Save this for the next command.
Seeing an error about invalid parameters?
- If you see InvalidAMIID.NotFound, double check that you pasted the full AMI ID from the previous command.
- If you see InvalidSubnetID.NotFound or InvalidGroup.NotFound, make sure you're using the IDs from your PublicSubnet and WebServerSG created in earlier steps.
- If you see InsufficientInstanceCapacity, wait a minute and try the command again. AWS occasionally runs low on capacity in a specific Availability Zone.
Help me troubleshoot my EC2 launch error
Wait for the instance to start and connect to it
It takes a few seconds for AWS to boot your instance and assign it a public IP address. You can check the current status from the CLI.
- Check the instance state and retrieve the public IP address by running this command:
macOS
aws ec2 describe-instances \
--instance-ids [[INSTANCE_ID="your instance ID starting with i-"]] \
--query "Reservations[0].Instances[0].[State.Name,PublicIpAddress]" \
--output table
Windows
aws ec2 describe-instances `
--instance-ids [[INSTANCE_ID="your instance ID starting with i-"]] `
--query "Reservations[0].Instances[0].[State.Name,PublicIpAddress]" `
--output table
What should I see?
The output is a table with two columns. The first shows the instance state and the second shows the public IP.
If the state is running and you see an IP address, the instance is ready. If the state is pending, wait about 30 seconds and run the command again.
- Copy the public IP address from the output. You'll need it to connect and to test the web server later.
Now you'll connect to the instance using EC2 Instance Connect. This is a browser-based SSH terminal built into the AWS Console. No SSH client or key files needed on your local machine.
- Open the AWS Console in your browser and navigate to the EC2 service.
- Click Instances (running) in the left sidebar, or click Instances under Resources on the dashboard.
- Select the checkbox next to your WebServer instance.
- Click the Connect button near the top of the page.
- On the Connect to instance page, select the EC2 Instance Connect tab.
- Leave the default username as ec2-user and click Connect.
A new browser tab opens with a terminal session connected to your EC2 instance. You should see a command prompt showing ec2-user@ip-10-0-0-....
Connection failed or timed out?
- Make sure the instance state is running (not pending). If it's still starting up, wait a minute and try again.
- Verify that your WebServerSG security group allows inbound SSH (port 22) from 0.0.0.0/0. Without this rule, EC2 Instance Connect cannot reach the instance.
- Confirm the instance is in the public subnet with a public IP address. The instance needs internet connectivity for EC2 Instance Connect to work.
Help me troubleshoot my EC2 Instance Connect failure
Install and start Nginx
Nginx is a lightweight web server. Installing it on your EC2 instance turns the instance into a web server that responds to HTTP requests on port 80.
- In the EC2 Instance Connect terminal, install Nginx by running this command:
sudo dnf install nginx -y
- Start Nginx and configure it to launch automatically on reboot by running these commands:
sudo systemctl start nginx
sudo systemctl enable nginx
What do these commands do?
- sudo dnf install nginx -y uses the dnf package manager (Amazon Linux 2023's default) to download and install Nginx. The -y flag auto-confirms the installation.
- sudo systemctl start nginx starts the Nginx process right now.
- sudo systemctl enable nginx tells the system to start Nginx automatically whenever the instance reboots.
- Open a new tab in your browser.
- Paste your instance's public IP address into the address bar and press Enter.
You should see the Nginx welcome page with the heading "Welcome to nginx!" displayed in your browser.
Why does this confirm your entire VPC works?
Seeing the Nginx page means every networking layer is functioning. Your browser's request traveled from the internet, through the Internet Gateway, matched a route in the route table, passed the security group on port 80, and reached Nginx running on the instance.
This is your known-good baseline. In the troubleshooting step later, you'll break each of these layers one at a time and learn to diagnose which one failed.
Don't see the Nginx welcome page?
- Make sure you're using http:// (not https://) in the address bar. Some browsers default to HTTPS, which will fail because Nginx is only configured for HTTP on port 80.
- Confirm the instance has a public IP address. Re-run the describe-instances command from the previous substep.
- Verify your security group allows HTTP on port 80 by running aws ec2 describe-security-groups --group-ids with your WebServerSG ID.
- Make sure the route table for your public subnet has a 0.0.0.0/0 route pointing to the internet gateway.
Help me figure out why my Nginx page is not loading
Your web server is live and reachable from the internet. Next up, you'll set up a CloudWatch alarm to monitor CPU usage on the instance so you know when something is wrong before your users do.
Set Up CloudWatch Monitoring
Your web server is live and the Nginx welcome page loads in your browser. That confirms the entire network path is working.
A running server is only useful if you know when something goes wrong. CloudWatch is AWS's monitoring service that watches your resources and triggers alarms when metrics cross thresholds you define. In this step, you'll create a CPU utilization alarm for your EC2 instance and test how alarm states work.
In this step, get ready to:
- Create a CloudWatch alarm monitoring CPU utilization on your WebServer instance.
- Verify the alarm exists and understand its initial state.
- Test alarm state transitions by manually cycling through all three states.
Create a CPU utilization alarm
CloudWatch alarms evaluate a single metric over time and change state when that metric crosses a threshold. You'll create an alarm that watches average CPU usage on your WebServer instance.
Switch back to your local terminal
In the previous step, you connected to the EC2 instance through EC2 Instance Connect in your browser. The commands in this step are AWS CLI commands that run on your local machine. Switch back to your local PowerShell or Command Prompt window.
- Retrieve your WebServer instance ID by running this command:
aws ec2 describe-instances `
--filters "Name=tag:Name,Values=WebServer" `
--query "Reservations[0].Instances[0].InstanceId" `
--output text
- You should see an instance ID like i-0abc123def456789a. Copy this value for the next command.
Seeing an empty result or error?
Make sure you are in your local terminal, not the EC2 Instance Connect session. The AWS CLI must be configured with your credentials locally.
If the output is blank, check that your instance is tagged exactly WebServer (case-sensitive). You can list all instances with aws ec2 describe-instances --query "Reservations[*].Instances[*].[InstanceId,Tags]" --output table to double check.
Help me troubleshoot why my describe-instances command returns empty results
- Create the CloudWatch alarm by running this command, replacing the instance ID placeholder with the value you just copied:
# Create a CloudWatch alarm that triggers when average CPU exceeds 80%
aws cloudwatch put-metric-alarm `
--alarm-name WebServer-CPU-High `
--alarm-description "Alarm when CPU exceeds 80 percent" `
--metric-name CPUUtilization `
--namespace AWS/EC2 `
--statistic Average `
--period 300 `
--threshold 80 `
--comparison-operator GreaterThanThreshold `
--dimensions Name=InstanceId,Value=[[INSTANCE_ID="your WebServer instance ID"]] `
--evaluation-periods 2 `
--unit Percent
What does this command do?
- --alarm-name WebServer-CPU-High gives the alarm a descriptive name you can reference later.
- --metric-name CPUUtilization tells CloudWatch which metric to watch. EC2 publishes this metric automatically.
- --period 300 sets the evaluation window to 300 seconds (5 minutes). EC2 sends basic monitoring data at this interval by default.
- --evaluation-periods 2 means the threshold must be breached for 2 consecutive periods (10 minutes total) before the alarm triggers. This prevents false alarms from short CPU spikes.
- --threshold 80 with --comparison-operator GreaterThanThreshold means the alarm fires when average CPU goes above 80%.
This command produces no output on success. If you see no error message, the alarm was created.
Seeing a "parameter" or "value" error?
Check that you replaced the instance ID placeholder with your actual instance ID (e.g. i-0abc123def456789a). The value after Value= must be a real instance ID with no spaces around it.
If you are using PowerShell, make sure each line continuation uses a backtick (`) at the end, not a backslash.
Help me fix my put-metric-alarm command
Verify the alarm was created
- Confirm the alarm exists by running this command:
# Check the alarm details and current state
aws cloudwatch describe-alarms --alarm-names WebServer-CPU-High
You should see a JSON response with your alarm details, including AlarmName, Threshold, and StateValue.
Why does it say INSUFFICIENT_DATA?
Every newly created CloudWatch alarm starts in the INSUFFICIENT_DATA state. This is normal. CloudWatch has not yet collected enough metric data points to evaluate the alarm against the threshold.
Since your alarm requires 2 consecutive 5-minute periods, CloudWatch needs at least 10 minutes of data before it can determine whether CPU is above or below 80%.
Seeing an empty MetricAlarms list?
Double check the alarm name is exactly WebServer-CPU-High (case-sensitive). If the previous command returned no error but this returns nothing, the alarm name may have a typo.
Help me find my CloudWatch alarm
Test the alarm by forcing state changes
CloudWatch alarms have three possible states: INSUFFICIENT_DATA, OK, and ALARM. Instead of waiting for real CPU data to flow in, you can manually set the alarm state to test how transitions work.
- Set the alarm state to OK by running this command:
# Manually transition the alarm from INSUFFICIENT_DATA to OK
aws cloudwatch set-alarm-state `
--alarm-name WebServer-CPU-High `
--state-reason "Testing alarm state transitions" `
--state-value OK
- Verify the state changed by running:
aws cloudwatch describe-alarms `
--alarm-names WebServer-CPU-High `
--query "MetricAlarms[0].StateValue" `
--output text
You should see OK in the output.
- Now set the alarm state to ALARM by running:
# Manually transition the alarm from OK to ALARM
aws cloudwatch set-alarm-state `
--alarm-name WebServer-CPU-High `
--state-reason "Testing alarm state transitions" `
--state-value ALARM
- Verify the state changed to ALARM:
aws cloudwatch describe-alarms `
--alarm-names WebServer-CPU-High `
--query "MetricAlarms[0].StateValue" `
--output text
You should see ALARM in the output.
What are the three alarm states?
- INSUFFICIENT_DATA means CloudWatch does not have enough data points yet to evaluate the metric against the threshold.
- OK means the metric is within the acceptable range (below 80% CPU in your case).
- ALARM means the metric has breached the threshold for the required number of evaluation periods. In production, this state would trigger notifications or automated actions.
State not changing?
Make sure the --state-value is exactly OK or ALARM (all uppercase). Lowercase values like ok or alarm will cause an error.
Help me fix my set-alarm-state command
Your monitoring is in place. You have a working web server, proper VPC networking, and a CloudWatch alarm watching CPU utilization. Next up, you'll intentionally break three different networking components and systematically troubleshoot each one.
Troubleshoot VPC Network Failures
Your CloudWatch alarm is in place, confirming your monitoring setup works. Now it is time for the most valuable part of this project: learning to diagnose and fix real networking failures.
In a real AWS Cloud Support Associate role, you would receive tickets where a customer's instance suddenly loses connectivity. The root cause could be a routing issue, a firewall misconfiguration, or a Network ACL conflict. In this step, you will intentionally break three different networking layers and practice the systematic diagnosis workflow for each one.
In this step, get ready to:
- Diagnose and fix a broken route table entry that kills internet access.
- Discover how a NACL deny rule can override a security group allow rule.
- Troubleshoot a security group misconfiguration that blocks SSH access.
Break 1: Remove the internet gateway route
Your Internet Gateway is attached to the VPC, but traffic only flows through it because the route table has a rule pointing 0.0.0.0/0 (all internet-bound traffic) to the gateway. Removing that route cuts off all internet access, even though the gateway itself is still attached.
- Delete the default route from your PublicRouteTable by running this command:
aws ec2 delete-route --route-table-id [[RTB_ID="your PublicRouteTable ID"]] --destination-cidr-block 0.0.0.0/0
- Open your browser and navigate to your WebServer's public IP address.
- The page will time out or fail to load. This is expected.
The instance still has a public IP, and the security group still allows HTTP. So what went wrong?
- Diagnose the issue by inspecting the route table with this command:
aws ec2 describe-route-tables --route-table-ids [[RTB_ID="your PublicRouteTable ID"]] --query "RouteTables[0].Routes"
What should I see?
The output shows only one route: the local route (10.0.0.0/16 targeting local). The 0.0.0.0/0 route to the Internet Gateway is missing. Without it, packets have no path to or from the public internet.
- Fix the issue by re-adding the route to the Internet Gateway:
aws ec2 create-route --route-table-id [[RTB_ID="your PublicRouteTable ID"]] --destination-cidr-block 0.0.0.0/0 --gateway-id [[IGW_ID="your TroubleshootIGW ID"]]
- Refresh your browser. You should see the Nginx welcome page loading again.
Page still not loading?
Make sure the --gateway-id value matches your actual TroubleshootIGW ID. You can verify the route was added by re-running the describe-route-tables command from the diagnosis step. You should now see two routes: the local route and the 0.0.0.0/0 route pointing to your IGW.
Help me fix my route table. I re-added the 0.0.0.0/0 route but my Nginx page still won't load.
Break 2: Block HTTP with a Network ACL
Your security group allows HTTP on port 80. So if you add a separate firewall layer that blocks port 80, will the security group's allow rule win? This scenario demonstrates one of the most common misunderstandings in AWS networking.
You will create a custom Network ACL (NACL) that explicitly denies inbound HTTP traffic, and then associate it with your public subnet.
- Create a custom NACL in your VPC by running this command:
aws ec2 create-network-acl --vpc-id [[VPC_ID="your TroubleshootVPC ID"]]
What does this command do?
This creates a new Network ACL in your VPC. By default, a new NACL has a deny-all rule (rule number 32767) for both inbound and outbound traffic. You will add specific rules next.
Save the NetworkAclId from the output. You will need it for the following commands.
- Add three rules to the NACL by running each of the following commands. First, add a deny rule for inbound HTTP traffic (rule 100):
aws ec2 create-network-acl-entry `
--network-acl-id [[NACL_ID="your custom NACL ID"]] `
--rule-number 100 `
--protocol tcp `
--port-range From=80,To=80 `
--cidr-block 0.0.0.0/0 `
--rule-action deny `
--ingress
- Add a rule allowing all other inbound traffic (rule 200):
aws ec2 create-network-acl-entry `
--network-acl-id [[NACL_ID="your custom NACL ID"]] `
--rule-number 200 `
--protocol -1 `
--cidr-block 0.0.0.0/0 `
--rule-action allow `
--ingress
- Add a rule allowing all outbound traffic (rule 100 outbound):
aws ec2 create-network-acl-entry `
--network-acl-id [[NACL_ID="your custom NACL ID"]] `
--rule-number 100 `
--protocol -1 `
--cidr-block 0.0.0.0/0 `
--rule-action allow `
--egress
Why these three rules?
Rule 100 inbound denies TCP port 80 (HTTP). Rule 200 inbound allows everything else (SSH, ICMP, etc.). NACLs evaluate rules in ascending order by rule number, so rule 100 fires first and blocks HTTP before rule 200 can allow it.
Rule 100 outbound allows all traffic out, so responses to SSH and other allowed protocols can leave the instance.
Now you need to associate this custom NACL with your public subnet. This requires the current association ID.
- Look up the current NACL association ID for your PublicSubnet by running:
aws ec2 describe-network-acls `
--filters Name=association.subnet-id,Values=[[PUBLIC_SUBNET_ID="your PublicSubnet ID"]] `
--query "NetworkAcls[0].Associations[0].NetworkAclAssociationId" `
--output text
What is a NACL association ID?
Every subnet is linked to a NACL through an association. To swap one NACL for another, you need the ID of that association (not the NACL ID or the subnet ID). The command above returns the association ID you will use in the next step.
- Replace the subnet's NACL with your custom NACL by running this command:
aws ec2 replace-network-acl-association `
--association-id [[NACL_ASSOC_ID="your NACL association ID"]] `
--network-acl-id [[NACL_ID="your custom NACL ID"]]
What does this command return?
The output includes a NewAssociationId. Save this value. You will need it later when you reassociate the default NACL to fix the issue.
- Open your browser and refresh the WebServer's public IP address.
- The Nginx page fails to load. The security group still allows HTTP on port 80, but the page is blocked.
What is stateful vs stateless?
This is the classic stateful vs stateless trap in AWS networking. Security groups are stateful. When they allow inbound traffic, return traffic is automatically allowed outbound. You only write one rule.
NACLs are stateless. Every direction must be explicitly allowed, and rules are evaluated in ascending order by rule number. A deny at rule 100 takes effect before an allow at rule 200. The NACL deny blocked HTTP before the security group ever had a chance to evaluate it.
This distinction is one of the most common categories of AWS support tickets.
Time to fix the issue by reassociating the default NACL with your public subnet.
- Find the default NACL ID for your VPC by running:
aws ec2 describe-network-acls `
--filters Name=vpc-id,Values=[[VPC_ID="your TroubleshootVPC ID"]] Name=default,Values=true `
--query "NetworkAcls[0].NetworkAclId" `
--output text
- Reassociate the default NACL with your subnet by running this command:
aws ec2 replace-network-acl-association `
--association-id [[NEW_NACL_ASSOC_ID="the NewAssociationId from the previous replace command"]] `
--network-acl-id [[DEFAULT_NACL_ID="your default NACL ID"]]
- Refresh your browser. The Nginx welcome page should load again.
Nginx page still blocked?
Make sure you used the NewAssociationId that was returned when you associated the custom NACL (not the original association ID). Each call to replace-network-acl-association returns a new association ID that replaces the previous one.
You can verify the current NACL by running describe-network-acls filtered by your subnet ID. The result should show the default NACL (IsDefault: true).
Help me fix my NACL association. My Nginx page is still blocked after reassociating the default NACL.
Break 3: Remove the SSH security group rule
The final scenario tests the most straightforward failure: a missing security group rule. Without the SSH inbound rule, EC2 Instance Connect cannot reach your instance on port 22.
- Revoke the SSH inbound rule from your security group by running this command:
aws ec2 revoke-security-group-ingress `
--group-id [[SG_ID="your WebServerSG ID"]] `
--protocol tcp `
--port 22 `
--cidr 0.0.0.0/0
- Open the EC2 Console in your browser.
- Select your WebServer instance, then click Connect.
- Choose the EC2 Instance Connect tab and click Connect.
- The connection will fail. This is expected.
- Diagnose the issue by inspecting the security group rules:
aws ec2 describe-security-groups `
--group-ids [[SG_ID="your WebServerSG ID"]] `
--query "SecurityGroups[0].IpPermissions"
What should I see?
The output shows only one inbound rule: HTTP on port 80. The SSH rule (port 22) is missing. EC2 Instance Connect uses SSH under the hood, so it needs port 22 open in the security group to reach the instance.
- Fix the issue by re-adding the SSH inbound rule:
aws ec2 authorize-security-group-ingress `
--group-id [[SG_ID="your WebServerSG ID"]] `
--protocol tcp `
--port 22 `
--cidr 0.0.0.0/0
- Go back to the EC2 Console, select your WebServer instance, click Connect, and try EC2 Instance Connect again.
- You should see a terminal window open in your browser, confirming the connection works.
Still can't connect via EC2 Instance Connect?
Verify the SSH rule was added correctly by running describe-security-groups again. You should see two rules: one for TCP port 22 and one for TCP port 80, both from 0.0.0.0/0.
Also confirm that your instance is still in the running state. If the instance was stopped or terminated, EC2 Instance Connect will not be able to reach it.
Help me reconnect to my EC2 instance. I re-added the SSH rule but EC2 Instance Connect still fails.
You just completed all three troubleshooting scenarios. Your VPC is fully restored to its working state with the route table, NACL, and security group all correctly configured. If you are ready for an extra challenge, the Secret Mission extends your VPC by deploying an instance in the private subnet with a NAT gateway.
Secret mission
Deploy to the Private Subnet with NAT Gateway
Your VPC has a private subnet sitting idle with no internet access. In this Secret Mission, deploy a second EC2 instance into that private subnet and give it outbound internet access through a NAT gateway. Then prove it can reach the internet but cannot be reached from outside.
Before you go
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. The EC2 t3.micro instance is covered by Free Tier, so compute charges are minimal while the instance is running.
Cost warning
If you completed the Secret Mission, the NAT gateway costs $0.045/hr plus data processing charges. The Elastic IP also incurs charges when not associated with a running instance.
Delete the NAT gateway and release the Elastic IP as soon as you are done to avoid unexpected charges.
Resources you used:
- 1 VPC (TroubleshootVPC).
- 2 subnets (PublicSubnet, PrivateSubnet).
- 1 internet gateway (TroubleshootIGW).
- 1-2 route tables (PublicRouteTable, plus a private route table if you completed the Secret Mission).
- 1-2 security groups (WebServerSG, plus a private instance security group if you completed the Secret Mission).
- 1 custom Network ACL (created during troubleshooting).
- 1-2 EC2 instances (WebServer, plus a private instance if you completed the Secret Mission).
- 1 CloudWatch alarm (WebServer-CPU-High).
- Secret Mission only: 1 NAT gateway and 1 Elastic IP.
Keep everything running
No action needed. Choose this if you are still actively exploring your VPC or plan to continue experimenting soon.
- The t3.micro instance is covered by Free Tier, so compute charges stay minimal.
- If you completed the Secret Mission, delete the NAT gateway and release the Elastic IP now. These are not covered by Free Tier.
- All other VPC resources (subnets, route tables, security groups, internet gateway) have no hourly cost.
Pause - I'll come back to this later
Stop your EC2 instances to eliminate compute charges while keeping all your VPC networking in place.
- Stop your WebServer instance by running:
aws ec2 stop-instances --instance-ids [[INSTANCE_ID="your WebServer instance ID"]]
- If you completed the Secret Mission, stop the private instance as well using the same command with its instance ID.
- Delete the NAT gateway and release the Elastic IP if you created them. These incur charges even when instances are stopped.
- Stopped instances do not incur compute charges. The EBS volumes remain but are covered by Free Tier.
- When you are ready to resume, run:
aws ec2 start-instances --instance-ids [[INSTANCE_ID="your WebServer instance ID"]]
Your public IP will change
When you stop and restart an EC2 instance, it gets a new public IP address. You will need to look up the new IP with describe-instances before you can access Nginx again.
Delete - I don't want to use this again
Remove all project resources completely. Follow this order to avoid dependency errors.
- Terminate all EC2 instances by running:
aws ec2 terminate-instances --instance-ids [[INSTANCE_ID="your WebServer instance ID"]]
- If you completed the Secret Mission, terminate the private instance as well.
- Wait for instances to reach the terminated state before proceeding. You can check with:
aws ec2 describe-instances --instance-ids [[INSTANCE_ID="your WebServer instance ID"]] --query "Reservations[*].Instances[*].State.Name" --output text
- If you created a NAT gateway (Secret Mission), delete it and release the Elastic IP by running:
aws ec2 delete-nat-gateway --nat-gateway-id [[NAT_GW_ID="your NAT gateway ID"]]
aws ec2 release-address --allocation-id [[EIP_ALLOC_ID="your Elastic IP allocation ID"]]
- Delete the CloudWatch alarm by running:
aws cloudwatch delete-alarms --alarm-names WebServer-CPU-High
- Reassociate any subnets using custom NACLs back to the default NACL, then delete the custom NACLs by running:
aws ec2 delete-network-acl --network-acl-id [[NACL_ID="your custom NACL ID"]]
- Delete non-default security groups by running:
aws ec2 delete-security-group --group-id [[SG_ID="your WebServerSG security group ID"]]
- Disassociate and delete the route tables. You cannot delete the main route table, only custom ones.
aws ec2 disassociate-route-table --association-id [[RTB_ASSOC_ID="your route table association ID"]]
aws ec2 delete-route-table --route-table-id [[RTB_ID="your PublicRouteTable route table ID"]]
- Detach and delete the internet gateway by running:
aws ec2 detach-internet-gateway --internet-gateway-id [[IGW_ID="your TroubleshootIGW internet gateway ID"]] --vpc-id [[VPC_ID="your TroubleshootVPC VPC ID"]]
aws ec2 delete-internet-gateway --internet-gateway-id [[IGW_ID="your TroubleshootIGW internet gateway ID"]]
- Delete both subnets by running:
aws ec2 delete-subnet --subnet-id [[PUBLIC_SUBNET_ID="your PublicSubnet subnet ID"]]
aws ec2 delete-subnet --subnet-id [[PRIVATE_SUBNET_ID="your PrivateSubnet subnet ID"]]
- Finally, delete the VPC by running:
aws ec2 delete-vpc --vpc-id [[VPC_ID="your TroubleshootVPC VPC ID"]]
- Verify the VPC is gone by running:
aws ec2 describe-vpcs --query "Vpcs[*].[VpcId,Tags[?Key=='Name'].Value|[0]]" --output table
You should only see your default VPC remaining. TroubleshootVPC should no longer appear in the list.
Mission accomplished
That's a Wrap!
Nice work! You just built a VPC from scratch, deployed a monitored web server, and troubleshot three real-world network failures using the AWS CLI.
You've learned how to:
- Built a custom VPC with public and private subnets, an internet gateway, route tables, and security groups entirely from the command line.
- Launched an EC2 instance running Nginx and set up a CloudWatch alarm to monitor CPU utilization.
- Diagnosed and fixed three network failures across routing, NACLs, and security groups. Discovered the critical difference between stateful and stateless firewalls firsthand.
- Secret Mission: Deployed a private subnet instance with a NAT gateway for outbound internet access and verified it cannot be reached from outside.
Ready to quiz yourself?