CloudFormation templates can’t grow infinitely; they’re capped at 51,200 bytes for the JSON or YAML content.

Let’s see what that looks like in practice. Imagine you’re deploying a moderately complex VPC with a few subnets, an internet gateway, a NAT gateway, and some security groups.

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: MyVPC

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: MyInternetGateway

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: us-east-1a
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: PublicSubnet1

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.2.0/24
      AvailabilityZone: us-east-1b
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: PublicSubnet2

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.3.0/24
      AvailabilityZone: us-east-1a
      Tags:
        - Key: Name
          Value: PrivateSubnet1

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.4.0/24
      AvailabilityZone: us-east-1b
      Tags:
        - Key: Name
          Value: PrivateSubnet2

  NatGatewayEip:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

  NatGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGatewayEip.AllocationId
      SubnetId: !Ref PublicSubnet1
      Tags:
        - Key: Name
          Value: MyNatGateway

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: PublicRouteTable

  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  PublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: PrivateRouteTable

  PrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway

  PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateRouteTable

  PublicSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VPC
      GroupName: PublicSG
      Description: Allow SSH and HTTP/S
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: PublicSecurityGroup

  PrivateSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VPC
      GroupName: PrivateSG
      Description: Allow all from within VPC
      SecurityGroupIngress:
        - IpProtocol: -1
          CidrIp: 10.0.0.0/16
      Tags:
        - Key: Name
          Value: PrivateSecurityGroup

This template is already quite substantial. As you add more resources, more complex configurations, or even just more tags, you’ll quickly bump against that 51,200-byte limit. When that happens, you need strategies to break down your monolithic template.

The most straightforward approach is template nesting, also known as using nested stacks. You split your large template into smaller, logical units, and then create a parent template that references these nested templates.

Here’s how you’d restructure the above VPC example:

  1. Create individual templates for logical components:

    • vpc-template.yaml: Defines the VPC, Internet Gateway, and VPC Gateway Attachment.
    • subnet-template.yaml: Defines the public and private subnets.
    • nat-gateway-template.yaml: Defines the EIP and NAT Gateway.
    • route-tables-template.yaml: Defines the public and private route tables and their associations.
    • security-groups-template.yaml: Defines the security groups.
  2. Create a parent template (main-template.yaml) to orchestrate them:

    AWSTemplateFormatVersion: '2010-09-09'
    Description: Parent template for VPC deployment
    
    Resources:
      VPCStack:
        Type: AWS::CloudFormation::Stack
        Properties:
          TemplateURL: https://your-s3-bucket.s3.amazonaws.com/vpc-template.yaml
          Parameters:
            # Pass any parameters needed by vpc-template.yaml
            VpcCidrBlock: 10.0.0.0/16
    
      SubnetsStack:
        Type: AWS::CloudFormation::Stack
        Properties:
          TemplateURL: https://your-s3-bucket.s3.amazonaws.com/subnet-template.yaml
          Parameters:
            VpcId: !GetAtt VPCStack.Outputs.VPCId # Reference output from VPCStack
            PublicSubnetCidr1: 10.0.1.0/24
            PublicSubnetCidr2: 10.0.2.0/24
            PrivateSubnetCidr1: 10.0.3.0/24
            PrivateSubnetCidr2: 10.0.4.0/24
            AvailabilityZone1: us-east-1a
            AvailabilityZone2: us-east-1b
    
      NatGatewayStack:
        Type: AWS::CloudFormation::Stack
        Properties:
          TemplateURL: https://your-s3-bucket.s3.amazonaws.com/nat-gateway-template.yaml
          Parameters:
            VpcId: !GetAtt VPCStack.Outputs.VPCId
            PublicSubnetId: !GetAtt SubnetsStack.Outputs.PublicSubnet1Id # Reference output from SubnetsStack
    
      RouteTablesStack:
        Type: AWS::CloudFormation::Stack
        Properties:
          TemplateURL: https://your-s3-bucket.s3.amazonaws.com/route-tables-template.yaml
          Parameters:
            VpcId: !GetAtt VPCStack.Outputs.VPCId
            InternetGatewayId: !GetAtt VPCStack.Outputs.InternetGatewayId
            NatGatewayId: !GetAtt NatGatewayStack.Outputs.NatGatewayId
            PublicSubnet1Id: !GetAtt SubnetsStack.Outputs.PublicSubnet1Id
            PublicSubnet2Id: !GetAtt SubnetsStack.Outputs.PublicSubnet2Id
            PrivateSubnet1Id: !GetAtt SubnetsStack.Outputs.PrivateSubnet1Id
            PrivateSubnet2Id: !GetAtt SubnetsStack.Outputs.PrivateSubnet2Id
    
      SecurityGroupsStack:
        Type: AWS::CloudFormation::Stack
        Properties:
          TemplateURL: https://your-s3-bucket.s3.amazonaws.com/security-groups-template.yaml
          Parameters:
            VpcId: !GetAtt VPCStack.Outputs.VPCId
            VpcCidrBlock: 10.0.0.0/16 # For private SG ingress
    
    Outputs:
      VPCId:
        Description: The VPC ID
        Value: !GetAtt VPCStack.Outputs.VPCId
      PublicSubnet1Id:
        Description: The ID of the first public subnet
        Value: !GetAtt SubnetsStack.Outputs.PublicSubnet1Id
      # ... other outputs
    

    Each nested template will need Outputs sections to expose the IDs or ARNs of created resources that other stacks might depend on. For example, vpc-template.yaml would have:

    Outputs:
      VPCId:
        Description: The VPC ID
        Value: !Ref VPC
      InternetGatewayId:
        Description: The Internet Gateway ID
        Value: !Ref InternetGateway
    

    You upload each of these smaller YAML files to an S3 bucket and provide the S3 URL in the TemplateURL property of the AWS::CloudFormation::Stack resource. CloudFormation then fetches and processes each template as a distinct stack, linked under the parent stack.

