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

  1. Identify the circular dependency by reviewing error messages or using aws cloudformation validate-template --template-body file://template.yaml
  2. Locate the resources involved in the cycle (often security groups or IAM/Lambda combinations)
  3. Refactor to use separate resources (AWS::EC2::SecurityGroupIngress, separate policies) instead of inline references
  4. Validate the template: aws cloudformation validate-template --template-body file://template.yaml
  5. 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.