AI-Powered AWS Cost Optimizer

Build a Python CLI tool that analyzes AWS costs and generates AI reports.

Introduction

30 Second Summary

Monthly bills have a way of growing when nobody checks the details. AWS accounts are no different, with idle servers and forgotten resources quietly wasting hundreds of dollars each month.

In this project, you will build a Python command-line tool that pulls real cost data from your AWS account, detects idle resources, and uses AI to generate a professional optimization report with prioritized savings recommendations.

What You'll Build

You'll build a CLI tool called cost-optimizer that queries your real AWS Cost Explorer spending data, identifies idle resources with AWS Compute Optimizer, and feeds everything to Amazon Bedrock's Claude Sonnet 4.6 to produce a Markdown optimization report.

By the end of this project, you'll have:

  • A working Python script (cost_optimizer.py) that queries the AWS Cost Explorer API for your real spending breakdown by service.
  • Integration with AWS Compute Optimizer to automatically detect idle and underutilized resources in your account.
  • An AI-generated optimization report produced by Amazon Bedrock (Claude Sonnet 4.6) with cost breakdowns and prioritized savings recommendations.
  • Secret Mission: Historical trend analysis that compares month-over-month spending and flags anomalous cost spikes.

Are there any prerequisites?

You'll need an AWS account with billing access enabled and basic Python knowledge. The project walks you through installing all other tools in Step 1.

Before We Start

Your cost optimizer needs three things to work: Python for the script itself, the AWS CLI to authenticate with your account, and access to Amazon Bedrock's Claude Sonnet 4.6 model for generating AI-powered reports.

In this step, you will install and configure all three so the rest of the project focuses purely on building.

In this step, get ready to:
  • Verify Python 3.10+ and install the AWS CLI.
  • Create a virtual environment and install boto3.
  • Enable Bedrock model access for Claude Sonnet 4.6.
Install Python and AWS CLI

Python runs your cost optimizer script, and the AWS CLI lets you authenticate your terminal session so that boto3 (the AWS SDK for Python) can talk to your AWS account.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Check your Python version by running this command:
python3 --version

✔️ I see version 3.10 or higher

Python is ready to go. Move on to the AWS CLI check below.

ⓧ I see a version lower than 3.10

You need Python 3.10+ for this project. macOS often ships an older version as the system default.

  • Download the latest Python installer from python.org/downloads.
  • Run the installer and follow the on-screen prompts.
  • Close and reopen your terminal, then run python3 --version again to confirm it now shows 3.10 or higher.

macOS users with an older default

If python3 --version still shows the old version after installing, try running python3.12 --version (or whatever version you installed). You may need to use the full binary name or set up a version manager like pyenv.

ⓧ Command not found

Python is not installed yet.

  • Download the latest Python installer from python.org/downloads.
  • Run the installer. On Windows, check the box that says Add python.exe to PATH before clicking Install Now.
  • Close and reopen your terminal, then run python3 --version to confirm it shows 3.10 or higher.

Now check whether the AWS CLI is installed by running:

aws --version

✔️ I see a version number starting with aws-cli/2

The AWS CLI v2 is installed. Skip ahead to configuring your credentials below.

ⓧ I see aws-cli/1 or command not found

You need to install AWS CLI v2.

macOS

  • Download the installer package by running:
  • Install it by running:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /

You will be prompted for your password since this installs system-wide.

Windows

  • Download and run the MSI installer from:
msiexec.exe /i https://awscli.amazonaws.com/AWSCLIV2.msi

Follow the on-screen prompts. Once it finishes, close and reopen PowerShell.

  • Confirm the install by running aws --version again. You should see output starting with aws-cli/2.

Now configure the CLI with your AWS credentials.

  • Run the following command to start the configuration wizard:
aws configure

What does aws configure ask for?

The wizard prompts you for four values:

  • AWS Access Key ID and Secret Access Key from your IAM user credentials.
  • Default region name: enter us-east-1 (required for Bedrock access later).
  • Default output format: enter json.

If you do not have an IAM access key yet, create one in the AWS console under IAM > Users > your user > Security credentials > Create access key.

  • Enter your access key ID, secret key, us-east-1 for region, and json for output format when prompted.

Getting an access denied error later?

Your IAM user needs permissions for Cost Explorer, Compute Optimizer, and Bedrock. If you are using the root account, these are granted by default. For IAM users, ensure your policy includes ce:GetCostAndUsage, compute-optimizer:GetIdleRecommendations, and bedrock:InvokeModel.

help me set up IAM permissions for Cost Explorer, Compute Optimizer, and Bedrock

Install Python dependencies

A virtual environment keeps this project's packages separate from your system Python. The only external package you need is boto3, the AWS SDK for Python.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir cost-optimizer
cd cost-optimizer
  • Create and activate a virtual environment by running:

macOS

python3 -m venv venv
source venv/bin/activate

Windows

python -m venv venv
venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

  • Install boto3 by running:
python -m pip install boto3
  • Confirm the install succeeded by running:
python -m pip show boto3

You should see package metadata printed, including a version number and the install location inside your venv folder.

Seeing a permissions error?

Make sure your virtual environment is active (you see (venv) in your prompt). If not, re-run the activate command from the platform tab above.

