Sitemap
Carrick Bradley

Carrick Bradley

AWS EC2: AWS CLI Expanded

19 min readApr 12, 2025

--

Press enter or click to view image in full size
AcmeLabs.Cloud Architecture — Created In Draw.io
AcmeLabs.Cloud Architecture — Created In Draw.io

Use Case

AcmeLabs has an EC2 instance they migrated to AWS awhile back. Now, they want take advantage of AWS services to add or improve their: Scalability, Reliability, Security, Cost Savings, and Flexibility.

To achieve this, our statement of work is:

  • Create a VPC with the CIDR block: 10.10.0.0/16
  • Create an Internet Gateway & attach it to our new VPC
  • Create three public subnets with CIDR blocks: 10.10.1.0/24, 10.10.2.0/24, and 10.10.3.0/24
  • Create an autoscaling group using t2.micro instances, use the public subnets, set the minimum amount of EC2 instances to 3 and the maximum 6, using the latest Ubuntu 24.x, ensure Apache web server is installed along with a web page
  • Create a scaling policy when 50% of the CPU utilization is used to scale out then scale in when CPU utilization dips below 50%
  • Create an Application Load Balancer to distribute the traffic to the EC2 instances
  • Create two Security groups, one for the Application Load Balancer allowing HTTP, second on for the EC2 instances allowing HTTP

Prerequisites

  1. AWS Account
  2. AWS CLI Installed & Configured
  3. Terminal Emulator
  4. Bash 3.2.57 or newer
  5. Project Files

Lets Get Started

For each AWS CLI command executed, review the output and take note of the resource ID, ARN, and or Name where applicable. We’ll need those to build out the rest of our commands from start to finish. We’ll also tag our resources with a “Name” tag where possible. Below is the list of IDs, ARNs, and Names I kept track of. For each one you create, find and replace mine with yours.

# List of IDs/ARNs/Names you must replace with yours.
vpc_id: vpc-0868038970bd1ab9d
igw_id: igw-00fe90aafd807d86b
rtb_id: rtb-045a89caa3b86ab5e
public_subnet_1_id: subnet-0c112f381394f54d9
public_subnet_2_id: subnet-09e8d4b16b4f7dd2b
public_subnet_3_id: subnet-0441acfbd58d84c74
alb_sg_id: sg-053350f6ad17b7f3f
ec2_web_sg_id: sg-0be64343db85129e6
bucket_name: acmelabs-web-2025
efs_fs_id: fs-0493f75b9bc987268
keypair_name: AcmeLabs-WEB-KP
iam_role_ec2_s3_name: AcmeLabsWEBEC2S3AccessRole
s3_permissions_policy_arn: arn:aws:iam::047719623795:policy/AcmeLabsWEBEC2S3ReadAccessPolicy
ec2_s3_instance_profile_name: AcmeLabsWEBEC2S3AccessProfile
ubuntu_ami_id: ami-09eb231ad55c3963d
ec2_instance_src_id: i-04dffcc34f586ffe6
our_ami_id: ami-06c4decc36f7ce21f
alb_arn: arn:aws:elasticloadbalancing:us-east-1:047719623795:loadbalancer/app/AcmeLabs-WEB-ALB/14929f913954185d
alb_dns: AcmeLabs-WEB-ALB-1156157756.us-east-1.elb.amazonaws.com
target_group_arn: arn:aws:elasticloadbalancing:us-east-1:047719623795:targetgroup/AcmeLabs-WEB-TG/086f1097ab15af59
launch_template_name: AcmeLabs-WEB-LT
asg_tracking_policy_arn: arn:aws:autoscaling:us-east-1:047719623795:scalingPolicy:3cc16975-0ede-44a0-b8b0-a8171e1c912e:autoScalingGroupName/AcmeLabs-ASG:policyName/ScaleUp
tls_arn: arn:aws:acm:us-east-1:047719623795:certificate/89f90795-1831-4b68-97a1-7043eec28c13
certificate_arn: arn:aws:acm:us-east-1:047719623795:certificate/89f90795-1831-4b68-97a1-7043eec28c13
elastic_ip_1: eipalloc-06324a683da7e015a
elastic_ip_2: eipalloc-0238459015730e598
elastic_ip_3: eipalloc-0f3f2f3f2f3f2f3f

Virtual Private Cloud (VPC)

Lets carve out our piece of the network pie to create our VPC and other networking components we’ll need.

# Create VPC
aws ec2 create-vpc \
--cidr-block 10.10.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=AcmeLabs-VPC}]'

# Enable DNS Resolution
aws ec2 modify-vpc-attribute \
--vpc-id vpc-0868038970bd1ab9d \
--enable-dns-support

# Enable DNS Hostnames
aws ec2 modify-vpc-attribute \
--vpc-id vpc-0868038970bd1ab9d \
--enable-dns-hostnames

# Create Internet Gateway
aws ec2 create-internet-gateway \
--tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=AcmeLabs-IGW}]'

# Attach Internet Gateway
aws ec2 attach-internet-gateway \
--vpc-id vpc-0868038970bd1ab9d \
--internet-gateway-id igw-00fe90aafd807d86b

# Create Route Table
aws ec2 create-route-table \
--vpc-id vpc-0868038970bd1ab9d \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=AcmeLabs-RT}]'

# Create Route To Internet Gateway
aws ec2 create-route \
--route-table-id rtb-045a89caa3b86ab5e \
--destination-cidr-block 0.0.0.0/0 \
--gateway-id igw-00fe90aafd807d86b

