Auto scaling
Adding servers when busy, replacing them when they die.
Part 1: The problem
QuickCart ran on two servers behind a load balancer. That was plenty for a normal evening.
Then marketing sent a push notification: free delivery tonight. Within minutes, orders were five times higher than usual. Both servers hit 100% CPU. Pages took ten seconds, then started timing out. Sam launched two more servers by hand and added them to the load balancer. By the time they were ready, the rush was over — and the two extra servers ran, unused, for the rest of the week.
A month later, one server died at 3am. Nobody noticed. The other server carried everything alone until Priya saw the alarm at breakfast.
Two problems, one cause: the number of servers was decided by a person, and people are slow, asleep, or busy.
QuickCart needs something that adds servers when it’s busy, removes them when it’s quiet, and replaces any that die. That’s an Auto Scaling group.
Part 2: The idea
An Auto Scaling group looks after a set of identical servers. You tell it how many you want. It keeps checking how many are running, and fixes any difference: launching a server when there are too few, terminating one when there are too many.
You stop managing servers one by one. You manage a number.
Three numbers
| Number | What it means | QuickCart’s value |
|---|---|---|
| Minimum | Never fewer than this, whatever happens | 2 — one in each zone |
| Desired | How many there should be right now | 2 on a normal day. Scaling changes this number |
| Maximum | Never more than this, however busy | 4 — a limit that protects the bill |
Scaling never touches servers directly. It only changes desired. Then the group launches or terminates servers to match.
The recipe: a launch template
To launch a server, the group needs to know what kind. It reads a launch template: everything you’d choose when launching a server on EC2, saved once.
| Part of the template | QuickCart uses |
|---|---|
| Image (AMI) | Amazon Linux 2023 |
| Instance type | t3.micro |
| Security group | The web servers’ group |
| Instance profile | A role that allows Session Manager, so no SSH keys |
| Metadata version | IMDSv2 only |
| User data | The start-up script that installs and starts the app |
Launch templates are versioned. To change something — a newer image, say — you make version 2 and tell the group to use it. Version 1 stays, so going back is one click.
The template leaves out the subnet. The group picks subnets, because it decides where each server goes.
Spreading across zones
You give the group subnets in two or more availability zones. It keeps the servers balanced between them: with four servers and two zones, two in each.
If a whole zone fails, the servers there are lost. The group notices it’s short and launches replacements in the zones that still work. That’s why QuickCart’s minimum is 2: never fewer than one per zone.
How the group knows a server is broken
The group replaces any server it thinks is unhealthy. What “unhealthy” means depends on which checks you turn on:
| Health check | Asks | Catches | Misses |
|---|---|---|---|
| EC2 — always on | Is the machine running? | A server that stopped, was terminated, or failed its hardware checks | An app that crashed on a machine that’s still running |
| ELB — turn it on when you use a load balancer | Does the app answer on its health path? | A crashed or stuck app | — |
With EC2 checks alone, a server whose app has crashed looks fine forever. It stays in the group, doing nothing useful. With a load balancer in front, turn on ELB health checks too, so the group replaces servers the load balancer has given up on.
The grace period
A new server needs time to boot and run its start-up script before its app can answer. For the health check grace period, the group ignores health checks on a new server. The default is 300 seconds.
Scaling: changing desired on its own
A scaling policy changes the desired number for you.
| Policy | How it works | Use it when |
|---|---|---|
| Target tracking | You pick a metric and a target, like average CPU 50%. The group adds or removes servers to stay near it | Almost always. Start here |
| Step scaling | You set thresholds: above 70%, add 2; above 90%, add 4 | You need finer control than target tracking gives |
| Scheduled | At a set time, set desired to a number: every day at 6pm, 4 | Busy times you can predict, like QuickCart’s dinner rush |
Target tracking works like a thermostat. Behind the scenes it creates two CloudWatch alarms: one that adds servers when the metric stays above the target, and one that removes them when it stays well below. It adds quickly and removes slowly, so a short dip doesn’t throw away servers you’ll need again a minute later.
The instance warmup setting tells target tracking to ignore a brand-new server’s metrics for a while. A server that’s still booting shows low CPU, which would otherwise make the average look better than it is.
Terminating a server on purpose
Terminating a server from the EC2 console is a hard stop. The load balancer keeps sending it requests until its health checks notice, and any requests it was handling fail.
Asking the group to terminate it is gentler. The group first takes it out of the load balancer, waits for the deregistration delay so requests in progress can finish, and only then terminates it. The CLI is the clearest way to do this:
aws autoscaling terminate-instance-in-auto-scaling-group --instance-id INSTANCE_ID --no-should-decrement-desired-capacity| Part | What it means |
|---|---|
aws autoscaling | the Auto Scaling part of the AWS CLI |
terminate-instance-in-auto-scaling-group | ask the group to remove one of its servers, draining it first |
--instance-id INSTANCE_ID | which server. Replace INSTANCE_ID with the ID from EC2 → Instances, like i-0123abcd |
--no-should-decrement-desired-capacity | keep desired the same, so the group launches a replacement. Use --should-decrement-desired-capacity to shrink the group by one instead |
To replace every server — after a new launch template version, for example — use Instance refresh on the group. It replaces servers in batches, waiting for each new one to pass its health checks. That’s a rolling release, done for you.
What this means for your app
Servers now come and go without warning. So nothing important can live on one server: not uploaded files (they go to S3), not orders (they go to the database), not a customer’s login session. Any server must be able to handle any request, and losing one must lose nothing.
Part 3: See it
Below is QuickCart’s group: two zones, minimum 2, maximum 4, target tracking at 50% CPU. Raise the traffic and watch the group add servers. Then terminate a server and watch it come back. Try the dinner rush to see what the maximum does.
- Average CPU
- 30%
- Target
- 50%
- Desired
- 2
- In service
- 2
- Min / max
- 2 / 4
Zone a
- web 1 In service CPU 30%
Zone b
- web 2 In service CPU 30%
What to notice: a new server doesn’t help straight away. It boots, sits out its grace period, and only then takes traffic. That delay is why teams schedule extra servers before a rush they can predict, instead of waiting for CPU to climb.
Part 4: Do it
Costs money — two t3.micro servers, at about $0.0104 an hour each on demand in us-east-1, plus about $0.005 an hour for each server’s public IP address. Up to four servers run during the scale-out step, and detailed monitoring adds a little. Well under $0.25 if you clean up within two hours. Prices change; check the EC2 pricing page.
You’ll build a launch template and a group of two servers across two zones, reach a server through Session Manager, kill one and watch it come back, then load the servers until the group scales out. About 45 minutes, most of it watching.
This lab uses your account’s default VPC, which already has a public subnet in every zone. That keeps it cheap: no NAT gateway. The servers get public addresses so they can reach Systems Manager, but their security group lets nothing in. Check the region selector says N. Virginia (us-east-1).
Create a security group for the servers
In the AWS console: EC2Security GroupsCreate security group
Field Enter Security group name quickcart-web-sgDescription QuickCart web serversVPC the default VPC Inbound rules none — delete any row the form adds Outbound rules leave the default: All traffic to 0.0.0.0/0Choose Create security group.
You should seeThe group, with 0 inbound rules and 1 outbound rule. Session Manager needs no inbound rule: the agent on the server connects out to Systems Manager.
Create a role for Session Manager
If you still have a role with
AmazonSSMManagedInstanceCorefrom Servers on EC2, you can use that and skip this step.In the AWS console: IAMRolesCreate role
Field Enter Trusted entity type AWS service Service or use case EC2, then the use case EC2 Permissions policies search for and tick AmazonSSMManagedInstanceCoreRole name quickcart-web-roleChoose Create role. The console also creates an instance profile with the same name — the wrapper that lets a server wear the role.
Create the launch template
In the AWS console: EC2Launch TemplatesCreate launch template
Field Enter Launch template name quickcart-web-ltTemplate version description nginx with a health pageAuto Scaling guidance tick Provide guidance to help me set up a template that I can use with EC2 Auto Scaling Application and OS Images Quick Start → Amazon Linux → Amazon Linux 2023 AMI, architecture 64-bit (x86) Instance type t3.microKey pair name Don’t include in launch template Subnet Don’t include in launch template — the group chooses Firewall (security groups) Select existing security group → quickcart-web-sgThen open Advanced details:
Field Enter IAM instance profile quickcart-web-roleDetailed CloudWatch monitoring Enable — CPU every minute instead of every five, so scaling reacts sooner. It costs a little extra Metadata accessible Enabled Metadata version V2 only (token required) User data the script below user-data.sh#!/bin/bash dnf install -y nginx echo "QuickCart web server" > /usr/share/nginx/html/index.html echo "ok" > /usr/share/nginx/html/health systemctl enable --now nginxLine What it does #!/bin/bashrun this script with bash. User data must start with this line, or it isn’t run as a script dnf install -y nginxinstall the nginx web server. -yanswers yes, because nobody is there to type itecho "QuickCart web server" > /usr/share/nginx/html/index.htmlwrite a home page. >sends the text into that fileecho "ok" > /usr/share/nginx/html/healthwrite a tiny page at /healthfor health checks to ask forsystemctl enable --now nginxstart nginx now, and every time the server boots The script runs once, as root, on first boot. Choose Create launch template.
You should seeSuccessfully created quickcart-web-lt, with version 1.
Create the Auto Scaling group
In the AWS console: EC2Auto Scaling groupsCreate Auto Scaling group
The wizard has several pages. The page names change from time to time; the fields are what matter.
Choose launch template
Field Enter Auto Scaling group name quickcart-web-asgLaunch template quickcart-web-ltVersion Default (1) Choose instance launch options
Field Enter VPC the default VPC Availability Zones and subnets the default subnets in us-east-1a and us-east-1b Availability Zone distribution Balanced best effort Integrate with other services
Field Enter Load balancing No load balancer Turn on Elastic Load Balancing health checks leave unticked — there’s no load balancer Health check grace period 300secondsConfigure group size and scaling
Field Enter Desired capacity 2Min desired capacity 2Max desired capacity 4Automatic scaling Target tracking scaling policy Metric type Average CPU utilization Target value 50Instance warmup 120secondsInstance maintenance policy No policy Skip notifications. On Add tags, add Key
Name, Valuequickcart-web, so the servers have a name in the instance list. Then Create Auto Scaling group.Watch the group launch its servers
In the AWS console: EC2Auto Scaling groupsquickcart-web-asgActivity
The Activity history shows lines likeLaunching a new EC2 instance: i-0123abcd Cause: At 2026-09-25T10:02:14Z an instance was started in response to a difference between desired and actual capacity, increasing the capacity from 0 to 2.Your IDs and times will differ. Now open EC2 → Instances.
You should seeTwo instances called
quickcart-web, one in us-east-1a and one in us-east-1b, both Running. You didn’t launch either of them.Check the app from inside a server
Wait a couple of minutes after the servers show Running, so the agent and the start-up script can finish.
In the AWS console: EC2Instancesone quickcart-web instanceConnectSession ManagerConnect
A terminal opens in your browser. Ask nginx for the health page:
Terminalcurl -s localhost/healthPart What it means curlfetch a web address and print what comes back -ssilent: no progress bar localhost/healththe /healthpage on this same serverYou should seeokThe user data ran, and no SSH key or open port was needed to check. Leave this tab open; you’ll use it in a later step.
Kill a server and watch it come back
In the AWS console: EC2Instancesthe other quickcart-web instanceInstance stateTerminate (delete) instance
Confirm. Then go back to the group’s Activity tab and refresh it every half-minute or so.
Within a minute or two, lines likeTerminating EC2 instance: i-0456cdef Cause: an instance was taken out of service in response to an EC2 health check indicating it has been terminated or stopped. Launching a new EC2 instance: i-0789abcd Cause: an instance was started in response to a difference between desired and actual capacity, increasing the capacity from 1 to 2.You should seeIn EC2 → Instances, the terminated server and a brand-new one, launched in the same zone to keep the group balanced. That’s self-healing: nobody launched it.
Load the servers and watch the group scale out
To push the average CPU above 50%, load both servers. A
t3.microhas two virtual CPUs, so you run two busy processes on each.In the Session Manager tab from before, run this twice:
Terminaltimeout 900 yes > /dev/null &Part What it means yesa tiny program that prints yforever, as fast as it can. It keeps one CPU at 100%> /dev/nullthrow the output away, so it doesn’t fill your screen timeout 900stop it automatically after 900 seconds (15 minutes), so you can’t forget it &run it in the background and give you the prompt back Each time, it prints a job number and a process ID, like[1] 4321Now connect to the other server the same way (Connect → Session Manager) and run the same command twice there too.
Watch the group’s Activity tab. After about five minutes:
A line likeCause: At 2026-09-25T10:31:40Z a monitor alarm TargetTracking-quickcart-web-asg-AlarmHigh-4f2c9e1a in state ALARM triggered policy Target Tracking Policy changing the desired capacity from 2 to 4.You should seeDesired capacity: 4 on the group’s Details tab, and two new servers launching. With the new servers idle, the average CPU across all four comes back towards 50%. In CloudWatch → Alarms you’ll find the two alarms target tracking made for you: one AlarmHigh and one AlarmLow.
After 15 minutes the
yesprocesses stop by themselves. About 15 minutes after that, the low alarm lowers desired back to 2 and the group terminates two servers. You don’t need to wait for it — scaling in slowly is deliberate. Go on to clean up.Clean-up step: Clean up
Order matters: the group first. Delete the servers first and the group just launches more.
In the AWS console: EC2Auto Scaling groupsquickcart-web-asgActionsDelete
Type
deleteand confirm. The group sets its capacity to 0, terminates every server, and then disappears. It takes a few minutes. The alarms target tracking made are deleted with it.You should seeEC2 → Instances shows every
quickcart-webserver as Terminated, and the group is gone from the list.Then remove the rest:
- EC2 → Launch Templates →
quickcart-web-lt→ Actions → Delete template, and typeDeleteto confirm. - EC2 → Security Groups →
quickcart-web-sg→ Actions → Delete security groups. If it says the group is in use, wait until every server has finished terminating. - IAM → Roles →
quickcart-web-role→ Delete, unless you’re keeping it for the next lessons.
- EC2 → Launch Templates →
Part 5: Check yourself
An Auto Scaling group has min 2, desired 2 and max 4. You terminate one of its two servers from the EC2 console. What happens next?
The app on one server has crashed, but the server itself is still running. The group uses EC2 health checks only. What does the group do?
The app takes about 90 seconds to start. The group uses ELB health checks with a 30-second grace period. What happens?
Target tracking aims for 50% average CPU. Max is 4. A rush needs six servers' worth of CPU. What happens?
Why does QuickCart pick subnets in two availability zones for its group?
Part 6: Cheat sheet
| Term | In one line |
|---|---|
| Auto Scaling group | Keeps the number of running servers equal to desired |
| Min / desired / max | Never fewer / right now / never more. Scaling only changes desired |
| Launch template | The recipe for each server. Versioned; no subnet |
| Zones | Give the group subnets in two or more; it balances between them |
| EC2 health check | Is the machine running? Always on |
| ELB health check | Does the app answer? Turn on when behind a load balancer |
| Grace period | Time before health checks count. Longer than real start-up time |
| Target tracking | Keep a metric near a target, like average CPU 50%. Out fast, in slowly |
| Terminate gracefully | Through the group, so it drains first |
| Instance refresh | Replace every server in batches, for a new template version |
| Clean up | Delete the group first. Deleting servers just makes it launch more |