help me fix a pip install error in my virtual environment

Enable Bedrock model access

Amazon Bedrock gives you API access to foundation models from multiple providers. Your script uses Claude Sonnet 4.6 from Anthropic to analyze cost data and write optimization reports.

  • Open your browser and go to the AWS Management Console.
  • Make sure your region is set to US East (N. Virginia) (us-east-1) in the top-right dropdown.
  • In the search bar at the top, type Bedrock and select Amazon Bedrock from the results.
  • In the left navigation pane, click Model catalog.
  • Search for Claude Sonnet 4.6 in the search bar.
  • Select the Claude Sonnet 4.6 model card.

What happens when I select the model?

Access to Bedrock foundation models is enabled by default with the correct AWS Marketplace permissions. For Anthropic models, first-time users may need to submit use case details before access is granted.

If prompted, fill out the use case form and click Submit. Access typically takes a few minutes to reflect in the console.

  • If you see a Request access or Submit use case details prompt, fill out the short form describing your use case and click Submit.
  • Wait until the model status shows Access granted or you can open it in the playground.

Why us-east-1?

The script hardcodes us-east-1 as the region for all AWS clients. This region has the widest selection of Bedrock models and is where Anthropic models are most reliably available.

Model access taking too long?

Access changes can take several minutes to be reflected in the console. If it has been more than 10 minutes, refresh the page and check again. Ensure your account has AWS Marketplace permissions (the AmazonBedrockFullAccess managed policy covers this).

help me get Bedrock model access for Claude Sonnet 4.6

Finally, open your code editor.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the cost-optimizer folder you just created, then click Open.

You should see the cost-optimizer folder name in the VS Code sidebar. Your environment is ready.

Your development environment is locked and loaded. Next up, you will write the code that pulls real cost data from your AWS account.

Query Cost Data with Cost Explorer API

With your environment ready, it's time to connect to your AWS account and pull real spending data. The AWS Cost Explorer API gives you programmatic access to the same spending breakdowns you see in the AWS Console.

In this step, you'll write a Python function that queries Cost Explorer for the last 30 days of spending, groups it by service, and returns a sorted breakdown of where your money is going.

In this step, get ready to:
  • Build a function that queries the Cost Explorer API with boto3.
  • Parse the API response into a structured cost breakdown.
  • Test the function against your live AWS account.
Build the cost retrieval module

The Cost Explorer client in boto3 exposes a method called get_cost_and_usage. It accepts a time period, granularity, metrics to retrieve, and grouping dimensions. You'll wrap this in a reusable function that returns a clean list of services sorted by cost.

  • Create a new file called cost_optimizer.py in your project folder and add the following code:
import boto3
import json
from datetime import datetime, timedelta


def get_cost_data(ce_client, days=30):
    """Query AWS Cost Explorer for spending by service."""
    end_date = datetime.now().strftime("%Y-%m-%d")
    start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")

    response = ce_client.get_cost_and_usage(
        TimePeriod={"Start": start_date, "End": end_date},
        Granularity="MONTHLY",
        Metrics=["UnblendedCost", "UsageQuantity"],
        GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}],
    )

    services = []
    for result in response["ResultsByTime"]:
        for group in result["Groups"]:
            service_name = group["Keys"][0]
            cost = float(group["Metrics"]["UnblendedCost"]["Amount"])
            if cost > 0.01:
                services.append({"service": service_name, "cost": round(cost, 2)})

    services.sort(key=lambda x: x["cost"], reverse=True)
    return services, start_date, end_date

What does this code do?

The get_cost_data function calculates a date range (today minus 30 days) and passes it to the Cost Explorer API. The Granularity parameter groups results by month, and GroupBy breaks costs down by AWS service name.

The function requests two metrics: UnblendedCost (your actual charges before credits) and UsageQuantity (how much of each service you consumed).

  • Save cost_optimizer.py.
  • Verify the module loads without errors by running this command in your terminal:
python -c "from cost_optimizer import get_cost_data; print('Module loaded successfully')"

You should see Module loaded successfully printed with no errors.

Seeing a ModuleNotFoundError?

Make sure your terminal is in the same folder where you saved cost_optimizer.py. If you see No module named 'boto3', confirm your virtual environment is activated.

help me fix my import error when loading cost_optimizer

Parse and format cost data

The API response from Cost Explorer has a nested structure. Understanding it helps you debug issues if the output looks unexpected.

How does the response parsing work?

Cost Explorer returns a ResultsByTime list, where each entry represents a time period (in this case, one month). Inside each time period, Groups contains one entry per AWS service.

Each group has a Keys list (the service name) and a Metrics dictionary with the cost amount as a string. The code converts this to a float, filters out negligible costs below $0.01, and sorts the final list from highest to lowest spend.

Display a cost summary

Now run the function against your real AWS account to verify it pulls live data.

  • Test the function by running this command:
python -c "
import boto3
from cost_optimizer import get_cost_data

ce_client = boto3.client('ce', region_name='us-east-1')
data, start, end = get_cost_data(ce_client)
print(f'Period: {start} to {end}')
print(f'Services with costs: {len(data)}')
for s in data[:5]:
    print(f'  {s[chr(39)+chr(39)}service{chr(39)+chr(39)}]: \${s[chr(39)+chr(39)}cost{chr(39)+chr(39)}]}')