# Create Public Subnet 1 in US-EAST-1A
aws ec2 create-subnet \
--vpc-id vpc-0868038970bd1ab9d \
--cidr-block 10.10.1.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=AcmeLabsPublicSubnet1}]'

# Create Public Subnet 2 in US-EAST-1B
aws ec2 create-subnet \
--vpc-id vpc-0868038970bd1ab9d \
--cidr-block 10.10.2.0/24 \
--availability-zone us-east-1b \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=AcmeLabsPublicSubnet2}]'

# Create Public Subnet 3 in US-EAST-1C
aws ec2 create-subnet \
--vpc-id vpc-0868038970bd1ab9d \
--cidr-block 10.10.3.0/24 \
--availability-zone us-east-1c \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=AcmeLabsPublicSubnet3}]'

# Associate Public Subnet 1 To Route Table
aws ec2 associate-route-table \
--subnet-id subnet-0c112f381394f54d9 \
--route-table-id rtb-045a89caa3b86ab5e

# Associate Public Subnet 2 To Route Table
aws ec2 associate-route-table \
--subnet-id subnet-09e8d4b16b4f7dd2b \
--route-table-id rtb-045a89caa3b86ab5e

# Associate Public Subnet 3 To Route Table
aws ec2 associate-route-table \
--subnet-id subnet-0441acfbd58d84c74 \
--route-table-id rtb-045a89caa3b86ab5e

# Create Load Balancer Security Group
aws ec2 create-security-group \
--group-name WEBALBSG \
--description "Security group for our application load balancer to our web server EC2 instances." \
--vpc-id vpc-0868038970bd1ab9d \
--tag-specifications 'ResourceType=security-group,Tags=[{Key=Name,Value=AcmeLabs-WEB-ALB-SG}]'

# Allow HTTP traffic from anywhere
aws ec2 authorize-security-group-ingress \
--group-id sg-053350f6ad17b7f3f \
--protocol tcp \
--port 80 \
--cidr 0.0.0.0/0

# Create EC2 Security Group
aws ec2 create-security-group \
--group-name WEBEC2SG \
--description "Security group for our web server EC2 instances." \
--vpc-id vpc-0868038970bd1ab9d \
--tag-specifications 'ResourceType=security-group,Tags=[{Key=Name,Value=AcmeLabs-WEB-EC2-SG}]'

# Allow HTTP traffic from ALB SG To EC2 SG
aws ec2 authorize-security-group-ingress \
--group-id sg-0be64343db85129e6 \
--protocol tcp \
--port 80 \
--source-group sg-053350f6ad17b7f3f

Simple Storage Service (S3)

Taking advantage of S3 to store some files that will downloaded into an EC2 instance that we’ll create our own Amazon Machine Image (AMI) from. I’m on a Mac so I use the “exclude” option when uploading to avoid making a mess out of our S3 bucket. Plus, why pay for storage on files that we’re not going to use.

Due to globally unique bucket names, please set the name to something unique to avoid using the same name.

# Create S3 Bucket
aws s3api create-bucket \
--bucket acmelabs-web-2025 \
--region us-east-1

# Tag Bucket
aws s3api put-bucket-tagging \
--bucket acmelabs-web-2025 \
--tagging 'TagSet=[{Key=Name,Value=AcmeLabs-Web-2025}]'

# Upload Scripts
aws s3 cp \
scripts/ \
s3://acmelabs-web-2025/scripts/ \
--recursive \
--exclude "*.DS_Store"

# Upload Images
aws s3 cp \
images/ \
s3://acmelabs-web-2025/images/ \
--recursive \
--exclude "*.DS_Store"

# Upload HTML
aws s3 cp \
html/ \
s3://acmelabs-web-2025/html/ \
--recursive \
--exclude "*.DS_Store"

Elastic File System (EFS)

We’ll use EFS to store our site images in.

# Create EFS File System
aws efs create-file-system \
--creation-token AcmeLabsEFS \
--performance-mode generalPurpose \
--encrypted \
--tags Key=Name,Value=AcmeLabs-EFS

# Create Mount Targets
aws efs create-mount-target \
--file-system-id fs-0493f75b9bc987268 \
--subnet-id subnet-0c112f381394f54d9 \
--security-groups sg-0be64343db85129e6

aws efs create-mount-target \
--file-system-id fs-0493f75b9bc987268 \
--subnet-id subnet-09e8d4b16b4f7dd2b \
--security-groups sg-0be64343db85129e6

aws efs create-mount-target \
--file-system-id fs-0493f75b9bc987268 \
--subnet-id subnet-0441acfbd58d84c74 \
--security-groups sg-0be64343db85129e6

# Update EC2 Security Group To Allow EFS Traffic
aws ec2 authorize-security-group-ingress \
--group-id sg-0be64343db85129 \
--protocol tcp \
--port 2049 \

Key-Pair

We’ll create a key-pair so we can access our EC2 instances if needed. Otherwise, we’ll have to generate one, then redeploy our existing EC2 instances should we choose not to at this time.

# Create Key-Pair
aws ec2 create-key-pair \
--key-name AcmeLabs-WEB-KP \
--query 'KeyMaterial' \
--output text > AcmeLabs-WEB-KP.pem

# Tag Key-Pair
aws ec2 create-tags \
--resources $(aws ec2 describe-key-pairs \
--key-name AcmeLabs-WEB-KP \
--query 'KeyPairs[0].KeyPairId' \
--output text) \
--tags Key=Name,Value=AcmeLabs-WEB-KP

