Skip to content

Instantly share code, notes, and snippets.

@russau
Last active Nov 28, 2020
Embed
What would you like to do?
Scheduled Lamba function posts your AWS spend to a slack webhook
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