diff --git a/files/lambda/README_daily_billing_email.md b/files/lambda/README_daily_billing_email.md new file mode 100644 index 0000000000..25d86e3d29 --- /dev/null +++ b/files/lambda/README_daily_billing_email.md @@ -0,0 +1,348 @@ +# Daily Billing Email Lambda Function + +## Overview + +This Lambda function sends a daily email report of your current AWS month-to-date charges via SNS. The report includes: + +- Total month-to-date cost +- Cost breakdown by AWS service +- Top 15 services by cost with percentages + +## Files + +- `daily_billing_email.py` - Main Lambda function code + +## Requirements + +### IAM Permissions + +The Lambda execution role needs the following permissions: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ce:GetCostAndUsage" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "sns:Publish" + ], + "Resource": "arn:aws:sns:REGION:ACCOUNT_ID:TOPIC_NAME" + }, + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": "arn:aws:logs:*:*:*" + } + ] +} +``` + +### Environment Variables + +- `SNS_TOPIC_ARN` - ARN of the SNS topic to send billing notifications + +## Deployment + +### Step 1: Create SNS Topic and Subscribe Email + +You can use the existing `commons-sns` Terraform module: + +```hcl +module "billing_alerts_sns" { + source = "../modules/commons-sns" + vpc_name = "billing-alerts" + topic_display = "Daily AWS Billing Reports" + emails = ["your-email@example.com"] +} +``` + +Or create manually: + +```bash +# Create SNS topic +aws sns create-topic --name daily-billing-alerts + +# Subscribe your email +aws sns subscribe \ + --topic-arn arn:aws:sns:us-east-1:123456789012:daily-billing-alerts \ + --protocol email \ + --notification-endpoint your-email@example.com + +# Confirm subscription via email +``` + +### Step 2: Deploy Lambda Function + +Using the existing `lambda-function` Terraform module: + +```hcl +# Create IAM role for Lambda +module "billing_lambda_role" { + source = "../modules/iam-role" + + role_name = "daily-billing-lambda-role" + role_description = "Role for daily billing email Lambda function" + role_assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json +} + +data "aws_iam_policy_document" "lambda_assume_role" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + } +} + +# Attach Cost Explorer and SNS permissions +resource "aws_iam_role_policy" "billing_lambda_policy" { + name = "daily-billing-lambda-policy" + role = module.billing_lambda_role.role_id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "ce:GetCostAndUsage" + ] + Resource = "*" + }, + { + Effect = "Allow" + Action = [ + "sns:Publish" + ] + Resource = module.billing_alerts_sns.topic_arn + }, + { + Effect = "Allow" + Action = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + Resource = "arn:aws:logs:*:*:*" + } + ] + }) +} + +# Deploy Lambda function +module "daily_billing_lambda" { + source = "../modules/lambda-function" + + lambda_function_file = "${path.module}/../../files/lambda/daily_billing_email.py" + lambda_function_name = "daily-billing-email" + lambda_function_handler = "daily_billing_email.lambda_handler" + lambda_function_runtime = "python3.9" + lambda_function_timeout = 30 + lambda_function_memory_size = 256 + lambda_function_iam_role_arn = module.billing_lambda_role.role_arn + + lambda_function_env = { + SNS_TOPIC_ARN = module.billing_alerts_sns.topic_arn + } +} +``` + +### Step 3: Schedule Daily Execution + +Use CloudWatch Events to trigger the Lambda daily: + +```hcl +# Create CloudWatch Event Rule for daily trigger +resource "aws_cloudwatch_event_rule" "daily_billing_schedule" { + name = "daily-billing-email-schedule" + description = "Trigger daily billing email at 9 AM UTC" + schedule_expression = "cron(0 9 * * ? *)" +} + +# Add Lambda as target +resource "aws_cloudwatch_event_target" "daily_billing_target" { + rule = aws_cloudwatch_event_rule.daily_billing_schedule.name + target_id = "DailyBillingLambda" + arn = module.daily_billing_lambda.lambda_function_arn +} + +# Grant CloudWatch Events permission to invoke Lambda +resource "aws_lambda_permission" "allow_cloudwatch" { + statement_id = "AllowExecutionFromCloudWatch" + action = "lambda:InvokeFunction" + function_name = module.daily_billing_lambda.lambda_function_name + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.daily_billing_schedule.arn +} +``` + +### Manual Deployment (AWS CLI) + +If you prefer to deploy manually: + +```bash +# Package the Lambda function +cd /home/user/cloud-automation/files/lambda +zip daily_billing_email.zip daily_billing_email.py + +# Create the Lambda function +aws lambda create-function \ + --function-name daily-billing-email \ + --runtime python3.9 \ + --role arn:aws:iam::ACCOUNT_ID:role/daily-billing-lambda-role \ + --handler daily_billing_email.lambda_handler \ + --zip-file fileb://daily_billing_email.zip \ + --timeout 30 \ + --memory-size 256 \ + --environment Variables="{SNS_TOPIC_ARN=arn:aws:sns:REGION:ACCOUNT_ID:daily-billing-alerts}" + +# Create CloudWatch Event Rule (daily at 9 AM UTC) +aws events put-rule \ + --name daily-billing-email-schedule \ + --schedule-expression "cron(0 9 * * ? *)" + +# Add Lambda as target +aws events put-targets \ + --rule daily-billing-email-schedule \ + --targets "Id"="1","Arn"="arn:aws:lambda:REGION:ACCOUNT_ID:function:daily-billing-email" + +# Grant permission for CloudWatch Events to invoke Lambda +aws lambda add-permission \ + --function-name daily-billing-email \ + --statement-id AllowExecutionFromCloudWatch \ + --action lambda:InvokeFunction \ + --principal events.amazonaws.com \ + --source-arn arn:aws:events:REGION:ACCOUNT_ID:rule/daily-billing-email-schedule +``` + +## Configuration + +### Schedule Customization + +The cron expression `cron(0 9 * * ? *)` runs daily at 9:00 AM UTC. You can customize this: + +- `cron(0 14 * * ? *)` - 2:00 PM UTC (9:00 AM EST) +- `cron(0 0 * * ? *)` - Midnight UTC +- `cron(0 12 * * ? *)` - Noon UTC + +**Note:** CloudWatch Events uses UTC time zone. + +### Function Configuration + +You can adjust the Lambda function settings in the Terraform module or AWS Console: + +- **Timeout**: Default 30 seconds (usually completes in 5-10 seconds) +- **Memory**: Default 256 MB (128 MB may be sufficient for smaller accounts) +- **Runtime**: Python 3.9 or higher recommended + +## Testing + +### Test the Lambda Function + +```bash +# Invoke the function manually +aws lambda invoke \ + --function-name daily-billing-email \ + --payload '{}' \ + response.json + +# Check the response +cat response.json +``` + +### Test Locally + +```bash +# Set environment variables +export SNS_TOPIC_ARN="arn:aws:sns:us-east-1:123456789012:daily-billing-alerts" + +# Run with Python +python3 -c " +import daily_billing_email +result = daily_billing_email.lambda_handler({}, None) +print(result) +" +``` + +## Email Report Format + +The email will look like this: + +``` +Subject: AWS Billing Report - October 31, 2025 + +AWS Billing Report for October 2025 +Report Date: 2025-10-31 +============================================================ + +Total Month-to-Date Cost: $1,234.56 + +Cost Breakdown by Service: +------------------------------------------------------------ + Amazon Elastic Compute Cloud $ 456.78 ( 37.0%) + Amazon Simple Storage Service $ 234.56 ( 19.0%) + Amazon Relational Database Service $ 123.45 ( 10.0%) + Amazon Virtual Private Cloud $ 89.01 ( 7.2%) + AWS Lambda $ 67.89 ( 5.5%) + ... +------------------------------------------------------------ + +Note: This report shows charges from October 1, 2025 to October 31, 2025 +Final charges may vary based on usage throughout the rest of the month. +``` + +## Troubleshooting + +### No Email Received + +1. Check SNS subscription is confirmed (check your email for confirmation) +2. Verify Lambda execution in CloudWatch Logs +3. Check Lambda has correct SNS_TOPIC_ARN environment variable +4. Verify IAM permissions for SNS publish + +### Cost Explorer Errors + +- Ensure IAM role has `ce:GetCostAndUsage` permission +- Cost Explorer must be enabled in your AWS account +- Billing data may have a 24-hour delay + +### Lambda Timeout + +- Increase timeout if Cost Explorer API is slow +- Default 30 seconds should be sufficient for most accounts + +## Cost Considerations + +- **Lambda**: Minimal cost (~$0.00 per month with free tier) +- **SNS**: $0.00 for email notifications within free tier +- **Cost Explorer API**: First request is free, subsequent requests are $0.01 each + - Running daily = ~$0.30 per month + +## Security Best Practices + +1. Use least-privilege IAM permissions +2. Restrict SNS topic access +3. Enable CloudWatch Logs for monitoring +4. Use VPC endpoints if Lambda is in VPC +5. Rotate credentials regularly +6. Review billing alerts for anomalies + +## Additional Resources + +- [AWS Cost Explorer API Documentation](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetCostAndUsage.html) +- [AWS Lambda Python Documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html) +- [AWS SNS Documentation](https://docs.aws.amazon.com/sns/latest/dg/welcome.html) +- [CloudWatch Events Schedule Expressions](https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html) diff --git a/files/lambda/daily_billing_email.py b/files/lambda/daily_billing_email.py new file mode 100644 index 0000000000..a2d7cfcad9 --- /dev/null +++ b/files/lambda/daily_billing_email.py @@ -0,0 +1,190 @@ +""" +AWS Lambda function to send daily billing email via SNS. + +This function retrieves the current month-to-date AWS charges and sends +a formatted summary via SNS email notification. + +Environment Variables: + SNS_TOPIC_ARN: ARN of the SNS topic to publish billing notifications + +Handler: daily_billing_email.lambda_handler + +Required IAM Permissions: + - ce:GetCostAndUsage + - sns:Publish +""" + +import json +import os +from datetime import datetime, timedelta +from decimal import Decimal + +import boto3 + + +def get_month_to_date_cost(): + """ + Retrieve month-to-date cost from AWS Cost Explorer. + + Returns: + dict: Cost data including total amount and breakdown by service + """ + ce_client = boto3.client('ce') + + # Calculate date range: first day of month to today + today = datetime.now() + start_date = today.replace(day=1).strftime('%Y-%m-%d') + end_date = (today + timedelta(days=1)).strftime('%Y-%m-%d') + + # Get overall cost + response = ce_client.get_cost_and_usage( + TimePeriod={ + 'Start': start_date, + 'End': end_date + }, + Granularity='MONTHLY', + Metrics=['UnblendedCost'], + GroupBy=[ + { + 'Type': 'DIMENSION', + 'Key': 'SERVICE' + } + ] + ) + + return response + + +def format_billing_message(cost_data): + """ + Format the cost data into a human-readable message. + + Args: + cost_data (dict): Cost data from Cost Explorer API + + Returns: + str: Formatted billing message + """ + today = datetime.now() + month_name = today.strftime('%B %Y') + + # Parse results + results = cost_data.get('ResultsByTime', []) + if not results: + return f"No billing data available for {month_name}" + + total_cost = Decimal('0') + service_costs = [] + + # Extract costs by service + groups = results[0].get('Groups', []) + for group in groups: + service_name = group.get('Keys', ['Unknown'])[0] + amount = Decimal(group.get('Metrics', {}).get('UnblendedCost', {}).get('Amount', '0')) + + if amount > 0: + total_cost += amount + service_costs.append((service_name, amount)) + + # Sort services by cost (highest first) + service_costs.sort(key=lambda x: x[1], reverse=True) + + # Build message + message_lines = [ + f"AWS Billing Report for {month_name}", + f"Report Date: {today.strftime('%Y-%m-%d')}", + "=" * 60, + f"\nTotal Month-to-Date Cost: ${total_cost:.2f}", + "\nCost Breakdown by Service:", + "-" * 60 + ] + + # Add top services (limit to top 15 to keep email concise) + for service, cost in service_costs[:15]: + percentage = (cost / total_cost * 100) if total_cost > 0 else 0 + message_lines.append(f" {service:<40} ${cost:>10.2f} ({percentage:>5.1f}%)") + + if len(service_costs) > 15: + other_cost = sum(cost for _, cost in service_costs[15:]) + percentage = (other_cost / total_cost * 100) if total_cost > 0 else 0 + message_lines.append(f" {'Other Services':<40} ${other_cost:>10.2f} ({percentage:>5.1f}%)") + + message_lines.append("-" * 60) + message_lines.append(f"\nNote: This report shows charges from {today.strftime('%B 1, %Y')} to {today.strftime('%B %d, %Y')}") + message_lines.append("Final charges may vary based on usage throughout the rest of the month.") + + return "\n".join(message_lines) + + +def send_sns_notification(message): + """ + Send billing notification via SNS. + + Args: + message (str): Formatted billing message + + Returns: + dict: SNS publish response + """ + sns_client = boto3.client('sns') + topic_arn = os.environ.get('SNS_TOPIC_ARN') + + if not topic_arn: + raise ValueError("SNS_TOPIC_ARN environment variable is not set") + + today = datetime.now() + subject = f"AWS Billing Report - {today.strftime('%B %d, %Y')}" + + response = sns_client.publish( + TopicArn=topic_arn, + Subject=subject, + Message=message + ) + + return response + + +def lambda_handler(event, context): + """ + Lambda handler function for daily billing email. + + This function is triggered daily (typically via CloudWatch Events) to: + 1. Retrieve current month-to-date AWS costs + 2. Format the cost data into a readable message + 3. Send the message via SNS email notification + + Args: + event (dict): Lambda event data (not used) + context (object): Lambda context object + + Returns: + dict: Response with status code and message + """ + try: + print("Starting daily billing email function") + + # Get cost data + print("Fetching month-to-date costs from Cost Explorer") + cost_data = get_month_to_date_cost() + + # Format message + print("Formatting billing message") + message = format_billing_message(cost_data) + print(f"Billing message:\n{message}") + + # Send via SNS + print("Sending SNS notification") + sns_response = send_sns_notification(message) + print(f"SNS MessageId: {sns_response.get('MessageId')}") + + return { + 'statusCode': 200, + 'body': json.dumps({ + 'message': 'Billing email sent successfully', + 'messageId': sns_response.get('MessageId') + }) + } + + except Exception as e: + print(f"Error in lambda_handler: {str(e)}") + raise diff --git a/tf_files/aws/daily_billing_email_example.tf b/tf_files/aws/daily_billing_email_example.tf new file mode 100644 index 0000000000..8b33e05910 --- /dev/null +++ b/tf_files/aws/daily_billing_email_example.tf @@ -0,0 +1,176 @@ +# +# Terraform configuration example for deploying the daily billing email Lambda function +# +# This example demonstrates how to deploy the daily billing email Lambda function +# using the existing cloud-automation Terraform modules. +# +# Usage: +# 1. Copy this file to your terraform directory +# 2. Update the variables (email, vpc_name, etc.) +# 3. Run: terraform init && terraform plan && terraform apply +# + +# Variables +variable "billing_alert_email" { + description = "Email address to receive daily billing reports" + type = string + default = "your-email@example.com" +} + +variable "billing_alert_vpc_name" { + description = "VPC name for SNS topic naming" + type = string + default = "billing-alerts" +} + +variable "billing_alert_schedule" { + description = "Cron expression for daily billing email (UTC timezone)" + type = string + default = "cron(0 9 * * ? *)" # 9 AM UTC daily +} + +# +# SNS Topic for Billing Alerts +# +module "billing_alerts_sns" { + source = "./modules/commons-sns" + + vpc_name = var.billing_alert_vpc_name + topic_display = "Daily AWS Billing Reports" + emails = [var.billing_alert_email] +} + +# +# IAM Role for Lambda Execution +# +data "aws_iam_policy_document" "billing_lambda_assume_role" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + } +} + +module "billing_lambda_role" { + source = "./modules/iam-role" + + role_name = "daily-billing-lambda-role" + role_description = "Execution role for daily billing email Lambda function" + role_assume_role_policy = data.aws_iam_policy_document.billing_lambda_assume_role.json +} + +# +# IAM Policy for Lambda Function +# +data "aws_iam_policy_document" "billing_lambda_policy" { + # Cost Explorer permissions + statement { + effect = "Allow" + actions = [ + "ce:GetCostAndUsage" + ] + resources = ["*"] + } + + # SNS publish permissions + statement { + effect = "Allow" + actions = [ + "sns:Publish" + ] + resources = [module.billing_alerts_sns.topic_arn] + } + + # CloudWatch Logs permissions + statement { + effect = "Allow" + actions = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + resources = ["arn:aws:logs:*:*:*"] + } +} + +resource "aws_iam_role_policy" "billing_lambda_policy" { + name = "daily-billing-lambda-policy" + role = module.billing_lambda_role.role_id + policy = data.aws_iam_policy_document.billing_lambda_policy.json +} + +# +# Lambda Function +# +module "daily_billing_lambda" { + source = "./modules/lambda-function" + + lambda_function_file = "${path.module}/../../files/lambda/daily_billing_email.py" + lambda_function_name = "daily-billing-email" + lambda_function_handler = "daily_billing_email.lambda_handler" + lambda_function_runtime = "python3.9" + lambda_function_timeout = 30 + lambda_function_memory_size = 256 + lambda_function_iam_role_arn = module.billing_lambda_role.role_arn + + lambda_function_env = { + SNS_TOPIC_ARN = module.billing_alerts_sns.topic_arn + } +} + +# +# CloudWatch Event Rule for Daily Trigger +# +resource "aws_cloudwatch_event_rule" "daily_billing_schedule" { + name = "daily-billing-email-schedule" + description = "Trigger daily billing email Lambda function" + schedule_expression = var.billing_alert_schedule +} + +resource "aws_cloudwatch_event_target" "daily_billing_target" { + rule = aws_cloudwatch_event_rule.daily_billing_schedule.name + target_id = "DailyBillingLambda" + arn = module.daily_billing_lambda.lambda_function_arn +} + +# +# Lambda Permission for CloudWatch Events +# +resource "aws_lambda_permission" "allow_cloudwatch_invoke" { + statement_id = "AllowExecutionFromCloudWatch" + action = "lambda:InvokeFunction" + function_name = module.daily_billing_lambda.lambda_function_name + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.daily_billing_schedule.arn +} + +# +# Outputs +# +output "lambda_function_arn" { + description = "ARN of the daily billing Lambda function" + value = module.daily_billing_lambda.lambda_function_arn +} + +output "lambda_function_name" { + description = "Name of the daily billing Lambda function" + value = module.daily_billing_lambda.lambda_function_name +} + +output "sns_topic_arn" { + description = "ARN of the billing alerts SNS topic" + value = module.billing_alerts_sns.topic_arn +} + +output "cloudwatch_rule_name" { + description = "Name of the CloudWatch Events rule" + value = aws_cloudwatch_event_rule.daily_billing_schedule.name +} + +output "billing_alert_email" { + description = "Email address receiving billing alerts" + value = var.billing_alert_email +}