"

You should see a date range and a list of your top AWS services with their costs. If your account is new or has minimal usage, you may see only one or two services listed.

What if I see zero services?

If you see Services with costs: 0, your account may not have any charges above $0.01 in the last 30 days. This is fine. The function works correctly and will return data once your account has billable usage.

Getting an AccessDeniedException?

Your IAM user or role needs the ce:GetCostAndUsage permission. If you are using the root account, this permission is granted by default. For IAM users, attach a policy with ce:* on resource * to grant full Cost Explorer access.

help me fix my AccessDeniedException when calling the Cost Explorer API

✔️ Awesome, I've got everything!

Great. Double check you've saved cost_optimizer.py and confirmed the module loads without errors.

ⓧ I'd like to double check the full code

Here is the complete cost_optimizer.py so far:

import boto3
import json
from datetime import datetime, timedelta


def get_cost_data(ce_client, days=30):
    """Query AWS Cost Explorer for spending by service."""
    end_date = datetime.now().strftime("%Y-%m-%d")
    start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")

    response = ce_client.get_cost_and_usage(
        TimePeriod={"Start": start_date, "End": end_date},
        Granularity="MONTHLY",
        Metrics=["UnblendedCost", "UsageQuantity"],
        GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}],
    )

    services = []
    for result in response["ResultsByTime"]:
        for group in result["Groups"]:
            service_name = group["Keys"][0]
            cost = float(group["Metrics"]["UnblendedCost"]["Amount"])
            if cost > 0.01:
                services.append({"service": service_name, "cost": round(cost, 2)})

    services.sort(key=lambda x: x["cost"], reverse=True)
    return services, start_date, end_date

Your cost retrieval module is working. Next up, you'll add idle resource detection using AWS Compute Optimizer to find resources that are burning money without doing useful work.

Detect Idle Resources with Compute Optimizer

You have a clear picture of where your money is going. But knowing what you spend on each service does not tell you which specific resources are sitting idle and wasting money.

In this step, you will use AWS Compute Optimizer to automatically detect idle resources across your account. Then you will build a combined display that shows cost data and idle findings together in the terminal.

In this step, get ready to:
  • Query Compute Optimizer for idle resource recommendations.
  • Parse idle findings into a structured format with savings estimates.
  • Build a terminal display that merges cost data with idle resource alerts.
Query Compute Optimizer for idle recommendations

AWS Compute Optimizer monitors your resource utilization over a 14-day lookback period and flags anything that appears idle. Instead of building custom detection logic, you can query the get_idle_recommendations API to get pre-computed idle findings and savings estimates.

  • In cost_optimizer.py, find the closing line of the get_cost_data function:
    return services, start_date, end_date
  • Below that function, add the following get_idle_resources function:
def get_idle_resources(co_client):
    """Query Compute Optimizer for idle resource recommendations."""
    try:
        response = co_client.get_idle_recommendations(
            maxResults=50,
            orderBy={"dimension": "SavingsValue", "order": "Desc"},
        )

        idle_resources = []
        for rec in response.get("idleRecommendations", []):
            resource = {
                "resource_id": rec.get("resourceId", "Unknown"),
                "resource_type": rec.get("resourceType", "Unknown"),
                "finding": rec.get("finding", "Unknown"),
                "estimated_monthly_savings": rec.get("savingsOpportunity", {})
                .get("estimatedMonthlySavings", {})
                .get("value", 0),
            }
            idle_resources.append(resource)

        return idle_resources

    except Exception as e:
        print(f"Note: Could not fetch idle recommendations: {e}")
        print("This is normal if Compute Optimizer is not enabled or has no data yet.")
        return []

What does this code do?

This function queries AWS Compute Optimizer for idle resource findings and handles errors gracefully.

  • get_idle_recommendations retrieves up to 50 idle resources, ordered by savings value (highest first).
  • The for loop extracts four key fields from each recommendation: the resource ID, type, finding category, and estimated monthly savings.
  • The try/except block ensures the script continues running even if Compute Optimizer is not enabled or has no data. It returns an empty list instead of crashing.
  • Save cost_optimizer.py.

Seeing a syntax error?

Check that the function is indented at the module level (no extra indentation). The def get_idle_resources line should start at column 0, aligned with def get_cost_data above it.

Also verify you have two blank lines between the end of get_cost_data and the start of get_idle_resources.

help me fix a syntax error in my get_idle_resources function

Extract savings opportunities

Your function parses four fields from each idle recommendation. Understanding what these fields represent helps you make sense of the AI report later.

What resource types does Compute Optimizer scan?

The get_idle_recommendations API covers six resource types: EC2 instances, Auto Scaling groups, EBS volumes, ECS services, RDS database instances, and NAT Gateways.

Each resource gets a finding value of Idle, Unattached, or Unused. The estimated_monthly_savings field shows how much you would save by terminating or stopping that resource.

Why does the function handle errors gracefully?

Compute Optimizer requires opt-in and needs at least 14 days of utilization data before it generates recommendations. New AWS accounts or accounts that have not enabled the service will return an OptInRequiredException.

