├── .gitignore ├── LICENSE ├── README.md ├── cloudformation-notifications.py ├── cloudformation-notifications.yaml └── tests ├── failure.yaml └── success.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Claudio Bizzotto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AWS CloudFormation Notifications 2 | ================================ 3 | 4 | CloudFormation template to automate SNS notifications of specific CloudFormation events. 5 | 6 | * [Overview](#overview) 7 | * [How it works](#how-it-works) 8 | * [Usage](#usage) 9 | * [Changing the default parameters](#changing-the-default-parameters) 10 | * [Links](#links) 11 | 12 | ## Overview 13 | 14 | Instead of publishing your CloudFormation stack events directly to an email SNS topic, with the unwanted side-effect of getting irrelevant notifications in your inbox, you can instead use a Lambda function as an intermediary. This way, your CloudFormation stacks communicate with the Lambda function and, from there, you can do pretty much whatever you want - filter the notification messages, send emails through SNS, talk to an external service etc. 15 | 16 | This CloudFormation template allows you to send SNS notifications (email messages) to users during CloudFormation stack creation, update, deletion etc. Only certain events (like `CREATE_COMPLETE`) are notified. 17 | 18 | **Note about the Python file:** because the Lambda function is defined in the template itself (inline Python), there's no need to host the code anywhere. The [Python file](./cloudformation-notifications.py) is kept here just for reference - it is **not** directly used in the CloudFormation stack. 19 | 20 | ## How it works 21 | 22 | These are the main components of the CloudFormation stack created by this template: 23 | 24 | * `SNSTopicCloudFormation`: an SNS topic that _listens_ to CloudFormation events and forwards them to a Lambda function 25 | * `LambdaFunction`: a Lambda function capable of sending email messages through SNS. This function works like an input filter, as described below 26 | * `SNSTopicEmail`: an SNS topic that sends email messages to users - _called_ from the Lambda function above 27 | 28 | Because we generally don't want to get email notifications about all possible CloudFormation events, we can tell the Lambda function to only keep the types of notifications that interest us - it filters out unwanted noise. (See `NOTIFICATION_TYPES` [below](#changing-the-default-parameters)) 29 | 30 | When you create this CloudFormation stack, it outputs the ARN associated with `SNSTopicCloudFormation`. Later on, whenever you create other CloudFormation stacks, you can use that ARN with the `--notification-arns` option in order to let that topic _listen_ to events coming from those new stacks. 31 | 32 | ## Usage 33 | 34 | Start by opening the CloudFormation template and replacing the dummy email address. You can also add more email addresses if you want: 35 | 36 | ```YAML 37 | Resources: 38 | # SNS topic to send emails to users (used inside Lambda function) 39 | SNSTopicEmail: 40 | Type: "AWS::SNS::Topic" 41 | Properties: 42 | Subscription: 43 | - Endpoint: "my-real-email-address@example.com" 44 | Protocol: "email" 45 | - Endpoint: "sivuca@jazz.com.br" 46 | Protocol: "email" 47 | ``` 48 | 49 | ### Create stack 50 | 51 | Create the CloudFormation stack with the following command: 52 | 53 | ```SHELL 54 | $ aws cloudformation create-stack \ 55 | --stack-name cloudformation-notifications \ 56 | --template-body file://cloudformation-notifications.yaml \ 57 | --capabilities CAPABILITY_IAM 58 | ``` 59 | 60 | If you are not familiar with the `--capabilities` parameter, you can find more information about it [here](https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateStack.html#API_CreateStack_RequestParameters). 61 | 62 | After creating the stack, you should get an email message asking you to confirm your subscription to the email SNS topic (`SNSTopicEmail`). Confirm your subscription to the SNS topic before proceeding to the next step. 63 | 64 | ### Get SNS topic's ARN 65 | 66 | As said above, the main purpose of this stack is to create the SNS topic (`SNSTopicCloudFormation`) to be used at later stages of CloudFormation deployments. To get the ARN generated for that SNS topic, you can use this: 67 | 68 | ```SHELL 69 | $ aws cloudformation describe-stacks \ 70 | --stack-name cloudformation-notifications \ 71 | --output text \ 72 | --query "Stacks[0].Outputs[?OutputKey == 'SNSTopicCloudFormation'].OutputValue" 73 | ``` 74 | 75 | Take note of this ARN - you will need it when creating new CloudFormation stacks. (Note that the `--query` parameter is written in [JMESPath](http://jmespath.org/)) 76 | 77 | ### Testing 78 | 79 | Now that the main stack was created, you can run the tests to see the end result. Replace `${SNS_TOPIC_ARN}` below with the ARN you saved earlier. 80 | 81 | ```SHELL 82 | $ aws cloudformation create-stack \ 83 | --stack-name cloudformation-notifications-test-failure \ 84 | --template-body file://tests/failure.yaml \ 85 | --notification-arns ${SNS_TOPIC_ARN} 86 | ``` 87 | 88 | This test should fail and you should get an email notifying you of the `ROLLBACK_IN_PROGRESS` event. 89 | 90 | ```SHELL 91 | $ aws cloudformation create-stack \ 92 | --stack-name cloudformation-notifications-test-success \ 93 | --template-body file://tests/success.yaml \ 94 | --parameters "ParameterKey=BucketName,ParameterValue=testing-cloud-formation-sns-$(date +%s)" \ 95 | --notification-arns ${SNS_TOPIC_ARN} 96 | ``` 97 | 98 | This test should succceed and you should get an email notifying you of the `CREATE_COMPLETE` event. 99 | 100 | Note that no other events are sent to your inbox when you run the tests. 101 | 102 | You can delete these two stacks after running the tests. 103 | 104 | ## Changing the default parameters 105 | 106 | Update the `NOTIFICATION_TYPES` according to your needs (use a comma-separated value). For example: 107 | 108 | * `ROLLBACK_IN_PROGRESS` 109 | * `CREATE_COMPLETE,UPDATE_COMPLETE` 110 | 111 | ## Links 112 | 113 | * For a similar solution, but without CloudFormation, check [this tutorial](https://aws.amazon.com/premiumsupport/knowledge-center/cloudformation-rollback-email) 114 | -------------------------------------------------------------------------------- /cloudformation-notifications.py: -------------------------------------------------------------------------------- 1 | import os 2 | import boto3 3 | 4 | def handler(event, context): 5 | # Notification types 6 | env_notification_types = os.getenv("NOTIFICATION_TYPES", None) 7 | notification_types = env_notification_types.split(",") if env_notification_types else None 8 | if not notification_types: 9 | print("At least one CloudFormation notification type needs to be specified") 10 | return 11 | 12 | # SNS topic ARN 13 | sns_topic_arn = os.getenv("SNS_TOPIC_ARN", None) 14 | if not sns_topic_arn: 15 | print("The ARN of the SNS topic needs to be specified") 16 | return 17 | 18 | try: 19 | message = str(event["Records"][0]["Sns"]["Message"]).replace("\n", ",") 20 | except Exception: 21 | print("Message could not be parsed. Event: %s" % (event)) 22 | return 23 | 24 | # Ignore resources that are not the CloudFormation stack itself 25 | if "ResourceType='AWS::CloudFormation::Stack'" not in message: 26 | return 27 | 28 | for notification_type in notification_types: 29 | if notification_type not in message: 30 | continue 31 | 32 | sns_subject = "CloudFormation %s" % (notification_type) 33 | sns_message = message.replace(",", "\n") 34 | boto3.client('sns').publish( 35 | Subject=sns_subject, 36 | Message=sns_message, 37 | TopicArn=sns_topic_arn 38 | ) 39 | -------------------------------------------------------------------------------- /cloudformation-notifications.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Stack name: cloudformation-notifications 3 | AWSTemplateFormatVersion: "2010-09-09" 4 | Description: "Configure SNS topics to subscribe to specific CloudFormation notifications and forward them to users" 5 | 6 | Resources: 7 | # SNS topic to send emails to users (used inside Lambda function) 8 | SNSTopicEmail: 9 | Type: "AWS::SNS::Topic" 10 | Properties: 11 | Subscription: 12 | - Endpoint: "replaceme@example.com" # PUT YOUR EMAIL ADDRESS HERE 13 | Protocol: "email" 14 | 15 | # IAM role and inline policy for Lambda function 16 | LambdaRole: 17 | Type: "AWS::IAM::Role" 18 | Properties: 19 | AssumeRolePolicyDocument: 20 | Version: "2012-10-17" 21 | Statement: 22 | - 23 | Effect: "Allow" 24 | Principal: 25 | Service: 26 | - "lambda.amazonaws.com" 27 | Action: 28 | - "sts:AssumeRole" 29 | Path: "/" 30 | Policies: 31 | - 32 | PolicyName: "cloudformation-notifications-lambda-role-policy" 33 | PolicyDocument: 34 | Version: "2012-10-17" 35 | Statement: 36 | - 37 | Effect: "Allow" 38 | Action: 39 | - "sns:Publish" 40 | Resource: !Ref "SNSTopicEmail" 41 | - 42 | Effect: "Allow" 43 | Action: 44 | - "logs:CreateLogGroup" 45 | - "logs:CreateLogStream" 46 | - "logs:PutLogEvents" 47 | Resource: "arn:aws:logs:*:*:*" 48 | - 49 | Effect: "Allow" 50 | Action: 51 | - "xray:PutTelemetryRecords" 52 | - "xray:PutTraceSegments" 53 | Resource: "*" 54 | 55 | # Lambda function to catch CloudFormation events (forwarded by SNS) and create new SNS notifications from them 56 | LambdaFunction: 57 | Type: "AWS::Lambda::Function" 58 | Properties: 59 | FunctionName: "cloudformation-notifications-lambda" 60 | Description: "Forward CloudFormation notifications to SNS topic" 61 | Handler: "index.handler" 62 | Role: !GetAtt "LambdaRole.Arn" 63 | Environment: 64 | Variables: 65 | SNS_TOPIC_ARN: !Ref "SNSTopicEmail" 66 | NOTIFICATION_TYPES: "CREATE_COMPLETE,UPDATE_COMPLETE,ROLLBACK_IN_PROGRESS" 67 | Code: 68 | ZipFile: | 69 | import os 70 | import boto3 71 | 72 | def handler(event, context): 73 | # Notification types 74 | env_notification_types = os.getenv("NOTIFICATION_TYPES", None) 75 | notification_types = env_notification_types.split(",") if env_notification_types else None 76 | if not notification_types: 77 | print("At least one CloudFormation notification type needs to be specified") 78 | return 79 | 80 | # SNS topic ARN 81 | sns_topic_arn = os.getenv("SNS_TOPIC_ARN", None) 82 | if not sns_topic_arn: 83 | print("The ARN of the SNS topic needs to be specified") 84 | return 85 | 86 | try: 87 | message = str(event["Records"][0]["Sns"]["Message"]).replace("\n", ",") 88 | except Exception: 89 | print("Message could not be parsed. Event: %s" % (event)) 90 | return 91 | 92 | # Ignore resources that are not the CloudFormation stack itself 93 | if "ResourceType='AWS::CloudFormation::Stack'" not in message: 94 | return 95 | 96 | for notification_type in notification_types: 97 | if notification_type not in message: 98 | continue 99 | 100 | sns_subject = "CloudFormation %s" % (notification_type) 101 | sns_message = message.replace(",", "\n") 102 | boto3.client('sns').publish( 103 | Subject=sns_subject, 104 | Message=sns_message, 105 | TopicArn=sns_topic_arn 106 | ) 107 | Runtime: "python3.6" 108 | Timeout: "90" 109 | TracingConfig: 110 | Mode: "Active" 111 | 112 | # SNS topic and inline subscription to forward events to Lambda function 113 | SNSTopicCloudFormation: 114 | Type: "AWS::SNS::Topic" 115 | Properties: 116 | Subscription: 117 | - 118 | Endpoint: !GetAtt "LambdaFunction.Arn" 119 | Protocol: "lambda" 120 | DependsOn: "LambdaFunction" 121 | 122 | # Lambda permission to allow SNS to forward events to Lambda function 123 | LambdaPermission: 124 | Type: "AWS::Lambda::Permission" 125 | Properties: 126 | Action: "lambda:InvokeFunction" 127 | Principal: "sns.amazonaws.com" 128 | SourceArn: !Ref "SNSTopicCloudFormation" 129 | FunctionName: !GetAtt "LambdaFunction.Arn" 130 | 131 | Outputs: 132 | SNSTopicCloudFormation: 133 | Description: "ARN of CloudFormation SNS topic - use this value with --notification-arns when creating other stacks" 134 | Value: !Ref "SNSTopicCloudFormation" 135 | Export: 136 | Name: "sns-topic-cloudformation" 137 | -------------------------------------------------------------------------------- /tests/failure.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Stack name: cloudformation-notifications-test-failure 3 | AWSTemplateFormatVersion: "2010-09-09" 4 | Description: "Test CloudFormation SNS notifications with a failed build" 5 | 6 | Resources: 7 | # Try to create an SNS topic with a protocol that doesn't exist - this should fail and generate a ROLLBACK_IN_PROGRESS 8 | SNSEmailFailure: 9 | Type: "AWS::SNS::Topic" 10 | Properties: 11 | Subscription: 12 | - Endpoint: "xxx://i-dont-exist" 13 | Protocol: "xxx" 14 | -------------------------------------------------------------------------------- /tests/success.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Stack name: cloudformation-notifications-test-success 3 | AWSTemplateFormatVersion: "2010-09-09" 4 | Description: "Test CloudFormation SNS notifications with a successful build" 5 | 6 | Parameters: 7 | BucketName: 8 | Type: "String" 9 | Default: "testing-cloud-formation-sns" 10 | 11 | Resources: 12 | # Try to create a dummy S3 bucket - this should succeed and generate a CREATE_COMPLETE 13 | SNSEmailSuccess: 14 | Type: "AWS::S3::Bucket" 15 | Properties: 16 | BucketName: !Ref "BucketName" 17 | --------------------------------------------------------------------------------