DEV Community

Cover image for Troubleshooting a Broken EC2 Launch Script (LAMP Stack Edition) 🐛🔧
Hashir Saud Khan
Hashir Saud Khan

Posted on

Troubleshooting a Broken EC2 Launch Script (LAMP Stack Edition) 🐛🔧

Troubleshooting the Creation of an EC2 Instance

INTRO
This one's less about learning a new AWS service and more about a skill every cloud engineer eventually needs: reading someone else's script, figuring out why it's failing, and fixing it without rewriting the whole thing. You're handed a shell script that's supposed to launch a full LAMP stack instance (Linux, Apache, MariaDB, PHP) hosting a Café web app — except the script has two bugs baked into it on purpose. Your job is to find them the way you actually would in real life: by reading error messages, checking open ports, and following a log file.

WHAT THE SCRIPT IS SUPPOSED TO DO
Before touching anything broken, it's worth understanding what a working run of this script does:

  1. Loops through every AWS region looking for a VPC named Cafe VPC
  2. Once found, looks up the subnet, an existing key pair, and the latest Amazon Linux AMI ID
  3. Cleans up any leftover instance or security group from a previous run
  4. Creates a new security group
  5. Launches the EC2 instance, attaching a user data script that installs Apache, MariaDB, and PHP, then deploys the actual website files
  6. Waits for a public IP and prints it out

LAMP STACK

TASK 1: CONNECT AND SET UP
You connect to a CLI host instance using EC2 Instance Connect, then configure the AWS CLI with your access key, secret key, and region — same as any other CLI-based lab.

Why we did this: Nothing else in this lab works without a properly authenticated CLI session. This is the one step that has to be boring and correct before anything interesting happens.

TASK 2: READ THE SCRIPT BEFORE YOU RUN IT
Before running anything, you make a backup of the script and read through it in a text editor.

Why we did this: This is the habit that actually matters here. If you run a broken script blind and it fails, you're troubleshooting with zero context. If you read it first, you already have a mental model of what should happen, so when it breaks, you know roughly where to look instead of starting from scratch.

TASK 3: RUN IT — AND WATCH IT FAIL
You run the script, and it fails partway through with this error:

An error occurred (InvalidAMIID.NotFound) when calling the RunInstances operation:
The image id '[ami-xxxxxxxxxx]' does not exist
Enter fullscreen mode Exit fullscreen mode

BUG #1: THE HARDCODED REGION
Here's the part of the script that creates the instance:

