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:
-
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.
-
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 outputsEach nested template will need
Outputssections to expose the IDs or ARNs of created resources that other stacks might depend on. For example,vpc-template.yamlwould have:Outputs: VPCId: Description: The VPC ID Value: !Ref VPC InternetGatewayId: Description: The Internet Gateway ID Value: !Ref InternetGatewayYou upload each of these smaller YAML files to an S3 bucket and provide the S3 URL in the
TemplateURLproperty of theAWS::CloudFormation::Stackresource. 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.