By catching all exceptions and returning an empty list, your script can still generate a useful cost report using Cost Explorer data alone. The idle resource section will simply be empty.

Build a combined findings dictionary

Now you will create a display function that merges your cost data and idle resource findings into a single terminal summary. This gives you a quick overview before generating the AI report.

  • In cost_optimizer.py, add the following display_cost_summary function below get_idle_resources:
def display_cost_summary(cost_data, idle_resources):
    """Print a formatted cost summary to the terminal."""
    total_cost = sum(s["cost"] for s in cost_data)

    print("\n" + "=" * 60)
    print("  AWS COST OPTIMIZATION ASSISTANT")
    print("=" * 60)
    print(f"\n  Total Spend (Last 30 Days): ${total_cost:.2f}")
    print(f"  Services with Costs: {len(cost_data)}")
    print(f"  Idle Resources Detected: {len(idle_resources)}")
    print("\n  Top 5 Services by Cost:")
    print("  " + "-" * 40)

    for i, service in enumerate(cost_data[:5], 1):
        bar_length = int((service["cost"] / cost_data[0]["cost"]) * 20)
        bar = "█" * bar_length
        print(f"  {i}. {service['service'][:30]:<30} ${service['cost']:>8.2f} {bar}")

    if idle_resources:
        total_savings = sum(r["estimated_monthly_savings"] for r in idle_resources)
        print(f"\n  Potential Monthly Savings: ${total_savings:.2f}")
        print("  " + "-" * 40)
        for resource in idle_resources[:5]:
            print(
                f"  - [{resource['finding']}] {resource['resource_type']}: "
                f"{resource['resource_id']} (save ${resource['estimated_monthly_savings']:.2f}/mo)"
            )

    print("\n" + "=" * 60)

What does this code do?

This function prints a formatted dashboard to your terminal that combines both data sources.

  • The top section shows total spend, service count, and idle resource count as headline metrics.
  • The bar chart visualizes relative cost per service. Each bar's length is proportional to the highest-spending service.
  • The idle resources section only appears if findings exist. It shows the finding type, resource type, ID, and potential savings for each idle resource.
  • Save cost_optimizer.py.
  • You will not run this function yet. The next step wires everything together with Bedrock to produce the final report.

Seeing indentation errors in display_cost_summary?

Make sure the def display_cost_summary line starts at column 0 (module level). Each print statement inside should be indented one level (4 spaces).

Check that the f-string lines inside the for loops use matching quote types. The outer string uses double quotes, so internal dictionary keys use single quotes.

help me fix indentation issues in my display_cost_summary function

✔️ Awesome, I've got everything!

Great. Double check you have saved cost_optimizer.py with both new functions.

ⓧ I'd like to double check the full code

Here is the complete cost_optimizer.py at the end of this step:

import boto3
import json
from datetime import datetime, timedelta


def get_cost_data(ce_client, days=30):
    """Query AWS Cost Explorer for spending by service."""
    end_date = datetime.now().strftime("%Y-%m-%d")
    start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")

    response = ce_client.get_cost_and_usage(
        TimePeriod={"Start": start_date, "End": end_date},
        Granularity="MONTHLY",
        Metrics=["UnblendedCost", "UsageQuantity"],
        GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}],
    )

    services = []
    for result in response["ResultsByTime"]:
        for group in result["Groups"]:
            service_name = group["Keys"][0]
            cost = float(group["Metrics"]["UnblendedCost"]["Amount"])
            if cost > 0.01:
                services.append({"service": service_name, "cost": round(cost, 2)})

    services.sort(key=lambda x: x["cost"], reverse=True)
    return services, start_date, end_date


def get_idle_resources(co_client):
    """Query Compute Optimizer for idle resource recommendations."""
    try:
        response = co_client.get_idle_recommendations(
            maxResults=50,
            orderBy={"dimension": "SavingsValue", "order": "Desc"},
        )

        idle_resources = []
        for rec in response.get("idleRecommendations", []):
            resource = {
                "resource_id": rec.get("resourceId", "Unknown"),
                "resource_type": rec.get("resourceType", "Unknown"),
                "finding": rec.get("finding", "Unknown"),
                "estimated_monthly_savings": rec.get("savingsOpportunity", {})
                .get("estimatedMonthlySavings", {})
                .get("value", 0),
            }
            idle_resources.append(resource)

        return idle_resources

    except Exception as e:
        print(f"Note: Could not fetch idle recommendations: {e}")
        print("This is normal if Compute Optimizer is not enabled or has no data yet.")
        return []


