AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: Sets up Budget pieces for account management. (qs-1s3rsr7io) Metadata: SuperwerkerVersion: 0.13.2 cfn-lint: config: ignore_checks: - E9007 Parameters: BudgetLimitInUSD: Default: 100 Description: Initial value. Will be overwritten by the scheduled lambda function. Type: String Resources: BudgetAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmActions: - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:opsitem:3#CATEGORY=Cost # severity 3 (~medium) should suffice AlarmDescription: Superwerker default budget forecast exceed previous three months ComparisonOperator: GreaterThanThreshold Dimensions: - Name: TopicName Value: !GetAtt BudgetNotification.TopicName EvaluationPeriods: 1 MetricName: NumberOfMessagesPublished Namespace: AWS/SNS Period: 300 # publishing window of SNS metrics to CW Statistic: Sum Threshold: 0 TreatMissingData: missing # currently CW does not auto close ops items where it's alert transitions back to OK - but still transition back after one (1) initial SNS notification from budgets to "Ok" would communicate wrong semantics; BudgetLambda: Type: AWS::Serverless::Function Properties: Environment: Variables: StackName: !Sub ${AWS::StackName} Events: Schedule: Type: Schedule Properties: Schedule: cron(0 0 L * ? *) Handler: index.handler InlineCode: | from dateutil.relativedelta import * import boto3 import datetime import json import os def handler(event, context): ce = boto3.client('ce') end = datetime.date.today().replace(day=1) start = end + relativedelta(months=-3) start = start.strftime("%Y-%m-%d") end = end.strftime("%Y-%m-%d") response = ce.get_cost_and_usage( Granularity='MONTHLY', Metrics=[ 'UnblendedCost', ], TimePeriod={ 'Start': start, 'End': end, }, ) avg = 0 for result in response['ResultsByTime']: total = result['Total'] cost = total['UnblendedCost'] amount = int(float(cost['Amount'])) avg = avg + amount avg = int(avg/3) budget = str(avg) stack_name = os.environ['StackName'] log({ 'average': avg, 'budget': budget, 'end': end, 'event': event, 'level': 'debug', 'stack': stack_name, 'start': start, }) cf = boto3.client('cloudformation') cf.update_stack( Capabilities=[ 'CAPABILITY_IAM', ], Parameters=[ { 'ParameterKey': 'BudgetLimitInUSD', 'ParameterValue': budget, } ], StackName=stack_name, UsePreviousTemplate=True, ) def log(msg): print(json.dumps(msg), flush=True) Runtime: python3.7 Policies: - Version: 2012-10-17 Statement: - Effect: Allow Action: - ce:GetCostAndUsage Resource: '*' - Effect: Allow Action: - budgets:ModifyBudget Resource: !Sub arn:${AWS::Partition}:budgets::${AWS::AccountId}:budget/${BudgetReport} - Effect: Allow Action: - cloudformation:UpdateStack Resource: !Sub ${AWS::StackId} Timeout: 10 BudgetNotification: Type: AWS::SNS::Topic BudgetNotificationPolicy: Type: AWS::SNS::TopicPolicy Properties: PolicyDocument: Statement: Action: SNS:Publish Effect: Allow Principal: Service: budgets.amazonaws.com Resource: !Ref BudgetNotification Topics: - !Ref BudgetNotification BudgetReport: Type: AWS::Budgets::Budget Properties: Budget: BudgetLimit: Amount: !Ref BudgetLimitInUSD Unit: USD BudgetType: COST CostTypes: IncludeCredit: false IncludeRefund: false TimeUnit: MONTHLY NotificationsWithSubscribers: - Notification: ComparisonOperator: GREATER_THAN NotificationType: FORECASTED Threshold: 100 Subscribers: - SubscriptionType: SNS Address: !Ref BudgetNotification