AWS re/Start Lab · Amazon EC2

Creating EC2 Instances — Console + AWS CLI

Launched a bastion host from the AWS Management Console and then, from inside that bastion via EC2 Instance Connect, used the AWS CLI to launch a second EC2 instance running an Apache web server with a UserData bootstrap script. The bastion's IAM instance profile is what lets the CLI call EC2 APIs without explicit credentials.

Lab Summary

End-to-end flow: launch a bastion from the Console → connect with EC2 Instance Connect → resolve AMI / Subnet / Security Group dynamically via the CLI → run aws ec2 run-instances with a UserData script that installs Apache and the dashboard app → poll until running → open the public DNS in a browser.

Architecture

Both instances live in the same Public Subnet of the Lab VPC. The bastion has the IAM role; the web server has no role because it only needs to serve HTTP.

Lab VPC · Public Subnet

  • Pre-provisioned by a CloudFormation template during lab setup.
  • Public subnet has auto-assign public IPv4 enabled.
  • Tag Name=Public Subnet — used later by the CLI filter.

Bastion host (Console-launched)

  • AMI: Amazon Linux 2 · Type: t3.micro
  • SG: Bastion security group — allows SSH (port 22).
  • IAM instance profile: Bastion-Role (lets CLI call EC2/SSM).
  • No key pair — access via EC2 Instance Connect.

Web Server (CLI-launched)

  • AMI: same Amazon Linux 2 (resolved dynamically via SSM Parameter Store).
  • SG: WebSecurityGroup — allows HTTP (port 80).
  • Bootstrapped by UserData.txt: httpd + dashboard-app.zip.
  • Tag Name=Web Server.

Data path

  • Browser → public DNS of Web Server (HTTP/80) → Apache → static dashboard files.
  • Operator → EC2 Instance Connect → bastion shell → AWS CLI → EC2 API.

Bastion: Console launch & connect

No screenshots for this phase — captured later, once already inside the bastion shell. Documented here as compact parameter list since the choices are what matter, not the clicks.

01

Launch Bastion host from the Console

  • Name: Bastion host
  • AMI: Amazon Linux 2 AMI (HVM) · Instance type: t3.micro
  • Key pair: Proceed without key pair — connection is via EC2 Instance Connect, not SSH keys.
  • Network: Lab VPC → Public Subnet · Auto-assign public IP: Enable
  • Security group: create new — Bastion security group (permit SSH/22).
  • Advanced > IAM instance profile: Bastion-Role.
Why the IAM role matters: the bastion calls aws ec2 … later. Without an instance profile, those calls would need static access keys baked into the shell — bad practice. The role lets the bastion sign requests via IMDS without ever holding long-lived credentials.
02

Connect with EC2 Instance Connect

  • Selected the bastion in the Console → ConnectEC2 Instance Connect tab → Connect.
  • Result: in-browser shell as ec2-user on the bastion (visible in the screenshots below as ec2-user@ip-10-0-0-224).
EC2 Instance Connect pushes a one-time SSH public key to the instance through the EC2 API, valid for ~60 seconds. That's why no permanent key pair was needed.

Launching the Web Server via AWS CLI

From inside the bastion, the CLI workflow is: resolve Region → pull latest AMI from SSM → look up Subnet and SG by tag/name → download UserData → run-instances → poll state → fetch public DNS.

1. Region + AMI from instance metadata & SSM Parameter Store

bastion shell — resolve Region and latest Amazon Linux 2 AMI
# Get the Availability Zone from IMDS, then trim the trailing letter to get the Region.
AZ=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone)
export AWS_DEFAULT_REGION=${AZ::-1}

# Ask SSM for the latest Amazon Linux 2 AMI ID in this Region.
AMI=$(aws ssm get-parameters \
  --names /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2 \
  --query 'Parameters[0].[Value]' \
  --output text)
echo $AMI
# → ami-03bc3367c1f063162
Gotcha observed in foto1: right before $AMI printed, the shell logged -bash: -1: substring expression < 0. That comes from ${AZ::-1} when $AZ is empty or too short — typically a transient IMDS hiccup on the first curl. In this lab the next commands worked, so the Region was effectively exported, but the right hardening is to verify $AZ first (or use IMDSv2 with a token): [ -n "$AZ" ] || { echo "IMDS empty"; exit 1; }.

2. Subnet and Security Group by tag / name

bastion shell — resolve target network
# Subnet ID by tag Name=Public Subnet.
SUBNET=$(aws ec2 describe-subnets \
  --filters 'Name=tag:Name,Values=Public Subnet' \
  --query Subnets[].SubnetId \
  --output text)
