This article describes how to automate the AWS Cost Report creation and send it via email to specific subscribers.
Unfortunately, AWS Cost Explorer lacks a built-in capability for emailing reports. As a result, alternative services and functionalities are required to devise and transmit these reports.
In this solution, a Python script executed on AWS Lambda generates the report. The report is subsequently stored in an S3 bucket and dispatched to my email. An AWS EventBridge Scheduler triggers the Lambda function every Monday at 7am UK time.
AWS IAM roles and policies are formulated to manage permissions for both EventBridge and Lambda.
Summary of AWS Services and Features
The next services and/or features were used in the solution:
Create and send the report
Firstly, we need to create an S3 Bucket. This topic will store the Cost Report in Excel format. After that, create and verify the email(s) that will receive the report in the SES. Then, create AWS IAM Roles and Policies as follow:
# AWS EventBridge Scheduler Role
# Permission to Invoke the Lambda Function
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction"
],
"Resource": [
"arn:aws:lambda:<region>:<aws-account>:function:cost-report:*",
"arn:aws:lambda:<region>:<aws-account>:function:cost-report"
]
}
]
}
# AWS Lambda Role
# Permission to generate the Cost Report (ce), send email (see), and store the file in the S3 bucket (s3)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ses:VerifyEmailIdentity",
"ses:GetIdentityPolicies",
"ses:SendRawEmail",
"ses:GetIdentityMailFromDomainAttributes",
"ce:GetCostAndUsage",
"ses:SendBounce",
"ses:GetIdentityVerificationAttributes",
"ses:GetIdentityNotificationAttributes",
"ses:SendEmail",
"ses:SendTemplatedEmail",
"ses:SendCustomVerificationEmail",
"ses:SendBulkTemplatedEmail",
"ses:ListVerifiedEmailAddresses",
"ses:ListIdentities",
"ses:VerifyEmailAddress"
],
"Resource": "*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::cost-report-<aws-account>-<region>/*"
]
}
]
}
Configure the EventBridge to trigger the Lambda function at Monday 07am with the next crontab expression:
00 07 ? * MON *
Finally, create the AWS Lambda function. The following code will create a "MONTH-TO-DATE" Cost Report of your AWS account:
import csv
import os
import boto3
import datetime
from botocore.exceptions import NoCredentialsError
def lambda_handler(event, context):
# Set up the client
ce = boto3.client('ce', region_name='us-east-1')
# Set the time period for the report to the beginning of the current month
now = datetime.datetime.now()
start = datetime.datetime(now.year, now.month, 1)
end = datetime.datetime.now()
# Define the request
request = {
'TimePeriod': {
'Start': start.strftime('%Y-%m-%d'),
'End': end.strftime('%Y-%m-%d')
},
'Granularity': 'MONTHLY',
'Metrics': ['UnblendedCost'],
'GroupBy': [{'Type': 'DIMENSION', 'Key': 'SERVICE'}]
}
# Get the report
result = ce.get_cost_and_usage(**request)
# Extract the service names, cost amounts, units, and region
service_names = []
cost_amounts = []
cost_units = []
regions = []
for row in result['ResultsByTime']:
sorted_groups = sorted(row['Groups'], key=lambda x: float(x['Metrics']['UnblendedCost']['Amount']), reverse=True)
for group in sorted_groups:
service_names.append(group['Keys'][0])
cost_amount = round(float(group['Metrics']['UnblendedCost']['Amount']), 2)
cost_amounts.append(cost_amount)
cost_units.append(group['Metrics']['UnblendedCost']['Unit'])
regions.append(ce.meta.region_name)
# Create a list of rows for the CSV file
rows = zip(service_names, cost_amounts, cost_units, regions)
# Specify the file path for the CSV file
current_date = datetime.datetime.now().strftime("%Y-%m-%d")
csv_file_path = f'/tmp/cost_report_{current_date}.csv'
# Write the rows to the CSV file
with open(csv_file_path, 'w', newline='') as csv_file:
writer = csv.writer(csv_file)
writer.writerow(['Service', 'Cost', 'Unit', 'Region']) # Write the header
writer.writerows(rows) # Write the data rows
# Upload the CSV file to S3 bucket
bucket_name = '<your S3 bucket>'
s3_key = f'cost_report_{current_date}.csv'
s3_client = session.client('s3')
try:
s3_client.upload_file(csv_file_path, bucket_name, s3_key)
print("CSV file uploaded successfully to S3 bucket.")
except FileNotFoundError:
print("The CSV file was not found.")
except NoCredentialsError:
print("AWS credentials not found.")
# Send email
# Specify the email recipient
recipient_email = "<your verified email>"
# Read the CSV file content
with open(csv_file_path, 'rb') as csv_file:
csv_content = csv_file.read()
# Send the email with the CSV file attached using SES
ses_client = boto3.client('ses', region_name='<your SES region>')
# Specify the sender's email address
sender_email = "<your verified email>"
# Specify the email subject
email_subject = "AWS Cost Report CSV"
# Create the raw email content
raw_email = (
f"From: {sender_email}\n"
f"To: {recipient_email}\n"
f"Subject: {email_subject}\n"
"MIME-Version: 1.0\n"
"Content-Type: multipart/mixed; boundary=\"NextPart\"\n\n"
"--NextPart\n"
"Content-Type: text/plain\n\n"
"Please find the cost report attached.\n\n"
"--NextPart\n"
"Content-Type: text/csv;\n"
"Content-Disposition: attachment; filename=\"cost_report.csv\"\n\n"
f"{csv_content.decode('utf-8')}\n"
"--NextPart--"
)
try:
response = ses_client.send_raw_email(
Source=sender_email,
RawMessage={
'Data': raw_email.encode('utf-8')
}
)
print("Email sent successfully.")
except Exception as e:
print(f"An error occurred while sending the email: {e}")
At the scheduled time, you will receive an email with your Cost Report attached.
Comments