Capstone: costs & clean-up
Almost everyone who learns AWS gets one surprise bill. The usual cause isn't the big server you remember. It's the small things you forgot: an address nobody uses, a spare disk, a load balancer from last month's experiment, a test server in a region you never look at. This capstone gives you an account after a month of practice. Find out what it's costing, hunt down every leftover, and clean it all up with the tools from this path.
You will learn
- How AWS charges: per hour, per GB-month and per request
- The usual forgotten-bill suspects, and how to find each one
- Cost Explorer from the CLI (
aws ce get-cost-and-usage) - Finding things by tag, and searching every region with a loop
- Deleting in the right order, because things depend on each other
How the meter runs
| Thing | Charged | Rough price (us-east-1) | Still charged when… |
|---|---|---|---|
| EC2 instance | per second while running | t3.micro ≈ $0.0104/h ≈ $7.60/month | running. Stopped = no compute charge |
| EBS volume (disk) | per GB-month | gp3 ≈ $0.08/GB-month | the instance is stopped, or the volume isn't attached at all |
| Public IPv4 address | per hour | $0.005/h ≈ $3.65/month each | an Elastic IP isn't attached to anything |
| Load balancer | per hour + traffic | ALB ≈ $16+/month | it has no targets and no visitors |
| NAT gateway | per hour + per GB | ≈ $32/month + data | nothing uses it |
| Route 53 hosted zone | per month | $0.50 | it has no records you use |
| S3 | per GB-month + requests | ≈ $0.023/GB-month | old versions pile up in a versioned bucket |
| CloudWatch Logs | per GB stored | ≈ $0.03/GB-month | retention is "never expire" |
Prices change and differ by region, so check the AWS pricing pages (or the AWS Pricing Calculator) before building anything big. Moving data out of AWS to the internet also costs money, after a free allowance.
Where did the money go? Cost Explorer
aws ce get-cost-and-usage \ --time-period Start=$(date +%Y-%m-01),End=$(date -d tomorrow +%F) \ --granularity MONTHLY --metrics UnblendedCost \ --group-by Type=DIMENSION,Key=SERVICE --output table
That's this month so far, per service. "EC2 - Other" is mostly disks. Public IPv4 addresses show up under "Amazon Virtual Private Cloud". Cost Explorer is a little behind real time (hours, not seconds), and in a real account you switch it on once in the billing console.
Hunting leftovers
Tags make this easy, if you used them. One call finds everything with a tag, in one region:
aws resourcegroupstaggingapi get-resources --tag-filters Key=Project,Values=cht \ --query 'ResourceTagMappingList[].ResourceARN' --output text | tr '\t' '\n'
Most AWS things live in one region, so a server in us-west-2 is invisible while you look at us-east-1. Ask every region with a loop:
for r in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
echo "$r: $(aws ec2 describe-instances --region $r \
--query 'Reservations[].Instances[?State.Name!=`terminated`].InstanceId' --output text)"
done
And the classic forgotten things, each with a filter that finds only the leftovers:
aws ec2 describe-addresses --query 'Addresses[?AssociationId==null]' # unattached Elastic IPs aws ec2 describe-volumes --filters Name=status,Values=available # disks attached to nothing aws elbv2 describe-load-balancers --query 'LoadBalancers[].LoadBalancerName' aws route53 list-hosted-zones --query 'HostedZones[].Name' aws s3 ls
Deleting in the right order
AWS refuses to delete something another thing still uses, and it tells you with DependencyViolation, ResourceInUse or HostedZoneNotEmpty. Work from the outside in:
- Auto Scaling groups (they'd just recreate servers), then instances.
- Load balancers, then their target groups (a target group used by a listener can't go first).
- Elastic IPs, loose volumes, old snapshots.
- S3 buckets: empty first (
aws s3 rb --force), including old versions if versioning was on. - Route 53: delete your records, then the zone. (Keep it if you still use the domain!)
- Finally the network: security groups (except
default), subnets, internet gateway (detach, then delete), route tables, VPC. These are free, but tidy.
You built everything in this path by typing commands, which is great for learning. Real teams describe their infrastructure in files with Terraform/OpenTofu or AWS CloudFormation, and keep them in git. Then "create all of it" and "delete all of it" are one command each, and nothing is forgotten. That's a great next step after this path.
Before deleting, be sure it's yours and it's not in use. terminate and rb --force can't be undone. Tags like Project and Owner exist for exactly this moment: nobody wants to guess whether "test-server" is safe to delete.
Practice: stop the money leak 💸
This account has a month of practice left behind: a web server, a load balancer, a spare disk, an unused Elastic IP, a website bucket, a hosted zone… and something in another region. Everything was tagged Project=cht. Find it all and remove it, until nothing is left that costs money.
Quick check
1. You stopped all your instances, but the bill still grows a little every day. What's still charging?
✓ Stopped means no compute charge. Storage and addresses keep costing money.
2. aws ec2 describe-instances shows nothing, but Cost Explorer shows EC2 charges. Where do you look?
✓ A forgotten instance in another region is the classic surprise bill.
3. Why can't you delete the target group before the load balancer's listener?
✓ Work from the outside in: whatever uses a thing goes first.
Next up: Security basics · Defend your own server, starting with “Think like an attacker”.