Archive · AWS

AWS Cost Optimization, Part 1: Finding Where the Money Goes

Build cost visibility before cutting anything: tagging, Cost Explorer, Data Exports with Athena, budgets and anomaly alerts, and scripts that find idle and orphaned resources.

6 min read

  • AWS Cost Explorer
  • AWS Data Exports
  • Amazon Athena
  • Cost Optimization Hub
  • Compute Optimizer
  • AWS Budgets

Cost optimisation starts with visibility. Cutting without data means guessing, and guesses tend to hit the wrong things: the small, visible costs instead of the large, quiet ones. This post sets up the measurement. Part 2 covers what was found and changed, and how the bill came down by one-third.

The loop

flowchart LR
  I[Inform<br/>tags, reports, owners] --> O[Optimise<br/>remove, resize, commit]
  O --> P[Operate<br/>budgets, reviews, guardrails]
  P --> I

This is the FinOps loop: inform (who spends what), optimise (change things), operate (keep it from creeping back). One pass gives a saving; the loop keeps it.

1. Tag everything and enforce it

Without tags, a bill is a list of services. With tags, it is a list of teams, products and environments that someone owns.

Tag key Example values Answers
Project shop, hcm, reporting Which product is this for?
Env prod, stage, dev, test Can it be turned off at night?
Owner team or person Who do I ask before deleting it?
CostCenter finance code Who pays?

Enforce the keys and allowed values with an AWS Organizations tag policy:

{
  "tags": {
    "Env": {
      "tag_key": { "@@assign": "Env" },
      "tag_value": { "@@assign": ["prod", "stage", "dev", "test"] },
      "enforced_for": { "@@assign": ["ec2:instance", "ec2:volume", "rds:db", "s3:bucket"] }
    }
  }
}

Then activate the keys as cost allocation tags in the Billing console. Only activated tags appear in Cost Explorer and exports, and only from the day they are activated.

Find untagged resources with the Resource Groups Tagging API:

aws resourcegroupstaggingapi get-resources --region ap-south-1 \
  --query 'ResourceTagMappingList[?!not_null(Tags[?Key==`Owner`])].ResourceARN' --output text

2. Read the bill top-down in Cost Explorer

Work from the biggest lines down. Three views explain most bills:

  1. Group by Service, last 3 months, monthly. Which services dominate, and which are growing?
  2. Filter to the top service, group by Usage type. For EC2, this separates instance hours from EBS, data transfer, NAT gateway processing and public IPv4 addresses.
  3. Group by tag (Env, then Project). How much is non-production? Non-production is often a quarter or more of a bill and is the easiest to reduce.

Typical places money hides:

Usage type pattern What it is
BoxUsage EC2 instance hours
EBS:VolumeUsage.gp2, gp3 Volume storage, including unattached volumes
EBS:SnapshotUsage Snapshots, often years of them
NatGateway-Bytes Data processed by NAT gateways
DataTransfer-Regional-Bytes Cross-AZ traffic
PublicIPv4:InUseAddress, IdleAddress Public IPv4 charges, since February 2024
TimedStorage-ByteHrs S3 storage
Requests-Tier1 S3 PUT and LIST requests
DataTransfer-Out-Bytes (CloudFront) CDN egress

3. Line-level detail with Data Exports and Athena

Cost Explorer is good for trends. For questions such as “which exact resources cost the most”, create a Data Export (the CUR 2.0 format) to S3 and query it with Athena.

-- top 20 resources last month
SELECT line_item_product_code AS service,
       line_item_resource_id  AS resource,
       round(sum(line_item_unblended_cost), 2) AS cost
FROM   cur.data_export
WHERE  billing_period = '2026-08'
GROUP  BY 1, 2
ORDER  BY cost DESC
LIMIT  20;
-- cost by environment tag
SELECT resource_tags['user_env'] AS env,
       round(sum(line_item_unblended_cost), 2) AS cost
FROM   cur.data_export
WHERE  billing_period = '2026-08'
GROUP  BY 1
ORDER  BY cost DESC;

Column and tag names depend on your export settings, so check them with SHOW COLUMNS first.

4. Let AWS point at the obvious

