Last active
November 28, 2020 00:59
-
-
Save russau/4b2b29a473cef6819ac5fdcca4fc1321 to your computer and use it in GitHub Desktop.
Scheduled Lamba function posts your AWS spend to a slack webhook
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Description: Lambda function to post the spend into a slack webhook | |
Parameters: | |
Schedule: | |
Description: The rule schedule expression, defaults to every 8 hours | |
Default: "rate(8 hours)" | |
Type: String | |
SlackHook: | |
Description: The entire url of the slack webhook, e.g. https://hooks.slack.com/services/TABCDEFGH/BABCDEFGHIK/abcdefghijklmmopqrstuvwx | |
Type: String | |
DeleteAfterHours: | |
Description: Remove image from s3 bucket after n hours | |
Default: 48 | |
Type: Number | |
Resources: | |
############################################################################ | |
# | |
# Lambda functions | |
# | |
############################################################################ | |
SpendPosterFunction: | |
Type: AWS::Lambda::Function | |
Properties: | |
Environment: | |
Variables: | |
bucket_name: !Ref StaticBucket | |
slack_hook: !Ref SlackHook | |
delete_after_hours: !Ref DeleteAfterHours | |
Code: | |
ZipFile: | |
!Sub | | |
import os | |
import json | |
import time | |
import datetime | |
import http.client | |
from urllib.parse import urlparse | |
import boto3 | |
def handler(event, context): | |
region = 'us-east-1' | |
cw = boto3.client('cloudwatch', region_name=region) | |
s3 = boto3.client("s3", region_name=region) | |
bucket_name = os.getenv("bucket_name") | |
slack_hook = urlparse(os.getenv("slack_hook")) | |
delete_after_hours = int(os.getenv("delete_after_hours")) | |
# MetricWidget reference https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/CloudWatch-Metric-Widget-Structure.html | |
widget = { | |
"metrics": [ | |
[ | |
"AWS/Billing", | |
"EstimatedCharges", | |
"Currency", | |
"USD" | |
] | |
], | |
"view": "timeSeries", | |
"stacked": False, | |
"stat": "Maximum", | |
"period": 86400, | |
"width": 1184, | |
"height": 250, | |
"start": "-P7D", | |
"end": "P0D" | |
} | |
response = cw.get_metric_widget_image( | |
MetricWidget=json.dumps(widget) | |
) | |
# name the graph keys "graph/[epoch time].png" | |
epoch = int(time.time()) | |
key = f"graphs/{epoch}.png" | |
# save the image as an s3 object | |
s3.put_object( | |
Body=response["MetricWidgetImage"], | |
Bucket=bucket_name, | |
Key=key, | |
ContentType="image/png" | |
) | |
# post the BlockKit blob to slack | |
conn = http.client.HTTPSConnection(slack_hook.netloc) | |
# I wish could just post the base64 image data to slack | |
# https://twitter.com/SlackAPI/status/791299393823670273 | |
payload = { | |
"text": "Spend update", | |
"blocks": [ | |
{ | |
"type": "image", | |
"image_url": f"https://{bucket_name}.s3.{region}.amazonaws.com/{key}", | |
"alt_text": "Spend update" | |
} | |
] | |
} | |
payload_str = json.dumps(payload) | |
headers = { | |
'Content-Type': 'text/plain' | |
} | |
conn.request("POST", slack_hook.path, payload_str, headers) | |
res = conn.getresponse() | |
print("POSTed to slack. Response code: %s" % res.status) | |
# cleaning up objects over n hours old | |
response = s3.list_objects_v2( | |
Bucket=bucket_name, | |
Prefix='graphs/' | |
) | |
utc_now = datetime.datetime.now(datetime.timezone.utc) | |
graphs = [{ | |
"key": r["Key"], | |
"age_hours": (utc_now - r["LastModified"]).total_seconds() / 60 / 60 | |
} for r in response['Contents']] | |
graphs = [g for g in graphs if g["age_hours"] > delete_after_hours] | |
for g in graphs: | |
print("Deleting old key: %s" % g["key"]) | |
s3.delete_object( | |
Bucket=bucket_name, | |
Key=g["key"] | |
) | |
Handler: index.handler | |
MemorySize: '128' | |
Role: !GetAtt 'SpendPosterRole.Arn' | |
Runtime: python3.6 | |
Timeout: '90' | |
SpendPosterRole: | |
Type: AWS::IAM::Role | |
Properties: | |
AssumeRolePolicyDocument: | |
Version: '2012-10-17' | |
Statement: | |
- Effect: Allow | |
Principal: | |
Service: | |
- lambda.amazonaws.com | |
Action: | |
- sts:AssumeRole | |
Path: / | |
ManagedPolicyArns: | |
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole | |
Policies: | |
- PolicyName: root | |
PolicyDocument: | |
Version: '2012-10-17' | |
Statement: | |
- Effect: Allow | |
Action: | |
- cloudwatch:GetMetricWidgetImage | |
- s3:Put* | |
- s3:Delete* | |
- s3:List* | |
Resource: '*' | |
############################################################################ | |
# | |
# S3 bucket static resources | |
# | |
############################################################################ | |
StaticBucket: | |
Type: "AWS::S3::Bucket" | |
Properties: | |
BucketName: !Sub '${AWS::Region}-${AWS::AccountId}-${AWS::StackName}' | |
StaticBucketPolicy: | |
Type: "AWS::S3::BucketPolicy" | |
Properties: | |
Bucket: | |
Ref: "StaticBucket" | |
PolicyDocument: | |
Statement: | |
- | |
Action: | |
- "s3:GetObject" | |
Effect: "Allow" | |
Resource: !Sub "arn:aws:s3:::${StaticBucket}/graphs/*" | |
Principal: "*" | |
############################################################################ | |
# | |
# Schedule Lambda | |
# | |
############################################################################ | |
ScheduledRule: | |
Type: AWS::Events::Rule | |
Properties: | |
Description: "ScheduledRule" | |
ScheduleExpression: !Ref Schedule | |
State: "ENABLED" | |
Targets: | |
- | |
Arn: | |
Fn::GetAtt: | |
- "SpendPosterFunction" | |
- "Arn" | |
Id: "TargetFunctionV1" | |
PermissionForEventsToInvokeLambda: | |
Type: AWS::Lambda::Permission | |
Properties: | |
FunctionName: !Ref "SpendPosterFunction" | |
Action: "lambda:InvokeFunction" | |
Principal: "events.amazonaws.com" | |
SourceArn: | |
Fn::GetAtt: | |
- "ScheduledRule" | |
- "Arn" | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment