CloudFormation stacks can become unwieldy beasts, but breaking them into smaller, manageable nested stacks isn’t just about tidiness; it’s about enabling true modularity and parallel deployment.

Let’s see this in action. Imagine you have a single, massive CloudFormation template that defines your entire VPC, including subnets, internet gateways, NAT gateways, route tables, security groups, and EC2 instances. Deploying this takes ages, and if one small part fails, the whole thing rolls back.

Now, let’s break it down. We’ll create a "Parent" stack that orchestrates several "Child" stacks.

Parent Stack (parent-stack.yaml):

AWSTemplateFormatVersion: '2010-09-09'
Description: Parent stack to orchestrate nested infrastructure stacks.

Resources:
  VPCStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: s3://your-bucket-name/templates/vpc-stack.yaml
      Parameters:
        VPCCidrBlock: 10.0.0.0/16

  EC2Stack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: s3://your-bucket-name/templates/ec2-stack.yaml
      Parameters:
        InstanceType: t3.micro
        ImageId: ami-0abcdef1234567890 # Replace with a valid AMI ID
        VPCStackName: !Ref VPCStack # Pass the name of the VPC stack
        SubnetId: !GetAtt VPCStack.Outputs.PublicSubnetId # Get an output from the VPC stack

Outputs:
  EC2InstanceId:
    Description: The ID of the EC2 instance created by the EC2 stack.
    Value: !GetAtt EC2Stack.Outputs.InstanceId

Child Stack 1: VPC (vpc-stack.yaml):

AWSTemplateFormatVersion: '2010-09-09'
Description: Defines the VPC, subnets, and associated networking resources.

Parameters:
  VPCCidrBlock:
    Type: String
    Description: The CIDR block for the VPC.

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidrBlock
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: MyNestedVPC

  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: MyPublicSubnet

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

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

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

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

  SubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable

Outputs:
  VPCId:
    Description: The ID of the VPC.
    Value: !Ref VPC
  PublicSubnetId:
    Description: The ID of the public subnet.
    Value: !Ref PublicSubnet

Child Stack 2: EC2 (ec2-stack.yaml):

AWSTemplateFormatVersion: '2010-09-09'
Description: Defines an EC2 instance within a specified VPC and subnet.

Parameters:
  InstanceType:
    Type: String
    Description: The EC2 instance type.
  ImageId:
    Type: String
    Description: The AMI ID for the EC2 instance.
  VPCStackName:
    Type: String
    Description: The name of the CloudFormation stack that created the VPC.
  SubnetId:
    Type: AWS::EC2::Subnet::Id
    Description: The ID of the subnet to launch the instance into.

Resources:
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      ImageId: !Ref ImageId
      SubnetId: !Ref SubnetId
      Tags:
        - Key: Name
          Value: MyNestedInstance

Outputs:
  InstanceId:
    Description: The ID of the EC2 instance.
    Value: !Ref EC2Instance

To deploy this, you’d upload vpc-stack.yaml and ec2-stack.yaml to an S3 bucket (e.g., s3://your-bucket-name/templates/). Then, you’d create the parent-stack.yaml and deploy it via the AWS Console or AWS CLI.

When you deploy the parent stack, CloudFormation reads the TemplateURL for each nested stack and provisions them. Crucially, it understands dependencies: if EC2Stack needs information from VPCStack (like SubnetId), it uses !GetAtt VPCStack.Outputs.PublicSubnetId to retrieve that output. The VPCStackName: !Ref VPCStack parameter is a bit of a workaround; !Ref on a AWS::CloudFormation::Stack resource returns the stack name, which we can then pass to a child stack if it needs to reference its parent’s name for tagging or other logic.

The real power here is how CloudFormation manages the lifecycle. If vpc-stack.yaml fails, only the VPC resources are rolled back. The ec2-stack.yaml won’t even attempt to deploy. If ec2-stack.yaml fails after vpc-stack.yaml succeeds, only the EC2 instance is rolled back, leaving your VPC intact. This drastically reduces the blast radius of failures and speeds up deployments because related, independent stacks can be provisioned in parallel.

You can also reuse these child stacks across multiple parent stacks, promoting a DRY (Don’t Repeat Yourself) approach to infrastructure. For example, you could have a database-stack.yaml that’s included in multiple application stacks, all referencing a single, shared network-stack.yaml.

The one thing most people don’t realize is that the TemplateURL can also point to templates stored locally on your machine if you’re using the AWS CLI with the --template-body parameter, but for production, S3 is the standard and recommended approach. This allows you to keep your templates versioned and accessible.

The next logical step is to explore how to handle parameter and output passing between more deeply nested stacks, creating a hierarchical structure for complex environments.

Want structured learning?

Take the full Cloudformation course →