Another powerful technique is using CloudFormation macros. Macros allow you to transform your template before CloudFormation processes it. You can write a macro that, for example, takes a list of subnet definitions and expands it into individual AWS::EC2::Subnet resources. This keeps your source template concise while generating the necessary CloudFormation resources.

You define a macro using an AWS Lambda function. The Lambda function receives the template as JSON, performs transformations, and returns the modified template. To use it, you create an AWS::CloudFormation::Macro resource in your template.

AWSTemplateFormatVersion: '2010-09-09'
Description: Template using a macro to generate multiple resources

Resources:
  MySubnetGeneratorMacro:
    Type: AWS::CloudFormation::Macro
    Properties:
      Name: SubnetGenerator
      Description: Generates multiple subnets based on input parameters
      FunctionName: !GetAtt GenerateSubnetsLambda.Arn

  GenerateSubnetsLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: cloudformation-macro-subnet-generator
      Runtime: python3.9
      Handler: index.lambda_handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: |
          import json

          def lambda_handler(event, context):
              fragment = event['fragment']
              params = fragment['Parameters']
              resources = fragment['Resources']

              # Example: Generate 4 subnets based on a list parameter
              subnet_configs = params.get('SubnetConfigurations', {}).get('Default', [])

              for i, config in enumerate(subnet_configs):
                  subnet_name = f"GeneratedSubnet{i+1}"
                  resources[subnet_name] = {
                      "Type": "AWS::EC2::Subnet",
                      "Properties": {
                          "VpcId": {"Ref": "VPC"}, # Assuming VPC is defined elsewhere
                          "CidrBlock": config['CidrBlock'],
                          "AvailabilityZone": config['AvailabilityZone'],
                          "Tags": [
                              {"Key": "Name", "Value": subnet_name}
                          ]
                      }
                  }

              return {
                  "requestId": event['requestId'],
                  "status": "success",
                  "fragment": fragment
              }

  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: LambdaLoggingPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: arn:aws:*

  VPC: # Example VPC resource needed by the macro's output
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      Tags:
        - Key: Name
          Value: MyVPC

Parameters:
  SubnetConfigurations:
    Type: List
    Description: List of subnet configurations (CidrBlock, AvailabilityZone)
    Default:
      - CidrBlock: 10.0.1.0/24
        AvailabilityZone: us-east-1a
      - CidrBlock: 10.0.2.0/24
        AvailabilityZone: us-east-1b
      - CidrBlock: 10.0.3.0/24
        AvailabilityZone: us-east-1a
      - CidrBlock: 10.0.4.0/24
        AvailabilityZone: us-east-1b

Transform: AWS::LanguageExtensions # This enables macros

When CloudFormation processes this template, it first passes the fragment to the SubnetGenerator Lambda function. The Lambda function adds the GeneratedSubnetX resources to the fragment based on the SubnetConfigurations parameter. CloudFormation then continues processing the expanded template.

Using S3 for larger template files is another common pattern, especially when you have large, single-file templates that exceed the inline limit but don’t necessarily need to be broken into multiple logical stacks. You can upload your template to S3 and then create the stack by referencing the S3 URL instead of pasting the template content directly.

aws cloudformation create-stack \
    --stack-name my-large-template-stack \
    --template-url https://your-s3-bucket.s3.amazonaws.com/large-template.yaml \
    --parameters ParameterKey=MyParam,ParameterValue=MyValue

This works because CloudFormation reads the template from S3 at the time of creation. The template itself can be larger than the 51,200-byte limit for inline templates, as long as the S3 object itself is manageable. The practical limit here is more about the Lambda function limits for macros (if you use macros to generate parts of the template) and the overall complexity CloudFormation can handle rather than just the initial template size.

Finally, consider using the AWS Cloud Development Kit (CDK) or Terraform. These Infrastructure as Code tools abstract away CloudFormation’s limits. CDK, for instance, allows you to write your infrastructure in familiar programming languages (like Python, TypeScript, Java). It synthesizes CloudFormation templates behind the scenes. If a CDK construct would generate a template exceeding CloudFormation’s limits, the CDK can automatically handle splitting it into nested stacks or using other strategies to stay within the bounds.

The one thing that often trips people up with nested stacks is managing dependencies and outputs. If Stack B needs to reference a resource created in Stack A, Stack A must explicitly export that resource’s ID or ARN via an Output in its template. Then, in Stack B’s template, you use !GetAtt StackAResourceName.Outputs.ExportedOutputName to retrieve the value. Without these explicit outputs, the nested stacks become isolated, and you can’t wire them together correctly.

The next hurdle you’ll likely encounter is managing state and drift detection across many nested stacks.

Want structured learning?

Take the full Cloudformation course →