EC2 Instance Types Explained — Stop Paying for Resources You Don't Need
Here's a scenario that happens every single day: a team launches a c5.4xlarge for an app that uses 8% CPU. That's $500/month wasted. Multiply by 50 instances and you're throwing away $25,000 every month. Let's fix that.
Understanding Instance Families
EC2 instance types follow a naming convention: c5.xlarge means family (c) + generation (5) + size (xlarge). Each family is optimized for different workloads.
| Family | Optimized For | Example Types | Real Use Case |
|---|---|---|---|
| t3/t3a | Burstable general purpose | t3.micro - t3.2xlarge | Dev/test environments, low-traffic web apps |
| m5/m6i | Balanced compute/memory | m5.large - m5.24xlarge | App servers, backend APIs, mid-size databases |
| c5/c6i | Compute-intensive | c5.large - c5.24xlarge | Batch processing, video encoding, ML inference |
| r5/r6i | Memory-intensive | r5.large - r5.24xlarge | In-memory caches (Redis), real-time analytics |
| g4dn/g5 | GPU-accelerated | g4dn.xlarge - g4dn.16xlarge | ML training, video transcoding, 3D rendering |
| i3/i3en | Storage-optimized | i3.large - i3.16xlarge | Data warehouses, distributed file systems |
The a suffix (like t3a, m5a) means AMD processors — typically 10% cheaper with comparable performance.
Pricing Models — This Is Where Real Savings Happen
The biggest cost optimization lever in AWS isn't choosing the right instance type. It's choosing the right pricing model.
| Pricing Model | Discount vs On-Demand | Commitment | Best For |
|---|---|---|---|
| On-Demand | 0% (baseline) | None | Unpredictable workloads, short-term testing |
| Reserved (1yr) | ~40% | 1 year | Steady-state production workloads |
| Reserved (3yr) | ~60% | 3 years | Core infrastructure you know you'll keep |
| Savings Plans | ~40-60% | 1-3 years | Flexible commitment across instance families |
| Spot Instances | Up to 90% | None (can be interrupted) | Batch jobs, CI/CD runners, stateless workers |
# Launch a spot instance (up to 90% cheaper)
aws ec2 run-instances \
--image-id ami-0abcdef1234567890 \
--instance-type c5.xlarge \
--key-name my-key \
--instance-market-options '{
"MarketType": "spot",
"SpotOptions": {
"MaxPrice": "0.08",
"SpotInstanceType": "one-time",
"InstanceInterruptionBehavior": "terminate"
}
}'
# Check current spot prices
aws ec2 describe-spot-price-history \
--instance-types c5.xlarge \
--product-descriptions "Linux/UNIX" \
--start-time $(date -u +"%Y-%m-%dT%H:%M:%SZ") \
--query 'SpotPriceHistory[*].[AvailabilityZone,SpotPrice]' \
--output table
Key Pairs and Security Groups
Before launching any instance, you need an SSH key pair and a security group:
# Create a key pair
aws ec2 create-key-pair \
--key-name my-app-key \
--key-type ed25519 \
--query 'KeyMaterial' \
--output text > my-app-key.pem
chmod 400 my-app-key.pem
# Create a security group
aws ec2 create-security-group \
--group-name web-server-sg \
--description "Allow HTTP, HTTPS, and SSH" \
--vpc-id vpc-0abc123def456
# Add rules — be specific with source IPs
aws ec2 authorize-security-group-ingress \
--group-id sg-0abc123 \
--protocol tcp --port 22 \
--cidr 203.0.113.50/32
aws ec2 authorize-security-group-ingress \
--group-id sg-0abc123 \
--protocol tcp --port 443 \
--cidr 0.0.0.0/0
Never open SSH (port 22) to 0.0.0.0/0. Restrict it to your IP or use Systems Manager Session Manager instead.
User Data Scripts — Automate Instance Setup
User data runs on first boot. It's perfect for installing packages, pulling code, and starting services:
# Launch an instance with user data
aws ec2 run-instances \
--image-id ami-0abcdef1234567890 \
--instance-type t3.medium \
--key-name my-app-key \
--security-group-ids sg-0abc123 \
--user-data file://setup.sh \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=web-server-01},{Key=Environment,Value=production}]'
Example setup.sh:
#!/bin/bash
yum update -y
yum install -y nginx
systemctl enable nginx
systemctl start nginx
echo "<h1>Server $(hostname)</h1>" > /usr/share/nginx/html/index.html
EBS Volume Types — Choose Wisely
| Volume Type | IOPS | Throughput | Cost (per GB/month) | Use Case |
|---|---|---|---|---|
| gp3 | 3,000 (baseline), up to 16,000 | 125-1,000 MB/s | $0.08 | Default for most workloads |
| gp2 | Burst to 3,000 | 128-250 MB/s | $0.10 | Legacy, migrate to gp3 |
| io2 | Up to 64,000 | 1,000 MB/s | $0.125 + $0.065/IOPS | Databases needing consistent IOPS |
| st1 | 500 baseline | 500 MB/s | $0.045 | Big data, log processing |
| sc1 | 250 baseline | 250 MB/s | $0.015 | Cold storage, infrequent access |
# Create a gp3 volume with custom performance
aws ec2 create-volume \
--volume-type gp3 \
--size 100 \
--iops 5000 \
--throughput 250 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=volume,Tags=[{Key=Name,Value=app-data}]'
Pro tip: gp3 is almost always better than gp2. It's 20% cheaper and you can independently configure IOPS and throughput.
Instance Metadata Service v2 (IMDSv2)
The instance metadata service lets EC2 instances discover information about themselves. IMDSv1 was vulnerable to SSRF attacks, so always enforce IMDSv2:
# Enforce IMDSv2 on a new instance
aws ec2 run-instances \
--image-id ami-0abcdef1234567890 \
--instance-type t3.medium \
--metadata-options "HttpTokens=required,HttpPutResponseHopLimit=1,HttpEndpoint=enabled"
# Enforce IMDSv2 on an existing instance
aws ec2 modify-instance-metadata-options \
--instance-id i-0abc123def456 \
--http-tokens required \
--http-put-response-hop-limit 1
Using IMDSv2 from inside the instance:
# Get a token (valid for 6 hours)
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
# Use the token to query metadata
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/instance-id
Right-Sizing with CloudWatch
You can't optimize what you don't measure. Set up CloudWatch to track actual utilization:
# Get average CPU for the last 7 days
aws cloudwatch get-metric-statistics \
--namespace AWS/EC2 \
--metric-name CPUUtilization \
--dimensions Name=InstanceId,Value=i-0abc123def456 \
--start-time $(date -u -d '7 days ago' +"%Y-%m-%dT%H:%M:%SZ") \
--end-time $(date -u +"%Y-%m-%dT%H:%M:%SZ") \
--period 3600 \
--statistics Average \
--output table
If your average CPU is below 20%, you're probably one or two sizes too big. A m5.xlarge running at 15% CPU should be a m5.large or even a t3.large.
What's Next?
Your instances need a network to live in. Next up: VPC networking from scratch — we'll build subnets, configure NAT gateways, and set up security groups and NACLs the right way.
This is Part 4 of our AWS series. Right-size your instances before anything else — it's the easiest money you'll ever save.