Identity and Access Manager (IAM) — Roles

Our EC2 instances out of the box can’t talk to S3. To achieve this, we’ll create a role along with a policy and other components to enable access.

# Create IAM Role For EC2 S3 Access
aws iam create-role \
--role-name AcmeLabsWEBEC2S3AccessRole \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}'

# Create S3 Permissions Policy
aws iam create-policy \
--policy-name AcmeLabsWEBEC2S3ReadAccessPolicy \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::acmelabs-web-2025",
"arn:aws:s3:::acmelabs-web-2025/*"
]
}
]
}'

# Attach EC2 S3 IAM Role Policy
aws iam attach-role-policy \
--role-name AcmeLabsWEBEC2S3AccessRole \
--policy-arn arn:aws:iam::047719623795:policy/AcmeLabsWEBEC2S3ReadAccessPolicy

# Create EC2 S3 Instance Profile
aws iam create-instance-profile \
--instance-profile-name AcmeLabsWEBEC2S3AccessProfile

# Associate EC2 S3 Instance Profile To EC2 S3 IAM Role
aws iam add-role-to-instance-profile \
--instance-profile-name AcmeLabsWEBEC2S3AccessProfile \
--role-name AcmeLabsWEBEC2S3AccessRole

Elastice Cloud Compute (EC2)

Now we’re cooking with gasoline. Let’s build out our compute components.

Note:

  • I found this nice article from Canonical that explains how to query their latest AMIs in AWS: https://documentation.ubuntu.com/aws/aws-how-to/instances/find-ubuntu-images/.
  • Update the BUCKET_NAME=”acmelabs-web-2025" to your bucket name
  • Update the EFS_FS=”” to your EFS FS ID
  • Allow the EC2 instance to launch and become healthy
  • Make sure Query EC2 Instance Status = Running & Query EC2 Instance Health State = ok, before creating the AMI
  • After the AMI is available, move forward with deleting our source EC2 instance as it’s no longer needed
# Query Ubuntu 24.04 Latest AMI
aws ssm get-parameters --names \
/aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id

# Launch EC2 Instance & Bake Our Goods
aws ec2 run-instances \
--image-id "ami-09eb231ad55c3963d" \
--count "1" \
--instance-type "t2.micro" \
--key-name "AcmeLabs-WEB-KP" \
--subnet-id "subnet-0c112f381394f54d9" \
--associate-public-ip-address \
--security-group-ids "sg-0be64343db85129e6" \
--user-data '#!/bin/bash
export BUCKET_NAME="acmelabs-web-2025"
export EFS_FS_ID="fs-0493f75b9bc987268"
apt-get update
apt-get install -y unzip
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "/tmp/awscliv2.zip"
unzip /tmp/awscliv2.zip -d /tmp
/tmp/aws/install
rm -rf /tmp/aws /tmp/awscliv2.zip
if aws s3 cp s3://"$BUCKET_NAME"/scripts/user-data.sh /tmp/user-data.sh; then
chmod +x /tmp/user-data.sh
bash /tmp/user-data.sh
else
echo "Failed to download user-data.sh from S3 bucket: '"$BUCKET_NAME"'. Exiting."
exit 1
fi' \
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=AcmeLabs-WEB-SRC}]" \
--iam-instance-profile Name="AcmeLabsWEBEC2S3AccessProfile" \
--metadata-options "HttpTokens=required,HttpEndpoint=enabled" \
--query 'Instances[0].InstanceId' \
--output text

# Query EC2 Instane Status
aws ec2 describe-instances \
--instance-ids "i-04dffcc34f586ffe6" \
--query 'Reservations[0].Instances[0].State.Name' \
--output text

# Query EC2 Instante Health State
aws ec2 describe-instance-status \
--instance-ids "i-04dffcc34f586ffe6" \
--query 'InstanceStatuses[0].InstanceStatus.Status' \
--output text

# Create AMI
aws ec2 create-image \
--instance-id i-04dffcc34f586ffe6 \
--name "AcmeLabs-WEB-Ubuntu24-AMI" \
--no-reboot

# Tag AMI
aws ec2 create-tags \
--resources ami-06c4decc36f7ce21f \
--tags Key=Name,Value=AcmeLabs-Ubuntu24-AMI

# AMI Creation Status (Optional)
aws ec2 describe-images \
--image-ids ami-06c4decc36f7ce21f \
--query 'Images[0].State' \
--output text

# Delete EC2 Instance Used As Our Source To Create Our AMI
aws ec2 terminate-instances \
--instance-ids i-04dffcc34f586ffe6

Elastic Load Balancer (ELB)

To evenly distribute traffic and increase are availability to our EC2 instances we’ll create an Application Load Balancer (ALB). If we lose an instance, the ALB will remove it and route traffic to healthy EC2 instances. We’ll create a health check to achieve this.

# Create ALB
aws elbv2 create-load-balancer \
--name AcmeLabs-WEB-ALB \
--subnets subnet-0c112f381394f54d9 \
subnet-09e8d4b16b4f7dd2b \
subnet-0441acfbd58d84c74 \
--security-groups sg-053350f6ad17b7f3f \
--tags Key=Name,Value=AcmeLabs-WEB-ALB

