Skip to content
This repository has been archived by the owner on Nov 27, 2021. It is now read-only.

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
akash1810 committed Nov 9, 2015
0 parents commit e92216e
Show file tree
Hide file tree
Showing 6 changed files with 344 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
@@ -0,0 +1,3 @@
*.pyc
slack.py
cf-notify.zip
89 changes: 89 additions & 0 deletions README.md
@@ -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!
102 changes: 102 additions & 0 deletions cf-notify.json
@@ -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" }
}
}
}
41 changes: 41 additions & 0 deletions deploy.sh
@@ -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
Binary file added example.jpeg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
109 changes: 109 additions & 0 deletions lambda_notify.py
@@ -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()]
}

0 comments on commit e92216e

Please sign in to comment.