diff --git a/environments/prod/main.tf b/environments/prod/main.tf index 83932a5..ee92bca 100644 --- a/environments/prod/main.tf +++ b/environments/prod/main.tf @@ -42,13 +42,19 @@ provider "aws" { module "billing_report" { source = "../../modules/billing_report" - lambda_function_name = "billing-report-lambda" - ses_sender_email = "root@kiraum.it" - ses_recipient_email = "tfgoncalves@xpto.it" - ses_domain = "kiraum.it" + lambda_function_name = "billing-report-lambda" + ses_sender_email = "root@kiraum.it" + ses_domain = "kiraum.it" + recipient_email = "tfgoncalves@xpto.it" + notification_service = "SNS" + daily_cost_threshold = "0.01" + weekly_cost_threshold = "1.00" + monthly_cost_threshold = "5.00" + yearly_cost_threshold = "60.00" } + module "route53" { source = "../../modules/route53" diff --git a/modules/billing_report/lambda_function.py b/modules/billing_report/lambda_function.py index 0ebae41..2f54436 100644 --- a/modules/billing_report/lambda_function.py +++ b/modules/billing_report/lambda_function.py @@ -12,6 +12,8 @@ MONTHLY_COST_THRESHOLD = float(os.environ.get("MONTHLY_COST_THRESHOLD", "0.01")) YEARLY_COST_THRESHOLD = float(os.environ.get("YEARLY_COST_THRESHOLD", "0.01")) +NOTIFICATION_SERVICE = os.environ.get("NOTIFICATION_SERVICE", "SNS").upper() + def calculate_time_periods(time_period, current_date): """ @@ -85,6 +87,80 @@ def process_cost_data(response, compare_response): return current_costs, compare_costs, unit +def generate_text_report( + time_period, + start, + end, + current_costs, + compare_costs, + unit, + response, + compare_response, + cost_threshold, +): + """ + Generate a detailed text cost report. + Args: + time_period (str): The time period of the report. + start (datetime.date): The start date of the report. + end (datetime.date): The end date of the report. + current_costs (float): The total costs for the current period. + compare_costs (float): The total costs for the comparison period. + unit (str): The currency unit. + response (dict): The response from AWS Cost Explorer for the current period. + compare_response (dict): The response from AWS Cost Explorer for the comparison period. + cost_threshold (float): The cost threshold for the time period. + Returns: + str: A formatted text cost report. + """ + text_template = """ +AWS Cost Report for {time_period} + +Period: {start_date} to {end_date} + +Summary: +Current {time_period} cost: {current_costs:.7f} {unit} +Previous {time_period} cost: {compare_costs:.7f} {unit} +Difference: {difference:.7f} {unit} +Threshold: {threshold:.7f} {unit} + +Breakdown by Service: +{service_breakdown} + """ + + current_services = { + group["Keys"][0]: float(group["Metrics"]["UnblendedCost"]["Amount"]) + for result in response["ResultsByTime"] + for group in result.get("Groups", []) + } + previous_services = { + group["Keys"][0]: float(group["Metrics"]["UnblendedCost"]["Amount"]) + for result in compare_response["ResultsByTime"] + for group in result.get("Groups", []) + } + service_breakdown = "" + for service, cost in current_services.items(): + if cost > 0: + previous_cost = previous_services.get(service, 0) + difference = cost - previous_cost + service_breakdown += f"{service}:\n" + service_breakdown += f" Current: {cost:.7f} {unit}\n" + service_breakdown += f" Previous: {previous_cost:.7f} {unit}\n" + service_breakdown += f" Difference: {difference:.7f} {unit}\n\n" + + return text_template.format( + time_period=time_period, + start_date=start.isoformat(), + end_date=(end - datetime.timedelta(days=1)).isoformat(), + current_costs=current_costs, + compare_costs=compare_costs, + unit=unit, + difference=current_costs - compare_costs, + threshold=cost_threshold, + service_breakdown=service_breakdown, + ) + + def generate_html_report( time_period, start, @@ -160,6 +236,7 @@ def generate_html_report( """ + current_services = { group["Keys"][0]: float(group["Metrics"]["UnblendedCost"]["Amount"]) for result in response["ResultsByTime"] @@ -177,12 +254,13 @@ def generate_html_report( difference = cost - previous_cost service_rows += f""" - {service} - {cost:.7f} {unit} - {previous_cost:.7f} {unit} - {difference:.7f} {unit} + {service} + {cost:.7f} {unit} + {previous_cost:.7f} {unit} + {difference:.7f} {unit} """ + return html_template.format( time_period=time_period, start_date=start.isoformat(), @@ -196,6 +274,26 @@ def generate_html_report( ) +def send_sns(message, subject): + """ + Send a message using AWS SNS. + Args: + message (str): The message to be sent. + subject (str): The subject of the message. + """ + sns = boto3.client("sns") + topic_arn = os.environ["SNS_TOPIC_ARN"] + + if not topic_arn: + raise ValueError("SNS_TOPIC_ARN must be set in the environment variables") + + try: + response = sns.publish(TopicArn=topic_arn, Message=message, Subject=subject) + print(f"Message sent to SNS! Message ID: {response['MessageId']}") + except ClientError as e: + print(f"An error occurred while sending message via SNS: {e}") + + def send_ses(message, subject): """ Send an HTML message using AWS SES. @@ -234,12 +332,25 @@ def send_ses(message, subject): print(f"An error occurred while sending email via SES: {e}") +def send_notification(message, subject): + """ + Send a notification using either SES or SNS. + Args: + message (str): The message to be sent. + subject (str): The subject of the message. + """ + if NOTIFICATION_SERVICE == "SES": + send_ses(message, subject) + else: # Default to SNS + send_sns(message, subject) + + def lambda_handler(event, context): """ AWS Lambda function to report AWS costs for various time periods. This function retrieves cost data from AWS Cost Explorer for a specified time period, - compares it with the previous period, and generates an HTML cost report. If the cost exceeds - a predefined threshold, it sends a notification via SES. + compares it with the previous period, and generates a cost report. If the cost exceeds + a predefined threshold, it sends a notification via SNS or SES. Args: event (dict): The Lambda event object containing input parameters. - time_period (str, optional): The time period for the cost report. @@ -287,20 +398,33 @@ def lambda_handler(event, context): "monthly": MONTHLY_COST_THRESHOLD, "yearly": YEARLY_COST_THRESHOLD, }.get(time_period, DAILY_COST_THRESHOLD) - html_report = generate_html_report( - time_period, - start, - end, - current_costs, - compare_costs, - unit, - response, - compare_response, - cost_threshold, - ) + if NOTIFICATION_SERVICE == "SES": + report = generate_html_report( + time_period, + start, + end, + current_costs, + compare_costs, + unit, + response, + compare_response, + cost_threshold, + ) + else: + report = generate_text_report( + time_period, + start, + end, + current_costs, + compare_costs, + unit, + response, + compare_response, + cost_threshold, + ) if current_costs > cost_threshold: print("Cost threshold exceeded. Sending notification.") - send_ses(html_report, f"AWS Cost Report - {time_period.capitalize()}") + send_notification(report, f"AWS Cost Report - {time_period.capitalize()}") else: print( f"Total cost ({current_costs:.7f} {unit}) did not exceed the threshold ({cost_threshold:.7f} {unit}). No notification sent." diff --git a/modules/billing_report/main.tf b/modules/billing_report/main.tf index 27123cb..55f80a3 100644 --- a/modules/billing_report/main.tf +++ b/modules/billing_report/main.tf @@ -1,5 +1,6 @@ terraform { required_version = ">= 1.0.0" + required_providers { aws = { source = "hashicorp/aws" @@ -27,8 +28,14 @@ resource "aws_lambda_function" "billing_report" { environment { variables = { - SES_SENDER_EMAIL = var.ses_sender_email - SES_RECIPIENT_EMAIL = var.ses_recipient_email + SES_SENDER_EMAIL = var.ses_sender_email + recipient_email = var.recipient_email + SNS_TOPIC_ARN = aws_sns_topic.billing_report.arn + NOTIFICATION_SERVICE = var.notification_service + DAILY_COST_THRESHOLD = var.daily_cost_threshold + WEEKLY_COST_THRESHOLD = var.weekly_cost_threshold + MONTHLY_COST_THRESHOLD = var.monthly_cost_threshold + YEARLY_COST_THRESHOLD = var.yearly_cost_threshold } } @@ -93,11 +100,23 @@ resource "aws_iam_role_policy" "lambda_policy" { "ses:SendRawEmail" ] Resource = "*" + }, + { + Effect = "Allow" + Action = [ + "sns:Publish" + ] + Resource = aws_sns_topic.billing_report.arn } ] }) } +# Create SNS topic for billing report +resource "aws_sns_topic" "billing_report" { + name = "billing-report-topic" +} + # Create CloudWatch event rules resource "aws_cloudwatch_event_rule" "daily_trigger" { name = "billing-report-daily-schedule" @@ -243,3 +262,9 @@ resource "aws_ses_domain_mail_from" "ses_domain_mail_from" { domain = aws_ses_domain_identity.ses_domain.domain mail_from_domain = "mail.${aws_ses_domain_identity.ses_domain.domain}" } + +resource "aws_sns_topic_subscription" "billing_report_email" { + topic_arn = aws_sns_topic.billing_report.arn + protocol = "email" + endpoint = var.recipient_email +} diff --git a/modules/billing_report/outputs.tf b/modules/billing_report/outputs.tf index 501a83b..54a8150 100644 --- a/modules/billing_report/outputs.tf +++ b/modules/billing_report/outputs.tf @@ -33,7 +33,7 @@ output "ses_sender_email" { value = var.ses_sender_email } -output "ses_recipient_email" { - description = "The email address receiving SES emails" - value = var.ses_recipient_email +output "recipient_email" { + description = "The email address receiving emails" + value = var.recipient_email } diff --git a/modules/billing_report/variables.tf b/modules/billing_report/variables.tf index 3020dd1..a0707d3 100644 --- a/modules/billing_report/variables.tf +++ b/modules/billing_report/variables.tf @@ -8,12 +8,42 @@ variable "ses_sender_email" { type = string } -variable "ses_recipient_email" { - description = "Email address to receive SES emails" +variable "recipient_email" { + description = "Email address to receive emails" type = string } variable "ses_domain" { description = "Domain for SES" type = string -} \ No newline at end of file +} + +variable "notification_service" { + description = "The notification service to use (SNS or SES)" + type = string + default = "SNS" +} + +variable "daily_cost_threshold" { + description = "The daily cost threshold for billing alerts" + type = string + default = "0.01" +} + +variable "weekly_cost_threshold" { + description = "The weekly cost threshold for billing alerts" + type = string + default = "0.01" +} + +variable "monthly_cost_threshold" { + description = "The monthly cost threshold for billing alerts" + type = string + default = "0.01" +} + +variable "yearly_cost_threshold" { + description = "The yearly cost threshold for billing alerts" + type = string + default = "0.01" +}