Introduction

Welcome back to the series on automating daily AWS cost reports. In the previous tutorial, we created a Python-based AWS Lambda function using Boto3 to query the Cost Explorer API and retrieve cost data broken down by service.

In this post, we’ll build on that foundation by integrating Amazon SNS (Simple Notification Service). Specifically, we’ll:

  • Add an SNS Topic with an email subscription.
  • Modify our SAM template to include the new SNS resources.
  • Update the Lambda function to send the cost report via email.
  • Test the full flow locally and in the AWS Console.

By the end of this tutorial, you’ll have an automated cost report emailed to you, powered by a serverless stack using AWS Lambda, Cost Explorer, and SNS.

Ladybug Image

Ressources


Why automated cost reports are useful

Keeping track of your AWS costs shouldn’t require logging into the AWS Console every day. As your cloud usage grows, so does the risk of unexpected cost spikes — whether from a forgotten EC2 instance, a misconfigured service, or just regular usage patterns that get out of hand.

This post is about taking proactive control of your cloud spending by automating daily cost visibility. Instead of waiting for your monthly bill or digging through Cost Explorer manually, you’ll receive a simple daily email with a breakdown of your current month-to-date spend by service.

This helps you:

🔍 Catch anomalies early — e.g., sudden spend increases from a new service or deployment.

💸 Avoid surprises at the end of the month.

📊 Monitor trends in usage and spending over time.

🛠️ Build internal visibility without relying on expensive third-party tools.

Whether you’re running a dev environment, a SaaS product, or just learning AWS, this setup gives you a lightweight, serverless solution to stay in control of your cloud budget — and it’s fully customizable to your needs.

Ladybug Image

Architecture

The solution uses a scheduled EventBridge rule to invoke a Lambda function once per day. This function queries the AWS Cost Explorer API to retrieve the latest month-to-date cost data, grouped by service. The resulting cost report is then published to an SNS topic. An email subscription to the SNS topic ensures the report is automatically delivered to the specified recipient’s inbox.

Architecture Diagram - DailyCostReportLambda

Ladybug Image

Prerequisites

Before we begin, ensure you have the following:

  • AWS Account: Active account with appropriate permissions.
  • AWS CLI: Installed and configured with your credentials.
  • SAM CLI: Installed and configured on your local machine.
  • AWS SDK for Python (Boto3): Installed in your development environment.
Ladybug Image

Addding SNS to the SAM Template

First, we define a parameter for the email address that will receive the alerts:

Parameters:
  EmailAddress:
    NoEcho: true
    Description: E-Mail Address for SNS Subscription
    Type: String

Next, add an SNS Topic with an email subscription to the Resources section:

  CostReportSNSTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: DailyCostReport
      Subscription:
        - Endpoint: !Ref EmailAddress
          Protocol: email

We’ll also pass the SNS Topic ARN as an environment variable to the Lambda function and update its permissions:

  CostReportFunction:
    Type: AWS::Serverless::Function 
    Properties:
      ...
      Policies:
        - Statement:
            - Effect: Allow
              Action:
                - ce:GetCostAndUsage
              Resource: "*"
        - SNSPublishMessagePolicy:
            TopicName: !GetAtt CostReportSNSTopic.TopicName
      Environment:
        Variables:
          SNS_TOPIC_ARN: !Ref CostReportSNSTopic
Ladybug Image

Passing Parameter Values to SAM

To pass the values for the parameters defined in the SAM Template, we use the --parameter-overrides flag in the sam cli. You will see this later when we deploy the Stack.

#For Example:
sam deploy [...] --parameter-overrides EmailAddress=<YOUR EMAIL ADDRESS>
Ladybug Image

Publishing Data to SNS using Python Boto3

To publish data to an SNS Topic using boto3 we use the SNS Client like so:

	[...]
	sns_client = boto3.client('sns')
    topic_arn = os.environ['SNS_TOPIC_ARN']
	[...]
	response = sns_client.publish(
        TopicArn=topic_arn,
        Message=json.dumps(message),
        Subject='AWS Daily Cost Report'
    )
	[...]
Ladybug Image

Update the Lambda Function

Update your Lambda to:

  • Use ce_client instead of client to avoid name clashes.
  • Create an sns_client.
  • Format the cost data
  • Send it to SNS.

Here’s the updated Python code:

import boto3
import os
import json
from datetime import datetime

def lambda_handler(event, context):
    ce_client = boto3.client('ce')
    sns_client = boto3.client('sns')
    topic_arn = os.environ['SNS_TOPIC_ARN']
    
    # Get first day of current month and today's date
    end_date = datetime.now().date()        #current date
    start_date = end_date.replace(day=1)    #first day of the month
    
    response = ce_client.get_cost_and_usage(
        TimePeriod={
            'Start': start_date.strftime('%Y-%m-%d'),
            'End': end_date.strftime('%Y-%m-%d')
        },
        Granularity='MONTHLY',  # Changed to MONTHLY for month-to-date view
        Metrics=['UnblendedCost'],
        GroupBy=[
            {
                'Type': 'DIMENSION',
                'Key': 'SERVICE'
            }
        ]
    )
    
    # Process and format the response
    cost_data = []
    total_cost = 0.00
    for result in response['ResultsByTime']:
        for group in result['Groups']:
            service_name = group['Keys'][0]
            amount = float(group['Metrics']['UnblendedCost']['Amount'])
            unit = group['Metrics']['UnblendedCost']['Unit']
            total_cost += amount        #aggregate total cost
            if amount > 0.00:           #filter out zero-cost services
                cost_data.append({
                    'Service': service_name,
                    'Cost': f"{amount:.2f}",
                    'Currency': unit
                })
    
    # Sort services by cost (highest to lowest)
    cost_data.sort(key=lambda x: float(x['Cost']), reverse=True)
    
    message = {
            'startDate': start_date.strftime('%Y-%m-%d'),
            'endDate': end_date.strftime('%Y-%m-%d'),
            'totalCost': f"{total_cost:.2f}",
            'costs': cost_data,
            'currency': unit    # Currency unit
        }
    
    response = sns_client.publish(
        TopicArn=topic_arn,
        Message=json.dumps(message),
        Subject='AWS Daily Cost Report'
    )
    
    return {
        'statusCode': 200,
        'body': json.dumps({
            'message': 'Cost report sent successfully!',
            'sns_response': response,
            
        })
    }
