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.

Ressources
- GitHub: DevOpsBug/AWS_DailyCostReport (Branch: “sns_topic”).
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.

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.

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.

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

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>

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'
)
[...]

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,
})
}

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"

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:
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.

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

Test the Lambda Function in the AWS Web Console
Now we will test the Lambda Function in the Web Console.
- navigate to: Lambda > CostReportFunction.
- Open the Code tab.
- Click Test, then Create new test event (you can leave the payload empty).
- Run the test and verify that you received the Email Alert

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.

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.

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.

🙂 Happy coding!