instanceDetails=$(aws ec2 run-instances \
--image-id $imageId \
--count 1 \
--instance-type $instanceType \
--region us-east-1 \
--subnet-id $subnetId \
--security-group-ids $securityGroup \
...
Enter fullscreen mode Exit fullscreen mode

See it? Earlier in the script, it dynamically finds the correct region by searching every region for the Cafe VPC and stores it in a variable called $region. But then, right here in the run-instances command, it ignores that variable completely and hardcodes --region us-east-1.

AMI IDs are region-specific — the exact same AMI does not exist under the same ID in every region. So the script looked up a perfectly valid AMI ID in, say, us-west-2, and then tried to launch it in us-east-1, where that ID means nothing. That's exactly why AWS says the image doesn't exist — from that region's point of view, it genuinely doesn't.

The fix: swap the hardcoded value for the variable that was already found earlier in the script:

--region $region \
Enter fullscreen mode Exit fullscreen mode

Run the script again, and this time run-instances succeeds — you get back a public IP address.

Why this bug mattered: This is a classic copy-paste trap. Somewhere along the way, someone probably tested the script against us-east-1 directly, hardcoded it to move fast, and forgot to swap it back to the dynamic variable. It's a one-line bug, but it perfectly explains a very confusing-looking error.

TASK 3 CONTINUED: THE WEBSITE STILL DOESN'T LOAD
With the instance launched, you try opening http://<public-ip> in a browser. Nothing loads.

The instance is running, it has a public IP — so what's left? This is where you stop guessing and start checking, one layer at a time.

BUG #2: THE WRONG PORT IN THE SECURITY GROUP
Look at the part of the script that opens ports:

echo "Opening port 22 in the new security group"
aws ec2 authorize-security-group-ingress \
--group-id $securityGroup \
--protocol tcp \
--port 22 \
...

echo "Opening port 80 in the new security group"
aws ec2 authorize-security-group-ingress \
--group-id $securityGroup \
--protocol tcp \
--port 8080 \
...
Enter fullscreen mode Exit fullscreen mode

Read that carefully — the echo line says port 80, but the actual --port flag right below it says 8080. The message lies about what the command actually does. The security group only ever opens SSH (22) and 8080 — never 80, which is the port a web server actually needs for a plain http:// request.

How you'd actually catch this without reading the script: install nmap on the CLI host and scan the instance directly:

sudo yum install -y nmap
nmap -Pn <public-ip>
Enter fullscreen mode Exit fullscreen mode

The scan shows you exactly which ports are open from the outside — and port 80 simply isn't one of them. That's your confirmation the security group, not the web server itself, is the problem.

The fix: change --port 8080 to --port 80 so it matches what the echo statement (and the actual website) needs.

Why this bug mattered: This is why you never fully trust comments or echoed log messages — they describe intent, not necessarily what the code underneath actually does. nmap sidesteps that entirely by checking the real, observable state of the network instead of what the script claims it configured.

CONFIRMING THE FIX WORKED
Reload http://<public-ip> — you should now see "Hello From Your Web Server!". That confirms Apache itself is up and reachable.

To confirm the rest of the user data script ran correctly (MariaDB, PHP, the actual Café app files), tail the cloud-init log on the instance:

sudo tail -f /var/log/cloud-init-output.log
Enter fullscreen mode Exit fullscreen mode

You're looking for clean installation messages and no errors — plus a line confirming the database setup script completed.

Why we did this: A web page loading doesn't automatically mean everything worked — it only confirms the web server layer. The cloud-init log is where you'd catch a silent failure further down the stack, like the database script erroring out even though Apache came up fine.

TASK 4: VERIFY THE ACTUAL WEBSITE WORKS
Last step — visit http://<public-ip>/cafe and confirm the real Café web app loads, not just the placeholder message. From there, add a couple of items to an order, submit it, and check the Order History page shows both orders — proof that the database (MariaDB) is actually storing data, not just installed and idle.

Why we did this: This is the real end-to-end check. Ports open, Apache running, and the app loading are all necessary, but placing an order and seeing it persist is the only thing that proves the full LAMP stack — web server, PHP, and database — is genuinely working together.

WHY THIS MATTERS OVERALL

  • A confusing AWS error often has a boring, specific cause — InvalidAMIID.NotFound sounds scary, but it almost always just means "you're in the wrong region for that ID"
  • Never trust a hardcoded value sitting next to a variable that already exists for that exact purpose — that mismatch is one of the most common sources of "it worked yesterday" bugs
  • echo statements and comments describe what a script author intended — they're not proof of what the code actually does. Always check the real flag, not the message next to it
  • Tools like nmap let you verify the actual state of a system from the outside, instead of trusting that your configuration commands did what you assumed they did
  • Success at one layer (the web server responding) doesn't guarantee success at every layer underneath it (the database) — always confirm the deepest thing that actually matters, which here was placing a real order

AWS #EC2 #CLI #Troubleshooting #LAMP #Nmap #CloudComputing

📜 Click to view the full create-lamp-instance-v2.sh script

#!/bin/bash
DATE=`date '+%Y-%m-%d %H:%M:%S'`
echo
echo "Running create-instance.sh on "$DATE
echo
# Hard coded values
instanceType="t3.small"
echo "Instance Type: "$instanceType
profile="default"
echo "Profile: "$profile
echo
echo "Looking up account values..."
# get vpcId
vpc=""
while [[ "$vpc" == "" ]] ; do
  for i in $(aws ec2 describe-regions | grep RegionName | cut -d '"' -f4) ; do
    region=$i;
    vpc=$(aws ec2 describe-vpcs --region $i --filters "Name=tag:Name,Values='Cafe VPC'" --profile $profile | grep VpcId | cut -d '"' -f4 | sed -n 1p );
    if [[ "$vpc" != "" ]]; then
        break;
    fi
  done
done
echo
echo "VPC: "$vpc
echo "Region: "$region
vpc=$(aws ec2 describe-vpcs \
--filters "Name=tag:Name,Values='Cafe VPC'" \
--region $region \
--profile $profile | grep VpcId | cut -d '"' -f4 | sed -n 1p)
echo "VPC: "$vpc
# get subnetId
subnetId=$(aws ec2 describe-subnets \
--filters "Name=tag:Name,Values='Cafe Public Subnet 1'" \
--region $region \
--profile $profile \
--query "Subnets[*]" | grep SubnetId | cut -d '"' -f4 | sed -n 1p)
echo "Subnet Id: "$subnetId
# Get keypair name
key=$(aws ec2 describe-key-pairs \
--profile $profile --region $region | grep KeyName | cut -d '"' -f4 )
echo "Key: "$key
# Get AMI ID
imageId=$(aws ssm get-parameters \
--names '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2' \
--profile $profile \
--region $region | grep ami- | cut -d '"' -f4 | sed -n 2p)
echo "AMI ID: "$imageId
#check for existing cafe instance
existingEc2Instance=$(aws ec2 describe-instances \
--region $region \
--profile $profile \
--filters "Name=tag:Name,Values=cafeserver" "Name=instance-state-name,Values=running" \
| grep InstanceId | cut -d '"' -f4)
if [[ "$existingEc2Instance" != "" ]]; then
  echo
  echo "WARNING: Found existing running EC2 instance with instance ID "$existingEc2Instance"."
  echo "This script will not succeed if it already exists. "
  echo "Would you like to delete it? [Y/N]"
  echo "&gt;&gt;"
  validResp=0
  while [ $validResp -eq 0 ];
  do
      read answer
      if [[ "$answer" == "Y" || "$answer" == "y" ]]; then
          echo
          echo "Deleting the existing instance..."
          aws ec2 terminate-instances --instance-ids $existingEc2Instance --region $region --profile $profile
          #wait for confirmation it was terminated
          aws ec2 wait instance-terminated --instance-ids $existingEc2Instance --region $region --profile $profile
          validResp="1"
      elif [[ "$answer" == "N" || "$answer" == "n" ]]; then
          echo "Ok, exiting."
          exit 1
      else
          echo "Please reply with Y or N."
      fi
  done
  sleep 10 #give it 10 seconds before trying to delete the SG this instance used.
fi
#check for existing cafeSG security Group
existingMpSg=$(aws ec2 describe-security-groups \
--region $region \
--query "SecurityGroups[?contains(GroupName, 'cafeSG')]" \
--profile $profile | grep GroupId | cut -d '"' -f4)
if [[ "$existingMpSg" != "" ]]; then
  echo
  echo "WARNING: Found existing security group with name "$existingMpSg"."
  echo "This script will not succeed if it already exists. "
  echo "Would you like to delete it? [Y/N]"
  echo "&gt;&gt;"
  validResp=0
  while [ $validResp -eq 0 ];
  do
      read answer
      if [[ "$answer" == "Y" || "$answer" == "y" ]]; then
          echo
          echo "Deleting the existing security group..."
          aws ec2 delete-security-group --group-id $existingMpSg --region $region --profile $profile
          validResp="1"
      elif [[ "$answer" == "N" || "$answer" == "n" ]]; then
          echo "Ok, exiting."
          exit 1
      else
          echo "Please reply with Y or N."
      fi
  done
  sleep 10 #give it 10 seconds before trying to recreate the SG
fi
# CREATE a security group and capture the name of it
echo
echo "Creating a new security group..."
securityGroup=$(aws ec2 create-security-group --group-name "cafeSG" \
--description "cafeSG" \
--region $region \
--group-name "cafeSG" \
--vpc-id $vpc --profile $profile | grep GroupId | cut -d '"' -f4 )
echo "Security Group: "$securityGroup
# Open ports in the security group
echo
echo "Opening port 22 in the new security group"
aws ec2 authorize-security-group-ingress \
--group-id $securityGroup \
--protocol tcp \
--port 22 \
--cidr 0.0.0.0/0 \
--region $region \
--profile $profile
echo "Opening port 80 in the new security group"
aws ec2 authorize-security-group-ingress \
--group-id $securityGroup \
--protocol tcp \
--port 8080 \
--cidr 0.0.0.0/0 \
--region $region \
--profile $profile
echo
echo "Creating an EC2 instance in "$region
instanceDetails=$(aws ec2 run-instances \
--image-id $imageId \
--count 1 \
--instance-type $instanceType \
--region us-east-1 \
--subnet-id $subnetId \
--security-group-ids $securityGroup \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=cafeserver}]' \
--associate-public-ip-address \
--iam-instance-profile Name=LabInstanceProfile \
--profile $profile \
--user-data file://create-lamp-instance-userdata-v2.txt \
--key-name $key )
#if the create instance command failed, exit this script
if [[ "$?" -ne "0" ]]; then
  exit 1
fi
echo
echo "Instance Details...."
echo $instanceDetails | python -m json.tool
# Extract instanceId
instanceId=$(echo $instanceDetails | python -m json.tool | grep InstanceId | sed -n 1p | cut -d '"' -f4)
echo "instanceId="$instanceId
echo
echo "Waiting for a public IP for the new instance..."
pubIp=""
while [[ "$pubIp" == "" ]]; do
  sleep 10;
  pubIp=$(aws ec2 describe-instances --instance-id $instanceId --region $region --profile $profile | grep PublicIp | sed -n 1p | cut -d '"' -f4)
done
echo
echo "The public IP of your LAMP instance is: "$pubIp
echo
echo "Download the Key Pair from the lab console."
echo
echo "Then connect using this command (with .pem or .ppk added to the end of the keypair name):"
echo "ssh -i path-to/"$key" ec2-user@"$pubIp
echo
echo "The website should also become available at"
echo "http://"$pubIp"/cafe/"
echo
DATE=`date '+%Y-%m-%d %H:%M:%S'`
echo
echo "Done running create-instance.sh at "$DATE
echo
Enter fullscreen mode Exit fullscreen mode

Top comments (0)