AWS · lesson 17 of 19
Beginner
4Intermediate
13- 5 VPC and Networking Basics
- 6 RDS Basics
- 7 AWS Lambda Fundamentals
- 8 CloudFormation Basics
- 9 CloudWatch Monitoring Basics
- 11 DynamoDB: Tables and Keys
- 12 DynamoDB: Scan, Reserved Words, and Global Secondary Indexes
- 13 DynamoDB: Conditional Writes, Atomic Counters, and Streams
- 14 SQS: Visibility Timeout and Dead-Letter Queues
- 15 SQS: FIFO Queues, Message Groups, and Deduplication
- 16 SNS: Fan-Out to SQS and Filter Policies
- 17 ELB and Auto Scaling: Wiring an ASG Behind a Load Balancer
- 18 ECS: Task Definitions and Services
Also part of AWS
8- + Linting AWS IAM Policies Before Deploy with Parliament from Cloud Security
- + Auditing the AWS IAM Credential Report with Python from Cloud Security
- + Hunting in AWS CloudTrail Logs with Python from Cloud Security
- + Dead-Letter Queues: What Happens When Retries Keep Failing from Event-Driven Architecture
- + Idempotent Consumers: Surviving a Duplicate Delivery from Event-Driven Architecture
- + Load Balancing: Round Robin vs Least Connections from System Design
- + Kubernetes Fundamentals from Kubernetes
- + Horizontal Pod Autoscaling: Real Replicas, Real CPU Numbers from Kubernetes
ELB and Auto Scaling: Wiring an ASG Behind a Load Balancer
Wire a launch template, an Auto Scaling Group, and an Application Load Balancer's target group together, watch a terminated instance get replaced automatically, then hit several real moto emulator gaps this lesson states plainly.
By SkillByExample · Updated
An Auto Scaling Group (ASG) launches and replaces EC2 instances to match a desired count; an Application Load Balancer’s target group tracks which instances are healthy enough to receive traffic. Wiring them together is mostly about which ARNs point at which other ARNs — this lesson does that wiring for real against moto, then states plainly where the emulator’s fidelity runs out.
Launch Template, Target Group, ALB, and an ASG That Wires Itself In
#!/usr/bin/env bash
set -e
aws() { MSYS_NO_PATHCONV=1 docker run --rm --network aws17-lab -e AWS_ACCESS_KEY_ID=test -e AWS_SECRET_ACCESS_KEY=test -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli:2.39.5 "$@" --endpoint-url http://aws17-moto:5000; }
VPC_ID=$(aws ec2 describe-vpcs --query 'Vpcs[0].VpcId' --output text)
SUBNETS=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" --query 'Subnets[0:2].SubnetId' --output text)
SUBNET_A=$(echo "$SUBNETS" | cut -f1)
SUBNET_B=$(echo "$SUBNETS" | cut -f2)
LT_ID=$(aws ec2 create-launch-template --launch-template-name web-lt \
--launch-template-data '{"ImageId":"ami-12345678","InstanceType":"t3.micro"}' \
--query 'LaunchTemplate.LaunchTemplateId' --output text)
echo "launch template: $LT_ID"
TG_ARN=$(aws elbv2 create-target-group --name web-tg --protocol HTTP --port 80 \
--vpc-id "$VPC_ID" --query 'TargetGroups[0].TargetGroupArn' --output text)
echo "target group: $TG_ARN"
ALB_ARN=$(aws elbv2 create-load-balancer --name web-alb --subnets "$SUBNET_A" "$SUBNET_B" \
--query 'LoadBalancers[0].LoadBalancerArn' --output text)
echo "load balancer: $ALB_ARN"
aws elbv2 create-listener --load-balancer-arn "$ALB_ARN" --protocol HTTP --port 80 \
--default-actions "Type=forward,TargetGroupArn=$TG_ARN" --query 'Listeners[0].ListenerArn' --output text >/dev/null
echo "listener created"
aws autoscaling create-auto-scaling-group --auto-scaling-group-name web-asg \
--launch-template "LaunchTemplateId=$LT_ID" \
--min-size 1 --max-size 4 --desired-capacity 2 \
--vpc-zone-identifier "$SUBNET_A,$SUBNET_B" \
--target-group-arns "$TG_ARN"
echo "asg created, desired 2"
echo "$TG_ARN" > tg_arn.txt
$ docker network create aws17-lab
ce49fadba02f47ce575774bc2bc10c02344460e9c28c3220fb3c0931c52e02e7
$ docker run -d --name aws17-moto --network aws17-lab motoserver/moto:5.2.5
8b8e8104e0c3e342f4bd7fb5bb3f7962737b1cbcda032e62e08c8e0f25c3397a
$ until docker run --rm --network aws17-lab curlimages/curl:8.11.1 -s -o /dev/null http://aws17-moto:5000; do sleep 0.3; done
$ bash setup.sh
launch template: lt-dff54023308e714e6
target group: arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/web-tg/27541d7f65130784
load balancer: arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/web-alb/75086152ea00bf96
listener created
asg created, desired 2
Nothing here calls RegisterTargets — the ASG’s own --target-group-arns flag is what keeps the
target group in sync as instances come and go.
Both Instances Auto-Registered and Healthy
#!/usr/bin/env bash
set -e
aws() { MSYS_NO_PATHCONV=1 docker run --rm --network aws17-lab -e AWS_ACCESS_KEY_ID=test -e AWS_SECRET_ACCESS_KEY=test -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli:2.39.5 "$@" --endpoint-url http://aws17-moto:5000; }
TG_ARN=$(cat tg_arn.txt)
echo "asg instances:"
aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names web-asg \
--query 'AutoScalingGroups[0].Instances[].{InstanceId:InstanceId,LifecycleState:LifecycleState}'
echo "target group health:"
aws elbv2 describe-target-health --target-group-arn "$TG_ARN" \
--query 'TargetHealthDescriptions[].{Target:Target.Id,State:TargetHealth.State}'
$ bash check_asg.sh
asg instances:
[
{
"InstanceId": "i-063056c84879f5413",
"LifecycleState": "InService"
},
{
"InstanceId": "i-3de9736418a722bb6",
"LifecycleState": "InService"
}
]
target group health:
[
{
"Target": "i-063056c84879f5413",
"State": "healthy"
},
{
"Target": "i-3de9736418a722bb6",
"State": "healthy"
}
]
Both instances are InService in the ASG and healthy in the target group, with no explicit
registration step — exactly the wiring --target-group-arns was for. What moto can’t show here:
the “healthy” status is unconditional. There’s no real instance behind either ID, so there’s
nothing moto could check a health check against; a real target only reports healthy after actually
passing one.
Terminate One Instance, a Replacement Appears
#!/usr/bin/env bash
set -e
aws() { MSYS_NO_PATHCONV=1 docker run --rm --network aws17-lab -e AWS_ACCESS_KEY_ID=test -e AWS_SECRET_ACCESS_KEY=test -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli:2.39.5 "$@" --endpoint-url http://aws17-moto:5000; }
BEFORE=$(aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names web-asg \
--query 'AutoScalingGroups[0].Instances[].InstanceId' --output text)
echo "before: $BEFORE"
KILL=$(echo "$BEFORE" | cut -f1)
aws autoscaling terminate-instance-in-auto-scaling-group --instance-id "$KILL" \
--no-should-decrement-desired-capacity --query 'Activity.StatusCode' --output text
AFTER=$(aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names web-asg \
--query 'AutoScalingGroups[0].Instances[].InstanceId' --output text)
echo "after: $AFTER"
$ bash terminate.sh
before: i-063056c84879f5413 i-3de9736418a722bb6
InProgress
after: i-3de9736418a722bb6 i-337470dc5df6339fd
Still 2 instances afterward, but one ID changed — --no-should-decrement-desired-capacity tells
the ASG the terminated instance should be replaced to stay at DesiredCapacity, rather than
lowering the desired count by one. The replacement’s ID is proof the ASG actually launched a new
instance, not that it un-terminated the old one.
Practice
1. Set desired capacity to 6 — one above MaxSize (4) — and see that moto accepts it instead of rejecting it like real AWS would.
#!/usr/bin/env bash
set -e
aws() { MSYS_NO_PATHCONV=1 docker run --rm --network aws17-lab -e AWS_ACCESS_KEY_ID=test -e AWS_SECRET_ACCESS_KEY=test -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli:2.39.5 "$@" --endpoint-url http://aws17-moto:5000; }
aws autoscaling set-desired-capacity --auto-scaling-group-name web-asg --desired-capacity 6
aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names web-asg \
--query 'AutoScalingGroups[0].{Desired:DesiredCapacity,Max:MaxSize}'
$ bash practice1.sh
{
"Desired": 6,
"Max": 4
}
Desired: 6 sits right alongside Max: 4 — moto applied the request without the bounds check
real AWS’s API performs, which would reject this with a validation error instead. Never rely on
this emulator to catch an invalid desired-capacity value during local testing.
2. Check describe-scaling-activities after all the launches and terminations so far.
#!/usr/bin/env bash
aws() { MSYS_NO_PATHCONV=1 docker run --rm --network aws17-lab -e AWS_ACCESS_KEY_ID=test -e AWS_SECRET_ACCESS_KEY=test -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli:2.39.5 "$@" --endpoint-url http://aws17-moto:5000; }
aws autoscaling describe-scaling-activities --auto-scaling-group-name web-asg --query 'length(Activities)'
$ bash practice2.sh
0
0 — despite the create, the termination-triggered replacement, and the scale-out to 6 instances,
moto never records any scaling activity history. A real ASG’s console and API both show this as a
timeline; here there is none to inspect at all.
3. Check which Availability Zone every instance actually landed in, given the ASG spans two subnets in two different AZs.
#!/usr/bin/env bash
aws() { MSYS_NO_PATHCONV=1 docker run --rm --network aws17-lab -e AWS_ACCESS_KEY_ID=test -e AWS_SECRET_ACCESS_KEY=test -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli:2.39.5 "$@" --endpoint-url http://aws17-moto:5000; }
aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names web-asg \
--query 'AutoScalingGroups[0].Instances[].AvailabilityZone'
$ bash practice3.sh
[
"us-east-1a",
"us-east-1a",
"us-east-1a",
"us-east-1a",
"us-east-1a",
"us-east-1a"
]
Every single instance is in the same AZ, even though --vpc-zone-identifier named subnets in two
different AZs. Real AWS spreads instances across the ASG’s subnets’ AZs for fault tolerance; moto
doesn’t model that distribution logic at all.
4. Scale desired capacity down to 1 (without the no-decrement flag) and confirm an instance is actually terminated this time.
#!/usr/bin/env bash
set -e
aws() { MSYS_NO_PATHCONV=1 docker run --rm --network aws17-lab -e AWS_ACCESS_KEY_ID=test -e AWS_SECRET_ACCESS_KEY=test -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli:2.39.5 "$@" --endpoint-url http://aws17-moto:5000; }
BEFORE=$(aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names web-asg --query 'length(AutoScalingGroups[0].Instances)')
aws autoscaling update-auto-scaling-group --auto-scaling-group-name web-asg --desired-capacity 1 --min-size 1
sleep 1
AFTER=$(aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names web-asg --query 'length(AutoScalingGroups[0].Instances)')
echo "before: $BEFORE, after: $AFTER"
$ bash practice4.sh
before: 6, after: 1
The instance count drops to match the new DesiredCapacity — unlike Practice 1’s upward move
(accepted without validation), scaling down through UpdateAutoScalingGroup does actually
terminate the extra instances here; it’s specifically the over-MaxSize validation that moto
skips, not scaling mechanics in general.
Cleanup
$ docker rm -f aws17-moto
aws17-moto
$ docker network rm aws17-lab
aws17-lab
Next: ECS task definitions and services — a versioned control-plane artifact, with the actual container run separately to show what a task really is.