Cloudformation chalange #1 - conditional resources dependency
Backstory
This is based on true story of one of my clients. The requirements were simple:- Send notification each time:
- File is uploaded to S3 bucket
- File is deleted from S3 bucket
- Event is caught by CloudWatch Rule
- Notifications must be send to provided SQS Topic and/or Lambda function (on the same or different AWS Account)
- If no ARN is provided, notifications resources shouldn't be created.
- Solution must be generic and easy to automate
Design
SNS is perfect service for this scenario. It is push notification service and it supports multiple targets.
Solution should be generic and reusable, so I decided to prepare CloudFormation template (I'm a big fan of CloudFormation) with all required logic to fulfill the requirements.
Version 1
First version was easy, define all required resources and put parameters everywhere in resource names.You can check v1.template in repository
AWSTemplateFormatVersion: 2010-09-09
Description: |
SNS Notifications for S3 events and CloudWatch Rules
Parameters:
stage:
Default: dev
Type: String
Description: Stage Name
bucketName:
Default: data
Type: String
Description: Consumable bucket name prefix
projectName:
Default: s3-notification-example
Type: String
Description: Project Name SSM Parameter
Resources:
s3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${projectName}-${bucketName}-${stage}
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
NotificationConfiguration:
TopicConfigurations:
-
Event: s3:ObjectCreated:*
Filter:
S3Key:
Rules:
- Name: suffix
Value: .json
Topic: !Ref notificationTopic
-
Event: s3:ObjectRemoved:*
Filter:
S3Key:
Rules:
- Name: suffix
Value: .json
Topic: !Ref notificationTopic
Tags:
- Key: Project
Value: !Ref projectName
- Key: Stage
Value: !Ref stage
notificationTopic:
Type: AWS::SNS::Topic
Properties:
DisplayName: !Sub ${projectName}-notifications-${stage}
Tags:
- Key: Project
Value: !Ref projectName
- Key: Stage
Value: !Ref stage
TopicName: !Sub ${projectName}-notifications-${stage}
notificationTopicPolicy:
Type: AWS::SNS::TopicPolicy
Properties:
PolicyDocument:
Version: '2012-10-17'
Id: Policy
Statement:
- Sid: Default
Effect: Allow
Principal:
AWS: "*"
Action:
- SNS:GetTopicAttributes
- SNS:SetTopicAttributes
- SNS:AddPermission
- SNS:RemovePermission
- SNS:DeleteTopic
- SNS:Subscribe
- SNS:ListSubscriptionsByTopic
- SNS:Publish
- SNS:Receive
Resource: !Ref notificationTopic
Condition:
StringEquals:
AWS:SourceOwner: !Ref AWS::AccountId
- Sid: CloudWatchEvents
Effect: Allow
Principal:
Service: events.amazonaws.com
Action: sns:Publish
Resource: !Ref notificationTopic
- Sid: S3
Effect: Allow
Principal:
AWS: "*"
Action:
- sns:Publish
Resource: !Ref notificationTopic
Condition:
ArnLike:
aws:SourceArn: !GetAtt s3Bucket.Arn
StringEquals:
aws:SourceAccount: !Ref AWS::AccountId
Topics:
- !Ref notificationTopic
When stack is creating, cloudformation tries to detect dependencies between defined resources.
In v1.template three resources are defined:
- s3Bucket with notifications configured to
- notificationTopic with
- notificationTopicPolicy which allows S3 bucket to Publish messages to notificationTopic
Problem:
In order to set S3 events notifications, SNS topic policy must be created before S3 bucket, otherwise deployment fails with following error:
Unable to validate the following destination configurations (Service: Amazon S3; Status Code: 400; Error Code: InvalidArgument
Solution:
Use "DependsOn" attribute, documentation says. This attribute helps cloudformation to understand resources dependencies. Sounds simple, so lets try with v2.
Version 2
Lets add "DependsOn" to s3Bucket definition:
s3Bucket:
Type: AWS::S3::Bucket
DependsOn:
- notificationTopicPolicy
Properties:
(...)
Problem:
Circular Dependencies for resource s3Bucket. Circular dependency with [notificationTopicPolicy]
There is reference to s3Bucket in notificationTopicPolicy definition, and "DependsOn" attribute forces cloudformation to create notificationTopicPolicy before S3 bucket.
Solution:
Solution is simple, just replace "!GetAtt s3Bucket.Arn" with generated ARN of S3 bucket. You can do it cause SNS doesn't check if S3 bucket already exists.
Problem solved. Resources are created in right order, S3 events for bucket are enabled...
but topic and topic policy are created each time even if it's not required.
Circular Dependencies for resource s3Bucket. Circular dependency with [notificationTopicPolicy]
There is reference to s3Bucket in notificationTopicPolicy definition, and "DependsOn" attribute forces cloudformation to create notificationTopicPolicy before S3 bucket.
Solution:
Solution is simple, just replace "!GetAtt s3Bucket.Arn" with generated ARN of S3 bucket. You can do it cause SNS doesn't check if S3 bucket already exists.
notificationTopicPolicy:
(...)
Condition:
ArnLike:
aws:SourceArn: !Sub arn:aws:s3:*:*:${projectName}-${bucketName}-${stage}
(...)
Problem solved. Resources are created in right order, S3 events for bucket are enabled...
but topic and topic policy are created each time even if it's not required.
Version 3 - conditions
We have to provide subscriber ARN and this can be done with template parameters. We also can use conditions to determine if subscribers ARNs are provided.Parameters:
(...)
sqsNotificationsArns:
Default: None,None
Type: CommaDelimitedList
Description: Comma delimited list of SQS ARNs to subscribe to SNS Topic (up to 2)
lambdaNotificationsArns:
Default: None,None
Type: CommaDelimitedList
Description: Comma delimited list of SQS ARNs to subscribe to SNS Topic (up to 2)
Conditions:
sqs1NotificationsArnsProvided:
!Not [!Equals [ !Select [ "0", !Ref sqsNotificationsArns], "None"]]
sqs2NotificationsArnsProvided:
!Not [!Equals [ !Select [ "1", !Ref sqsNotificationsArns], "None"]]
lambda1NotificationsArnsProvided:
!Not [!Equals [ !Select [ "0", !Ref lambdaNotificationsArns], "None"]]
lambda2NotificationsArnsProvided:
!Not [!Equals [ !Select [ "1", !Ref lambdaNotificationsArns], "None"]]
enableNotifications:
!Or
- Condition: sqs1NotificationsArnsProvided
- Condition: sqs2NotificationsArnsProvided
- Condition: lambda1NotificationsArnsProvided
- Condition: lambda2NotificationsArnsProvided
How does it work?
If no sqsNotificationsArns or lambdaNotificationsArns are provided, the defaults are used ("None","None"). Conditions check if any element of sqsNotificationsArns or lambdaNotificationsArns is different then default value (none). If it's different, then condition returns "True", otherwise "False"
Parameters:
notificationTopic:
Type: AWS::SNS::Topic
Condition: enableNotifications
Properties:
(...)
notificationTopicPolicy:
Type: AWS::SNS::TopicPolicy
Condition: enableNotifications
(...)Resources:
s3Bucket:
Type: AWS::S3::Bucket
DependsOn:
- notificationTopicPolicy
Properties:
NotificationConfiguration:
!If
- enableNotifications
- TopicConfigurations:
-
Event: s3:ObjectCreated:*
Filter:
S3Key:
Rules:
- Name: suffix
Value: .json
Topic: !Ref notificationTopic
-
Event: s3:ObjectRemoved:*
Filter:
S3Key:
Rules:
- Name: suffix
Value: .json
Topic: !Ref notificationTopic
- !Ref AWS::NoValue
Problem:
When sqsNotificationsArns or lambdaNotificationsArns are provided conditions work as expected. notificationTopicPolicy, notificationTopic and s3Bucket are created in right order, but
when no subscribers ARNs are provided, we encounter following error:
An error occurred (ValidationError) when calling the CreateStack operation: Template format error: Unresolved resource dependencies [notificationTopic, notificationTopicPolicy] in the Resources block of the template
Solution:
There is no simple solution. Cloudformation doesn't support conditions in "DependsOn" attribute. We need to bond s3Bucket with notificationTopicPolicy but with enableNotifications condition.
Version 4 - final
I've spent a while figuring out working solution, and finally did it in v4.template.
Quick look at AWS::SNS::TopicPolicy documentation and no return values, but I tried to use "!Ref notificationTopicPolicy" in s3Bucket Policy TAG. This should provide required dependency between notificationTopicPolicy and s3Bucket and conditions can be used in TAG value.
In fact Policy returns some kind of ID (i.e. notifications-notificationTopicPolicy-ANKFGG0LEMPM)
In fact Policy returns some kind of ID (i.e. notifications-notificationTopicPolicy-ANKFGG0LEMPM)
So lets remove "DependsOn" from s3Bucket definition
DependsOn:
- notificationTopicPolicy
and add TAG with value related to notificationTopicPolicy or just "No Policy" if subscribers ARNs wasn't provided. Tags:
- Key: Project
Value: !Ref projectName
- Key: Stage
Value: !Ref stage
- Key: Policy
Value:
!If
- enableNotifications
- !Ref notificationTopicPolicy
- 'No Policy'
This way all requirements are fulfilled. If subscribers ARNs are provided, notificationTopic and notificationTopicPolicy are created and S3 events are enabled. If no, nothing related to notifications is created.
Also stack updates are supported. So you can create S3 bucket first and add/remove notifications later.
Of course AWS::SNS::Subscription is missing but I'm sure you can handle it
Final template:
AWSTemplateFormatVersion: 2010-09-09
Description: |
SNS Notifications for S3 events and CloudWatch Rules
Parameters:
stage:
Default: dev
Type: String
Description: Stage Name
bucketName:
Default: data
Type: String
Description: Consumable bucket name prefix
projectName:
Default: s3-notification-example
Type: String
Description: Project Name SSM Parameter
sqsNotificationsArns:
Default: None,None
Type: CommaDelimitedList
Description: Comma delimited list of SQS ARNs to subscribe to SNS Topic (up to 2)
lambdaNotificationsArns:
Default: None,None
Type: CommaDelimitedList
Description: Comma delimited list of SQS ARNs to subscribe to SNS Topic (up to 2)
Conditions:
sqs1NotificationsArnsProvided:
!Not [!Equals [ !Select [ "0", !Ref sqsNotificationsArns], "None"]]
sqs2NotificationsArnsProvided:
!Not [!Equals [ !Select [ "1", !Ref sqsNotificationsArns], "None"]]
lambda1NotificationsArnsProvided:
!Not [!Equals [ !Select [ "0", !Ref lambdaNotificationsArns], "None"]]
lambda2NotificationsArnsProvided:
!Not [!Equals [ !Select [ "1", !Ref lambdaNotificationsArns], "None"]]
enableNotifications:
!Or
- Condition: sqs1NotificationsArnsProvided
- Condition: sqs2NotificationsArnsProvided
- Condition: lambda1NotificationsArnsProvided
- Condition: lambda2NotificationsArnsProvided
Resources:
s3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${projectName}-${bucketName}-${stage}
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
NotificationConfiguration:
!If
- enableNotifications
- TopicConfigurations:
-
Event: s3:ObjectCreated:*
Filter:
S3Key:
Rules:
- Name: suffix
Value: .json
Topic: !Ref notificationTopic
-
Event: s3:ObjectRemoved:*
Filter:
S3Key:
Rules:
- Name: suffix
Value: .json
Topic: !Ref notificationTopic
- !Ref AWS::NoValue
Tags:
- Key: Project
Value: !Ref projectName
- Key: Stage
Value: !Ref stage
- Key: Policy
Value:
!If
- enableNotifications
- !Ref notificationTopicPolicy
- 'No Policy'
notificationTopic:
Type: AWS::SNS::Topic
Condition: enableNotifications
Properties:
DisplayName: !Sub ${projectName}-notifications-${stage}
Tags:
- Key: Project
Value: !Ref projectName
- Key: Stage
Value: !Ref stage
TopicName: !Sub ${projectName}-notifications-${stage}
notificationTopicPolicy:
Type: AWS::SNS::TopicPolicy
Condition: enableNotifications
Properties:
PolicyDocument:
Version: '2012-10-17'
Id: Policy
Statement:
- Sid: Default
Effect: Allow
Principal:
AWS: "*"
Action:
- SNS:GetTopicAttributes
- SNS:SetTopicAttributes
- SNS:AddPermission
- SNS:RemovePermission
- SNS:DeleteTopic
- SNS:Subscribe
- SNS:ListSubscriptionsByTopic
- SNS:Publish
- SNS:Receive
Resource: !Ref notificationTopic
Condition:
StringEquals:
AWS:SourceOwner: !Ref AWS::AccountId
- Sid: CloudWatchEvents
Effect: Allow
Principal:
Service: events.amazonaws.com
Action: sns:Publish
Resource: !Ref notificationTopic
- Sid: S3
Effect: Allow
Principal:
AWS: "*"
Action:
- sns:Publish
Resource: !Ref notificationTopic
Condition:
ArnLike:
aws:SourceArn: !Sub arn:aws:s3:*:*:${projectName}-${bucketName}-${stage}
StringEquals:
aws:SourceAccount: !Ref AWS::AccountId
Topics:
- !Ref notificationTopic
Example deployment with update:

Komentarze
Prześlij komentarz