# Create Target Group w/Health Check
aws elbv2 create-target-group \
--name AcmeLabs-WEB-TG \
--protocol HTTP \
--port 80 \
--vpc-id vpc-0868038970bd1ab9d \
--health-check-protocol HTTP \
--health-check-path /health \
--health-check-interval-seconds 30 \
--health-check-timeout-seconds 5 \
--healthy-threshold-count 5 \
--unhealthy-threshold-count 2 \
--tags Key=Name,Value=AcmeLabs-WEB-TG

# Create Listeners
aws elbv2 create-listener \
--load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:047719623795:loadbalancer/app/AcmeLabs-WEB-ALB/14929f913954185d \
--port 80 \
--protocol HTTP \
--default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:us-east-1:047719623795:targetgroup/AcmeLabs-WEB-TG/086f1097ab15af59

Auto Scaling Group (ASG) — Launch Template

For our ASG to fill in the details of what we need for our EC2 instances, we’ll create a launch template.

# Create Launch Template 
aws ec2 create-launch-template \
--launch-template-name AcmeLabs-WEB-LT \
--version-description "Initial version" \
--launch-template-data '{
"ImageId": "ami-06c4decc36f7ce21f",
"InstanceType": "t2.micro",
"KeyName": "AcmeLabs-WEB-KP",
"IamInstanceProfile": {
"Name": "AcmeLabsWEBEC2S3AccessProfile"
},
"SecurityGroupIds": ["sg-0be64343db85129e6"],
"TagSpecifications": [
{
"ResourceType": "instance",
"Tags": [
{
"Key": "Name",
"Value": "AcmeLabs-WEB"
}
]
}
]
}'

Auto Scaling Group (ASG)

Here’s our ASG thats referencing our launch template. Here we can tell our ASG what we need for resources and which subnets they should be launched in.

# Create ASG
aws autoscaling create-auto-scaling-group \
--auto-scaling-group-name AcmeLabs-WEB-ASG \
--launch-template "LaunchTemplateName=AcmeLabs-WEB-LT,Version=1" \
--min-size 3 \
--max-size 6 \
--vpc-zone-identifier "subnet-0c112f381394f54d9,subnet-09e8d4b16b4f7dd2b,subnet-0441acfbd58d84c74" \
--target-group-arns "arn:aws:elasticloadbalancing:us-east-1:047719623795:targetgroup/AcmeLabs-WEB-TG/086f1097ab15af59"

Auto Scaling Group (ASG) — Target Tracking Policy

To track if we need to scale out or scale in, we’ll create a Target Tracking Policy. If the ASG goes above AVG CPU of 50%, scale out to our maximum size if needed and when we drop below 50% then we’ll scale in to our minimum size.

# Create Target Tracking Policy
aws autoscaling put-scaling-policy --auto-scaling-group-name AcmeLabs-WEB-ASG --policy-name ScaleUp --policy-type TargetTrackingScaling --target-tracking-configuration '{
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ASGAverageCPUUtilization"
},
"TargetValue": 50.0,
"DisableScaleIn": false
}'

Route53 (Optional)

I like clean URLs, it’s a me thing. You don’t have register a domain and setup a zone. Feel free to use the ALB DNS name to test. If you want to have fun with this, please take the time to review registering a domain, creating a zone and creating DNS records. Amazon Route 53 Getting Started

Get Carrick Bradley’s stories in your inbox

Join Medium for free to get updates from this writer.

My zone was already created so I’m going to skip to the Retrieve ZoneId step.

If you choose to do this, it comes at cost for the domain based on the TLD you pick and having a hosted zone. The hosted zone per month is around $.50 cents a month at the time of this article.

# Register a Domain with Route53
aws route53domains register-domain \
--domain-name acmelabs.cloud \
--duration-in-years 1 \
--admin-contact "FirstName=Acme,LastName=Admin,ContactType=PERSON,Email=acmeadmin@acmelabs.cloud,PhoneNumber=+12065551234,AddressLine1=123 Cloud Way,City=Cloudy,State=AZ,CountryCode=US,ZipCode=85224" \
--registrant-contact "FirstName=Acme,LastName=Admin,ContactType=PERSON,Email=acmeadmin@acmelabs.cloud,PhoneNumber=+12065551234,AddressLine1=123 Cloud Way,City=Cloudy,State=AZ,CountryCode=US,ZipCode=85224" \
--tech-contact "FirstName=Acme,LastName=Admin,ContactType=PERSON,Email=acmeadmin@acmelabs.cloud,PhoneNumber=+12065551234,AddressLine1=123 Cloud Way,City=Cloudy,State=AZ,CountryCode=US,ZipCode=85224" \
--privacy-protect-admin-contact \
--privacy-protect-registrant-contact \
--privacy-protect-tech-contact

# Create Zone
aws route53 create-hosted-zone \
--name acmelabs.cloud \
--caller-reference "$(date +%s)" \
--hosted-zone-config Comment="Hosted zone for acmelabs.cloud",PrivateZone=false \
--tags Key=Name,Value=AcmeCloud

# Retrieve ZoneId
aws route53 list-hosted-zones \
--query "HostedZones[?Name=='acmelabs.cloud.'].[Id]" \
--output text

# Create DNS Records
aws route53 change-resource-record-sets \
--hosted-zone-id Z087608618II6FJ5B93HU \
--change-batch '{
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "acmelabs.cloud",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z35SXDOTRQ7X7K",
"DNSName": "AcmeLabs-WEB-ALB-525810016.us-east-1.elb.amazonaws.com",
"EvaluateTargetHealth": false
}
}
},
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "www.acmelabs.cloud",
"Type": "CNAME",
"TTL": 300,
"ResourceRecords": [
{
"Value": "AcmeLabs-WEB-ALB-525810016.us-east-1.elb.amazonaws.com"
}
]
}
}
]
}'

