The "Chicken and Egg" problem when connecting S3 Buckets to Lambda Triggers.
If you have ever tried to build an Event-Driven architecture with AWS SAM, you have likely seen this error message. It usually appears 5 minutes into a deployment, just when you think everything is working:
Error: Failed to create changeset for the stack: s3entinel-stack
Status: FAILED. Reason: Circular dependency between resources:
[ImageProcessorFunctionRole, ImageProcessorFunction, S3entinelBucket, ImageProcessorFunctionFileUploadPermission]
I hit this wall while building S3entinel, a Spring Boot + Serverless hybrid application. Here is exactly why this happens and the simple Sub-trick I used to fix it.
The Architecture
The goal was simple:
S3 Bucket (S3entinelBucket) receives a file.
Lambda Function (ImageProcessorFunction) gets triggered automatically.
Lambda reads the file metadata and logs it.
To make this work, two things must happen in the infrastructure:
The Bucket needs to know the Lambda's ARN to send the notification.
The Lambda needs permission (IAM Policy) to read from The Bucket.
The Problem: Chicken vs. Egg
In CloudFormation (the engine underneath SAM), resources are created in a specific dependency order.
- To create the Bucket Notification, AWS needs the Lambda Function to exist first.
- To create the Lambda Function, AWS needs to create its IAM Role.
- If you reference the Bucket inside that IAM Role (to give it Read permissions), the Role now waits for the Bucket.
The Cycle:
Bucket waits for Lambda → Lambda waits for Role → Role waits for Bucket.
The "Bad" Code (What Causes the Loop)
Here is the intuitive (but wrong) way to write the template.yaml. Notice how I tried to rely on !Ref S3entinelBucket inside the function's policy.
In YAML file:
Resources:
S3entinelBucket:
Type: AWS::S3::Bucket
ImageProcessorFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .
Handler: com.codebymathabo.s3entinel.lambda.ImageProcessor::handleRequest
Events:
FileUpload:
Type: S3
Properties:
Bucket: !Ref S3entinelBucket # Lambda needs Bucket for Event
Events: s3:ObjectCreated:*
Policies:
- S3ReadPolicy:
BucketName: !Ref S3entinelBucket # Role needs Bucket for Permissions
This creates the deadlock. CloudFormation cannot resolve the dependency graph.
The Fix: Break the Chain with Strings
To solve this, we must remove one of the hard dependencies. We can’t change the Event trigger (that requires a real resource reference), but we can cheat on the IAM Policy.
Instead of asking CloudFormation to "Wait for the Bucket to exist and then give me its name" (!Ref), we can construct the bucket name manually using a deterministic naming pattern.
In AWS, if you know the Region and Account ID, you can predict the bucket name before it is even created.
The "Good" Code
Here is the fix I implemented in S3entinel. I replaced the !Ref in the Policy section with a !Sub string interpolation.
In YAML file:
Resources:
# Define the Bucket Name explicitly using ID and Region
S3entinelBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub 's3entinel-iac-${AWS::Region}-${AWS::AccountId}'
ImageProcessorFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .
Handler: com.codebymathabo.s3entinel.lambda.ImageProcessor::handleRequest
# Reconstruct the name string manually.
# This grants permission to "what the bucket WILL be named"
# without forcing the IAM Role to wait for the Bucket resource.
Policies:
- S3ReadPolicy:
BucketName: !Sub 's3entinel-iac-${AWS::Region}-${AWS::AccountId}'
Events:
FileUpload:
Type: S3
Properties:
# Keep the hard link for the trigger
Bucket: !Ref S3entinelBucket
Events: s3:ObjectCreated:*
Why This Works
By using !Sub 's3entinel-iac-${AWS::Region}-${AWS::AccountId}', we are giving the IAM Role a string of text, not a resource dependency.
CloudFormation creates the IAM Role immediately (it only needs the string, not the bucket).
It creates the Lambda Function using that Role.
Finally, it creates the S3 Bucket (which depends on the Lambda for the notification trigger).
The loop is broken, the stack deploys, and the Circular Dependency error disappears.
Code Reference:
Top comments (0)