diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8f389d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +slack.py +cf-notify.zip diff --git a/README.md b/README.md new file mode 100644 index 0000000..58f41db --- /dev/null +++ b/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! diff --git a/cf-notify.json b/cf-notify.json new file mode 100644 index 0000000..7137101 --- /dev/null +++ b/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" } + } + } +} diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..e0509da --- /dev/null +++ b/deploy.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +if [ $# -lt 1 ] +then + echo "usage: deploy.sh [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 <