Ladybug Image

Deploy SAM Template

Now we are ready to deploy the SAM Template with the new SNS Ressource and the updated Lambda code

sam build
sam deploy --parameter-overrides EmailAddress=<YOUR EMAIL ADDRESS>

The deploy command will ask you to confirm the changeset. And after that it will take one or two minutes to update the ressources. If everything works you will see the message

"Successfully created/updated stack"
Ladybug Image

Verify Changes

To verify the changes you can go the Web Console and check if the SNS Topic with the E-Mail subscription was created. To do this, navigate to the SNS Service, find the CostReport topic and check the subscriptions tab if the email subscription has been created. It looks something like this:

AWS SNS Screenshot

You should also get an E-Mail on the specified E-Mail Address asking you to confirm the subscription. You need to confirm the subscription to receive the E-Mail Alerts.

Ladybug Image

Invoke Lambda locally

Now we will invoke the lambda function locally to make sure we receive the E-Mail alert. If you remember we updated the Lambda Function Ressource in the SAM Template to pass the SNS Topic ARN as an environment variable. For the local invokation we need to pass this into the cli command.

Create an env.json file with the SNS topic ARN:

#env.json
{
  "CostReportFunction": {
    "SNS_TOPIC_ARN": "<ARN OF THE CREATED SNS TOPIC>"
  }
}

Make sure to replace the placeholder with the actual ARN of the created SNS Topic!

Then invoke the function locally (make sure you are authenticated with the aws cli):

sam local invoke --env-vars env.json

You should receive an email with a subject like:

AWS Daily Cost Report
Ladybug Image

Test the Lambda Function in the AWS Web Console

Now we will test the Lambda Function in the Web Console.

  • navigate to: Lambda > CostReportFunction. Function list
  • Open the Code tab. Function list
  • Click Test, then Create new test event (you can leave the payload empty). Function list
  • Run the test and verify that you received the Email Alert
Ladybug Image

Debugging the Lambda Function

If you get this error:

CostReportFunction is not authorized to perform: ce:GetCostAndUsage

This means that the function doesn’t have the permissions to access the Cost Explorer. You may wonder why we are getting a permissions error now even though it worked locally. This is because the local lambda invocation runs with the privileges of your personal IAM User but Lambda in the Cloud runs with its own Execution Role, which by default doesn’t have access to the Cost Explorer API.

Ladybug Image

Fix the IAM Permissions

We can easily fix this by adding the missing permission to the Function in our SAM Template. Go back to the SAM Template and add the following code to the policies list under the function ressource:

		- Statement:
            - Effect: Allow
              Action:
                - ce:GetCostAndUsage
              Resource: "*"

The complete SAM Template now looks like this:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  AWS_DailyCostReport

  Sample SAM Template for AWS_DailyCostReport

Globals:
  Function:
    Timeout: 3
    MemorySize: 128

Parameters:
  EmailAddress:
    NoEcho: true
    Description: E-Mail Address for SNS Subscription
    Type: String

Resources:
  CostReportSNSTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: DailyCostReport
      Subscription:
        - Endpoint: !Ref EmailAddress
          Protocol: email

  CostReportFunction:
    Type: AWS::Serverless::Function 
    Properties:
      FunctionName: CostReportFunction
      CodeUri: lambda/
      Handler: app.lambda_handler
      Runtime: python3.9
      Architectures:
        - x86_64
      Policies:
        - Statement:
            - Effect: Allow
              Action:
                - ce:GetCostAndUsage
              Resource: "*"
        - SNSPublishMessagePolicy:
            TopicName: !GetAtt CostReportSNSTopic.TopicName
      Environment:
        Variables:
          SNS_TOPIC_ARN: !Ref CostReportSNSTopic

Outputs:
  CostReportFunction:
    Description: "Cost Report Lambda Function ARN"
    Value: !GetAtt CostReportFunction.Arn
  CostReportFunctionIamRole:
    Description: "Implicit IAM Role created for Cost Report function"
    Value: !GetAtt CostReportFunctionRole.Arn

Rebuild and redeploy:

sam build
sam deploy --parameter-overrides EmailAddress=<YOUR EMAIL ADDRESS>

After deploying, re-test the function in the AWS Console. You should now receive the cost report via email.

Ladybug Image

Conclusion

In this post, we extended our cost reporting Lambda to send E-Mail alerts via AWS SNS. You learned how to:

  • Add SNS resources to a SAM template.
  • Pass parameters and environment variables.
  • Send structured data via SNS from Lambda.
  • Debug IAM permission issues.

With this setup, you now receive automatic AWS cost summaries straight to your inbox — a simple but powerful way to stay on top of your cloud spending.

COMING UP NEXT - Learn how to automatically trigger the Lambda Function every day using EventBridge to receive daily automated Cost Reports by E-Mail.

Ladybug Image
DevOpsBug-Logo

Feel free to explore my work and connect with me:

  • GitHub Repo - Explore my repositories and projects.
  • LinkedIn - Connect with me and grow your professional network.

Stay tuned to DevOpsBug for more updates, tips, and project showcases!


🙂 Happy coding!