Cost basics and the course project
Budgets, billing alerts, Cost Explorer, tags and the ways to pay less, read in a real account. Then the project brief that ties the whole AWS course together.
By the end of this lesson you'll be able to
0 of 6 completeThe bill is part of the design
Every lesson in this course ended with a cost line and a clean-up. That habit matters more than any tool here: most surprise AWS bills come from something left running, like a NAT gateway, a load balancer or a database nobody deleted. The tools in this lesson don't stop that. They make sure you find out the same day, and then show you where the money went.
All of it lives in Billing and Cost Management, a global console (no Region). The screenshots and outputs
come from the practice account used for this course. Its account ID is replaced with 111122223333.
Billing and the Free Tier
The dashboard answers "am I spending, and on what?" in one screen. The currency switch is only for display: AWS bills in US dollars, so learn to read the USD figure. Bills has the itemised invoice for each month.
The Free Tier page tracks every free offer you're using and how close each one is to its limit:
Two kinds of offer show up here. Always free ones (KMS requests, SQS requests, CloudWatch storage) reset every month and never expire. Others depend on when your account was created and change over time, so read the current AWS Free Tier page rather than trusting a blog post. Free Tier covers usage, not mistakes: a NAT gateway or an idle load balancer was never free.
Budgets and alerts
A budget compares what you spend with a limit you set, and emails you (or posts to SNS, or runs an action) when a threshold is crossed. Billing → Budgets:
This page has two real lessons in it.
Actual versus forecast. Each alert watches either actual spend (money already spent) or forecasted spend (where AWS thinks the month will end). Here the forecast is already at 164%, but no email went out, because both alerts watch actual spend:
$ aws budgets describe-notifications-for-budget --account-id 111122223333 --budget-name academy-5usd \ --query 'Notifications[].[NotificationType,ComparisonOperator,Threshold]' --output text ACTUAL GREATER_THAN 100.0 ACTUAL GREATER_THAN 80.0
aws budgets describe-notifications-for-budget --account-id 111122223333 --budget-name academy-5usd \awsthe AWS command line
A forecast alert warns you mid-month, while there's still time to act; an actual alert confirms it happened. Use both on real budgets. Forecasts early in a month are rough, since they extrapolate from a few days.
Verify the email. Since September 30, 2026, new email recipients must click a verification link before budget emails reach them. A budget with an unverified address looks healthy and stays silent. After creating one, open the budget and check Email verification says the address is verified.
A zero-spend budget
For a learning account, the best budget is one that fires on the first cent. Create budget starts with a choice between a template and full customisation:
This walkthrough stopped here and pressed Cancel: the practice account already has its budget. In your own account, press Create budget, then verify the address from the email AWS sends. The Monthly cost budget template is the next step up: a dollar limit with an actual alert and a forecast alert.
From the CLI, the same check works for any budget:
$ aws budgets describe-budgets --account-id 111122223333 \ --query 'Budgets[].{Name:BudgetName,Limit:BudgetLimit.Amount,Actual:CalculatedSpend.ActualSpend.Amount,Forecast:CalculatedSpend.ForecastedSpend.Amount}' --output table ------------------------------------------------- | DescribeBudgets | +--------+------------+--------+----------------+ | Actual | Forecast | Limit | Name | +--------+------------+--------+----------------+ | 0.0 | 8.219 | 5.0 | academy-5usd | +--------+------------+--------+----------------+
aws budgets describe-budgets --account-id 111122223333 \awsthe AWS command line
Cost Explorer
A budget tells you that you spent; Cost Explorer tells you on what. It shows up to 13 months of history (once it's enabled), grouped by service, Region, account, usage type or tag:
Twenty-seven services used, and the total still rounds to zero. That's the clean-up habit paying off. The usual investigation goes: group by Service to find the expensive one, then by Usage type to see what in it (hours, GB stored, data out), then by Region to find where.
The same data from the CLI. The Cost Explorer API lives in us-east-1, and every request costs $0.01, so script it
sparingly:
$ aws ce get-cost-and-usage --time-period Start=2026-10-01,End=2026-10-18 --granularity MONTHLY \ --metrics UnblendedCost --group-by Type=DIMENSION,Key=SERVICE \ --query 'ResultsByTime[0].Groups[].[Keys[0],Metrics.UnblendedCost.Amount]' --output text \ | sort -t$'\t' -k2 -gr | head -5 EC2 - Other 0.0000155226 Amazon Simple Storage Service 0.0000003192 Tax 0 AmazonCloudWatch 0 Amazon Virtual Private Cloud 0
aws ce get-cost-and-usage --time-period Start=2026-10-01,End=2026-10-18 --granularity MONTHLY \awsthe AWS command line
EC2 - Other is everything EC2 bills apart from instance hours: EBS volumes and snapshots, NAT gateways, Elastic IPs. It's the line to check first when compute looks cheap but the bill doesn't. Here it's a fraction of a cent.
Cost Anomaly Detection, in the same menu, learns your normal spend per service and emails you when a day looks unusual. It catches the slow leaks that never cross a budget threshold.
Tags and cost categories
The bill knows services and Regions; it doesn't know that the RDS database belongs to the payments team.
Tags fix that. Put the same keys on everything (project, env, owner), then activate them as cost
allocation tags so they become a dimension in Cost Explorer:
Two rules catch people out. Tags only count from the moment you activate them (and take up to a day to
appear), so activate early. And a tag missing from a resource is a gap in every report, so enforce tags in
Terraform with default_tags on the provider instead of hoping people remember.
$ aws ce list-cost-allocation-tags --status Active --query 'CostAllocationTags[].TagKey' []
aws ce list-cost-allocation-tags --status Active --query 'CostAllocationTags[].TagKey'awsthe AWS command line--querypick fields from the JSON response
Cost categories group spend by your own rules, for example "everything tagged team=payments plus account
prod-payments" → category Payments. They're how finance gets a report by team without reading tags. Build
them after the tags are in place.
Paying less
Once you know where the money goes, there are four levers, in the order to try them:
| Lever | What it does | Fits |
|---|---|---|
| Delete | remove what nobody uses: old snapshots, unattached EBS volumes, idle load balancers | always first, and free |
| Right-size | move to a smaller or newer instance type that still meets the load | servers mostly idle on CPU and RAM |
| Savings Plans | commit to a dollar amount per hour for 1 or 3 years, pay a lower rate | steady load that will still be here |
| Spot (lesson 3) | spare capacity at a large discount, taken back with a 2-minute warning | CI runners, batch jobs, test fleets |
Compute Optimizer does the right-sizing maths for you: it reads CloudWatch metrics from EC2, Auto Scaling groups, EBS, Lambda and ECS on Fargate and recommends a better fit. It has to be turned on first (opt-in), and this account hasn't opted in:
$ aws compute-optimizer get-enrollment-status --region ap-south-1 --query status "Inactive"
aws compute-optimizer get-enrollment-status --region ap-south-1 --query statusawsthe AWS command line--regionwhich Region--querypick fields from the JSON response
Savings Plans → Recommendations looks at your past on-demand spend and suggests a commitment:
Zero recommendations is the right answer for a practice account: AWS only recommends one when your average on-demand spend is above $0.10 an hour. A commitment is a contract; buy one for the baseline you're sure of, never for the peak.
Trusted Advisor runs checks across cost, security, fault tolerance and service limits:
On the free Basic plan you get the core security checks and service limits. The cost checks (idle load balancers, underused instances, unassociated Elastic IPs) come with a paid Support plan, which a company account usually has. In a personal account, Cost Explorer and the clean-up habit cover the same ground.
The course project
You've built every piece separately. Now put them together: a production-like AWS environment created
from a single terraform apply, and gone again with one terraform destroy. Build it in your own GitHub
repository.
Requirements
- A VPC across two AZs with public and private subnets (lesson 4)
- An Application Load Balancer in the public subnets, an Auto Scaling group of at least two instances in the private subnets, reachable only from the load balancer (lesson 5)
- A static site in a private S3 bucket behind CloudFront with Origin Access Control (lessons 6 and 9)
- An RDS database in the private subnets, its password in Secrets Manager, never in code (lessons 10 and 15)
- Instances get permissions from an IAM role; no access keys anywhere (lesson 2)
- CloudWatch alarms on unhealthy targets and 5xx errors, sent to an SNS topic you subscribe to (lesson 13)
default_tagswithproject,envandowneron the provider, and a monthly AWS Budget with forecast and actual alerts, both defined in Terraform- A GitHub Actions workflow that runs
terraform fmt -check,validateandplanon pull requests, authenticated with OIDC (lesson 2)
Suggested repository layout
aws-project/ ├── .github/workflows/terraform.yml ├── modules/ │ ├── network/ # VPC, subnets, routes, endpoints │ ├── web/ # launch template, ASG, ALB, security groups │ ├── data/ # RDS, Secrets Manager secret │ └── site/ # S3 bucket, CloudFront, OAC ├── main.tf # wires the modules together ├── budget.tf # aws_budgets_budget with alerts ├── variables.tf ├── outputs.tf # ALB DNS name, CloudFront domain └── README.md # what it builds, what it costs, how to destroy it
How to check it works
terraform applyfinishes without errors from an empty account.curlthe ALB DNS name a few times: answers come from both AZs.- The CloudFront domain serves your page; the S3 URL itself returns
AccessDenied. - Terminate one instance: the Auto Scaling group replaces it and the alarm fires, then clears.
- In Cost Explorer the next day, filter by your
projecttag: the cost shows up there, not as untagged. terraform destroy, then confirm in the console that no load balancer, database or NAT gateway is left.
Stretch goals
- Remote state in S3 with locking, so a second person can run the same code safely.
- HTTPS on your own domain with Route 53 and an ACM certificate (lessons 8 and 9).
- Run the app on ECS Fargate instead of EC2 (lesson 11), and compare the cost in Cost Explorer.
The load balancer, NAT (if you add one) and database bill by the hour. Build, test and destroy in the same session, and keep the zero-spend budget on while you do.
A cost check of your account
Without creating anything, find out how your account is set up for cost: which budgets exist and what they alert on, what this month cost by service, which tags split the bill, and whether right-sizing is turned on. Write down one thing to fix.
The budget's limit, actual and forecast spend and alert types, the top services this month, the active cost tags and the Compute Optimizer statusReveal one safe solution
$ export AWS_REGION=us-east-1 # the Budgets and Cost Explorer APIs live here $ ACC=$(aws sts get-caller-identity --query Account --output text) $ aws budgets describe-budgets --account-id $ACC \ --query 'Budgets[].[BudgetName,BudgetLimit.Amount,CalculatedSpend.ActualSpend.Amount,CalculatedSpend.ForecastedSpend.Amount]' --output text academy-5usd 5.0 0.0 8.219 $ aws budgets describe-notifications-for-budget --account-id $ACC --budget-name academy-5usd \ --query 'Notifications[].[NotificationType,ComparisonOperator,Threshold]' --output text ACTUAL GREATER_THAN 100.0 ACTUAL GREATER_THAN 80.0 $ aws ce get-cost-and-usage --time-period Start=2026-10-01,End=2026-10-18 --granularity MONTHLY \ --metrics UnblendedCost --group-by Type=DIMENSION,Key=SERVICE \ --query 'ResultsByTime[0].Groups[].[Keys[0],Metrics.UnblendedCost.Amount]' --output text \ | sort -t$'\t' -k2 -gr | head -5 EC2 - Other 0.0000155226 Amazon Simple Storage Service 0.0000003192 Tax 0 AmazonCloudWatch 0 Amazon Virtual Private Cloud 0 $ aws ce list-cost-allocation-tags --status Active --query 'CostAllocationTags[].TagKey' [] $ aws compute-optimizer get-enrollment-status --region ap-south-1 --query status "Inactive"
aws budgets describe-budgets --account-id $ACC \awsthe AWS command line
aws ce list-cost-allocation-tags --status Active --query 'CostAllocationTags[].TagKey'--querypick fields from the JSON response
aws compute-optimizer get-enrollment-status --region ap-south-1 --query status--regionwhich Region
The Cost Explorer call costs $0.01; everything else is free, and nothing is created, so there's nothing to clean up. In this account the one thing to fix was clear: the budget only alerts on actual spend and its email isn't verified yet.
Try it yourself
0 of 6 steps doneRun each task in your own account or terminal, then tick it off.
Quick check
You created an AWS account for practice. What is the first cost control to set up?
Official reading for this lesson
Or get every quiz answer right and it completes itself.