Transport Layer Security (TLS) (Optional)

To add a bit more security we can adjust our work to use a certificate issued by AWS to secure our inflight connections to our EC2 instances.
Note: These changes can take minutes to hours. If you zone is setup wrong and you delete it then recreate it, there is the possiblity it may take 48 hours to work. If that happens, test with the ALB DNS URL.

# Create Certificate
aws acm request-certificate \
--domain-name acmelabs.cloud \
--validation-method DNS \
--subject-alternative-names www.acmelabs.cloud

# Get Status
# ValidationStatus = Success before moving on
aws acm describe-certificate \
--certificate-arn arn:aws:acm:us-east-1:047719623795:certificate/89f90795-1831-4b68-97a1-7043eec28c13

# Create HTTPS Listeners on our ALB
aws elbv2 create-listener \
--load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:047719623795:loadbalancer/app/AcmeLabs-WEB-ALB/14929f913954185d \
--port 443 \
--protocol HTTPS \
--default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:us-east-1:047719623795:targetgroup/AcmeLabs-WEB-TG/086f1097ab15af59 \
--certificates CertificateArn=arn:aws:acm:us-east-1:047719623795:certificate/89f90795-1831-4b68-97a1-7043eec28c13

# Allow HTTPS traffic
aws ec2 authorize-security-group-ingress \
--group-id sg-053350f6ad17b7f3f \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0

# Get AcmeLabs-WEB-ALB HTTP Listener ARN
# Look for the HTTP ARN
aws elbv2 describe-listeners \
--load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:047719623795:loadbalancer/app/AcmeLabs-WEB-ALB/14929f913954185d

# Remove HTTP From AcmeLabs-WEB-ALB
aws elbv2 delete-listener \
--listener-arn arn:aws:elasticloadbalancing:us-east-1:047719623795:listener/app/AcmeLabs-WEB-ALB/14929f913954185d/f599459d9d0a930a

# Remove HTTP From AcmeLabs-WEB-ALB-SG
aws ec2 revoke-security-group-ingress \
--group-id sg-0be64343db85129e6 \
--protocol tcp \
--port 80 \
--source-group sg-053350f6ad17b7f3f
# Create CNAME for acmecloud.com Validation
aws route53 change-resource-record-sets \
--hosted-zone-id Z087608618II6FJ5B93HU \
--change-batch '{
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "_edb26fc083581701d2095e9baad2dc80.acmelabs.cloud.",
"Type": "CNAME",
"TTL": 300,
"ResourceRecords": [
{
"Value": "_e4533c7ad46b60ff55b80ecbfdf51746.xlfgrmvvlj.acm-validations.aws."
}
]
}
}
]
}'

# Create CNAME for www.acmecloud.com Validation
aws route53 change-resource-record-sets \
--hosted-zone-id Z087608618II6FJ5B93HU \
--change-batch '{
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "_8fd320bdffc18b7578c3739785ca0680.www.acmelabs.cloud.",
"Type": "CNAME",
"TTL": 300,
"ResourceRecords": [
{
"Value": "_36b686ce647ce6c6a8cf38051c3f62fb.xlfgrmvvlj.acm-validations.aws."
}
]
}
}
]
}'

# This may take minutes to hours

Test

If you registered a domain, please use that to test otherwise please use the ALB DNS . If all works we should see a page like this. Take note of the top right corner of the page. Refresh the page and you’ll bounce around three availability zones (AZ).

Press enter or click to view image in full size
US-EAST-1A
US-EAST-1A
Press enter or click to view image in full size
US-EAST-1B
US-EAST-1B
Press enter or click to view image in full size
US-EAST-1C
US-EAST-1C

Stress Test

Lets watch our ASG in action. We’ll do the following:

  • Adjust our AcmeLabs-EC2-SG to accept inbound SSH from anywhere
# Allow SSH traffic to Our EC2 Instances
aws ec2 authorize-security-group-ingress \
--group-id sg-0be64343db85129e6 \
--protocol tcp \
--port 22 \
--cidr 0.0.0.0/0
  • Create an elastic IP to assign to one of our EC2 instances
# Get Elastic IP 1
aws ec2 allocate-address \
--domain vpc

# Get Elastic IP 2
aws ec2 allocate-address \
--domain vpc

# Get Elastic IP 3
aws ec2 allocate-address \
--domain vpc

# Associate Elastic IP to EC2 Instance 1
aws ec2 associate-address \
--instance-id i-0a700cec15e1f3f56 \
--allocation-id eipalloc-06324a683da7e015a

# Associate Elastic IP to EC2 Instance 2
aws ec2 associate-address \
--instance-id i-0e6e7c5da63b2cf2a \
--allocation-id eipalloc-0238459015730e598

# Associate Elastic IP to EC2 Instance 3
aws ec2 associate-address \
--instance-id i-0d9441ff20c4e61fe \
--allocation-id eipalloc-0d2f60238bf4aa60f
  • Shell Over to Our EC2 Instance(s)
    Note: Make sure your chmod + 400 our private key.
# Shell Over To EC2 Instance 1
ssh -i ~/path/to/AcmeLabs-WEB-KP.pem ubuntu@<IP>

# Shell Over To EC2 Instance 2
ssh -i ~/path/to/AcmeLabs-WEB-KP.pem ubuntu@<IP>

