top of page
Writer's pictureRafael Natali

Automate the AWS Cost Report creation

This article describes how to automate the AWS Cost Report creation and send it via email to specific subscribers.

AWS - Cost Report via Email

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 *

AWS EventBridge

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.






11 views0 comments

Comments


bottom of page