A guide to Fn::Cidr, subnet planning, and CIDR migration risks in AWS CloudFormation
Infrastructure-as-Code is powerful until a one-line networking change silently deletes your production database. Network configuration in AWS is one of those decisions that is easy to get wrong and very hard to fix later. Subnet CIDR blocks are immutable once created. You cannot resize them. You cannot shift them. The only way to change them is to delete and recreate, and that cascades to everything attached.
This guide covers two things: how to plan your CIDR blocks correctly from the start using CloudFormation's Fn::Cidr intrinsic function, and what to be aware of when changing them in an existing stack with live databases.
To make this practical, we will follow a real PR review scenario, in which a teammate replaced a hardcoded subnet CIDR with a dynamic Fn::Cidr expression. The intent was right, but it raised important questions: Does it produce the same IP range? And what would happen if this ran against a live Aurora cluster?
1. The Problem With Hardcoded CIDRs
The most common way to define a subnet CIDR in CloudFormation is to hardcode it:
DatabaseSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: "172.32.0.0/26" # hardcoded
This works fine for a single environment. But the moment you need to deploy the same template to staging, production, or a different region, every environment needs a different CIDR to avoid overlap, which means either duplicating templates or manually overriding parameters.
Hardcoded CIDRs also make it easy to introduce mistakes:
- Two subnets accidentally assigned the same CIDR block
- Subnet CIDRs that fall outside the VPC CIDR range
- No room for future subnets because the VPC space was carved up too aggressively
- Overlapping CIDRs when peering with another VPC or on-premises network
The fix is Fn::Cidr, a CloudFormation intrinsic function that dynamically generates subnet CIDRs from a parent VPC CIDR at deploy time.
2. Fn::Cidr - How It Actually Works
Fn::Cidr splits a parent CIDR block into a list of equally sized subnets. You pick one (or more) from that list using Fn::Select.
Syntax:
# YAML full form
Fn::Cidr:
- ipBlock # Parent VPC CIDR e.g. 172.32.0.0/24
- count # How many subnets to generate (1 - 256)
- cidrBits # Host bits per subnet (controls subnet size)
# Short form
!Cidr [ ipBlock, count, cidrBits ]
# Picking the first subnet from the list
CidrBlock: !Select [ 0, !Cidr [ !Ref VpcCidr, 4, 6 ] ]
Understanding cidrBits - The Most Misunderstood Parameter:
cidrBits is not the subnet prefix number. It is the number of host bits, the free bits at the end of the address. The formula is:
subnet prefix = 32 - cidrBits
So, cidrBits: 6 means 6 host bits, giving a /26 subnet (32 - 6 = 26). Specifying a value of 8 for this parameter will create a CIDR with a mask of /24 (32 - 8 = 24).
| cidrBits | Formula | Subnet Prefix | Total IPs | Usable IPs* |
| 4 | 32 - 4 | /28 | 16 | 11 |
| 5 | 32 - 5 | /27 | 32 | 27 |
| 6 | 32 - 6 | /26 | 64 | 59 |
| 7 | 32 - 7 | /25 | 128 | 123 |
| 8 | 32 - 8 | /24 | 256 | 251 |
| 12 | 32 - 12 | /20 | 4096 | 4091 |
*AWS reserves the first 4 IP addresses and the last IP address in every subnet (Network address, VPC router, DNS, future use, and Network broadcast address). A /26 gives 64 - 5 = 59 usable IPs, not 62.
Worked Example: The PR Change
Given VpcCidr = 172.32.0.0/24 and the expression !Cidr ["172.32.0.0/24", 4, 6]:
- cidrBits = 6 → subnet prefix = 32 - 6 = /26 → 64 IPs each
- count = 4 → generates 4 subnets
- 4 x 64 = 256 IPs total = exactly fills the /24 parent
| Index | CIDR | IP Range | Usable IPs |
| 0 | 172.32.0.0/26 | .0 - .63 | 59 |
| 1 | 172.32.0.64/26 | .64 - .127 | 59 |
| 2 | 172.32.0.128/26 | .128 - .191 | 59 |
| 3 | 172.32.0.192/26 | .192 - .255 | 59 |
!Select [0, ...] picks index 0 –>172.32.0.0/26. The code is correct. The only issue in the PR was an imprecise comment using an X placeholder instead of the actual resolved value.
3. Planning CIDR for a New Stack
If you are starting fresh, getting the CIDR plan right upfront saves you from painful migrations later. Here is a reference structured approach.
Step 1: Size Your VPC First
Your VPC CIDR must be large enough to fit all subnets across all Availability Zones, plus room to grow. AWS supports VPC CIDRs from /16 (65,536 IPs) down to /28 (16 IPs).
| Workload | VPC CIDR | Suggested Split | Approx. Usable IPs |
| Dev / Sandbox | /24 | 4 x /26 subnets | ~236 |
| Staging | /22 | 8 x /25 subnets | ~1,000 |
| Production | /20 | 16 x /24 subnets | ~4,000 |
| Enterprise | /16 | 256 x /24 subnets | ~65,000 |
Step 2: Leave Room for Growth
The AWS Well-Architected Reliability Pillar (REL02-BP03) explicitly warns against filling your entire VPC CIDR at deploy time. Subnet CIDRs cannot be changed after creation, only deleted and recreated, which triggers resource replacement and potential data loss.
- Never carve your entire VPC CIDR into subnets at initial deploy
- Leave at least 20-30% of the VPC address space unallocated
- Plan for multi-AZ from the start: 3 AZs = at minimum 6 subnets (public + private per AZ)
- EMR clusters, Spot Fleets, and Redshift clusters can consume IPs in bursts
Step 3: Use Fn::Cidr for Reusable Templates
With a parameter-driven VpcCidr and Fn::Cidr, the same template deploys correctly to any environment by changing one value:
Parameters:
VpcCidr:
Type: String
Default: "172.32.0.0/22"
Description: "VPC CIDR block (must be unique per environment)"
Resources:
PublicSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Select [0, !Cidr [!Ref VpcCidr, 8, 6]] # /26
AvailabilityZone: !Select [0, !GetAZs ""]
PublicSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Select [1, !Cidr [!Ref VpcCidr, 8, 6]] # /26
AvailabilityZone: !Select [1, !GetAZs ""]
DatabaseSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Select [4, !Cidr [!Ref VpcCidr, 8, 6]] # /26
AvailabilityZone: !Select [0, !GetAZs ""]
DatabaseSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Select [5, !Cidr [!Ref VpcCidr, 8, 6]] # /26
AvailabilityZone: !Select [1, !GetAZs ""]
Tip: Use count: 8 even if you only need 4 subnets today. This reserves index slots consistently and prevents CIDR collisions when you add subnets later. Indices 0-3 for public, 4-7 for private/database is a clean convention.
4. Changing CIDRs in an Existing Stack (The Danger Zone)
Updating a subnet's CidrBlock in CloudFormation is not an in-place modification. It is a replacement operation. CloudFormation creates a new subnet and deletes the old one. Everything attached to that subnet is affected.
Replacement Cascade: What Gets Affected
| Resource | Impact |
| AWS::EC2::Subnet (CidrBlock) | Subnet deleted and recreated |
| AWS::RDS::DBInstance | DB deleted — automated snapshots also deleted |
| AWS::RDS::DBCluster (Aurora) | Cluster deleted if subnet group changes |
| AWS::EC2::Instance | Instance terminated, instance store data lost |
| AWS::EC2::NetworkInterface (ENI) | Private IP changes, existing connections drop |
Warning: If RDS or Aurora is replaced during a stack update, CloudFormation deletes ALL automated snapshots. Without a manual snapshot or UpdateReplacePolicy: Snapshot set on your DBCluster, your data is permanently lost.
Always Run a Change Set First
Before applying any networking change to an existing stack, run a CloudFormation Change Set to preview exactly which resources will be replaced. Below is an example using the AWS SAM commands.
# Build and deploy with --no-execute-changeset to preview changes only
sam build && sam deploy \
--stack-name my-vpc-stack \
--profile my-aws-profile \
--no-execute-changeset \
--region us-east-1 \
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \
--parameter-overrides VpcCidr=172.32.0.0/24
# Review the change set in AWS Console or via CLI
aws cloudformation describe-change-set \
--stack-name my-vpc-stack \
--change-set-name <change-set-name-from-sam-output>
In the output, look for any resource showing "Replacement": "True". If your DBCluster or DBInstance appears in that list, stop and do not apply the change set until you have added the following protection attributes to your template.
DatabaseCluster:
Type: AWS::RDS::DBCluster
DeletionPolicy: Snapshot # Snapshot before stack delete or resource removal
UpdateReplacePolicy: Snapshot # Critical: snapshot before replacement during stack update
Properties:
DeletionProtection: true # Hard stop at DB level — blocks deletion regardless of CF
Engine: aurora-postgresql
DBSubnetGroupName: !Ref DBSubnetGroup
...
```
DeletionPolicy and UpdateReplacePolicy are resource-level attributes, not Properties. They sit at the same indentation level as Type and Properties. DeletionProtection is the only one that lives inside Properties.
DeletionProtection: true is enforced at the RDS API level, not the CloudFormation level. When CloudFormation attempts to delete the cluster for any reason, the RDS API rejects the call outright, and CloudFormation rolls back with an error; the database is never touched. Unlike DeletionPolicy and UpdateReplacePolicy, which control what happens before a delete, DeletionProtection prevents the delete from completing entirely. For production clusters, this should be enabled for all database resources without exception.
5. Edge Cases and Gotchas
Parent CIDR Too Small for the Requested Subnets
If the parent VPC CIDR does not have enough space for the requested subnets, CloudFormation will error at deploy time. Always validate the math before deploying:
# Parent: 172.32.0.0/28 = 16 IPs total
# cidrBits: 6 = /26 = 64 IPs per subnet
# count: 4 = 4 x 64 = 256 IPs required
# 256 > 16 → CloudFormation will error at deploy time
# Rule: count x (2 ^ cidrBits) <= parent IP count
Hardcoded to Dynamic: Verify the Resolved Value Matches
When refactoring from a hardcoded CIDR to a dynamic Fn::Cidr expression, CloudFormation will not trigger a replacement only if the resolved value is identical. Verify the output locally before deploying to a live stack:
# Hardcoded original
CidrBlock: "172.32.0.0/26"
# Dynamic replacement — must resolve to the same value
CidrBlock: !Select [0, !Cidr ["172.32.0.0/24", 4, 6]]
# Verify locally with Python or check the changeset before deployment
python3 -c "
import ipaddress
subnets = list(ipaddress.IPv4Network('172.32.0.0/24').subnets(new_prefix=26))
for i, s in enumerate(subnets): print(i, s)
"
# 0 172.32.0.0/26 ← matches the hardcoded value, safe to deploy
Wrong or Misleading Comments in IaC
The PR that started this discussion had a comment # 172.32.X.0/26 with an ambiguous X placeholder. In IaC, imprecise comments mislead future engineers who may not trace through the function to verify the resolved value.
# Ambiguous — what is X?
CidrBlock: !Select [0, !Cidr [!Ref VpcCidr, 4, 6]] # 172.32.X.0/26
# Clear — states the actual resolved value for the default VpcCidr
CidrBlock: !Select [0, !Cidr [!Ref VpcCidr, 4, 6]] # 172.32.0.0/26 (when VpcCidr=172.32.0.0/24)
Conclusion
Network planning is one of the most consequential infrastructure decisions you make in AWS. Get it right upfront, and it is invisible; everything just works. Get it wrong, and you are looking at manual subnet migrations, Aurora failovers, and potential data loss to fix it.
The combination of Fn::Cidr and parameter-driven VPC CIDRs gives you reusable, consistent, environment-agnostic CloudFormation templates. Pair that with proper growth planning, and you avoid the most common traps. Key takeaways:
- cidrBits = host bits. Formula: 32 - cidrBits = subnet prefix. Not an addition to the parent prefix.
- Always account for 5 reserved IPs per subnet (first 4 + last 1) when planning capacity.
- Leave unused CIDR space in your VPC — subnet IPv4 CIDRs cannot be changed after creation.
- Use count: 8, even when you only need 4 subnets today — reserve index slots for future growth.
- Snapshot and UpdateReplacePolicy: Snapshot to DBCluster/DBInstance before any structural networking change. These are resource attributes, not Properties.
- Verify AI-generated IaC against official AWS documentation. The cidrBits parameter is frequently misrepresented.
References
- AWS CloudFormation Fn::Cidr Documentation: https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/intrinsic-function-reference-cidr.html
- AWS Well-Architected REL02-BP03: https://docs.aws.amazon.com/wellarchitected/latest/reliability-pillar/rel_planning_network_topology_ip_subnet_allocation.html
- VPC CIDR Blocks Amazon VPC User Guide: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-cidr-blocks.html