def display_cost_summary(cost_data, idle_resources):
    """Print a formatted cost summary to the terminal."""
    total_cost = sum(s["cost"] for s in cost_data)

    print("\n" + "=" * 60)
    print("  AWS COST OPTIMIZATION ASSISTANT")
    print("=" * 60)
    print(f"\n  Total Spend (Last 30 Days): ${total_cost:.2f}")
    print(f"  Services with Costs: {len(cost_data)}")
    print(f"  Idle Resources Detected: {len(idle_resources)}")
    print("\n  Top 5 Services by Cost:")
    print("  " + "-" * 40)

    for i, service in enumerate(cost_data[:5], 1):
        bar_length = int((service["cost"] / cost_data[0]["cost"]) * 20)
        bar = "█" * bar_length
        print(f"  {i}. {service['service'][:30]:<30} ${service['cost']:>8.2f} {bar}")

    if idle_resources:
        total_savings = sum(r["estimated_monthly_savings"] for r in idle_resources)
        print(f"\n  Potential Monthly Savings: ${total_savings:.2f}")
        print("  " + "-" * 40)
        for resource in idle_resources[:5]:
            print(
                f"  - [{resource['finding']}] {resource['resource_type']}: "
                f"{resource['resource_id']} (save ${resource['estimated_monthly_savings']:.2f}/mo)"
            )

    print("\n" + "=" * 60)

Your script can now pull cost data and detect idle resources. Next up, you will feed both datasets into Amazon Bedrock to generate a professional AI-powered optimization report.

Generate an AI Optimization Report with Bedrock

You now have cost data from Cost Explorer and idle resource findings from Compute Optimizer. Raw data alone is hard to act on. A busy team needs a structured report with clear priorities and dollar amounts.

In this step, you will use Amazon Bedrock to send your collected data to Claude Sonnet 4.6. The model will analyze the numbers and generate a professional optimization report in Markdown format.

In this step, get ready to:
  • Build a detailed prompt that frames Claude as a FinOps analyst and feeds it your cost data.
  • Call the Bedrock converse API to generate an optimization report.
  • Wire up a main function that orchestrates the full pipeline and saves the report to a Markdown file.
Construct the Bedrock prompt

The quality of an AI-generated report depends entirely on the prompt. You need to give Claude all the context it needs: the cost data, idle resource findings, and clear instructions for the report structure.

  • In cost_optimizer.py, find the get_idle_resources function. Add the following generate_report function directly below it (before display_cost_summary):
def generate_report(bedrock_client, cost_data, idle_resources, start_date, end_date):
    """Use Bedrock Claude Sonnet 4.6 to generate an optimization report."""
    total_cost = sum(s["cost"] for s in cost_data)
    total_idle_savings = sum(r["estimated_monthly_savings"] for r in idle_resources)

    prompt = f"""You are a senior FinOps analyst. Analyze the following AWS cost data and idle resource findings, then produce a professional cost optimization report in Markdown format.

## Cost Data (Period: {start_date} to {end_date})
Total Spend: ${total_cost:.2f}
Top Services by Cost:
{json.dumps(cost_data[:10], indent=2)}

## Idle Resource Findings
Total Potential Monthly Savings from Idle Resources: ${total_idle_savings:.2f}
Idle Resources Detected:
{json.dumps(idle_resources[:10], indent=2) if idle_resources else "No idle resources detected by Compute Optimizer (this may mean the service is not yet enabled or no idle resources exist)."}

## Instructions
Generate a report with these sections:
1. **Executive Summary** - One paragraph overview of spending health
2. **Cost Breakdown** - Top 5 services with spending context
3. **Idle Resource Alerts** - List each idle resource with recommended action
4. **Savings Recommendations** - Prioritized list of 3-5 actionable savings opportunities
5. **Next Steps** - Concrete actions the team should take this week

Keep the tone professional but actionable. Include specific dollar amounts where possible."""

    # Call Bedrock's converse API with Claude Sonnet 4.6
    response = bedrock_client.converse(
        modelId="us.anthropic.claude-sonnet-4-6",
        messages=[{"role": "user", "content": [{"text": prompt}]}],
        inferenceConfig={"maxTokens": 2048, "temperature": 0.3},
    )

    # Extract the generated text from the response
    report_text = response["output"]["message"]["content"][0]["text"]
    return report_text

What does this code do?

This function does two things: builds a detailed prompt and calls the Bedrock API.

  • The prompt frames Claude as a "senior FinOps analyst" and feeds it your actual cost data (top 10 services) and idle resource findings (top 10 resources). It also specifies exactly which sections the report should contain.
  • The converse API is Bedrock's recommended method for invoking models. It handles message formatting consistently across different model providers.
  • The temperature: 0.3 setting keeps output focused and consistent. A lower temperature means less creative randomness in the analysis.
  • The model ID us.anthropic.claude-sonnet-4-6 is the geo inference ID for Claude Sonnet 4.6 in Bedrock. This routes to the optimal endpoint for your region.
  • Save cost_optimizer.py.

Seeing a syntax error in the prompt string?

The prompt uses an f-string with triple quotes (f"""..."""). Make sure your opening and closing triple quotes are on their own lines and the curly braces around variables like {start_date} are not escaped.

Also check that the json.dumps calls inside the f-string are correctly nested. The conditional expression (if idle_resources else ...) must be on a single logical line.

Help me fix a syntax error in my generate_report function's f-string prompt

Invoke Claude Sonnet 4.6 via Bedrock

The generate_report function is ready, but nothing calls it yet. You need a main function that initializes all three AWS clients, runs the pipeline, and saves the AI-generated report to disk.

  • At the very bottom of cost_optimizer.py, after the display_cost_summary function, add the following main function and entry point:
def main():
    """Main entry point for the cost optimizer."""
    print("Initializing AWS Cost Optimizer...")

    # Create AWS service clients (all in us-east-1)
    ce_client = boto3.client("ce", region_name="us-east-1")
    co_client = boto3.client("compute-optimizer", region_name="us-east-1")
    bedrock_client = boto3.client("bedrock-runtime", region_name="us-east-1")

    # Fetch cost data from Cost Explorer
    print("Fetching cost data from Cost Explorer...")
    cost_data, start_date, end_date = get_cost_data(ce_client)

    # Check for idle resources
    print("Checking for idle resources...")
    idle_resources = get_idle_resources(co_client)

    # Display summary in terminal
    display_cost_summary(cost_data, idle_resources)

    # Generate AI optimization report
    print("\nGenerating AI optimization report with Bedrock...")
    report = generate_report(bedrock_client, cost_data, idle_resources, start_date, end_date)

    # Save report to a timestamped Markdown file
    report_filename = f"cost_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
    with open(report_filename, "w", encoding="utf-8") as f:
        f.write(f"# AWS Cost Optimization Report\n")
        f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
        f.write(report)

    print(f"\nReport saved to: {report_filename}")
    print("\nDone! Open the report file to see your AI-generated optimization analysis.")


if __name__ == "__main__":
    main()

What does this code do?

  • Three boto3 clients are created: ce for Cost Explorer, compute-optimizer for idle resource detection, and bedrock-runtime for invoking Claude. All use us-east-1 since that is where you enabled Bedrock model access.
  • The pipeline runs sequentially: fetch costs, check idle resources, display summary, generate AI report, save to file.
  • The report saves to a timestamped filename like cost_report_20260605_143022.md so you can run it multiple times without overwriting previous reports.
  • The if __name__ == "__main__" guard ensures main() only runs when you execute the script directly, not when importing it as a module.
  • Save cost_optimizer.py.

Seeing an indentation error?

Make sure main() is defined at the top level (no indentation), not nested inside another function. The if __name__ block should also be at the top level, flush with the left margin.

Help me fix an indentation error in my cost_optimizer.py main function

Save the report

Your script is complete. Let's verify the full file looks correct before running it in the next step.

✔️ Awesome, I've got everything!

Great. Double check you have saved cost_optimizer.py with all your changes.

ⓧ I'd like to double check the full code

Here is the complete cost_optimizer.py file at this point. Compare yours against it:

import boto3
import json
from datetime import datetime, timedelta


def get_cost_data(ce_client, days=30):
    """Query AWS Cost Explorer for spending by service."""
    end_date = datetime.now().strftime("%Y-%m-%d")
    start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")

    response = ce_client.get_cost_and_usage(
        TimePeriod={"Start": start_date, "End": end_date},
        Granularity="MONTHLY",
        Metrics=["UnblendedCost", "UsageQuantity"],
        GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}],
    )

    services = []
    for result in response["ResultsByTime"]:
        for group in result["Groups"]:
            service_name = group["Keys"][0]
            cost = float(group["Metrics"]["UnblendedCost"]["Amount"])
            if cost > 0.01:
                services.append({"service": service_name, "cost": round(cost, 2)})

    services.sort(key=lambda x: x["cost"], reverse=True)
    return services, start_date, end_date


def get_idle_resources(co_client):
    """Query Compute Optimizer for idle resource recommendations."""
    try:
        response = co_client.get_idle_recommendations(
            maxResults=50,
            orderBy={"dimension": "SavingsValue", "order": "Desc"},
        )

        idle_resources = []
        for rec in response.get("idleRecommendations", []):
            resource = {
                "resource_id": rec.get("resourceId", "Unknown"),
                "resource_type": rec.get("resourceType", "Unknown"),
                "finding": rec.get("finding", "Unknown"),
                "estimated_monthly_savings": rec.get("savingsOpportunity", {})
                .get("estimatedMonthlySavings", {})
                .get("value", 0),
            }
            idle_resources.append(resource)

        return idle_resources

    except Exception as e:
        print(f"Note: Could not fetch idle recommendations: {e}")
        print("This is normal if Compute Optimizer is not enabled or has no data yet.")
        return []


def generate_report(bedrock_client, cost_data, idle_resources, start_date, end_date):
    """Use Bedrock Claude Sonnet 4.6 to generate an optimization report."""
    total_cost = sum(s["cost"] for s in cost_data)
    total_idle_savings = sum(r["estimated_monthly_savings"] for r in idle_resources)

    prompt = f"""You are a senior FinOps analyst. Analyze the following AWS cost data and idle resource findings, then produce a professional cost optimization report in Markdown format.

## Cost Data (Period: {start_date} to {end_date})
Total Spend: ${total_cost:.2f}
Top Services by Cost:
{json.dumps(cost_data[:10], indent=2)}

## Idle Resource Findings
Total Potential Monthly Savings from Idle Resources: ${total_idle_savings:.2f}
Idle Resources Detected:
{json.dumps(idle_resources[:10], indent=2) if idle_resources else "No idle resources detected by Compute Optimizer (this may mean the service is not yet enabled or no idle resources exist)."}

## Instructions
Generate a report with these sections:
1. **Executive Summary** - One paragraph overview of spending health
2. **Cost Breakdown** - Top 5 services with spending context
3. **Idle Resource Alerts** - List each idle resource with recommended action
4. **Savings Recommendations** - Prioritized list of 3-5 actionable savings opportunities
5. **Next Steps** - Concrete actions the team should take this week

Keep the tone professional but actionable. Include specific dollar amounts where possible."""

    response = bedrock_client.converse(
        modelId="us.anthropic.claude-sonnet-4-6",
        messages=[{"role": "user", "content": [{"text": prompt}]}],
        inferenceConfig={"maxTokens": 2048, "temperature": 0.3},
    )

    report_text = response["output"]["message"]["content"][0]["text"]
    return report_text


