forked from guardian/cf-notify
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit e92216e
Showing
6 changed files
with
344 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
*.pyc | ||
slack.py | ||
cf-notify.zip |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
# CF Notify | ||
|
||
## What? | ||
An AWS Lambda function that will post Cloud Formation status updates to a Slack channel via a Slack Web Hook. | ||
|
||
|
||
## Why? | ||
To give visibility of Cloud Formation changes to the whole team in a quick and simple manner. For example: | ||
|
||
![example Slack messages](./example.jpeg) | ||
|
||
|
||
## How? | ||
CF Notify has a stack of AWS resources consisting of: | ||
- An SNS Topic | ||
- A Lambda function, which uses the SNS Topic as an event source | ||
- An IAM Role to execute the Lambda function | ||
|
||
We add the SNS Topic of CF Notify to the notification ARNs of the Stack we want to monitor. | ||
Search for `NotificationARNs.member.N` [here](http://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_UpdateStack.html) | ||
for more information on notification ARNs. | ||
|
||
|
||
## Setup | ||
|
||
To setup CF Notify, we need to do the following. | ||
|
||
### Prerequisites | ||
|
||
CF Notify has two prerequisites: a S3 Bucket and a Slack incoming webhook. | ||
|
||
#### S3 Bucket | ||
You can use a pre-existing bucket in your account, however, to maintain isolation, it's generally best to create a bucket: | ||
|
||
```sh | ||
BUCKET="cf-notify-`pwgen -1 --no-capitalize 20`" | ||
aws s3 mb "s3://$BUCKET" | ||
``` | ||
|
||
#### Slack incoming webhook | ||
You can create an incoming webhook [here](https://my.slack.com/services/new/incoming-webhook/). | ||
|
||
|
||
### Deploy Lambda | ||
|
||
This is done using the script [deploy.sh](./deploy.sh). | ||
|
||
```sh | ||
./deploy.sh $ENV $BUCKET $WEBHOOK [$CHANNEL] | ||
``` | ||
|
||
Where: | ||
- ENV is the environment of the Stack we are monitoring, e.g. DEV, TEST, PROD. It will be used in the naming of the Lambda artifact file stored in S3. | ||
- BUCKET is the S3 bucket to store the Lambda artifact. | ||
- WEBHOOK is the Web Hook URL of an Incoming Web Hook (see https://api.slack.com/incoming-webhooks). | ||
- CHANNEL is optional and is the Slack channel or user to send messages to. Defaults to the channel chosen when the webhook was created. | ||
|
||
If you don't want to send messages to the channel of the webhook, set `$CHANNEL` as another channel or user. For example `#general` or `@foo`. | ||
This is useful if you want to setup CF Notify for your own DEV stack. In this case, you'd want to set `$ENV` as your AWS IAM name: | ||
|
||
```sh | ||
ENV="DEV-`aws iam get-user | jq '.User.UserName' | tr -d '"'`" | ||
``` | ||
|
||
`deploy.sh` will create a zip file and upload it to `s3://$BUCKET/cf-notify-$ENV.zip`. | ||
|
||
|
||
### Create CF Notify Stack | ||
|
||
Create a Stack using the [template](./cf-notify.json). | ||
|
||
```sh | ||
aws cloudformation create-stack --template-body file://cf-notify.json \ | ||
--stack-name cf-notify-$ENV \ | ||
--capabilities CAPABILITY_IAM \ | ||
--parameters ParameterKey=Bucket,ParameterValue=$BUCKET ParameterKey=Environment,ParameterValue=$ENV | ||
``` | ||
|
||
## Usage | ||
|
||
Once setup is complete, all you need to do now is set the notification ARN when you update your Cloud Formation stack: | ||
|
||
```sh | ||
SNS_ARN=`aws cloudformation describe-stacks --stack-name cf-notify-$ENV | jq ".Stacks[].Outputs[].OutputValue" | tr -d '"'` | ||
|
||
aws cloudformation [create-stack|update-stack|delete-stack] --notification-arns $SNS_ARN | ||
``` | ||
|
||
You should now see messages in Slack! |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
{ | ||
"AWSTemplateFormatVersion": "2010-09-09", | ||
"Description": "cf notify stack", | ||
"Parameters": { | ||
"Bucket": { | ||
"Description": "S3 bucket to locate lambda function (cf-notify-$ENVIRONMENT.zip)", | ||
"Type": "String" | ||
}, | ||
"Environment": { | ||
"Description": "Environment name", | ||
"Type": "String" | ||
} | ||
}, | ||
"Resources": { | ||
"CFNotifyRole": { | ||
"Type": "AWS::IAM::Role", | ||
"Properties": { | ||
"AssumeRolePolicyDocument": { | ||
"Version": "2012-10-17", | ||
"Statement": [ | ||
{ | ||
"Effect": "Allow", | ||
"Principal": { | ||
"Service": [ "lambda.amazonaws.com" ] | ||
}, | ||
"Action": [ "sts:AssumeRole" ] | ||
} | ||
] | ||
}, | ||
"Path": "/", | ||
"Policies": [ | ||
{ | ||
"PolicyName": "CFNotifyPolicy", | ||
"PolicyDocument": { | ||
"Version": "2012-10-17", | ||
"Statement": [ | ||
{ | ||
"Effect": "Allow", | ||
"Action": [ | ||
"logs:CreateLogGroup", | ||
"logs:CreateLogStream", | ||
"logs:PutLogEvents" | ||
], | ||
"Resource": "arn:aws:logs:*:*:*" | ||
}, | ||
{ | ||
"Effect": "Allow", | ||
"Action": [ | ||
"cloudformation:DescribeStackResources" | ||
], | ||
"Resource": "arn:aws:cloudformation:*:*:*/*/*" | ||
} | ||
] | ||
} | ||
} | ||
] | ||
} | ||
}, | ||
"CFNotifyTopic": { | ||
"Type": "AWS::SNS::Topic", | ||
"Properties": { | ||
"Subscription": [ | ||
{ | ||
"Endpoint": { "Fn::GetAtt": [ "CFNotifyFunction", "Arn" ] }, | ||
"Protocol": "lambda" | ||
} | ||
] | ||
} | ||
}, | ||
"CFNotifyFunction": { | ||
"Type": "AWS::Lambda::Function", | ||
"Properties": { | ||
"Description" : "Lambda function to post CF updates to Slack", | ||
"Handler": "lambda_notify.lambda_handler", | ||
"Role": { | ||
"Fn::GetAtt": [ "CFNotifyRole", "Arn" ] | ||
}, | ||
"Code": { | ||
"S3Bucket": { "Ref": "Bucket" }, | ||
"S3Key": { "Fn::Join": [ "", [ "cf-notify-", { "Ref": "Environment" }, ".zip" ] ] } | ||
}, | ||
"Runtime": "python2.7", | ||
"Timeout": "30" | ||
} | ||
}, | ||
"CFNotifyInvokePermission": { | ||
"Type": "AWS::Lambda::Permission", | ||
"Properties": { | ||
"FunctionName" : { "Ref" : "CFNotifyFunction" }, | ||
"Action": "lambda:InvokeFunction", | ||
"Principal": "sns.amazonaws.com", | ||
"SourceArn": { "Ref": "CFNotifyTopic" } | ||
} | ||
} | ||
}, | ||
"Outputs": { | ||
"CFNotifyEventSource": { | ||
"Description": "ARN of CF SNS Topic", | ||
"Value": { "Ref": "CFNotifyTopic" } | ||
} | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
#!/usr/bin/env bash | ||
|
||
if [ $# -lt 1 ] | ||
then | ||
echo "usage: deploy.sh <ENV> <BUCKET> <WEBHOOK> [CHANNEL]" | ||
exit 1 | ||
fi | ||
|
||
ENV=$1 | ||
BUCKET=$2 | ||
WEBHOOK=$3 | ||
CHANNEL=$4 | ||
|
||
if [ -z $ENV ]; | ||
then | ||
echo "Please specify an environment."; | ||
exit 1 | ||
fi | ||
|
||
if [ -z $BUCKET ]; | ||
then | ||
echo "Please specify a destination bucket"; | ||
exit 1 | ||
fi | ||
|
||
if [ -z $WEBHOOK ]; | ||
then | ||
echo "Please specify a Slack WebHook"; | ||
exit 1 | ||
fi | ||
|
||
cat > slack.py <<EOL | ||
WEBHOOK='$WEBHOOK' | ||
CHANNEL='$CHANNEL' | ||
EOL | ||
|
||
zip cf-notify.zip lambda_notify.py slack.py | ||
aws s3 cp cf-notify.zip s3://$BUCKET/cf-notify-$ENV.zip | ||
|
||
rm slack.py | ||
rm cf-notify.zip |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import json | ||
import shlex | ||
import urllib2 | ||
import slack | ||
import boto3 | ||
from itertools import groupby | ||
|
||
# Mapping CloudFormation status codes to colors for Slack message attachments | ||
# Status codes from http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html | ||
STATUS_COLORS = { | ||
'CREATE_COMPLETE': 'good', | ||
'CREATE_IN_PROGRESS': 'good', | ||
'CREATE_FAILED': 'danger', | ||
'DELETE_COMPLETE': 'good', | ||
'DELETE_FAILED': 'danger', | ||
'DELETE_IN_PROGRESS': 'good', | ||
'ROLLBACK_COMPLETE': 'warning', | ||
'ROLLBACK_FAILED': 'danger', | ||
'ROLLBACK_IN_PROGRESS': 'warning', | ||
'UPDATE_COMPLETE': 'good', | ||
'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS': 'good', | ||
'UPDATE_IN_PROGRESS': 'good', | ||
'UPDATE_ROLLBACK_COMPLETE': 'warning', | ||
'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS': 'warning', | ||
'UPDATE_ROLLBACK_FAILED': 'danger', | ||
'UPDATE_ROLLBACK_IN_PROGRESS': 'warning' | ||
} | ||
|
||
# List of CloudFormation status that will trigger a call to `get_stack_summary_attachment` | ||
DESCRIBE_STACK_STATUS = [ | ||
'CREATE_COMPLETE', | ||
'DELETE_IN_PROGRESS' | ||
] | ||
|
||
# List of properties from ths SNS message that will be included in a Slack message | ||
SNS_PROPERTIES_FOR_SLACK = [ | ||
'StackId', | ||
'ResourceStatus', | ||
'ResourceType', | ||
'Timestamp', | ||
'StackName', | ||
] | ||
|
||
|
||
def lambda_handler(event, context): | ||
message = event['Records'][0]['Sns'] | ||
sns_message = message['Message'] | ||
cf_message = dict(token.split('=', 1) for token in shlex.split(sns_message)) | ||
|
||
# ignore messages that do not pertain to the Stack as a whole | ||
if not cf_message['ResourceType'] == 'AWS::CloudFormation::Stack': | ||
return | ||
|
||
message = get_stack_update_message(cf_message) | ||
data = json.dumps(message) | ||
req = urllib2.Request(slack.WEBHOOK, data, {'Content-Type': 'application/json'}) | ||
urllib2.urlopen(req) | ||
|
||
|
||
def get_stack_update_message(cf_message): | ||
attachments = [ | ||
get_stack_update_attachment(cf_message) | ||
] | ||
|
||
if cf_message['ResourceStatus'] in DESCRIBE_STACK_STATUS: | ||
attachments.append(get_stack_summary_attachment(cf_message['StackName'])) | ||
|
||
message = { | ||
'icon_emoji': ':cloud:', | ||
'username': 'cf-bot', | ||
'text': 'Cloud Formation stack change in {}'.format(cf_message['StackName']), | ||
'attachments': attachments | ||
} | ||
|
||
if slack.CHANNEL: | ||
message['channel'] = slack.CHANNEL | ||
|
||
return message | ||
|
||
|
||
def get_stack_update_attachment(cf_message): | ||
title = 'Stack {stack} is now status {status}'.format( | ||
stack=cf_message['StackName'], | ||
status=cf_message['ResourceStatus']) | ||
|
||
return { | ||
'fallback': title, | ||
'title': title, | ||
'fields': [{'title': k, 'value': v, 'short': True} | ||
for k, v in cf_message.iteritems() if k in SNS_PROPERTIES_FOR_SLACK], | ||
'color': STATUS_COLORS.get(cf_message['ResourceStatus'], '#000000'), | ||
} | ||
|
||
|
||
def get_stack_summary_attachment(stack_name): | ||
client = boto3.client('cloudformation') | ||
resources = client.describe_stack_resources(StackName=stack_name) | ||
sorted_resources = sorted(resources['StackResources'], key=lambda res: res['ResourceType']) | ||
grouped_resources = groupby(sorted_resources, lambda res: res['ResourceType']) | ||
resource_count = {key: len(list(group)) for key, group in grouped_resources} | ||
|
||
title = 'Breakdown of all {} resources'.format(len(resources['StackResources'])) | ||
|
||
return { | ||
'fallback': title, | ||
'title': title, | ||
'fields': [{'title': 'Type {}'.format(k), 'value': 'Total {}'.format(v), 'short': True} | ||
for k, v in resource_count.iteritems()] | ||
} |