From 2561d7569b0b87658ffe52b37421c1699f00cb04 Mon Sep 17 00:00:00 2001 From: Joseph Manley Date: Sat, 6 Jul 2019 21:38:21 -0400 Subject: [PATCH] Basic stack and a decent readme --- .gitignore | 2 + ReadMe.md | 40 +++- cloudformation/top.yaml | 268 ++++++++++++++++++++++--- lambda/alertHandler/lambda_function.py | 70 +++++++ lambda/alertHandler/requirements.txt | 2 + 5 files changed, 356 insertions(+), 26 deletions(-) create mode 100644 .gitignore create mode 100644 lambda/alertHandler/lambda_function.py create mode 100644 lambda/alertHandler/requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93772f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +config +*.zip \ No newline at end of file diff --git a/ReadMe.md b/ReadMe.md index 7064491..24ff178 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1 +1,39 @@ -# TBD +# Billing Alerts Stack + +![Billing Alerts Diagram](https://static.cloudsumu.com/billingalerts/diagram.png) + +Uses SNS, Lambda, and CloudWatch to send billing alerts via: +- Email +- Text +- Discord +- Slack + +Easy to launch and configure! + +## Launching Stack + +### Option 1: Launch via Link + +Make sure you are logged into the AWS Console and have permissions then click: + +[![Launch in AWS Console](https://static.cloudsumu.com/billingalerts/launch_button.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/create/template?stackName=BillingAlerts&templateURL=https://sumu-billingalerts.s3.amazonaws.com/production/cloudformation/top.yaml) + +Fill out the parameters and launch! + +### Option 2: Manually input template into AWS Console + +1. Download top template or copy URL for later. + + *Development S3 URL:* [https://sumu-billingalerts.s3.amazonaws.com/develop/cloudformation/top.yaml](https://sumu-billingalerts.s3.amazonaws.com/production/cloudformation/top.yaml) + + *Production S3 URL:* [https://sumu-billingalerts.s3.amazonaws.com/production/cloudformation/top.yaml](https://sumu-billingalerts.s3.amazonaws.com/production/cloudformation/top.yaml) + +2. Go to [CloudFormation on the AWS Console](https://console.aws.amazon.com/cloudformation/home) +3. Click the "Create stack" button +4. Under "Prepare template" make sure that "Template is ready" is selected, it should be the default. + + Then under "Template Source" either paste in the template URL or upload the downloaded template. + + Then click "Next" + +5. Fill out stack parameters then launch! \ No newline at end of file diff --git a/cloudformation/top.yaml b/cloudformation/top.yaml index ce10874..00aeced 100644 --- a/cloudformation/top.yaml +++ b/cloudformation/top.yaml @@ -1,44 +1,262 @@ -AWSTemplateFormatVersion: '2010-09-09' -Transform: 'AWS::Serverless-2016-10-31' -Description: 'Top level stack for Command Relay API resources' +AWSTemplateFormatVersion: 2010-09-09 +Description: Stack that sends out billing alerts Parameters: - cloudToolsBucket: - Type: String - Description: 'S3 Bucket containing Cloud Tools' + #------------------------ + # Deployment Information + #------------------------ environment: Type: String - Description: 'Environment' + Description: Name of the environment to use in naming. + Default: production release: Type: String - Description: 'Release' - Default: 'develop' - notificationDiscordId: + Description: Name of the release name of the stack version to use. + Default: production + AllowedValues: ['develop', 'production'] + ConstraintDescription: "Must be a possible release version." + + + #--------------- + # Alert Methods + #--------------- + discordWebhook: Type: String - Description: 'Username of discord user to notify' + Description: A webhook for notifications for Discord. (Leave empty for none) + Default: "" + slackWebhook: + Type: String + Description: An incoming webhook for notifications for Slack. (Leave empty for none) + Default: "" notificationEmail: Type: String - Description: 'Email of to send SNS notifications to' + Description: An email address to subscribe to the alerting topic. (Leave empty for none) + Default: "" + notificationPhone: + Type: String + Description: A mobile number to subscribe to the alerting topic. Formatted as '+1XXXXXXXXXX' (Leave empty for none) + Default: "" + + + #------------------- + # Alarm Information + #------------------- + lowPriorityAlert: + Type: Number + Description: Estimated monthly cost in USD to send a low priority alert. + Default: 10 + MinValue: 1 + mediumPriorityAlert: + Type: Number + Description: Estimated monthly cost in USD to send a normal alert. + Default: 15 + MinValue: 2 + highPriorityAlert: + Type: Number + Description: Estimated monthly cost in USD to send a low priority alert. + Default: 20 + MinValue: 3 + + updateInterval: + Type: Number + Description: Time in seconds alarms are updated. + Default: 21600 # 6 hours + ConstraintDescription: Minimum alarm time is a minute. + MinValue: 60 + +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: "Deployment Information" + Parameters: + - environment + - release + - Label: + default: "Alert Methods" + Parameters: + - notificationEmail + - notificationPhone + - discordWebhook + - slackWebhook + - Label: + default: "Alarm Settings" + Parameters: + - lowPriorityAlert + - mediumPriorityAlert + - highPriorityAlert + ParameterLabels: + environment: + default: "Environment" + release: + default: "Release" + notificationEmail: + default: "Email" + notificationPhone: + default: "SMS Number" + discordWebhook: + default: "Discord Webhook" + slackWebhook: + default: "Slack Webhook" + lowPriorityAlert: + Default: "Cost for Low Priority Alert" + mediumPriorityAlert: + Default: "Cost for Medium Priority Alert" + highPriorityAlert: + Default: "Cost for High Priority Alert" + +Conditions: + SubscribeEmail: !Not [!Equals [!Ref "notificationEmail", ""]] + SubscribePhone: !Not [!Equals [!Ref "notificationPhone", ""]] Resources: - - NotifyDiscord: - Type: 'AWS::Serverless::Function' + AlertSnsTopic: + Type: AWS::SNS::Topic + Properties: + TopicName: !Sub "BillingAlerts-${environment}" + + EmailAlertSubscription: + Type: AWS::SNS::Subscription + Condition: SubscribeEmail + Properties: + Protocol: email + Endpoint: !Ref notificationEmail + TopicArn: !Ref AlertSnsTopic + + PhoneAlertSubscription: + Type: AWS::SNS::Subscription + Condition: SubscribePhone + Properties: + Protocol: sms + Endpoint: !Ref notificationPhone + TopicArn: !Ref AlertSnsTopic + + AlertExecutionerRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyName: LambdaLogging + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: "*" + - PolicyName: AlertSNS + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - sns:Publish + Resource: !Ref AlertSnsTopic + + + AlertHandler: + Type: AWS::Lambda::Function Properties: Handler: lambda_function.lambda_handler Runtime: python3.6 - # CodeUri: - # Bucket: !Ref CloudToolsBucket - # Key: !Ref TicketsArchive - FunctionName: !Sub "BillingBot-${Environment}-FnNotifyDiscord" - Description: 'Lambda receives API Gateway requests and generates tickets in FreshDesk.' + Code: + S3Bucket: "sumu-billingalerts" + S3Key: !Sub "${release}/lambda/alertHandler.zip" + FunctionName: !Sub "FnAlert-${environment}" + Description: Lambda receives CloudWatch events and alerts channels. MemorySize: 128 Timeout: 10 - #Role: !GetAtt CommandRelayIAM.Outputs.TicketsRoleArn + Role: !GetAtt AlertExecutionerRole.Arn Environment: Variables: - region: !Ref 'AWS::Region' - discordId: !Ref NotificationDiscordId + discordWebhook: !Ref discordWebhook + slackWebhook: !Ref slackWebhook + snsTopic: !Ref AlertSnsTopic - #CloudWatch CRON - #CloudWatch Billing Limit (3 Tiers (Low Priority, Medium Priority, High Priority)) + + CloudWatchReciever: + Type: AWS::SNS::Topic + Properties: + TopicName: !Sub "Billing-CloudWatch-${environment}" + + CloudWatchAlertSubscription: + Type: AWS::SNS::Subscription + Properties: + Protocol: lambda + Endpoint: !GetAtt AlertHandler.Arn + TopicArn: !Ref CloudWatchReciever + + SnsLambdaPermission: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + Principal: sns.amazonaws.com + SourceArn: !Ref CloudWatchReciever + FunctionName: !GetAtt AlertHandler.Arn + + LowPriorityAlarm: + Type: AWS::CloudWatch::Alarm + Properties: + ActionsEnabled: true + AlarmActions: + - !Ref CloudWatchReciever + ComparisonOperator: GreaterThanThreshold + Dimensions: + - Name: Currency + Value: USD + EvaluationPeriods: 1 + MetricName: EstimatedCharges + Namespace: "AWS/Billing" + OKActions: + - !Ref CloudWatchReciever + Period: !Ref updateInterval + Statistic: Maximum + Threshold: !Ref lowPriorityAlert + + MediumPriorityAlarm: + Type: AWS::CloudWatch::Alarm + Properties: + ActionsEnabled: true + AlarmActions: + - !Ref CloudWatchReciever + ComparisonOperator: GreaterThanThreshold + Dimensions: + - Name: Currency + Value: USD + EvaluationPeriods: 1 + MetricName: EstimatedCharges + Namespace: "AWS/Billing" + OKActions: + - !Ref CloudWatchReciever + Period: !Ref updateInterval + Statistic: Maximum + Threshold: !Ref mediumPriorityAlert + + HighPriorityAlarm: + Type: AWS::CloudWatch::Alarm + Properties: + ActionsEnabled: true + AlarmActions: + - !Ref CloudWatchReciever + ComparisonOperator: GreaterThanThreshold + Dimensions: + - Name: Currency + Value: USD + EvaluationPeriods: 1 + MetricName: EstimatedCharges + Namespace: "AWS/Billing" + OKActions: + - !Ref CloudWatchReciever + Period: !Ref updateInterval + Statistic: Maximum + Threshold: !Ref highPriorityAlert diff --git a/lambda/alertHandler/lambda_function.py b/lambda/alertHandler/lambda_function.py new file mode 100644 index 0000000..53b3d76 --- /dev/null +++ b/lambda/alertHandler/lambda_function.py @@ -0,0 +1,70 @@ +import json, requests, os, boto3 + + +def lambda_handler(event, context): + if 'Records' in event: + for record in event['Records']: + if record['EventSource'] == "aws:sns": + if "Message" in record['Sns']: + message = json.loads(record['Sns']['Message']) + if "NewStateValue" in message: + print(message) + is_important = "HighPriority" in message["AlarmName"] and (message["NewStateValue"] != "OK") + send_message(message["NewStateReason"], "AWS Billing Alert", is_important) + else: + send_message(message, record['Sns']['Subject']) + +#------------------- +# Messaging Methods +#------------------- +def send_discord_message(message, name = None, high_priority = False): + if high_priority: + send_discord_message("@everyone \n**HIGH PRIORITY:** " + message, name) + return + if "discordWebhook" in os.environ and os.environ["discordWebhook"] != "": + r = requests.post(os.environ['discordWebhook'], + data={'content': message, 'username': name}) + print("Sent to Discord: " + message) + else: + print("Discord webhook not setup. Skipping...") + +def send_sns_message(message, name = "Billing Alert", high_priority = False): + if high_priority: + send_sns_message(message, "HIGH PRIORITY: " + name) + return + if 'snsTopic' in os.environ and os.environ["snsTopic"] != "": + boto3.client('sns').publish( + TargetArn=os.environ['snsTopic'], + Message=json.dumps({'default':message, + 'sms' : name +"\n---\n" + message}), + Subject=name, + MessageStructure='json' + ) + print("Published to SNS: " + message) + else: + print("SNS not setup. Skipping...") + +def send_slack_message(message, name = None, high_priority = False): + if high_priority: + send_slack_message(" \n*HIGH PRIORITY:* " + message, name) + return + if "slackWebhook" in os.environ and os.environ["slackWebhook"] != "": + r = requests.post(os.environ['slackWebhook'], + data=json.dumps({'text': message, 'username' : name}), + headers={'Content-Type' : 'application/json'}) + print("Sent to Slack: " + message) + else: + print("Slack webhook not setup. Skipping...") + +#Function sends message to every messaging method +def send_message(message, name = None, high_priority = False): + send_discord_message(message, name, high_priority) + send_slack_message(message, name, high_priority) + send_sns_message(message, name, high_priority) + + +# os.environ['snsTopic'] = "arn:aws:sns:us-east-1:959431236163:BillingDev" +# os.environ['slackWebhook'] = "https://hooks.slack.com/services/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" +# os.environ['discordWebhook'] = "https://discordapp.com/api/webhooks/597128213373648900/Ys4iB0MCeJiwne_KSJIE_Q8geZluKsFYzQQ02GJIMp2fGka1-tx47ZmH0aTxbGVz6fJ6" + +# send_message("Test", "AWS Billing", True) \ No newline at end of file diff --git a/lambda/alertHandler/requirements.txt b/lambda/alertHandler/requirements.txt new file mode 100644 index 0000000..e15a20b --- /dev/null +++ b/lambda/alertHandler/requirements.txt @@ -0,0 +1,2 @@ +boto3>=1.9.57 +requests>=2.21.0