AWSTemplateFormatVersion: '2010-09-09' Description: Stack to automate daily AWS cost reports with Step Functions parallel execution, including past cost for 24h/7d/1m and forecast in HTML dashboard. Parameters: ReEmailAddress: Type: String Description: Email address to receive billing notifications. SendEmailAddress: Type: String Description: Email address to send billing notifications. Resources: SOALambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: AllowCostExplorer PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ce:GetCostAndUsage - ce:GetCostForecast - ce:GetDimensionValues - ce:GetUsageForecast Resource: '*' - PolicyName: AllowSESEmail PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: ses:SendEmail Resource: '*' SOALambdaGetPastCost: Type: AWS::Lambda::Function Properties: FunctionName: SOA-Lambda-GetPastCost Handler: index.lambda_handler Role: !GetAtt SOALambdaExecutionRole.Arn Runtime: python3.12 Code: ZipFile: | import boto3 import datetime import logging from botocore.exceptions import ClientError logger = logging.getLogger() logger.setLevel(logging.INFO) cost_explorer = boto3.client('ce') def lambda_handler(event, context): today = datetime.date.today() end_date = today - datetime.timedelta(days=1) periods = [ {'label': '24h', 'days': 1, 'granularity': 'DAILY'}, {'label': '7_days', 'days': 7, 'granularity': 'DAILY'}, {'label': '30_days', 'days': 30, 'granularity': 'DAILY'} ] results = {} for period in periods: try: start_date = end_date - datetime.timedelta(days=period['days']) response = cost_explorer.get_cost_and_usage( TimePeriod={ 'Start': start_date.strftime('%Y-%m-%d'), 'End': (end_date + datetime.timedelta(days=1)).strftime('%Y-%m-%d') # End after Start }, Granularity=period['granularity'], Metrics=['UnblendedCost'], GroupBy=[{'Type': 'DIMENSION', 'Key': 'SERVICE'}] ) period_total = 0.0 service_breakdown = [] currency = 'USD' for timeframe in response.get('ResultsByTime', []): for group in timeframe.get('Groups', []): service = group['Keys'][0] cost = float(group['Metrics']['UnblendedCost']['Amount']) currency = group['Metrics']['UnblendedCost']['Unit'] service_breakdown.append({ 'service': service, 'cost': round(cost, 2), 'percentage': 0 }) period_total += cost if period_total > 0: for service in service_breakdown: service['percentage'] = round((service['cost'] / period_total) * 100, 1) results[period['label']] = { 'start_date': start_date.strftime('%Y-%m-%d'), 'end_date': end_date.strftime('%Y-%m-%d'), 'total_cost': round(period_total, 2), 'currency': currency, 'services': sorted(service_breakdown, key=lambda x: x['cost'], reverse=True)[:5] } except ClientError as e: logger.error(f"AWS Error ({period['label']}): {str(e)}") results[period['label']] = {'error': f"AWS Error: {e.response['Error']['Message']}"} except Exception as e: logger.error(f"Unexpected Error ({period['label']}): {str(e)}", exc_info=True) results[period['label']] = {'error': f"Processing Error: {str(e)}"} return { 'status': 'success', 'data': results } SOALambdaGetForecast: Type: AWS::Lambda::Function Properties: FunctionName: SOA-Lambda-CostForecast Handler: index.lambda_handler Role: !GetAtt SOALambdaExecutionRole.Arn Runtime: python3.12 Code: ZipFile: | import boto3 import datetime from botocore.exceptions import ClientError cost_explorer = boto3.client('ce') def lambda_handler(event, context): forecasts = {} try: today = datetime.date.today() # For 7 days start_7d = today.strftime('%Y-%m-%d') end_7d = (today + datetime.timedelta(days=7)).strftime('%Y-%m-%d') res_7d = cost_explorer.get_cost_forecast( TimePeriod={'Start': start_7d, 'End': end_7d}, Granularity='DAILY', Metric='UNBLENDED_COST' ) # For 30 days start_30d = today.strftime('%Y-%m-%d') end_30d = (today + datetime.timedelta(days=30)).strftime('%Y-%m-%d') res_30d = cost_explorer.get_cost_forecast( TimePeriod={'Start': start_30d, 'End': end_30d}, Granularity='DAILY', Metric='UNBLENDED_COST' ) forecasts['7_days'] = { 'total': round(float(res_7d['Total']['Amount']), 2), 'currency': res_7d['Total']['Unit'], 'periods': [ { 'date': day['TimePeriod']['Start'], 'amount': round(float(day['MeanValue']), 2) } for day in res_7d['ForecastResultsByTime'] ] } forecasts['30_days'] = { 'total': round(float(res_30d['Total']['Amount']), 2), 'currency': res_30d['Total']['Unit'], 'periods': [ { 'date': day['TimePeriod']['Start'], 'amount': round(float(day['MeanValue']), 2) } for day in res_30d['ForecastResultsByTime'] ] } return { 'status': 'success', 'data': forecasts } except ClientError as e: return {'status': 'error', 'message': f"AWS Error: {e.response['Error']['Message']}"} except Exception as e: return {'status': 'error', 'message': f"Unexpected Error: {str(e)}"} SOALambdaSendHTMLReport: Type: AWS::Lambda::Function Properties: FunctionName: SOA-Lambda-SendReport Handler: index.lambda_handler Role: !GetAtt SOALambdaExecutionRole.Arn Runtime: python3.12 Environment: Variables: REC_EMAIL_ADDRESS: !Ref ReEmailAddress SEND_EMAIL_ADDRESS: !Ref SendEmailAddress Code: ZipFile: | import boto3 import os import datetime import json ses = boto3.client('ses') def lambda_handler(event, context): sender_email = os.environ['SEND_EMAIL_ADDRESS'] recipient_email = os.environ['REC_EMAIL_ADDRESS'] html = f'''
Lỗi: {period_data['error']}
''' continue tables += f'''| Service | Cost ({period_data['currency']}) | % Trên Tổng (Biểu Đồ Cột) |
|---|---|---|
| {service['service']} | {service['cost']:,.2f} {period_data['currency']} |
|
| Tổng Chi Phí | {period_data['total_cost']:,.2f} {period_data['currency']} | |
| Period | Amount ({forecast_data['currency']}) |
|---|---|
| {period['date']} | {period['amount']:,.2f} {forecast_data['currency']} |
| Tổng Dự Báo | {forecast_data['total']:,.2f} {forecast_data['currency']} |