EC2 and Auto Scaling¶
What You'll Learn¶
- How to choose instance types, AMIs, and EBS volumes
- How to launch instances from launch templates with user data and IMDSv2
- How to reach instances with Session Manager instead of SSH and bastion hosts
- How Auto Scaling groups keep capacity healthy, roll out changes, and mix Spot capacity
Choosing an Instance Type¶
Instance names encode their purpose: m7g.large is family m (general purpose), generation 7, g for Graviton (ARM), size large.
| Family | Optimized for | Examples |
|---|---|---|
t |
Burstable, low baseline CPU | Small dev servers, bastions |
m |
General purpose, balanced | Web apps, most services |
c |
Compute | CPU-heavy APIs, batch, CI runners |
r, x |
Memory | Caches, in-memory databases, JVM-heavy apps |
i, d |
Local NVMe storage | Databases and search needing fast local disk |
g, p |
GPUs | ML training and inference |
Suffixes to know: g = Graviton (ARM), a = AMD, i = Intel, d = local NVMe, n = enhanced networking.
Graviton instances typically cost less than comparable x86 instances for the same performance. Most interpreted languages and containers built for arm64 run unchanged.
Burstable t instances
t instances earn CPU credits while idle and spend them under load. With unlimited mode off, a busy instance drops to its baseline CPU when credits run out — showing up as high CPU steal time and sudden slowness. Watch CPUCreditBalance.
AMIs¶
An Amazon Machine Image is the root disk template for an instance.
- Use current, vendor-maintained images: Amazon Linux 2023, Ubuntu LTS, or Bottlerocket for containers.
- Look up the latest AMI through a public SSM parameter instead of hard-coding IDs, which differ per region:
aws ssm get-parameter --name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64 \
--query Parameter.Value --output text
- For fleets, build your own golden AMI with Packer or EC2 Image Builder: patched, hardened, with agents preinstalled. Replace instances with a new AMI instead of patching them in place.
Launch Templates¶
A launch template captures everything needed to launch an instance, and it's versioned.
data "aws_ssm_parameter" "al2023" {
name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
}
resource "aws_launch_template" "orders" {
name_prefix = "orders-api-"
image_id = data.aws_ssm_parameter.al2023.value
instance_type = "m7g.large"
iam_instance_profile {
name = aws_iam_instance_profile.orders.name
}
vpc_security_group_ids = [aws_security_group.app.id]
metadata_options {
http_tokens = "required" # IMDSv2 only
http_put_response_hop_limit = 1 # containers on the host can't reach metadata through an extra hop
}
block_device_mappings {
device_name = "/dev/xvda"
ebs {
volume_type = "gp3"
volume_size = 30
encrypted = true
delete_on_termination = true
}
}
user_data = base64encode(templatefile("${path.module}/user-data.sh", {
app_version = var.app_version
}))
tag_specifications {
resource_type = "instance"
tags = { Service = "orders-api", Environment = "prod" }
}
}
User data¶
User data runs once, as root, at first boot:
#!/bin/bash
set -euo pipefail
dnf install -y amazon-cloudwatch-agent
aws s3 cp "s3://acme-artifacts/orders-api/${app_version}/orders-api.tar.gz" /tmp/
mkdir -p /opt/orders-api && tar xzf /tmp/orders-api.tar.gz -C /opt/orders-api
systemctl enable --now orders-api
Keep user data short: bake dependencies into the AMI, and pull only the application version at boot. Debug it with /var/log/cloud-init-output.log.
IMDSv2¶
The instance metadata service at 169.254.169.254 hands out the instance role's credentials. IMDSv1 answers any GET request, so a server-side request forgery bug in an application could leak credentials. IMDSv2 requires a session token obtained with a PUT, which SSRF attacks generally can't perform.
TOKEN=$(curl -sX PUT http://169.254.169.254/latest/api/token -H "X-aws-ec2-metadata-token-ttl-seconds: 300")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id
Require IMDSv2 on every launch template, and set it as the account default:
aws ec2 modify-instance-metadata-defaults --region eu-west-1 --http-tokens required --http-put-response-hop-limit 1
Access Without SSH: Session Manager¶
AWS Systems Manager Session Manager gives shell access through the SSM agent (preinstalled on Amazon Linux and recent Ubuntu AMIs) using IAM — no open inbound ports, no SSH keys, no bastion hosts, and every session is logged.
Requirements: the instance role includes AmazonSSMManagedInstanceCore, and the instance can reach the SSM endpoints (through NAT or interface endpoints).
aws ssm start-session --target i-0abc1234def567890
# Port forwarding to a private database through an instance
aws ssm start-session --target i-0abc1234def567890 \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"host":["orders.cluster-abc.eu-west-1.rds.amazonaws.com"],"portNumber":["5432"],"localPortNumber":["5432"]}'
EBS Volumes¶
| Type | Use | Notes |
|---|---|---|
| gp3 | Default for almost everything | 3,000 IOPS and 125 MB/s baseline regardless of size; raise IOPS and throughput independently |
| gp2 | Legacy | IOPS tied to size — migrate to gp3, usually cheaper for the same performance |
| io2 Block Express | Latency-sensitive databases | Provisioned IOPS, highest durability |
| st1, sc1 | Large sequential throughput, cold data | HDD, not for boot volumes |
- Enable EBS encryption by default per region:
aws ec2 enable-ebs-encryption-by-default. - Snapshots are incremental and stored in S3. Automate them with Data Lifecycle Manager or AWS Backup.
- Growing a volume is online — see Grow a Cloud Disk Without Downtime.
Auto Scaling Groups¶
An Auto Scaling group (ASG) keeps a fleet at the desired size across AZs, replaces unhealthy instances, and scales on demand.
resource "aws_autoscaling_group" "orders" {
name_prefix = "orders-api-"
vpc_zone_identifier = aws_subnet.app[*].id # spread across AZs
min_size = 3
max_size = 12
desired_capacity = 3
target_group_arns = [aws_lb_target_group.orders.arn]
health_check_type = "ELB" # replace instances that fail load balancer health checks
health_check_grace_period = 120
launch_template {
id = aws_launch_template.orders.id
version = aws_launch_template.orders.latest_version
}
instance_refresh {
strategy = "Rolling"
preferences {
min_healthy_percentage = 90
instance_warmup = 120
auto_rollback = true
}
}
}
resource "aws_autoscaling_policy" "cpu" {
name = "target-cpu-50"
autoscaling_group_name = aws_autoscaling_group.orders.name
policy_type = "TargetTrackingScaling"
target_tracking_configuration {
predefined_metric_specification {
predefined_metric_type = "ASGAverageCPUUtilization"
}
target_value = 50
}
}
Key points:
health_check_type = "ELB"— the defaultEC2check only notices dead hardware, not a crashed application.- Target tracking keeps a metric near a target, like a thermostat. Prefer it to step scaling. For request-driven services,
ALBRequestCountPerTargetoften tracks load better than CPU. - Instance refresh rolls a new launch template version through the fleet with health checks and automatic rollback — a safe deployment mechanism for instance-based services.
- Lifecycle hooks pause launch or termination so you can register, drain, or ship logs.
Spot Instances¶
Spot capacity costs far less than On-Demand, but AWS can reclaim it with a two-minute warning.
Good for stateless services behind load balancers, CI runners, batch jobs, and Kubernetes worker nodes. Not for single instances holding state.
mixed_instances_policy {
instances_distribution {
on_demand_base_capacity = 2 # always keep 2 On-Demand
on_demand_percentage_above_base_capacity = 25 # then 25% On-Demand, 75% Spot
spot_allocation_strategy = "price-capacity-optimized"
}
launch_template {
launch_template_specification {
launch_template_id = aws_launch_template.orders.id
version = "$Latest"
}
override { instance_type = "m7g.large" }
override { instance_type = "m6g.large" }
override { instance_type = "c7g.xlarge" }
}
}
Diversify across several instance types and all AZs so a shortage in one pool doesn't take out your capacity. Handle the interruption notice by draining work — ASG capacity rebalancing launches replacements proactively when AWS signals elevated interruption risk.
Common Mistakes¶
- Hard-coded AMI IDs that differ by region and go stale with unpatched images.
- IMDSv1 still allowed, leaving instance credentials exposed to SSRF bugs.
- Port 22 open to the internet with shared SSH keys instead of Session Manager.
- ASGs using the default
EC2health check, so crashed apps keep receiving traffic. - gp2 volumes sized up just to get more IOPS, instead of gp3.
- Spot fleets using a single instance type in a single AZ.
- Burstable instances in production that silently throttle when CPU credits run out.
Interview Questions¶
- How do you choose between
m,c, andrinstance families? What does thegsuffix mean? - Why require IMDSv2?
- How would you give engineers shell access to private instances without SSH or a bastion?
- How does an Auto Scaling group decide to replace an instance? Why use ELB health checks?
- How would you roll out a new AMI to a fleet with zero downtime?
- When are Spot instances a good fit, and how do you design for interruptions?
Next¶
Continue to Load Balancing and Route 53.