AWS EC2
How EC2 instances actually work. Families and sizing, pricing models, EBS vs instance store, IMDSv2, Session Manager instead of SSH, and stop vs terminate.
Amazon EC2
EC2 gives you virtual machines. That sounds unremarkable until you consider that the decisions around them, which family, which pricing model, which storage, how you get a shell, account for a large share of most AWS bills and most AWS security findings.
EC2: Instance Lifecycle
Stopped instances retain their EBS root volume. You only stop paying for compute
Reading an instance type
m6i.xlarge decomposes into four parts:
| Part | Means |
|---|---|
m | Family (general purpose) |
6 | Generation: higher is newer, faster and usually cheaper per unit |
i | Processor: i Intel, a AMD, g Graviton (ARM) |
xlarge | Size within the family |
| Family | For |
|---|---|
t | Burstable (dev, low-traffic services) |
m | Balanced general purpose |
c | Compute optimised (CPU-bound work, build agents) |
r, x | Memory optimised (databases, caches) |
i, d | Storage optimised (local NVMe) |
p, g, inf | GPU and ML accelerators |
Two easy wins hide in that table. Always use the newest generation available. An m7i typically costs less than an m5 and performs better. And Graviton instances are roughly 20% cheaper for equivalent performance; if your workload is a container, an interpreted language or anything with an ARM build, the migration is often trivial and the saving is permanent.
Burstable instances have a credit system
t family instances earn CPU credits while below their baseline and spend them above it. Run out and the instance is throttled hard, sometimes to 20% of a core, which presents as an application that is inexplicably slow while CloudWatch shows low CPU.
aws cloudwatch get-metric-statistics \
--namespace AWS/EC2 --metric-name CPUCreditBalance \
--dimensions Name=InstanceId,Value=i-0abc123 \
--start-time 2026-09-11T00:00:00Z --end-time 2026-09-12T00:00:00Z \
--period 3600 --statistics Average
unlimited mode lets them burst past the balance for an extra charge, which is fine until a runaway process produces a surprising bill. For anything with sustained load, an m family instance is both cheaper and more predictable.
Pricing
| Model | Discount | Trade-off |
|---|---|---|
| On-Demand | None | Maximum flexibility |
| Savings Plans | Up to ~72% | 1 or 3 year spend commitment |
| Reserved Instances | Up to ~72% | Commitment tied to instance attributes |
| Spot | Up to ~90% | Can be reclaimed with 2 minutes' notice |
| Dedicated Hosts | () | Licensing and compliance requirements |
Compute Savings Plans are the pragmatic default for steady workloads: they commit to an hourly spend rather than a specific instance type, so they keep applying as you change families, sizes and regions. Reserved Instances are more rigid and usually only worth it for a workload you are certain will not change.
Spot is the largest single saving available and is underused. Anything fault-tolerant. CI runners, batch jobs, stateless web tiers behind a load balancer, Kubernetes worker nodes. Can run on Spot with a mixed-instances policy that falls back to On-Demand. Handle the two-minute interruption notice and the risk is manageable.
Storage: EBS and instance store
EBS is network-attached block storage that persists independently of the instance.
| Type | Use |
|---|---|
gp3 | The default. Baseline 3,000 IOPS and 125 MB/s, independent of size |
gp2 | Legacy. IOPS scale with size (migrate to gp3) |
io2 | High, provisioned IOPS for demanding databases |
st1, sc1 | Throughput-optimised HDD for large sequential data |
Moving gp2 volumes to gp3 is typically around 20% cheaper and faster, because gp3 no longer ties performance to volume size. It is a live change with no downtime:
aws ec2 modify-volume --volume-id vol-0abc123 --volume-type gp3
Instance store is physically attached NVMe. It is far faster and entirely ephemeral: stopping the instance loses it permanently. Use it for caches, scratch space and temporary build output, never for anything you need to keep.
Two EBS defaults worth changing: enable encryption by default at the account level, and note that the root volume is set to delete on termination while additional volumes are not.
Getting a shell: use Session Manager, not SSH
The traditional approach, a key pair, port 22 open, a bastion host, has a lot of moving parts to get wrong. SSM Session Manager replaces all of it:
aws ssm start-session --target i-0abc123
No open inbound ports, no key pairs to distribute or rotate, no bastion to maintain. Access is controlled by IAM, every session is logged to CloudTrail, and it works for instances in private subnets with no public IP at all.
It needs the SSM agent (preinstalled on current Amazon Linux and Ubuntu AMIs) and an instance profile with AmazonSSMManagedInstanceCore. That is the whole setup.
If you still need SSH, at least restrict the security group to a known CIDR rather than 0.0.0.0/0. An open port 22 is found by automated scanners within minutes.
User data and metadata
User data runs on first boot:
#!/bin/bash
dnf install -y nginx
systemctl enable --now nginx
It runs once, as root, on the first start only. A common confusion when people expect it on every reboot. Output goes to /var/log/cloud-init-output.log, which is the first place to look when an instance comes up wrong.
The instance metadata service provides instance identity and, importantly, the temporary credentials from its IAM role. Require IMDSv2:
aws ec2 modify-instance-metadata-options \
--instance-id i-0abc123 \
--http-tokens required \
--http-endpoint enabled
IMDSv1 answered a plain GET, which meant a server-side request forgery bug in your application could be used to read the instance's AWS credentials. IMDSv2 requires a session token obtained by PUT, which closes that path. New instances default to requiring it; older ones need this change.
Stop, terminate, hibernate
| Action | Instance | Root EBS | Instance store | Public IP |
|---|---|---|---|---|
| Stop | Halted, no compute charge | Kept, still billed | Lost | Lost unless Elastic IP |
| Terminate | Gone | Deleted by default | Lost | Lost |
| Hibernate | RAM saved to disk | Kept | Lost | Lost unless Elastic IP |
Two things catch people. A stopped instance still bills for its EBS volumes. Stopping saves the compute charge only. And a stopped instance gets a new public IP when restarted, which breaks anything pointing at the old one; attach an Elastic IP, or better, put a load balancer in front.
Enable termination protection on anything that matters:
aws ec2 modify-instance-attribute --instance-id i-0abc123 --disable-api-termination
Treat instances as replaceable
The pattern that saves the most operational pain: define the instance in a launch template, bake or bootstrap it reproducibly, and put it in an Auto Scaling group even if the desired capacity is one.
Then a failed instance is replaced automatically rather than repaired by hand at 3am, and nothing has state that only exists on one machine.
Go deeper in this section
| Sub-topic | What you will learn |
|---|---|
| Launching an Instance | AMIs, launch options and first boot |
| Instance Types | Choosing families and sizes |
| Security Groups | Stateful firewall rules and tiered access |
| Key Pairs | SSH keys and safer alternatives |
Quick reference
| Task | Command |
|---|---|
| List running instances | aws ec2 describe-instances --filters Name=instance-state-name,Values=running |
| Shell without SSH | aws ssm start-session --target <id> |
| Stop / start | aws ec2 stop-instances --instance-ids <id> |
| Terminate | aws ec2 terminate-instances --instance-ids <id> |
| Require IMDSv2 | aws ec2 modify-instance-metadata-options --instance-id <id> --http-tokens required |
| gp2 to gp3 | aws ec2 modify-volume --volume-id <id> --volume-type gp3 |
| Console output | aws ec2 get-console-output --instance-id <id> |
| Latest Amazon Linux AMI | aws ssm get-parameter --name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 --query Parameter.Value |
Key takeaways
- Use the newest generation, and consider Graviton: both are cheaper and faster for most workloads.
tinstances throttle when credits run out, which looks like a mysterious slowdown at low reported CPU.- Compute Savings Plans for steady load, Spot for anything fault-tolerant. Spot is the biggest saving most teams ignore.
- Migrate
gp2togp3: cheaper and faster, changed live with no downtime. - Use SSM Session Manager instead of SSH. No open ports, no keys, IAM-controlled and fully audited.
- Require IMDSv2, which closes the SSRF path to your instance's AWS credentials.
- Stopping still bills for EBS, and restarting gives a new public IP.
Check your understanding
Slow at low CPU
A t3.medium web server becomes very slow under moderate load, but CloudWatch shows CPU utilisation around 20%. What is the likely cause?
Why IMDSv2 matters
What class of vulnerability does requiring IMDSv2 on an EC2 instance mitigate?
Frequently Asked Questions
How do I choose an EC2 instance type?
Start with the family that matches the bottleneck, m for balanced workloads, c when CPU-bound, r when memory-bound, t only for genuinely bursty or low-traffic work. Then pick the newest generation available, since newer generations are typically both faster and cheaper. Consider Graviton (g in the name, such as m7g) for around 20% saving where your workload runs on ARM, which most containerised and interpreted applications do. Size from measurement rather than estimate, and use Compute Optimizer, which recommends right-sizing from real utilisation data.
What is the difference between stopping and terminating an instance?
Stopping halts the instance and ends compute charges, but the root EBS volume is preserved and still billed, and you can start it again. Terminating destroys the instance and, by default, deletes its root volume permanently. Two details surprise people regularly: a stopped instance still costs money for its storage, and restarting a stopped instance assigns a new public IP unless an Elastic IP is attached. Data on instance store volumes is lost on stop as well as on terminate.
How do I connect to an EC2 instance without SSH keys?
Use SSM Session Manager: aws ssm start-session --target i-0abc123. It needs the SSM agent, which is preinstalled on current Amazon Linux and Ubuntu AMIs, and an instance profile with the AmazonSSMManagedInstanceCore policy. There are no inbound ports to open, no keys to distribute or rotate, and no bastion host. Access is granted through IAM and every session is recorded in CloudTrail. It works for instances in private subnets with no public IP at all.
How can I reduce my EC2 costs?
In order of impact: buy Compute Savings Plans for baseline steady-state usage, which discount up to around 72% while remaining flexible across instance families and regions. Move fault-tolerant workloads, CI runners, batch processing, stateless web tiers, Kubernetes nodes, to Spot for up to 90% off. Migrate to Graviton where the workload supports ARM. Upgrade to the newest instance generation and convert gp2 volumes to gp3. Then find idle and oversized instances with Compute Optimizer, and stop non-production instances outside working hours.
What is IMDSv2 and should I enforce it?
The instance metadata service provides instance identity and the temporary credentials from the instance's IAM role. IMDSv1 served these over a plain GET request, which meant a server-side request forgery flaw in your application could be used to read those credentials and act as the instance in AWS. IMDSv2 requires a session token obtained via PUT, which closes that route. Enforce it, aws ec2 modify-instance-metadata-options --http-tokens required, on every instance. New instances default to requiring it, but older ones do not.
Why doesn't my user data script run when I restart the instance?
Because user data executes only on the first boot by design. Cloud-init records that it has run and skips it thereafter, so changes to the script have no effect on an existing instance. For configuration that must apply on every boot, use a systemd unit or a configuration management tool instead. When a first boot does not produce the expected result, /var/log/cloud-init-output.log on the instance contains the script's output and is almost always where the answer is.