def display_cost_summary(cost_data, idle_resources):
    """Print a formatted cost summary to the terminal."""
    total_cost = sum(s["cost"] for s in cost_data)

    print("\n" + "=" * 60)
    print("  AWS COST OPTIMIZATION ASSISTANT")
    print("=" * 60)
    print(f"\n  Total Spend (Last 30 Days): ${total_cost:.2f}")
    print(f"  Services with Costs: {len(cost_data)}")
    print(f"  Idle Resources Detected: {len(idle_resources)}")
    print("\n  Top 5 Services by Cost:")
    print("  " + "-" * 40)

    for i, service in enumerate(cost_data[:5], 1):
        bar_length = int((service["cost"] / cost_data[0]["cost"]) * 20)
        bar = "\u2588" * bar_length
        print(f"  {i}. {service['service'][:30]:<30} ${service['cost']:>8.2f} {bar}")

    if idle_resources:
        total_savings = sum(r["estimated_monthly_savings"] for r in idle_resources)
        print(f"\n  Potential Monthly Savings: ${total_savings:.2f}")
        print("  " + "-" * 40)
        for resource in idle_resources[:5]:
            print(
                f"  - [{resource['finding']}] {resource['resource_type']}: "
                f"{resource['resource_id']} (save ${resource['estimated_monthly_savings']:.2f}/mo)"
            )

    print("\n" + "=" * 60)


def main():
    """Main entry point for the cost optimizer."""
    print("Initializing AWS Cost Optimizer...")

    ce_client = boto3.client("ce", region_name="us-east-1")
    co_client = boto3.client("compute-optimizer", region_name="us-east-1")
    bedrock_client = boto3.client("bedrock-runtime", region_name="us-east-1")

    print("Fetching cost data from Cost Explorer...")
    cost_data, start_date, end_date = get_cost_data(ce_client)

    print("Checking for idle resources...")
    idle_resources = get_idle_resources(co_client)

    display_cost_summary(cost_data, idle_resources)

    print("\nGenerating AI optimization report with Bedrock...")
    report = generate_report(bedrock_client, cost_data, idle_resources, start_date, end_date)

    report_filename = f"cost_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
    with open(report_filename, "w", encoding="utf-8") as f:
        f.write(f"# AWS Cost Optimization Report\n")
        f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
        f.write(report)

    print(f"\nReport saved to: {report_filename}")
    print("\nDone! Open the report file to see your AI-generated optimization analysis.")


if __name__ == "__main__":
    main()

Your cost optimizer is fully wired up. In the next step, you will run the script end-to-end against your real AWS account and review the AI-generated report.

Run Your Cost Optimizer End-to-End

Your cost optimizer script is fully assembled. It can query Cost Explorer, detect idle resources, and generate AI-powered reports.

Now it's time to run the full pipeline against your live AWS account and see what your actual spending looks like through the lens of an AI FinOps analyst.

In this step, get ready to:
  • Execute the full cost optimizer pipeline against your live AWS account.
  • Review the AI-generated Markdown report for actionable insights.
  • Cross-reference the AI recommendations with your AWS console.
Execute the full pipeline

Your script connects to three AWS services in sequence: Cost Explorer for spending data, Compute Optimizer for idle resources, and Bedrock for AI analysis. Running it triggers the entire chain.

  • Make sure your virtual environment is still activated (you should see the environment name in your terminal prompt).
  • Run the cost optimizer by executing this command:
python cost_optimizer.py

The script takes 10-30 seconds to complete. You'll see progress messages as each stage runs.

What should I see?

Your terminal displays a formatted cost summary table with your top 5 AWS services by spending, a bar chart visualization, the number of idle resources detected, and potential monthly savings.

After the summary, the script prints the filename of your saved report. If Compute Optimizer has no data for your account, you'll see a note saying idle recommendations could not be fetched. This is normal for newer accounts.

Seeing an error when running the script?

  • If you see an AccessDeniedException for Cost Explorer, your IAM user may not have the required permissions. You need the ce:GetCostAndUsage permission. Root account users have this by default.
  • If you see an AccessDeniedException for Bedrock, double-check that you enabled model access for Claude Sonnet 4.6 in the Bedrock console in the us-east-1 region.
  • If you see a NoCredentialError, make sure your virtual environment is activated and your AWS CLI is configured with valid credentials.

Help me debug my cost optimizer script error

Review the AI-generated report

