Load-Balanced VPC with the AWS CLI

Build a custom VPC with two EC2 web servers behind an Application Load Balancer.

Introduction

30 Second Summary

Every time you visit a popular website, your request could land on any one of dozens of servers behind the scenes. A load balancer decides which server gets each request, keeping the site fast and online even when individual servers fail.

In this project, you will build a load-balanced web architecture on AWS entirely from the command line. You will create a custom network, launch two web servers in different data centers, and place a load balancer in front of them so one URL distributes traffic across both.

What You'll Build

Open your load balancer's URL in a browser, hit refresh a few times, and watch the page alternate between "Hello from Instance 1" and "Hello from Instance 2" as traffic bounces between two servers in different data centers.

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

  • Your own isolated network on AWS (a custom VPC) with internet-connected servers, built entirely from the command line.
  • Two EC2 web servers running in different Availability Zones, each displaying which server handled your request.
  • An Application Load Balancer that gives you a single URL to reach both servers, automatically distributing every request between them.
  • Secret Mission: Simulate a server failure and watch the ALB automatically reroute all traffic to the remaining healthy instance.

Are there any prerequisites?

No prior AWS experience is needed. This guide walks you through everything from creating an AWS account to building every resource, and the project is Free Tier eligible so it costs nothing to complete.

Before We Start

Before diving into the terminal, let's lock in what you're about to build and why it matters.

Set Up the AWS CLI

Every command in this project talks to AWS through the AWS CLI. Without a configured CLI and valid credentials, you cannot create a single cloud resource.

In this step, you will create an AWS account, set up an IAM user with the right permissions, and install and configure the AWS CLI on your Linux terminal.

In this step, get ready to:
  • Create an AWS account and IAM user with programmatic access.
  • Install AWS CLI v2 on your Linux terminal.
  • Configure your credentials and verify the setup works.
Create an AWS account and IAM user

Every AWS resource you build in this project lives inside an AWS account. You also need an IAM user with its own access keys so the CLI can authenticate on your behalf.

  • Open https://aws.amazon.com in your browser.
  • Click Create an AWS Account and follow the prompts to sign up.
  • If you already have an AWS account, click Sign In instead.

What is IAM?

IAM (Identity and Access Management) is the AWS service that controls who can do what in your account. Instead of using your root account for everything, you create an IAM user with only the permissions it needs.

For this project, you will attach the AdministratorAccess policy so the CLI can create VPCs, EC2 instances, and load balancers on your behalf.

Once you are signed in to the AWS Management Console, create an IAM user:

  • Type IAM into the search bar at the top of the console.
  • Select IAM from the search results.
  • In the left navigation pane, click Users.
  • Click Create user.
  • For User name, enter workshop-admin.
  • Click Next.

Now attach the permissions policy:

  • On the Set permissions page, select Attach policies directly.
  • In the search box under Permissions policies, type AdministratorAccess.
  • Select the checkbox next to the AdministratorAccess policy.
  • Click Next.
  • Review the details, then click Create user.

Your user is created. Now generate access keys so the CLI can authenticate as this user:

  • Click on your new user's name (workshop-admin) to open its details.
  • Click the Security credentials tab.
  • Scroll down to the Access keys section and click Create access key.
  • On the Access key best practices & alternatives page, select Other.
  • Click Next.
  • Click Create access key.

You will see your Access Key ID and Secret Access Key on the Retrieve access keys page. Save both values somewhere safe right now.

Save your credentials securely

This is the only time the Secret Access Key is displayed. If you lose it, you will need to delete this key and create a new one. Copy both values to a secure location before clicking Done.

Never share your access keys or commit them to version control. Treat them like a password.

  • After saving both values, click Done.
Install AWS CLI v2

The AWS CLI is the command-line tool that lets you create and manage AWS resources from your terminal. This project requires version 2.

  • Check whether the AWS CLI is already installed by running this command:
aws --version

✔️ I see aws-cli/2.x.x

Your output starts with something like aws-cli/2.27.41. AWS CLI v2 is already installed and ready to go. Skip ahead to the next substep.