echo $SUBNET
# → subnet-0440cb1333588e7b5

# Security group ID for the WebSecurityGroup (allows HTTP/80 inbound).
SG=$(aws ec2 describe-security-groups \
  --filters Name=group-name,Values=WebSecurityGroup \
  --query SecurityGroups[].GroupId \
  --output text)
echo $SG
# → sg-07f067d492bc6f4b5

3. UserData bootstrap script

Downloaded the script from the lab S3 bucket with wget and inspected it with cat. The file is what the new instance will execute as root on first boot.

UserData.txt — runs on first boot of the Web Server
#!/bin/bash
# Install Apache Web Server
yum install -y httpd

# Turn on web server
systemctl enable httpd.service
systemctl start  httpd.service

# Download App files and serve from /var/www/html
wget https://aws-tc-largeobjects.s3.amazonaws.com/CUR-TF-100-RESTRT-1/171-lab-%5BJAWS%5D-create-ec2/dashboard-app.zip
unzip dashboard-app.zip -d /var/www/html/

4. run-instances

bastion shell — launch the Web Server
INSTANCE=$(aws ec2 run-instances \
  --image-id $AMI \
  --subnet-id $SUBNET \
  --security-group-ids $SG \
  --user-data file:///home/ec2-user/UserData.txt \
  --instance-type t3.micro \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=Web Server}]' \
  --query 'Instances[*].InstanceId' \
  --output text)
echo $INSTANCE

Every CLI parameter mirrors a Console field: --image-id = AMI choice, --subnet-id = network step, --security-group-ids = firewall step, --user-data = the bootstrap, and --tag-specifications applies the Name tag at create time so the instance shows up labeled in the console list.

5. Poll state & get public DNS

bastion shell — wait for running, then fetch the URL
# Repeat until it prints "running".
aws ec2 describe-instances --instance-ids $INSTANCE \
  --query 'Reservations[].Instances[].State.Name' \
  --output text

# Then grab the public DNS name to open in a browser.
aws ec2 describe-instances --instance-ids $INSTANCE \
  --query Reservations[].Instances[].PublicDnsName \
  --output text

Verifying the web server

Pasted the public DNS into a browser tab. The instance answered HTTP on port 80 with the dashboard that UserData unzipped into /var/www/html/.

Console vs CLI vs CloudFormation

The lab finishes with a take on when each launch method fits.

Management Console

Best for one-off or exploratory launches. Good for learning what fields exist and what the defaults are. Not repeatable.

AWS CLI / scripts

Repeatable automation with low overhead. Same command runs in a shell, a cron, a CI job. Pairs well with IAM roles to avoid hard-coded credentials.

CloudFormation

When you need a set of related resources (VPC + subnets + SG + instances + role) created and destroyed as one unit, with drift detection and rollback on failure.

Optional challenges — not performed

The lab guide includes two optional troubleshooting challenges on a pre-existing Misconfigured Web Server instance. They were not performed in this run, so they are intentionally left out of this report instead of fabricating a diagnosis.

Challenge 1 · Connect to the misconfigured server

Goal would have been to diagnose why EC2 Instance Connect fails against Misconfigured Web Server and fix the misconfiguration. Not done.

Challenge 2 · Fix the web server install

Goal would have been to open the public DNS of that same instance, diagnose why the site doesn't load, and fix the issue. Not done.

Key Learnings

What stuck

An IAM instance profile turns an EC2 instance into a trusted CLI client — no access keys needed.
SSM Parameter Store has a public path for the latest Amazon Linux 2 AMI per Region; great for scripts that should always pick the patched image.
describe-subnets and describe-security-groups with --filters + --query make CLI scripts deterministic even when IDs change between accounts.
--user-data file://… is the same bootstrap mechanism as the Console's Advanced > User data field, just expressed as a CLI flag.
${AZ::-1} over IMDS output is brittle — if the first metadata call returns empty, the substring expression errors out. Worth guarding in real scripts.

Technical takeaway

The bastion pattern shown here is the seed of more elaborate setups: a small, hardened instance with an IAM role becomes the "control plane" you SSH/Connect into, and from there you operate on the rest of the fleet through APIs. The web server never holds credentials — it only serves HTTP and is replaceable in seconds because everything it needs is encoded in UserData.

The same flow scales naturally to CloudFormation: the parameters passed to run-instances become a AWS::EC2::Instance resource block, and the role + SG + subnet become sibling resources in the template.