# Shell Over To EC2 Instance 3
ssh -i ~/path/to/AcmeLabs-WEB-KP.pem ubuntu@<IP>
  • Install Stress Utility
# Update Package List
sudo apt-get update

# Install Stress Utility
sudo apt-get install -y stress
  • Stress Our EC2 Instance(s)
# Stress Out (TTL is 5 minutes)
sudo stress --cpu 2000 --timeout 3600
  • Results ASG Scale Out
Press enter or click to view image in full size
EC2 Instances Health (Stress Running)
EC2 Instances Health (Stress Running)
Press enter or click to view image in full size
ASG Scaling Out
ASG Scaling Out
# Check ASG Status
aws autoscaling describe-auto-scaling-groups \
--auto-scaling-group-names AcmeLabs-WEB-ASG

# ASG Output
{
"AutoScalingGroups": [
{
"AutoScalingGroupName": "AcmeLabs-WEB-ASG",
"AutoScalingGroupARN": "arn:aws:autoscaling:us-east-1:047719623795:autoScalingGroup:cc9cc878-8f03-4602-a179-18f74a3b420a:autoScalingGroupName/AcmeLabs-WEB-ASG",
"LaunchTemplate": {
"LaunchTemplateId": "lt-0c3b4835a80925fe0",
"LaunchTemplateName": "AcmeLabs-WEB-LT",
"Version": "1"
},
"MinSize": 3,
"MaxSize": 6,
"DesiredCapacity": 6,
"DefaultCooldown": 300,
"AvailabilityZones": [
"us-east-1a",
"us-east-1b",
"us-east-1c"
],
"LoadBalancerNames": [],
"TargetGroupARNs": [
"arn:aws:elasticloadbalancing:us-east-1:047719623795:targetgroup/AcmeLabs-WEB-TG/086f1097ab15af59"
],
"HealthCheckType": "EC2",
"HealthCheckGracePeriod": 0,
"Instances": [
{
"InstanceId": "i-03aa83c9c1f22e645",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1b",
"LifecycleState": "InService",
"HealthStatus": "Healthy",
"LaunchTemplate": {
"LaunchTemplateId": "lt-0c3b4835a80925fe0",
"LaunchTemplateName": "AcmeLabs-WEB-LT",
"Version": "1"
},
"ProtectedFromScaleIn": false
},
{
"InstanceId": "i-087a1a3cd1b59d1ee",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1c",
"LifecycleState": "InService",
"HealthStatus": "Healthy",
"LaunchTemplate": {
"LaunchTemplateId": "lt-0c3b4835a80925fe0",
"LaunchTemplateName": "AcmeLabs-WEB-LT",
"Version": "1"
},
"ProtectedFromScaleIn": false
},
{
"InstanceId": "i-08b2c8e4ae6e8440c",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1a",
"LifecycleState": "InService",
"HealthStatus": "Healthy",
"LaunchTemplate": {
"LaunchTemplateId": "lt-0c3b4835a80925fe0",
"LaunchTemplateName": "AcmeLabs-WEB-LT",
"Version": "1"
},
"ProtectedFromScaleIn": false
},
{
"InstanceId": "i-0a700cec15e1f3f56",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1a",
"LifecycleState": "InService",
"HealthStatus": "Healthy",
"LaunchTemplate": {
"LaunchTemplateId": "lt-0c3b4835a80925fe0",
"LaunchTemplateName": "AcmeLabs-WEB-LT",
"Version": "1"
},
"ProtectedFromScaleIn": false
},
{
"InstanceId": "i-0d9441ff20c4e61fe",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1c",
"LifecycleState": "InService",
"HealthStatus": "Healthy",
"LaunchTemplate": {
"LaunchTemplateId": "lt-0c3b4835a80925fe0",
"LaunchTemplateName": "AcmeLabs-WEB-LT",
"Version": "1"
},
"ProtectedFromScaleIn": false
},
{
"InstanceId": "i-0e6e7c5da63b2cf2a",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1b",
"LifecycleState": "InService",
"HealthStatus": "Healthy",
"LaunchTemplate": {
"LaunchTemplateId": "lt-0c3b4835a80925fe0",
"LaunchTemplateName": "AcmeLabs-WEB-LT",
"Version": "1"
},
"ProtectedFromScaleIn": false
}
],
"CreatedTime": "2025-04-12T00:11:56.612000+00:00",
"SuspendedProcesses": [],
"VPCZoneIdentifier": "subnet-09e8d4b16b4f7dd2b,subnet-0441acfbd58d84c74,subnet-0c112f381394f54d9",
"EnabledMetrics": [],
"Tags": [],
"TerminationPolicies": [
"Default"
],
"NewInstancesProtectedFromScaleIn": false,
"ServiceLinkedRoleARN": "arn:aws:iam::047719623795:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling",
"TrafficSources": [
{
"Identifier": "arn:aws:elasticloadbalancing:us-east-1:047719623795:targetgroup/AcmeLabs-WEB-TG/086f1097ab15af59",
"Type": "elbv2"
}
],
"AvailabilityZoneDistribution": {
"CapacityDistributionStrategy": "balanced-best-effort"
},
"CapacityReservationSpecification": {
"CapacityReservationPreference": "default"
}
}
]
}
Press enter or click to view image in full size
Target Group Now Has 6 EC2 Instances
Target Group Showing Maximum ASG EC2 Instances 6
# Check Target Group Status
aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:us-east-1:047719623795:targetgroup/AcmeLabs-WEB-TG/086f1097ab15af59

