AWS – EC2: Instances, Storage and What They Cost

June 4, 20244 min readUpdated 8/24/2026

EC2 is a virtual machine you rent by the second. Almost everything else in AWS is either built on it or exists to save you from managing it, so it is worth understanding even if you end up running containers instead.

Reading an instance type

Instance types look like a code, and they are — m7g.large decodes without a lookup once you know the pattern:

  • m — family. t burstable, m balanced, c compute-heavy, r memory-heavy, g/p GPU
  • 7 — generation. Higher is newer, faster, and usually cheaper for the same work
  • g — extra attributes. g means Graviton, AWS's own ARM processor; i Intel, a AMD, d local NVMe
  • large — size, doubling up the scale: large, xlarge, 2xlarge…

Graviton is worth a deliberate look: typically 20% cheaper than the equivalent x86 for better performance. The catch is that it is ARM, so container images and any compiled dependency must be built for arm64. For interpreted code with no native extensions, switching is usually just changing the type.

The t family is different in a way that surprises people. It earns CPU credits while idle and spends them under load; run out and the instance is throttled hard. Fine for development and bursty sites, dangerous for anything with a sustained floor of work.

The four ways to pay

ModelSavesFor
On-DemandAnything unpredictable, or short-lived
Savings Plansup to ~70%A committed hourly spend for 1 or 3 years. Flexible across family, size and region
Reserved Instancesup to ~70%Older, less flexible version of the same idea. Savings Plans are usually the better tool now
Spotup to ~90%Spare capacity, reclaimed with two minutes' notice. Excellent for batch and CI, wrong for a database

The cheapest instance is still the one that is switched off. Before committing to a year, check whether the workload needs to run at night at all.

Security groups are stateful; NACLs are not

This single distinction explains most "why can I not connect" problems.

A security group is a stateful firewall on the instance. You allow inbound port 443; the response goes back out automatically, because the connection is tracked. Security groups have allow rules only — there is no way to write a deny — and a request is permitted if any attached group allows it.

A network ACL sits on the subnet, is stateless, and has both allow and deny. Stateless means return traffic needs its own explicit rule, on the ephemeral port range. Forgetting that produces a connection that opens and then hangs.

Use security groups for essentially everything. Reach for a NACL only to block something at the subnet edge, which is rare.

The best pattern is to reference groups rather than IP ranges — the database group allows 5432 from the application group, and neither needs to know an address:

aws ec2 authorize-security-group-ingress \
  --group-id sg-database \
  --protocol tcp --port 5432 --source-group sg-application

Storage

EBS is a network-attached volume that survives a stop. Instance store is physically attached, faster, and erased when the instance stops.

For EBS, use gp3. It replaced gp2 and is better in both directions: cheaper per GB, and its baseline 3,000 IOPS is independent of volume size. Under gp2 performance scaled with capacity, so people bought a needlessly large volume to get IOPS — with gp3 you buy the two separately. Step up to io2 only when you need sustained IOPS beyond what gp3 offers.

Note the root volume's delete on termination flag defaults to true, and any extra volume you attach defaults to false. That asymmetry orphans volumes that quietly bill forever.

First boot

aws ec2 run-instances \
  --image-id ami-0abcdef1234567890 \
  --instance-type t3.micro \
  --security-group-ids sg-application \
  --subnet-id subnet-private-a \
  --iam-instance-profile Name=app-instance-profile \
  --user-data file://bootstrap.sh

--user-data runs once on first boot as root, which is how an instance configures itself without anyone logging in. Its output lands in /var/log/cloud-init-output.log — the first place to look when an instance comes up but the application does not.

Note --iam-instance-profile, not an access key in the user data. The instance gets rotating credentials from the instance metadata service, and every AWS SDK finds them with no configuration at all.

Stop using SSH

aws ssm start-session --target i-0abc123def456

Session Manager gives you a shell with no open inbound port, no key pair to distribute and no bastion host — the agent connects outbound to AWS and you attach to it. Access is IAM, so offboarding someone actually removes it, and sessions can be logged to S3 or CloudWatch for audit.

It needs the SSM agent (present on current Amazon Linux and Ubuntu images) and an instance profile with the managed node policy. Once it works, delete the port 22 rule and the key pair.

Where instances go wrong

Three failures account for most lost afternoons, and none of them announce themselves clearly.

The instance is in a private subnet with no route out. Package installs hang rather than fail. A private subnet needs a NAT gateway for outbound internet, and a NAT gateway costs more than most people expect — both per hour and per GB processed. VPC endpoints for S3 and DynamoDB are free by comparison and remove a chunk of that traffic.

The status checks are failing. A system status check failure is AWS's problem — stop and start the instance and it moves to different hardware. An instance status check failure is yours: a full disk, a kernel panic, or a misconfigured boot. The distinction tells you whether to escalate or debug.

The disk is full. Growing an EBS volume is online and takes seconds, but it only grows the block device — the filesystem does not notice until you extend it with growpart and resize2fs or xfs_growfs. People resize the volume, see no change, and conclude the resize failed.