ⓧ I see aws-cli/1.x.x

You have an older version of the AWS CLI. Version 1 is missing features this project needs. Install version 2 alongside it by running:

  • Install AWS CLI v2 by running this command:
curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash

What does this command do?

This downloads and runs the official AWS install script. It installs the CLI to $HOME/.local/share/aws-cli and creates a symlink in $HOME/.local/bin.

  • Close and reopen your terminal, then verify the new version by running:
aws --version

You should now see output starting with aws-cli/2.

ⓧ Command not found

The AWS CLI is not installed yet. Install it now.

  • Install AWS CLI v2 by running this command:
curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash

What does this command do?

This downloads and runs the official AWS install script. It installs the CLI to $HOME/.local/share/aws-cli and creates a symlink in $HOME/.local/bin.

  • Close and reopen your terminal, then verify the installation by running:
aws --version

You should see output starting with aws-cli/2 followed by version details.

Still seeing command not found?

The install script places the aws binary in $HOME/.local/bin. Make sure this directory is in your PATH. You can add it by running:

export PATH=$HOME/.local/bin:$PATH

Then run aws --version again. To make this permanent, add the export line to your ~/.bashrc or ~/.profile file.

If you are still stuck, help me troubleshoot the AWS CLI installation.

Configure your credentials and default region

The CLI is installed, but it does not know who you are yet. You need to give it the access keys you saved earlier so it can authenticate your requests to AWS.

The aws configure command will ask you for four values, one at a time. Have your access keys ready.

  • Start the configuration wizard by running:
aws configure
  • When prompted for AWS Access Key ID, paste your Access Key ID and press Enter.
  • When prompted for AWS Secret Access Key, paste your Secret Access Key and press Enter.
  • When prompted for Default region name, type us-east-1 and press Enter.
  • When prompted for Default output format, type json and press Enter.

What does aws configure do?

This command saves your credentials and preferences to ~/.aws/credentials and ~/.aws/config. Every AWS CLI command you run from now on reads these files to authenticate and to know which region to target.

Setting the region to us-east-1 means all resources you create will live in the US East (N. Virginia) region by default.

Before you run the next command, think about what you expect to see. If your credentials are correct, what kind of identity information would the CLI report back?

  • Verify your configuration by running:
aws sts get-caller-identity

You should see a JSON response containing three fields: your Account ID, your User ARN, and your UserId. This confirms the CLI can authenticate with AWS using your credentials.

Seeing an error instead of JSON?

  • If you see a message about invalid credentials, double check that you pasted the correct Access Key ID and Secret Access Key. Run aws configure again to re-enter them.
  • If you see a message about an expired token, you may have accidentally set a session token. Run aws configure again and leave all fields at their current values. Then check that ~/.aws/credentials does not contain an aws_session_token line.
  • If something else went wrong, help me troubleshoot aws sts get-caller-identity.

Your CLI is authenticated and ready to build. Next up, you will create the VPC network that will hold your web servers.

Build the VPC Network

Your AWS CLI is configured and authenticated. Now you need somewhere to put your web servers. In AWS, that place is a VPC (Virtual Private Cloud), your own isolated section of the AWS cloud.

Without a VPC and subnets, there is nowhere to launch EC2 instances. In this step, you will create a custom VPC, carve out two subnets in different Availability Zones, and connect everything to the internet so your future web servers can serve traffic.

In this step, get ready to:
  • Create a VPC with a 10.0.0.0/16 address range.
  • Add two public subnets in separate Availability Zones.
  • Connect the VPC to the internet with an Internet Gateway and route table.
Create the VPC with two subnets

A VPC is a logically isolated virtual network that you define inside AWS. Think of it as your own private data centre in the cloud, where you control the IP address range, subnets, and routing.

  • Create your VPC by running this command:
VPC_ID=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=workshop-vpc}]' --query 'Vpc.VpcId' --output text)
echo $VPC_ID

What does this command do?

  • aws ec2 create-vpc creates a new Virtual Private Cloud in your AWS account.
  • --cidr-block 10.0.0.0/16 defines the IP address range for your VPC. The /16 gives you over 65,000 available IP addresses.
  • --query 'Vpc.VpcId' --output text extracts just the VPC ID from the JSON response and saves it to the shell variable VPC_ID for use in later commands.

You should see a VPC ID starting with vpc- printed to your terminal. This confirms your VPC was created.

Don't see a VPC ID?

  • Make sure your AWS CLI is configured correctly by running aws sts get-caller-identity. If this returns an error, re-run aws configure with your credentials.
  • Check for typos in the --cidr-block and --tag-specifications flags. The single quotes around the tag specification are required.

Help me troubleshoot my VPC creation error.

  • Create your first subnet in the us-east-1a Availability Zone by running:
SUBNET_1_ID=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.1.0/24 --availability-zone us-east-1a --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=workshop-subnet-1}]' --query 'Subnet.SubnetId' --output text)
echo $SUBNET_1_ID

You should see a subnet ID starting with subnet- printed to your terminal.

  • Create the second subnet in us-east-1b by running:
SUBNET_2_ID=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.2.0/24 --availability-zone us-east-1b --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=workshop-subnet-2}]' --query 'Subnet.SubnetId' --output text)
echo $SUBNET_2_ID

You should see a second subnet ID printed. You now have two subnets in two different Availability Zones.

Why two different Availability Zones?

Availability Zones are physically separate data centres within an AWS region. By placing one subnet in us-east-1a and another in us-east-1b, your architecture survives a failure in either data centre.

The Application Load Balancer you build later also requires subnets in at least two different Availability Zones.

  • Enable auto-assign public IPs on both subnets so that any instance launched in them automatically gets a public IP address:
aws ec2 modify-subnet-attribute --subnet-id $SUBNET_1_ID --map-public-ip-on-launch
aws ec2 modify-subnet-attribute --subnet-id $SUBNET_2_ID --map-public-ip-on-launch

Why enable auto-assign public IPs?

By default, instances launched in a custom VPC subnet do not get a public IP address. Enabling --map-public-ip-on-launch ensures your web servers will be reachable from the internet without needing to assign IPs manually each time.

This command produces no output on success. No news is good news.

Create and attach an Internet Gateway

Your VPC is an isolated network. To let traffic flow between your VPC and the public internet, you need an Internet Gateway.

  • Create the Internet Gateway by running:
IGW_ID=$(aws ec2 create-internet-gateway --tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=workshop-igw}]' --query 'InternetGateway.InternetGatewayId' --output text)
echo $IGW_ID

You should see an Internet Gateway ID starting with igw- printed to your terminal.

  • Attach the Internet Gateway to your VPC by running:
aws ec2 attach-internet-gateway --internet-gateway-id $IGW_ID --vpc-id $VPC_ID

What is an Internet Gateway?

An Internet Gateway is a managed AWS component that connects your VPC to the public internet. Without it, nothing inside your VPC can send or receive traffic from outside.

Creating the gateway and attaching it to the VPC are two separate steps. The attach command produces no output on success.

Seeing an error on the attach command?

  • If you see a message about the gateway not being found, make sure $IGW_ID is set by running echo $IGW_ID. If it is empty, re-run the create-internet-gateway command above.
  • If you see a message about the VPC not being found, check $VPC_ID the same way. All shell variables are lost if you close your terminal.

Help me fix my Internet Gateway attach error.

Keep your terminal session alive

Every ID you have saved so far ($VPC_ID, $SUBNET_1_ID, $SUBNET_2_ID, $IGW_ID) lives in your current terminal session. If you close the terminal, you will need to look up these IDs in the AWS console and re-set them manually.

Create a route table and associate it with both subnets

The Internet Gateway is attached to your VPC, but your subnets do not know to send internet-bound traffic through it yet. A route table contains the rules that direct network traffic from your subnets to the right destination.

  • Create the route table by running:
RT_ID=$(aws ec2 create-route-table --vpc-id $VPC_ID --tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=workshop-rt}]' --query 'RouteTable.RouteTableId' --output text)
echo $RT_ID

You should see a route table ID starting with rtb- printed to your terminal.

  • Add a route that sends all internet-bound traffic (0.0.0.0/0) to your Internet Gateway by running:
aws ec2 create-route --route-table-id $RT_ID --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW_ID

What does 0.0.0.0/0 mean?

The destination 0.0.0.0/0 is a catch-all that matches any IP address not covered by a more specific route. It tells the route table: "for any traffic headed to the internet, send it through the Internet Gateway."

This command produces no output on success.

  • Associate the route table with your first subnet by running:
aws ec2 associate-route-table --route-table-id $RT_ID --subnet-id $SUBNET_1_ID

You should see an AssociationId in the response. This confirms the route table is linked to the subnet.

  • Associate the route table with your second subnet by running:
aws ec2 associate-route-table --route-table-id $RT_ID --subnet-id $SUBNET_2_ID

You should see a second AssociationId confirming the link.

Seeing a 'not found' error?

  • Run echo $RT_ID and echo $SUBNET_1_ID to confirm your shell variables are still set. If any are empty, your terminal session may have reset.
  • Make sure you are using the same terminal window you used for all previous commands in this step.

Help me fix my route table association error.

Before you run the verification command below, what do you expect to see? Think about how many subnets you created and which Availability Zones you put them in.

  • Verify your entire network setup by running:
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" --query "Subnets[*].[SubnetId,AvailabilityZone,CidrBlock]" --output table

You should see a table with two rows. One subnet in us-east-1a with CIDR 10.0.1.0/24, and another in us-east-1b with CIDR 10.0.2.0/24.

Your network foundation is in place. You have a VPC, two subnets in separate Availability Zones, an Internet Gateway, and a route table connecting everything to the internet. Next up, you will launch two web servers into these subnets.

Launch Two Web Servers

Your VPC network is ready. Two public subnets stretch across two Availability Zones, wired to the internet through an Internet Gateway and route table. But a network without servers is just empty infrastructure.

In this step, you'll launch two EC2 web servers, one in each subnet. Each server runs a simple web page that identifies which instance is responding. By the end, you'll be able to reach each server individually, but you'll also see the problem: two separate IP addresses to manage.

In this step, get ready to:
  • Create a security group to allow HTTP traffic to your instances.
  • Launch two EC2 instances with user-data startup scripts that install a web server.
  • Test each server individually by accessing its public IP address.
Create a security group for the web servers

Before launching instances, you need a Security Group that controls which traffic can reach them. Your web servers need to accept HTTP requests on port 80.

  • Create the security group inside your VPC by running this command:
aws ec2 create-security-group --group-name workshop-web-sg --description "Allow HTTP traffic" --vpc-id $VPC_ID

The output includes a GroupId value. It looks like sg-0abc1234def56789.

  • Copy the GroupId value and save it as a shell variable by running:
export WEB_SG_ID=[[WEB_SG_ID="your GroupId from the output above"]]
  • Allow inbound HTTP traffic on port 80 by running:
aws ec2 authorize-security-group-ingress --group-id $WEB_SG_ID --protocol tcp --port 80 --cidr 0.0.0.0/0

You should see a JSON response confirming the new inbound rule with Return set to true.

What is a security group?

A security group acts as a virtual firewall for your EC2 instances. By default, a new security group blocks all inbound traffic.

The authorize-security-group-ingress command opens TCP port 80 (HTTP) to all IP addresses (0.0.0.0/0). Without this rule, nobody could reach the web servers you're about to launch.

Seeing an error about the security group?

If you see InvalidGroup.NotFound when running the ingress command, the EC2 API may not have finished propagating the new group. Wait a few seconds and try the authorize-security-group-ingress command again.

If you see an error about the VPC not existing, confirm your $VPC_ID variable is still set by running echo $VPC_ID. If it is empty, re-export it with the VPC ID from Step 2.

Help me troubleshoot my security group creation

Create user-data scripts and launch both instances