# Targrt Group Output
{
"TargetHealthDescriptions": [
{
"Target": {
"Id": "i-08b2c8e4ae6e8440c",
"Port": 80
},
"HealthCheckPort": "80",
"TargetHealth": {
"State": "healthy"
},
"AdministrativeOverride": {
"State": "no_override",
"Reason": "AdministrativeOverride.NoOverride",
"Description": "No override is currently active on target"
}
},
{
"Target": {
"Id": "i-087a1a3cd1b59d1ee",
"Port": 80
},
"HealthCheckPort": "80",
"TargetHealth": {
"State": "healthy"
},
"AdministrativeOverride": {
"State": "no_override",
"Reason": "AdministrativeOverride.NoOverride",
"Description": "No override is currently active on target"
}
},
{
"Target": {
"Id": "i-0d9441ff20c4e61fe",
"Port": 80
},
"HealthCheckPort": "80",
"TargetHealth": {
"State": "healthy"
},
"AdministrativeOverride": {
"State": "no_override",
"Reason": "AdministrativeOverride.NoOverride",
"Description": "No override is currently active on target"
}
},
{
"Target": {
"Id": "i-0a700cec15e1f3f56",
"Port": 80
},
"HealthCheckPort": "80",
"TargetHealth": {
"State": "healthy"
},
"AdministrativeOverride": {
"State": "no_override",
"Reason": "AdministrativeOverride.NoOverride",
"Description": "No override is currently active on target"
}
},
{
"Target": {
"Id": "i-0e6e7c5da63b2cf2a",
"Port": 80
},
"HealthCheckPort": "80",
"TargetHealth": {
"State": "healthy"
},
"AdministrativeOverride": {
"State": "no_override",
"Reason": "AdministrativeOverride.NoOverride",
"Description": "No override is currently active on target"
}
},
{
"Target": {
"Id": "i-03aa83c9c1f22e645",
"Port": 80
},
"HealthCheckPort": "80",
"TargetHealth": {
"State": "healthy"
},
"AdministrativeOverride": {
"State": "no_override",
"Reason": "AdministrativeOverride.NoOverride",
"Description": "No override is currently active on target"
}
}
]
}
  • Results ASG Scale In
# Check ASG Status
aws autoscaling describe-auto-scaling-groups \
--auto-scaling-group-names AcmeLabs-WEB-ASG

# ASG Output
{
"AutoScalingGroups": [
{
"AutoScalingGroupName": "AcmeLabs-WEB-ASG",
"AutoScalingGroupARN": "arn:aws:autoscaling:us-east-1:047719623795:autoScalingGroup:cc9cc878-8f03-4602-a179-18f74a3b420a:autoScalingGroupName/AcmeLabs-WEB-ASG",
"LaunchTemplate": {
"LaunchTemplateId": "lt-0c3b4835a80925fe0",
"LaunchTemplateName": "AcmeLabs-WEB-LT",
"Version": "1"
},
"MinSize": 3,
"MaxSize": 6,
"DesiredCapacity": 3,
"DefaultCooldown": 300,
"AvailabilityZones": [
"us-east-1a",
"us-east-1b",
"us-east-1c"
],
"LoadBalancerNames": [],
"TargetGroupARNs": [
"arn:aws:elasticloadbalancing:us-east-1:047719623795:targetgroup/AcmeLabs-WEB-TG/086f1097ab15af59"
],
"HealthCheckType": "EC2",
"HealthCheckGracePeriod": 0,
"Instances": [
{
"InstanceId": "i-03aa83c9c1f22e645",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1b",
"LifecycleState": "InService",
"HealthStatus": "Healthy",
"LaunchTemplate": {
"LaunchTemplateId": "lt-0c3b4835a80925fe0",
"LaunchTemplateName": "AcmeLabs-WEB-LT",
"Version": "1"
},
"ProtectedFromScaleIn": false
},
{
"InstanceId": "i-087a1a3cd1b59d1ee",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1c",
"LifecycleState": "InService",
"HealthStatus": "Healthy",
"LaunchTemplate": {
"LaunchTemplateId": "lt-0c3b4835a80925fe0",
"LaunchTemplateName": "AcmeLabs-WEB-LT",
"Version": "1"
},
"ProtectedFromScaleIn": false
},
{
"InstanceId": "i-08b2c8e4ae6e8440c",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1a",
"LifecycleState": "InService",
"HealthStatus": "Healthy",
"LaunchTemplate": {
"LaunchTemplateId": "lt-0c3b4835a80925fe0",
"LaunchTemplateName": "AcmeLabs-WEB-LT",
"Version": "1"
},
"ProtectedFromScaleIn": false
},
{
"InstanceId": "i-0d9441ff20c4e61fe",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1c",
"LifecycleState": "Terminating",
"HealthStatus": "Healthy",
"LaunchTemplate": {
"LaunchTemplateId": "lt-0c3b4835a80925fe0",
"LaunchTemplateName": "AcmeLabs-WEB-LT",
"Version": "1"
},
"ProtectedFromScaleIn": false
},
{
"InstanceId": "i-0e6e7c5da63b2cf2a",
"InstanceType": "t2.micro",
"AvailabilityZone": "us-east-1b",
"LifecycleState": "Terminating",
"HealthStatus": "Healthy",
"LaunchTemplate": {
"LaunchTemplateId": "lt-0c3b4835a80925fe0",
"LaunchTemplateName": "AcmeLabs-WEB-LT",
"Version": "1"
},
"ProtectedFromScaleIn": false
}
],
"CreatedTime": "2025-04-12T00:11:56.612000+00:00",
"SuspendedProcesses": [],
"VPCZoneIdentifier": "subnet-09e8d4b16b4f7dd2b,subnet-0441acfbd58d84c74,subnet-0c112f381394f54d9",
"EnabledMetrics": [],
"Tags": [],
"TerminationPolicies": [
"Default"
],
"NewInstancesProtectedFromScaleIn": false,
"ServiceLinkedRoleARN": "arn:aws:iam::047719623795:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling",
"TrafficSources": [
{
"Identifier": "arn:aws:elasticloadbalancing:us-east-1:047719623795:targetgroup/AcmeLabs-WEB-TG/086f1097ab15af59",
"Type": "elbv2"
}
],
"AvailabilityZoneDistribution": {
"CapacityDistributionStrategy": "balanced-best-effort"
},
"CapacityReservationSpecification": {
"CapacityReservationPreference": "default"
}
}
]
}
Press enter or click to view image in full size
EC2 Instances Health (Stress No Longer Running)
Press enter or click to view image in full size
ASG Scaling In
ASG Scaling In
# Check Target Group Status
aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:us-east-1:047719623795:targetgroup/AcmeLabs-WEB-TG/086f1097ab15af59

