I was refactoring a CloudFormation template to add security group ingress rules, when suddenly the stack creation failed with a cryptic error about circular dependencies. Resource A needed Resource B, but Resource B also needed Resource A, creating an impossible situation. CloudFormation couldn’t figure out which resource to create first. In this post, I’ll walk through exactly what causes this and how to fix it.
The Problem
CloudFormation fails to create or update a stack with an error message like:
Circular dependency between resources: [SecurityGroupA, SecurityGroupB]
Template error: instance of Fn::GetAtt references undefined resource Resources/MyLambda
The stack creation aborts immediately, and no resources are created. The circular dependency prevents CloudFormation from determining the correct creation order.
| Error Type | Description |
|---|---|
| Circular dependency detected | Resource A depends on B, B depends on A (direct cycle) |
| Unresolvable circular reference | Indirect cycle: A→B→C→A |
| Invalid template reference | Resource references itself via Fn::GetAtt or DependsOn |
Why Does This Happen?
Circular dependencies occur when two or more resources reference each other, either directly or indirectly. CloudFormation builds a directed graph of resource dependencies to determine creation order. If the graph contains a cycle, CloudFormation cannot find a valid order to create the resources.
Common patterns:
- Security group self-referencing — SecurityGroup A has an ingress rule referencing SecurityGroup B, and SecurityGroup B has an ingress rule referencing SecurityGroup A. Each rule requires the other group to exist first.
- Lambda and IAM role — Lambda function references the IAM role’s ARN in its environment variables, and the IAM role’s inline policy references the Lambda function’s ARN. The Lambda can’t exist without the role, and the role can’t have a valid policy without the Lambda.
- VPC endpoint and security group — The VPC endpoint references a security group, and the security group has a rule referencing the VPC endpoint’s network interface.
The Fix
Option 1: Break Self-Referencing Security Groups
Instead of having both security groups reference each other directly, use a separate AWS::EC2::SecurityGroupIngress resource to add rules after both groups are created:
Resources:
SecurityGroupA:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "Security Group A"
VpcId: !Ref MyVpc
SecurityGroupB:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "Security Group B"
VpcId: !Ref MyVpc
# Add ingress rule after both groups exist
SecurityGroupAIngressRule:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref SecurityGroupA
IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref SecurityGroupB
SecurityGroupBIngressRule:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref SecurityGroupB
IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref SecurityGroupA
Option 2: Break Lambda and IAM Role Circular Dependency
Create the IAM role first without referencing the Lambda ARN, then attach a managed policy or separate inline policy with the Lambda ARN:
Resources:
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
MyLambda:
Type: AWS::Lambda::Function
Properties:
Runtime: python3.11
Handler: index.handler
Role: !GetAtt LambdaExecutionRole.Arn
Code:
ZipFile: |
def handler(event, context):
return {"statusCode": 200}
# Separate policy that references the Lambda ARN
LambdaPolicy:
Type: AWS::IAM::RolePolicy
Properties:
RoleName: !Ref LambdaExecutionRole
PolicyName: LambdaPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: lambda:InvokeFunction
Resource: !GetAtt MyLambda.Arn
Option 3: Use DependsOn for Explicit Ordering
If you can’t restructure the resources, explicitly tell CloudFormation the order using DependsOn:
Resources:
ResourceA:
Type: AWS::S3::Bucket
DependsOn: ResourceB
ResourceB:
Type: AWS::S3::Bucket
Properties: {}
However, this only works if there’s no actual circular reference—just a missing dependency declaration.
How to Run This
- Identify the circular dependency by reviewing error messages or using
aws cloudformation validate-template --template-body file://template.yaml - Locate the resources involved in the cycle (often security groups or IAM/Lambda combinations)
- Refactor to use separate resources (
AWS::EC2::SecurityGroupIngress, separate policies) instead of inline references - Validate the template:
aws cloudformation validate-template --template-body file://template.yaml - Create the stack:
aws cloudformation create-stack --stack-name my-stack --template-body file://template.yaml
Is This Safe?
Yes. Breaking circular dependencies makes your template more modular and maintainable. Using separate resources for rules and policies is CloudFormation best practice.
Key Takeaway
Circular dependencies in CloudFormation occur when resources reference each other, making creation order impossible. Break the cycle by separating inline rules and policies into standalone resources, or by reordering creation using explicit DependsOn declarations.
Have questions or ran into a different CloudFormation issue? Connect with me on LinkedIn or X.