EC2 instances can run a startup script called user data the first time they boot. You'll use this to automatically install a web server and create a simple web page on each instance.

  • Create the first user-data script by running this command:
cat > userdata1.sh << 'EOF'
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
EC2_AVAIL_ZONE=$(TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") && curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/availability-zone)
echo "<h1>Hello from Instance 1 in $EC2_AVAIL_ZONE</h1>" > /var/www/html/index.html
EOF
  • Create the second user-data script by running this command:
cat > userdata2.sh << 'EOF'
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
EC2_AVAIL_ZONE=$(TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") && curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/availability-zone)
echo "<h1>Hello from Instance 2 in $EC2_AVAIL_ZONE</h1>" > /var/www/html/index.html
EOF
  • Confirm both files were created by running:
ls userdata*.sh

You should see userdata1.sh and userdata2.sh in the output.

What do these scripts do?

  • yum update -y and yum install -y httpd install the Apache web server on the Amazon Linux 2023 instance.
  • systemctl start httpd and systemctl enable httpd start Apache immediately and ensure it restarts after every reboot.
  • The EC2_AVAIL_ZONE line fetches the instance's Availability Zone from EC2 instance metadata using IMDSv2 (the token-based approach AWS recommends).
  • The echo line creates a web page that identifies which instance is responding and which zone it sits in.
  • Launch Instance 1 in Subnet 1 by running this command:
aws ec2 run-instances --image-id resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 --instance-type t3.micro --subnet-id $SUBNET_1_ID --security-group-ids $WEB_SG_ID --user-data file://userdata1.sh --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=workshop-instance-1}]'

The output is a large JSON block describing the new instance. Look for the InstanceId field near the top.

  • Copy the InstanceId value and save it by running:
export INSTANCE_1_ID=[[INSTANCE_1_ID="your Instance 1 InstanceId from the output above"]]
  • Launch Instance 2 in Subnet 2 by running this command:
aws ec2 run-instances --image-id resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 --instance-type t3.micro --subnet-id $SUBNET_2_ID --security-group-ids $WEB_SG_ID --user-data file://userdata2.sh --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=workshop-instance-2}]'
  • Copy the InstanceId from this output and save it by running:
export INSTANCE_2_ID=[[INSTANCE_2_ID="your Instance 2 InstanceId from the output above"]]

What does the run-instances command do?

  • resolve:ssm: automatically fetches the latest Amazon Linux 2023 AMI ID from AWS Systems Manager Parameter Store. This avoids hardcoding an AMI ID that might change across regions.
  • --instance-type t3.micro selects a small, Free Tier eligible instance size.
  • --user-data file://userdata1.sh tells EC2 to run your startup script on first boot.
  • Each instance is placed in a different subnet, so they end up in different Availability Zones.
  • Wait for both instances to reach the running state by running:
aws ec2 wait instance-running --instance-ids $INSTANCE_1_ID $INSTANCE_2_ID

This command polls AWS every 15 seconds and produces no output. When it returns to your prompt, both instances are running.

Command seems to hang?

The wait command can take 1-2 minutes while the instances boot. If it takes longer than a few minutes, check that your $INSTANCE_1_ID and $INSTANCE_2_ID variables are set correctly by running echo $INSTANCE_1_ID $INSTANCE_2_ID.

Help me troubleshoot my EC2 instance launch

Test each instance individually

Both instances are running, but their user-data scripts also needed time to install Apache and write the web page. Let's grab the public IPs and test them.

  • Get the public IP addresses for both instances by running:
aws ec2 describe-instances --instance-ids $INSTANCE_1_ID $INSTANCE_2_ID --query "Reservations[*].Instances[*].[InstanceId,PublicIpAddress,Placement.AvailabilityZone]" --output table

You should see a table with two rows. Each row shows an instance ID, its public IP, and which Availability Zone it is in.

  • Save each public IP as a shell variable by running these commands:
export PUBLIC_IP_1=[[PUBLIC_IP_1="Instance 1 public IP from the table"]]
export PUBLIC_IP_2=[[PUBLIC_IP_2="Instance 2 public IP from the table"]]

Before you test the servers, take a moment to predict: do you think both instances will return the same page, or different pages?

  • Test Instance 1 by running:
curl http://$PUBLIC_IP_1
  • Test Instance 2 by running:
curl http://$PUBLIC_IP_2

Instance 1 returns Hello from Instance 1 in us-east-1a and Instance 2 returns Hello from Instance 2 in us-east-1b. Each page identifies the server and the zone it lives in.

Seeing 'connection refused' or no response?

The user-data script takes a minute or two to install Apache after the instance reaches the running state. Wait 60 seconds and try the curl commands again.

If it still fails, confirm the security group allows port 80 by running:

aws ec2 describe-security-groups --group-ids $WEB_SG_ID --query "SecurityGroups[*].IpPermissions" --output table

You should see a rule for TCP port 80 with source 0.0.0.0/0. If the rule is missing, re-run the authorize-security-group-ingress command from the first substep.

Help me troubleshoot my web server connection

Both servers are live, and each one returns a different page. But look at what you're doing: juggling two separate IP addresses. If Instance 1 goes down, anyone using that IP gets nothing. There's no single address that covers both servers.

Your two web servers are running, but they're hiding behind separate IP addresses that nobody wants to manage. In the next step, you'll put an Application Load Balancer in front that gives you a single URL for both.

Distribute Traffic with an Application Load Balancer

Your two web servers are running and each one serves a unique page at its own public IP address. You proved in the last step that both instances respond correctly.

The problem is that managing separate IP addresses does not scale. If one server goes down, anyone using that IP loses access entirely. An Application Load Balancer solves this by giving you a single DNS endpoint that automatically distributes traffic across both servers and stops sending requests to unhealthy instances.

In this step, get ready to:
  • Create a security group and Application Load Balancer for your VPC.
  • Create a target group and register both EC2 instances.
  • Create an HTTP listener and verify load balancing is working.
Create the ALB and its security group

The ALB needs its own security group to control which traffic can reach it. This is separate from the security group your web servers use.

  • Create a new security group for the ALB by running this command:
aws ec2 create-security-group --group-name workshop-alb-sg --description "Allow HTTP to ALB" --vpc-id $VPC_ID

Why a separate security group?

Each layer in your architecture should have its own security group. The ALB's group controls who can send traffic to the load balancer. The web servers' group controls who can reach the servers directly. Keeping them separate lets you tighten rules independently.

  • Find the GroupId value in the output and save it by running:
ALB_SG_ID=[[ALB_SG_ID="the GroupId from the output above"]]
  • Allow inbound HTTP traffic to the ALB by running:
aws ec2 authorize-security-group-ingress --group-id $ALB_SG_ID --protocol tcp --port 80 --cidr 0.0.0.0/0

You should see a return value confirming the ingress rule was added.

  • Create the Application Load Balancer spanning both of your subnets by running:
aws elbv2 create-load-balancer --name workshop-alb --subnets $SUBNET_1_ID $SUBNET_2_ID --security-groups $ALB_SG_ID

What does this command do?

This creates an internet-facing ALB named workshop-alb and places it across both of your subnets in us-east-1a and us-east-1b. The ALB requires subnets in at least two different Availability Zones, which is exactly how you set up your network in Step 2.

  • Find the LoadBalancerArn and DNSName values in the output. Save both by running these commands:
ALB_ARN=[[ALB_ARN="the LoadBalancerArn value from the output above"]]
ALB_DNS=[[ALB_DNS="the DNSName value from the output above"]]

ALB creation failed?

Make sure $VPC_ID, $SUBNET_1_ID, $SUBNET_2_ID, and $ALB_SG_ID are all set. Run echo $VPC_ID for each variable to confirm they contain values. If any are empty, go back and re-run the command that created that resource and save the ID again.

Help me troubleshoot my ALB creation error

Create a target group and register your instances

A target group tells the ALB where to send incoming traffic. You register your EC2 instances as targets so the ALB knows which servers are available.

  • Create the target group by running:
aws elbv2 create-target-group --name workshop-targets --protocol HTTP --port 80 --vpc-id $VPC_ID --target-type instance

What is a target group?

A target group is a collection of servers (targets) that receive traffic from the ALB. The ALB checks the health of each target and only sends requests to healthy ones. You can add or remove instances from a target group without changing the ALB itself.

  • Find the TargetGroupArn in the output and save it by running:
TG_ARN=[[TG_ARN="the TargetGroupArn value from the output above"]]
  • Register both EC2 instances in the target group by running:
aws elbv2 register-targets --target-group-arn $TG_ARN --targets Id=$INSTANCE_1_ID Id=$INSTANCE_2_ID
  • Verify both instances are registered by checking their health status:
aws elbv2 describe-target-health --target-group-arn $TG_ARN

You should see both instances listed. Their state will show as initial because the ALB has not started health-checking them yet. That is expected at this point.

Not seeing both instances?

Confirm that $INSTANCE_1_ID and $INSTANCE_2_ID are set by running echo $INSTANCE_1_ID $INSTANCE_2_ID. If either is empty, retrieve your instance IDs from the previous step.

Help me troubleshoot my target registration

Create a listener and test the ALB

A listener is the piece that connects incoming requests to your target group. It tells the ALB to listen on a specific port and forward matching traffic to the targets you registered.

  • Create an HTTP listener on port 80 that forwards traffic to your target group by running:
aws elbv2 create-listener --load-balancer-arn $ALB_ARN --protocol HTTP --port 80 --default-actions Type=forward,TargetGroupArn=$TG_ARN

What does this listener do?

This listener watches for HTTP traffic on port 80 and forwards every request to workshop-targets. The ALB then picks a healthy instance from the target group to handle each request.

  • The ALB takes a few minutes to finish provisioning. Wait for it to become active by running:
aws elbv2 wait load-balancer-available --load-balancer-arns $ALB_ARN

This command sits silently until the ALB reaches an active state. It may take 2-3 minutes.

Before you run the next command, think about this: you have a single ALB DNS name sitting in front of two different web servers. What do you expect to see when you send the same HTTP request to it multiple times?

  • Test the ALB by running this command:
curl http://$ALB_DNS
  • Run the same command three more times and watch the response change each time.

The response alternates between "Hello from Instance 1 in us-east-1a" and "Hello from Instance 2 in us-east-1b". The ALB is distributing your requests across both servers through a single endpoint. No more juggling separate IP addresses.

Getting an error instead of a greeting?

If you see an HTTP 503 response, the ALB's health checks have not finished yet. Wait 30 seconds and try again. Health checks need a few successful rounds before the ALB marks a target as healthy.

If the connection times out or is refused, confirm your ALB DNS name is correct by running echo $ALB_DNS. Also confirm the ALB is active by running aws elbv2 wait load-balancer-available --load-balancer-arns $ALB_ARN again.

Help me troubleshoot why my ALB is not returning responses

Secret mission

Simulate a Server Failure

What happens when one of your servers goes down? Stop an EC2 instance to simulate a crash and watch the ALB's health checks detect the failure. All traffic automatically reroutes to the surviving instance, proving your architecture can handle real-world failures.

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. The EC2 instances and ALB in this project are covered under the AWS Free Tier, but they will incur charges if left running beyond the Free Tier limits.

Cost warning

The AWS Free Tier covers 750 hours per month of t3.micro EC2 usage and 750 hours per month of Application Load Balancer usage for 6 months from account creation.

If your Free Tier period has expired or you are approaching those limits, leaving these resources running will result in charges. Delete everything when you are finished to avoid unexpected costs.

Resources you used:

  • workshop-vpc (VPC with CIDR 10.0.0.0/16).
  • workshop-subnet-1 and workshop-subnet-2 (two public subnets in us-east-1a and us-east-1b).
  • workshop-igw (Internet Gateway attached to the VPC).
  • workshop-rt (route table associated with both subnets).
  • workshop-web-sg and workshop-alb-sg (two security groups allowing HTTP traffic).
  • workshop-instance-1 and workshop-instance-2 (two t3.micro EC2 instances).
  • workshop-alb (Application Load Balancer with HTTP listener and workshop-targets target group).
  • userdata1.sh and userdata2.sh (local user-data scripts).

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with your load-balanced architecture.

  • Monitor your usage in the AWS Billing console to make sure you stay within Free Tier limits.
  • The local userdata1.sh and userdata2.sh scripts remain on your machine. They use negligible disk space.

Pause - I'll come back to this later

Stop the EC2 instances to avoid compute charges while keeping all your resources in place.

  • Stop both instances by running:
aws ec2 stop-instances --instance-ids $INSTANCE_1_ID $INSTANCE_2_ID

What stays running?

Stopping the instances eliminates EC2 compute charges, but the Application Load Balancer continues to run and count against your Free Tier hours. If your Free Tier period has expired, consider deleting the ALB as well.

  • When you are ready to resume, start the instances again by running:
aws ec2 start-instances --instance-ids $INSTANCE_1_ID $INSTANCE_2_ID

Delete - I don't want to use this again

Remove all project resources and start fresh. Run these commands in order. If your terminal session has ended since you built the project, you will need to set your resource ID variables again before running the commands below.

  • Delete the Application Load Balancer and wait for it to finish deleting:
aws elbv2 delete-load-balancer --load-balancer-arn $ALB_ARN
aws elbv2 wait load-balancers-deleted --load-balancer-arns $ALB_ARN
  • Delete the target group:
aws elbv2 delete-target-group --target-group-arn $TG_ARN
  • Terminate both EC2 instances and wait for them to finish terminating:
aws ec2 terminate-instances --instance-ids $INSTANCE_1_ID $INSTANCE_2_ID
aws ec2 wait instance-terminated --instance-ids $INSTANCE_1_ID $INSTANCE_2_ID
  • Delete both security groups (these cannot be deleted until the instances and ALB are fully gone):
aws ec2 delete-security-group --group-id $ALB_SG_ID
aws ec2 delete-security-group --group-id $WEB_SG_ID
  • Detach and delete the Internet Gateway:
aws ec2 detach-internet-gateway --internet-gateway-id $IGW_ID --vpc-id $VPC_ID
aws ec2 delete-internet-gateway --internet-gateway-id $IGW_ID
  • Delete both subnets, the route table, and the VPC:
aws ec2 delete-subnet --subnet-id $SUBNET_1_ID
aws ec2 delete-subnet --subnet-id $SUBNET_2_ID
aws ec2 delete-route-table --route-table-id $RT_ID
aws ec2 delete-vpc --vpc-id $VPC_ID
  • Remove the local user-data scripts:
rm userdata1.sh userdata2.sh

Getting a DependencyViolation error?

This means a resource you are trying to delete is still in use by another resource. The most common cause is trying to delete a security group before the ALB or EC2 instances have fully terminated.

Wait a minute and try the command again. You can check instance state by running aws ec2 describe-instances --instance-ids $INSTANCE_1_ID $INSTANCE_2_ID --query "Reservations[*].Instances[*].State.Name" --output text.

Help me troubleshoot a DependencyViolation error when deleting AWS resources.

Nice Work!

Nice Work!

You just built a production-style load-balanced architecture on AWS entirely from the command line. Your Application Load Balancer is distributing traffic across two web servers in different Availability Zones, all inside a custom VPC you designed yourself.

You've learned how to:

  • Build a custom VPC with two public subnets across separate Availability Zones, connected to the internet through an Internet Gateway and route table using the AWS CLI.
  • Launch EC2 instances running web servers configured with user-data startup scripts, and verify them individually by their public IPs.
  • Create an Application Load Balancer with a target group and HTTP listener that distributes traffic across both instances through a single DNS endpoint.
  • Secret Mission: Simulated a server failure and observed the ALB automatically reroute traffic to the healthy instance.

Ready to quiz yourself?