# Targrt Group Output
{
"TargetHealthDescriptions": [
{
"Target": {
"Id": "i-08b2c8e4ae6e8440c",
"Port": 80
},
"HealthCheckPort": "80",
"TargetHealth": {
"State": "healthy"
},
"AdministrativeOverride": {
"State": "no_override",
"Reason": "AdministrativeOverride.NoOverride",
"Description": "No override is currently active on target"
}
},
{
"Target": {
"Id": "i-087a1a3cd1b59d1ee",
"Port": 80
},
"HealthCheckPort": "80",
"TargetHealth": {
"State": "healthy"
},
"AdministrativeOverride": {
"State": "no_override",
"Reason": "AdministrativeOverride.NoOverride",
"Description": "No override is currently active on target"
}
},
{
"Target": {
"Id": "i-0e6e7c5da63b2cf2a",
"Port": 80
},
"HealthCheckPort": "80",
"TargetHealth": {
"State": "draining",
"Reason": "Target.DeregistrationInProgress",
"Description": "Target deregistration is in progress"
},
"AdministrativeOverride": {
"State": "no_override",
"Reason": "AdministrativeOverride.NoOverride",
"Description": "No override is currently active on target"
}
},
{
"Target": {
"Id": "i-03aa83c9c1f22e645",
"Port": 80
},
"HealthCheckPort": "80",
"TargetHealth": {
"State": "healthy"
},
"AdministrativeOverride": {
"State": "no_override",
"Reason": "AdministrativeOverride.NoOverride",
"Description": "No override is currently active on target"
}
}
]
}
Press enter or click to view image in full size
Target Group Deregisting EC2 Instances

Elastic File System (EFS) — Test

While we still have SSH and Elastic IPs associated to our three EC2 instances.

  • Shell Over to Our EC2 Instance(s)
# Shell Over To EC2 Instance 1
ssh -i ~/path/to/AcmeLabs-WEB-KP.pem ubuntu@<IP>

# Shell Over To EC2 Instance 2
ssh -i ~/path/to/AcmeLabs-WEB-KP.pem ubuntu@<IP>

# Shell Over To EC2 Instance 3
ssh -i ~/path/to/AcmeLabs-WEB-KP.pem ubuntu@<IP>
  • Validate the EFS FS share is present /var/www/html/images
Press enter or click to view image in full size
EFS FS Mounted To /var/www/html/images
EFS FS Mounted To /var/www/html/images
  • List the /var/www/html/images
Press enter or click to view image in full size
Listing Shows Files
Listing Shows Files
  • Create a file on one instance then list in the other two to validate it shows up
Press enter or click to view image in full size
File Created on 1 EC2 Instance — Appears when listing on the other instances.
File Created on 1 EC2 Instance — Appears when listing on the other instances.

Conclusion

We used AWS CLI to successfully create AcmeLabs infrastructure in AWS. AcmeLabs have moved from a single EC2 instance to a modern dynamic infrastructure that scales out or in depending on our ASG AVG CPU along with securing access through the ALB with TLS. We took advantage of S3 to help store some files that we could pull down into an EC2 instance that baked our goods. We create an AMI from the EC2 to be used with our ASG launch template. We used EFS to store our images for our website and to share with all EC2 instances. We saw the website successfully from more than one AZ. We stressed tested our first three EC2 instances to force the ASG to scale out when ASG AVG CPU went over 50%. Once CPU dipped below 50% we saw our ASG remove EC2 instances. Also note, our health check was still passing so none of the EC2 instances ever wen’t unhealthy, odd huh. I was able to break the commands to allow the EC2 instances to cool down and let the ASG remove the additional EC2 instances.

The joys I learned, there’s a lot more puzzle pieces to build out using the AWS CLI vs using the AWS Console which turned out to be frustrating at times though rewarding when moving through obstacles.

Stay hungry, stay curious, keep learning. You too can cloud.

LaSherr'e Osborne Sarah Obioma Dara Hibben Peter Nguyen Abdihakim Shukri Shaun Latimore Austin Towns Anthony Williams derrick pope Janita Williamson Carl Hayes Troy Ingram Tait Hoglund