AWS provides a broad set of native tools for running FinOps at scale. This guide walks step by step through building a FinOps practice with AWS-native services only, covering cost visibility, optimization and governance in line with the FinOps Foundation’s Inform, Optimize and Operate phases.
Bottom line: for most AWS-centric organizations, native tools cover the core FinOps capabilities without extra licensing. Third-party platforms earn their place mainly for multi-cloud, SaaS or advanced allocation needs.
The AWS FinOps Toolkit
Core Cost Management Tools
- AWS Cost Explorer: cost analysis and forecasting
- AWS Budgets: cost, usage and commitment budgets with alerts and actions
- AWS Data Exports: CUR 2.0, FOCUS 1.2, recommendations and carbon emissions data, delivered to Amazon S3 (legacy CUR remains available)
- AWS Cost Anomaly Detection: detection of unusual spend
- AWS Cost Categories: map accounts, tags and services to your business structure
- Billing and Cost Management Dashboards: custom and managed cost dashboards
- AWS Billing Conductor: pro forma billing for showback and chargeback
Optimization and Rightsizing Tools
- Cost Optimization Hub: rightsizing, idle resource and commitment recommendations across accounts and Regions, deduplicated and priced with your discounts
- AWS Compute Optimizer: rightsizing for EC2, Auto Scaling groups, EBS, Lambda, ECS on Fargate, RDS and more
- AWS Trusted Advisor: core checks for everyone, the full set with Business Support+ or higher; many cost checks now use Cost Optimization Hub data
- Savings Plans and Reserved Instances: Compute, EC2 Instance, Database and SageMaker AI Savings Plans; Standard and Convertible RIs
Governance and Automation Tools
- AWS Organizations: consolidated billing, tag policies and service control policies (SCPs)
- AWS Config: configuration and tag compliance tracking
- AWS CloudFormation: infrastructure as code; pair it with AWS Pricing Calculator estimates
- AWS Lambda and Amazon EventBridge: custom cost automation
- Instance Scheduler on AWS: scheduled start and stop for EC2, Auto Scaling groups and RDS
Phase 1: Foundation Setup – Building Cost Visibility
Step 1: Configure CUR 2.0 Through AWS Data Exports
CUR 2.0 is the most detailed billing dataset on AWS. Unlike legacy CUR, it has a fixed schema, nested columns for tags and cost categories, and basic SQL for selecting columns. Create it in the management account to cover the whole organization.
- In the Billing and Cost Management console, open Data Exports and create a standard export with the CUR 2.0 table.
- Include resource IDs, plus split cost allocation data if you run Amazon ECS or EKS.
- Select Parquet and the overwrite option to keep storage and query costs down.
- Deliver to a dedicated S3 bucket with versioning, lifecycle rules and a bucket policy for the Data Exports service (the console can create it).
- For multi-cloud reporting, add a second export using FOCUS 1.2 with AWS columns.
The same export as code, using the AWS CLI:
aws bcm-data-exports create-export --cli-input-json file://cur2-export.json
{
"Export": {
"Name": "org-cur2-daily",
"DataQuery": {
"QueryStatement": "SELECT bill_billing_period_start_date, line_item_usage_account_id, line_item_usage_account_name, line_item_product_code, line_item_usage_type, line_item_resource_id, line_item_line_item_type, line_item_unblended_cost, pricing_term, resource_tags, cost_category FROM COST_AND_USAGE_REPORT",
"TableConfigurations": {
"COST_AND_USAGE_REPORT": {
"TIME_GRANULARITY": "DAILY",
"INCLUDE_RESOURCES": "TRUE",
"INCLUDE_SPLIT_COST_ALLOCATION_DATA": "FALSE"
}
}
},
"DestinationConfigurations": {
"S3Destination": {
"S3Bucket": "your-org-cost-exports",
"S3Prefix": "cur2",
"S3Region": "your-bucket-region",
"S3OutputConfigurations": {
"OutputType": "CUSTOM",
"Format": "PARQUET",
"Compression": "PARQUET",
"Overwrite": "OVERWRITE_REPORT"
}
}
},
"RefreshCadence": { "Frequency": "SYNCHRONOUS" }
}
}
Query the export with Amazon Athena, or use the Cost and usage dashboard export type for a prebuilt QuickSight dashboard. Top cost drivers by account and service (adjust names to your setup):
SELECT line_item_usage_account_name,
line_item_product_code,
SUM(line_item_unblended_cost) AS unblended_cost
FROM cost_exports.cur2
WHERE bill_billing_period_start_date = TIMESTAMP '2026-09-01 00:00:00'
GROUP BY 1, 2
ORDER BY 3 DESC
LIMIT 20;
Step 2: Implement a Tagging Strategy
Tagging is the basis of cost allocation. AWS allows up to 50 user-created tags per resource.
An example of a mandatory tag schema:
{
"Environment": "production|staging|development|sandbox",
"Project": "project-name",
"Owner": "team-or-individual-email",
"CostCenter": "finance-cost-center-code",
"BusinessUnit": "marketing|engineering|sales|operations",
"Application": "application-name",
"Purpose": "web-server|database|analytics|backup",
"Schedule": "24x7|business-hours|weekend-only|on-demand"
}
Activate tags for billing: user-defined tags appear in billing data only after you activate them as cost allocation tags; allow up to 24 hours for new keys to appear and another 24 for activation.
Use the AWS-generated aws:createdBy tag instead of a custom auto-tagging function. Once the management account activates it, AWS adds it on a best-effort basis to supported resources created afterward; it isn’t retroactive.
Enforce tags at creation with SCPs, one statement per required tag. Keys combined in a single Null condition are evaluated together, so they would only deny requests missing every tag.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyRunInstancesWithoutEnvironment",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": { "Null": { "aws:RequestTag/Environment": "true" } }
},
{
"Sid": "DenyRunInstancesWithoutCostCenter",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": { "Null": { "aws:RequestTag/CostCenter": "true" } }
},
{
"Sid": "DenyRunInstancesWithoutOwner",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": { "Null": { "aws:RequestTag/Owner": "true" } }
}
]
}
Use tag policies in AWS Organizations to standardize values and capitalization. They can block noncompliant tagging on specified resource types, but they don’t stop untagged resources from being created.
Step 3: Configure AWS Budgets
Set up several budget types:
- Overall budget: monthly, based on your historical baseline, with actual alerts at, for example, 50%, 80% and 100%, plus a forecast alert
- Service budgets: for your largest services, such as EC2, RDS, S3 and data transfer
- Tag or cost category budgets: per environment, application or cost center
A tag-based budget with email and SNS notifications (CostFilters is deprecated; use FilterExpression):
import boto3
def create_tag_budget(name, tag_key, tag_value, monthly_limit_usd, email, sns_topic_arn):
account_id = boto3.client("sts").get_caller_identity()["Account"]
budgets = boto3.client("budgets")
budgets.create_budget(
AccountId=account_id,
Budget={
"BudgetName": name,
"BudgetType": "COST",
"TimeUnit": "MONTHLY",
"BudgetLimit": {"Amount": str(monthly_limit_usd), "Unit": "USD"},
"FilterExpression": {
"Tags": {"Key": tag_key, "Values": [tag_value], "MatchOptions": ["EQUALS"]}
},
},
NotificationsWithSubscribers=[
{
"Notification": {
"NotificationType": "ACTUAL",
"ComparisonOperator": "GREATER_THAN",
"Threshold": 80,
"ThresholdType": "PERCENTAGE",
},
"Subscribers": [{"SubscriptionType": "EMAIL", "Address": email}],
},
{
"Notification": {
"NotificationType": "FORECASTED",
"ComparisonOperator": "GREATER_THAN",
"Threshold": 100,
"ThresholdType": "PERCENTAGE",
},
"Subscribers": [
{"SubscriptionType": "EMAIL", "Address": email},
{"SubscriptionType": "SNS", "Address": sns_topic_arn},
],
},
],
)
The SNS topic policy must allow AWS Budgets to publish. Budget actions can also apply an IAM policy or SCP, or stop EC2 and RDS instances, when a threshold is crossed.
Phase 2: Deep Cost Analysis With Cost Explorer
Cost Categories for Business Alignment
Define Cost Categories in the Billing and Cost Management console so reports reflect your organization:
- Application tiers: Web (tag Purpose = web-server), App (tag Purpose = application-server), Data (tag Purpose = database or cache), Analytics (Amazon Redshift or Amazon EMR)
- Environment allocation: Production (tag Environment = production) and Non-production (everything else)
Cost Anomaly Detection
Create at least one monitor. AWS managed monitors pick up new services, accounts or tag values automatically; tag, account and cost category monitors must be created in the management account. In the API, use ThresholdExpression (Threshold is deprecated).
import boto3
ce = boto3.client("ce")
# AWS managed monitor that evaluates every AWS service
services = ce.create_anomaly_monitor(
AnomalyMonitor={
"MonitorName": "all-services",
"MonitorType": "DIMENSIONAL",
"MonitorDimension": "SERVICE",
}
)
# AWS managed monitor that evaluates each value of the Environment tag
by_environment = ce.create_anomaly_monitor(
AnomalyMonitor={
"MonitorName": "by-environment",
"MonitorType": "DIMENSIONAL",
"MonitorDimension": "TAG",
"MonitorSpecification": {"Tags": {"Key": "Environment"}},
}
)
# Daily email summary for anomalies with a total impact of at least 100 USD
ce.create_anomaly_subscription(
AnomalySubscription={
"SubscriptionName": "finops-daily-anomalies",
"MonitorArnList": [services["MonitorArn"], by_environment["MonitorArn"]],
"Subscribers": [{"Type": "EMAIL", "Address": "[email protected]"}],
"Frequency": "DAILY",
"ThresholdExpression": {
"Dimensions": {
"Key": "ANOMALY_TOTAL_IMPACT_ABSOLUTE",
"MatchOptions": ["GREATER_THAN_OR_EQUAL"],
"Values": ["100"],
}
},
}
)
For individual alerts, use IMMEDIATE frequency with an SNS topic, which Amazon Q Developer in chat applications can route to Slack or Microsoft Teams.
Phase 3: Resource Optimization With Native Tools
Enable Compute Optimizer and Cost Optimization Hub
Opt in from the management account to cover all member accounts. Enhanced infrastructure metrics is a paid option that extends the lookback from 14 to up to 93 days.
import boto3
boto3.client("compute-optimizer").update_enrollment_status(
status="Active", includeMemberAccounts=True
)
boto3.client("cost-optimization-hub").update_enrollment_status(
status="Active", includeMemberAccounts=True
)
# Optional, paid: longer lookback for EC2 recommendations
boto3.client("compute-optimizer").put_recommendation_preferences(
resourceType="Ec2Instance",
enhancedInfrastructureMetrics="Active",
)
Process Recommendations From Cost Optimization Hub
Cost Optimization Hub returns one deduplicated recommendation per resource, with savings that account for your commitments, which makes it the best source for automation. This example emails a weekly summary of low-effort rightsizing and stop recommendations; it reports, it doesn’t resize.
import json
import boto3
coh = boto3.client("cost-optimization-hub")
sns = boto3.client("sns")
def weekly_rightsizing_summary(topic_arn, min_monthly_savings=50):
args = {
"filter": {
"actionTypes": ["Rightsize", "Stop"],
"implementationEfforts": ["VeryLow", "Low"],
"tags": [{"key": "AutoOptimize", "value": "true"}],
},
"orderBy": {"dimension": "estimatedMonthlySavings", "order": "Desc"},
"includeAllRecommendations": False,
"maxResults": 100,
}
items, token = [], None
while True:
page = coh.list_recommendations(**args, **({"nextToken": token} if token else {}))
items += [i for i in page["items"] if i["estimatedMonthlySavings"] >= min_monthly_savings]
token = page.get("nextToken")
if not token:
break
summary = {
"count": len(items),
"estimated_monthly_savings": round(sum(i["estimatedMonthlySavings"] for i in items), 2),
"items": [
{
"account": i["accountId"],
"resource": i["resourceId"],
"action": i["actionType"],
"from": i.get("currentResourceSummary"),
"to": i.get("recommendedResourceSummary"),
"restart_needed": i["restartNeeded"],
}
for i in items
],
}
sns.publish(TopicArn=topic_arn, Subject="Weekly rightsizing summary",
Message=json.dumps(summary, indent=2, default=str))
Savings Plans and Reserved Instances
Rightsize first, then commit for the stable baseline. AWS recommends Savings Plans over Reserved Instances for compute, and Database Savings Plans now cover services such as Aurora, RDS, DynamoDB and ElastiCache. For commitment recommendations, reuse the function above with the PurchaseSavingsPlans and PurchaseReservedInstances action types, and check them against your roadmap before buying.
Phase 4: Advanced Governance and Automation
Cost Control Policies
An SCP can block instance families that need approval, such as large GPU and high-memory instances. Grant exceptions through a dedicated organizational unit.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnapprovedHighCostInstanceFamilies",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringLike": {
"ec2:InstanceType": ["p4*", "p5*", "x2*", "u-*"]
}
}
}
]
}
Automated Cost Optimization Workflows
Schedule non-production resources with Instance Scheduler on AWS, or a small Lambda function that uses your business time zone rather than UTC:
from datetime import datetime
from zoneinfo import ZoneInfo
import boto3
ec2 = boto3.client("ec2")
TZ = ZoneInfo("Europe/Madrid") # needs time zone data; package tzdata if your runtime lacks it
def outside_business_hours(now):
# Business hours: 08:00-18:00, Monday to Friday
return now.weekday() >= 5 or now.hour < 8 or now.hour >= 18
def lambda_handler(event, context):
if not outside_business_hours(datetime.now(TZ)):
return
paginator = ec2.get_paginator("describe_instances")
for page in paginator.paginate(Filters=[
{"Name": "tag:Schedule", "Values": ["business-hours"]},
{"Name": "instance-state-name", "Values": ["running"]},
]):
ids = [i["InstanceId"] for r in page["Reservations"] for i in r["Instances"]]
if ids:
ec2.stop_instances(InstanceIds=ids)
Clean up unused resources carefully. Cost Optimization Hub already flags many idle resources. If you script cleanup, report first, snapshot before deleting, and remember that a volume’s CreateTime isn’t its detach time.
from datetime import datetime, timedelta, timezone
import boto3
ec2 = boto3.client("ec2")
cw = boto3.client("cloudwatch")
elbv2 = boto3.client("elbv2")
def report_unused(delete=False, min_age_days=30):
cutoff = datetime.now(timezone.utc) - timedelta(days=min_age_days)
# Unattached EBS volumes (status "available")
for page in ec2.get_paginator("describe_volumes").paginate(
Filters=[{"Name": "status", "Values": ["available"]}]
):
for vol in page["Volumes"]:
tags = {t["Key"]: t["Value"] for t in vol.get("Tags", [])}
if tags.get("DoNotDelete", "").lower() == "true" or vol["CreateTime"] > cutoff:
continue
print(f"Unattached volume {vol['VolumeId']} ({vol['Size']} GiB)")
if delete:
snap = ec2.create_snapshot(VolumeId=vol["VolumeId"],
Description="Pre-deletion backup")
ec2.get_waiter("snapshot_completed").wait(SnapshotIds=[snap["SnapshotId"]])
ec2.delete_volume(VolumeId=vol["VolumeId"])
# Application Load Balancers with no requests in the last 7 days
end = datetime.now(timezone.utc)
for page in elbv2.get_paginator("describe_load_balancers").paginate():
for lb in page["LoadBalancers"]:
if lb["Type"] != "application":
continue
dimension = lb["LoadBalancerArn"].split(":loadbalancer/")[1] # app/name/id
stats = cw.get_metric_statistics(
Namespace="AWS/ApplicationELB", MetricName="RequestCount",
Dimensions=[{"Name": "LoadBalancer", "Value": dimension}],
StartTime=end - timedelta(days=7), EndTime=end,
Period=86400, Statistics=["Sum"],
)
if sum(p["Sum"] for p in stats["Datapoints"]) == 0:
print(f"Idle load balancer {lb['LoadBalancerName']}: review")
Phase 5: Reporting and Analytics
Use what AWS provides before building custom dashboards:
- Billing and Cost Management Dashboards: custom dashboards, plus five managed dashboards added in August 2026 at no additional cost, which you can copy, schedule and share
- Cost and usage dashboard: a QuickSight dashboard deployed through Data Exports
- Natural-language analysis: Amazon Q Developer can answer cost questions in the console
Cost Explorer API calls are billed per request, so pull on a schedule and cache the results. A monthly breakdown by service and environment (end date exclusive):
import boto3
ce = boto3.client("ce")
def monthly_costs(start="2026-09-01", end="2026-10-01"):
by_service = ce.get_cost_and_usage(
TimePeriod={"Start": start, "End": end},
Granularity="MONTHLY",
Metrics=["UnblendedCost", "AmortizedCost"],
GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}],
)
by_environment = ce.get_cost_and_usage(
TimePeriod={"Start": start, "End": end},
Granularity="MONTHLY",
Metrics=["AmortizedCost"],
GroupBy=[{"Type": "TAG", "Key": "Environment"}],
)
return by_service["ResultsByTime"], by_environment["ResultsByTime"]
Implementation Roadmap
Rough planning ranges for one AWS organization; large or regulated estates take longer.
Weeks 1–2: Foundation
- Create CUR 2.0 (and, if needed, FOCUS) exports
- Agree on the tagging strategy and activate cost allocation tags
- Configure AWS Budgets
- Set up Cost Anomaly Detection monitors and subscriptions
Weeks 3–4: Analysis Tools
- Define Cost Categories
- Enable Compute Optimizer and Cost Optimization Hub organization-wide
- Build the first recommendation reports
Weeks 5–6: Governance
- Deploy tag and cost control SCPs and tag policies
- Set up scheduling for non-production resources
- Introduce cleanup automation in report mode
- Add an approval path for high-cost resources
Weeks 7–8: Reporting and Optimization
- Publish dashboards and a regular reporting cadence
- Tune automation rules
- Train teams on the new processes
If you’re migrating, put tagging and budgets in place before the first wave; see lessons learned from application migrations.
Best Practices and Pro Tips
Tagging
- Use consistent naming conventions across all accounts
- Standardize values with tag policies in AWS Organizations
- Audit tag compliance with AWS Config
- Tag at creation through infrastructure as code rather than after the fact
Cost Optimization
- Start with the largest cost drivers
- Commit only for predictable baseline usage, after rightsizing
- Use Spot Instances for fault-tolerant workloads
- Scale on demand patterns, not peak estimates
Automation
- Build safety checks and dry-run modes into every script
- Handle errors and log every action
- Monitor the automation itself for failures
Organizational Change
- Train teams regularly on cloud economics
- Make cost visible to development teams
- Assign clear ownership for each cost center
- Share and recognize optimization wins
Measuring Success
| KPI category | Examples |
|---|---|
| Cost efficiency | Cost per application or service, month-over-month trends, Savings Plans and RI utilization and coverage, waste reduction |
| Operations | Time to detect anomalies, number of automated optimizations, tag compliance, share of recommendations implemented |
| Business value | Savings achieved, time saved on cost management, allocation accuracy, budget freed for new initiatives |
Conclusion
Native AWS tools give you a solid, low-cost FinOps foundation. Success comes from strong fundamentals (accurate tagging and CUR 2.0 data), incremental automation and a focus on business value rather than cost reduction alone.
Start with Data Exports and tagging, then add budgets, anomaly detection and Cost Optimization Hub, and keep refining as your organization and AWS’s cost services evolve.
Sources
- What is AWS Data Exports? – AWS Documentation
- Migrating from CUR to Data Exports CUR 2.0 – AWS Documentation
- Cost Optimization Hub – AWS Documentation
- Savings Plans types – AWS Documentation
- Getting started with AWS Cost Anomaly Detection – AWS Documentation
- Activating user-defined cost allocation tags – AWS Documentation
- Tag policies – AWS Organizations Documentation
- Billing and Cost Management Dashboards introduces managed dashboards – AWS
Last reviewed: October 2026.

Leave a Reply