The script saved a Markdown report to your project folder. The filename includes a timestamp so you can generate multiple reports without overwriting previous ones.

  • Find the report file in your project folder. It's named cost_report_YYYYMMDD_HHMMSS.md with the actual date and time of your run.
  • Open the report file in your code editor to view the formatted Markdown.

What's in the report?

The AI-generated report contains five sections, each tailored to your actual AWS data:

  • Executive Summary gives a one-paragraph health check of your spending patterns.
  • Cost Breakdown highlights your top 5 services with context about what's driving costs.
  • Idle Resource Alerts lists each idle resource with a specific recommended action.
  • Savings Recommendations provides 3-5 prioritized, actionable savings opportunities with dollar amounts.
  • Next Steps gives concrete actions your team should take this week.
  • Read through each section of the report. Pay attention to the dollar amounts and specific service names the AI references.
  • Check whether the Idle Resource Alerts section lists specific resources or notes that none were detected. Both outcomes are valid depending on your account's history.
Validate recommendations against your AWS console

AI-generated recommendations are only useful if they reflect reality. The final step is confirming the report's findings against your actual AWS account.

  • Open the AWS Management Console in your browser.
  • Navigate to Billing and Cost Management by searching for it in the console search bar.
  • Select Cost Explorer from the left sidebar.
  • Compare the top services shown in Cost Explorer's chart with the services your report identified as highest-cost.

Why validate AI output?

AI models can occasionally hallucinate details or misinterpret data. In a real FinOps workflow, you always cross-reference automated recommendations with the source before acting on them.

If you spot a recommendation that doesn't match your console data, that's a signal to refine the prompt or add more context to the input data.

  • If your report mentions idle resources, navigate to Compute Optimizer in the console and check the Idle resources section to confirm the findings match.
  • Note any discrepancies between the AI report and the console. These help you understand where the AI adds value (pattern recognition, prioritization) versus where raw data is more reliable (exact dollar amounts).

Report shows different numbers than the console?

  • Small differences in cost totals are normal. The API returns data with a slight delay, and Cost Explorer's UI may apply different rounding.
  • If major services are missing, check whether your script's if cost > 0.01 filter is excluding services you expected to see.
  • Make sure the date range in the console matches the last 30 days that your script queries.

Why does my report show different data than the AWS console?

Your cost optimizer is working end-to-end. You now have a repeatable tool that pulls live data, detects waste, and generates professional reports you can share with your team.

Secret mission

Add Month-over-Month Trend Analysis

Your cost optimizer shows what you're spending right now. But what about sudden spikes? Add trend analysis that compares this month against last month, flags services with suspicious cost increases, and feeds anomaly data into your AI report for automated explanations.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and creates no persistent AWS infrastructure. The only costs incurred are the Bedrock API token charges and Cost Explorer API calls you already made, which total under $0.50.

Decide whether to keep your project files to run periodically, or delete them entirely.

Resources you used:

  • Local project folder with cost_optimizer.py and generated report files.
  • Python virtual environment inside the project folder.
  • Amazon Bedrock model access for Claude Sonnet 4.6 (enabled in the AWS console).
  • AWS CLI credentials stored locally in ~/.aws/.

Keep everything running

No action needed. Choose this if you want to keep running the cost optimizer periodically to monitor your AWS spending.

  • Your cost_optimizer.py script is local and costs nothing to retain.
  • Run it weekly or monthly to generate fresh optimization reports as your AWS usage changes.
  • Each run costs approximately $0.03 in Bedrock token charges.
  • Your AWS CLI credentials and Bedrock model access remain configured for future use.

Pause - I'll come back to this later

Since this project creates no running infrastructure, there is nothing to pause. Your files sit idle on disk with zero ongoing cost.

  • Leave the project folder as-is on your Desktop.
  • When you return, activate your virtual environment and run the script again.

Delete - I don't want to use this again

Remove all project files and optionally revoke access you enabled during this project.

  • Delete the project folder by running:

macOS

cd ~/Desktop
rm -rf cost-optimizer

Windows

cd ~\Desktop
rmdir /s /q cost-optimizer
  • To revoke Bedrock model access, open the Amazon Bedrock console in us-east-1.
  • Select Model access in the left sidebar.
  • Click Manage model access, uncheck Claude Sonnet 4.6, and click Save changes.
  • To remove your stored AWS CLI credentials, delete the credentials file:

macOS

rm ~/.aws/credentials

Windows

del %USERPROFILE%\.aws\credentials

Keep credentials if you use AWS elsewhere

Only delete your AWS credentials file if you do not use the AWS CLI for other projects. Removing it means you will need to run aws configure again next time.

Nice Work!

Nice Work!

You just built a real cloud cost optimization tool that combines multiple AWS services into an automated FinOps pipeline.

You've learned how to:

  • Query live AWS cost data using the Cost Explorer API with boto3 to get a real-time breakdown of your spending by service.
  • Detect idle and underutilized resources using AWS Compute Optimizer to surface concrete savings opportunities.
  • Generate professional optimization reports using Amazon Bedrock (Claude Sonnet 4.6) that turn raw data into actionable recommendations.
  • Added month-over-month trend analysis with anomaly detection to catch unexpected cost spikes before they become budget problems.

Ready to quiz yourself?