S3 bucket notification to Lambda in CloudFormation without circular reference
Today I need to create a Lambda which subscribes to S3 bucket notification (create object event) directly. It is such a common scenario. It sounds trivial but actually NOT.
The steps are supposed to be:
- Create S3 bucket (
AWS::S3::Bucket
) - Create Lambda (
AWS::Lambda:Function
) - Allow S3 to invoke Lambda (
AWS::Lambda::Permission
) - Allow Lambda to read from S3 (
AWS::IAM:Role
) - Subscribe Lambda to S3 bucket notification
The actual issue is from step 5. There is NO separate AWS resource for S3 bucket notification. The LambdaConfiguration is part of S3 bucket Notification Config under Bucket resource definition which means it need to be part of step 1.
The official CloudFormation Documentation solved this issue by suggesting create all the resources first without specifying the notification configuration (step 5). Then, update the stack with a notification configuration. This blog post did something similar by using aws cli to specify the notification configuration.
But here I will provide a different solution
TL;DR: we will construct the S3 ARN before creating the bucket!
Detail steps:
- Create Lambda (
AWS::Lambda:Function
) - Allow S3 to invoke Lambda (
AWS::Lambda::Permission
): refer SourceArn by constructing S3 ARN - Allow Lambda to read from S3 (
AWS::IAM:Role
): refer S3 resource by constructing S3 ARN - Create S3 bucket (
AWS::S3::Bucket
) with LambdaConfguration section.
CloudFormation Example for reference:
AWSTemplateFormatVersion: '2010-09-09'
Description: Example Stack
Parameters:
BucketName:
Type: String
Default: unique-bucket-name
Resources:
Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref BucketName
...
NotificationConfiguration:
LambdaConfigurations:
- Event: 's3:ObjectCreated:*'
Filter:
S3Key:
Rules:
- Name: prefix
Value: test/
- Name: suffix
Value: .txt
Function: !GetAtt Lambda.Arn
Lambda:
Type: AWS::Lambda::Function
...
S3InvokeLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref Lambda
Principal: s3.amazonaws.com
SourceArn: !Sub arn:aws:s3:::${BucketName}
LambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action:
- sts:AssumeRole
Path: '/'
Policies:
- PolicyName: s3
PolicyDocument:
Statement:
- Effect: Allow
Action:
- s3:Get*
Resource:
- !Sub arn:aws:s3:::${BucketName}
- !Sub arn:aws:s3:::${BucketName}/*
You may ask why not create S3 Bucket first with constructed Lambda ARN? Because it is much easier to construct S3 ARN than Lambda ARN!