Tool Finds
Cost Optimization Hub One list across accounts: idle resources, rightsizing, Graviton moves, Savings Plans, with estimated savings
Compute Optimizer Over-provisioned EC2, EBS, Lambda, ECS on Fargate and RDS, based on CloudWatch utilisation
Trusted Advisor (cost checks) Idle load balancers, underused EBS volumes, unassociated Elastic IPs
S3 Storage Lens Buckets with no lifecycle rules, incomplete multipart uploads, noncurrent versions

Enable Compute Optimizer early: it needs about two weeks of metrics before recommendations are useful.

5. Find idle and orphaned resources yourself

A few CLI queries catch most of the leftovers. Run them per region.

Unattached EBS volumes:

aws ec2 describe-volumes --filters Name=status,Values=available \
  --query 'Volumes[].{ID:VolumeId,GB:Size,Type:VolumeType,Created:CreateTime}' --output table

Elastic IPs not attached to anything:

aws ec2 describe-addresses \
  --query 'Addresses[?AssociationId==null].{IP:PublicIp,Alloc:AllocationId,Tags:Tags}' --output table

Old snapshots:

aws ec2 describe-snapshots --owner-ids self \
  --query "Snapshots[?StartTime<'2025-09-01'].{ID:SnapshotId,GB:VolumeSize,Date:StartTime,Desc:Description}" \
  --output table

Instances with very low CPU over 14 days:

for id in $(aws ec2 describe-instances --filters Name=instance-state-name,Values=running \
            --query 'Reservations[].Instances[].InstanceId' --output text); do
  avg=$(aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name CPUUtilization \
        --dimensions Name=InstanceId,Value=$id --statistics Average --period 1209600 \
        --start-time $(date -u -d '14 days ago' +%FT%TZ) --end-time $(date -u +%FT%TZ) \
        --query 'Datapoints[0].Average' --output text)
  echo "$id $avg"
done | awk '$2 != "None" && $2 < 5' | sort -k2 -n

Load balancers with no healthy targets:

for tg in $(aws elbv2 describe-target-groups --query 'TargetGroups[].TargetGroupArn' --output text); do
  n=$(aws elbv2 describe-target-health --target-group-arn $tg --query 'length(TargetHealthDescriptions)')
  [ "$n" = "0" ] && echo "empty target group: $tg"
done

CloudWatch log groups that never expire:

aws logs describe-log-groups \
  --query 'logGroups[?retentionInDays==null].{Name:logGroupName,MB:storedBytes}' --output table

Stopped instances still pay for their volumes and any Elastic IPs:

aws ec2 describe-instances --filters Name=instance-state-name,Values=stopped \
  --query 'Reservations[].Instances[].{ID:InstanceId,Name:Tags[?Key==`Name`]|[0].Value,Since:StateTransitionReason}' \
  --output table

Put the results in a shared sheet with an owner and a decision (keep, resize, delete) per row. Nothing is deleted without an owner’s yes, or a tag-and-wait period for orphans.

6. Alerts so nothing surprises you again

Budgets per account or per Project tag, with alerts on actual and forecast spend:

{
  "BudgetName": "shop-monthly",
  "BudgetLimit": { "Amount": "1000", "Unit": "USD" },
  "BudgetType": "COST",
  "TimeUnit": "MONTHLY",
  "CostFilters": { "TagKeyValue": ["user:Project$shop"] }
}

Set the amount to your own baseline. Add notifications at 80 percent of actual spend and 100 percent of forecast.

Cost Anomaly Detection with a monitor per linked account or cost category, and a daily email or Slack alert for anomalies above a small threshold. It catches a forgotten load test or a runaway log group within a day, not at the end of the month.

7. Write down the baseline

Before changing anything, record:

  • The last three months’ total and the breakdown by service and environment.
  • Unit costs that matter to the business, such as cost per active customer or per thousand requests.

Every saving is measured against this baseline. Without it, “we saved a lot” is an opinion.

Summary

Step Outcome
Tags enforced and activated Every cost has an owner
Cost Explorer top-down The big lines are known
Data Exports and Athena Exact resources are known
Hub, Compute Optimizer, Storage Lens AWS’s own recommendations collected
Idle-resource queries A list of candidates with owners
Budgets and anomaly alerts Surprises caught within a day
Baseline recorded Savings can be proven

Part 2 is what came out of this: the changes made and how they added up to a one-third reduction.