Terraform Best Practices: Using cidrsubnet and Loops for Scalable Subnet Management
Managing scalable and maintainable subnets is a cornerstone of efficient infrastructure as code (IaC) with Terraform. Rather than hardcoding subnet CIDRs manually, using cidrsubnet in conjunction with Terraform loops (like for_each or count) enables you to create subnet blocks dynamically and programmatically. This approach reduces error, enhances clarity, and promotes reusability.
This post will explore using cidrsubnet and loops in Terraform to create scalable, clean, and DRY (Don't Repeat Yourself) subnet configurations for your AWS VPCs.
Why Not Hardcode Subnets?
Hardcoding subnets:
Leads to cluttered and redundant code.
Makes updates harder and error-prone.
Doesn’t scale well for large environments.
Dynamic CIDR generation avoids manual calculations and supports large and automated deployments.
Understanding cidrsubnet
Terraform’s cidrsubnet function allows you to create subnet CIDRs from a base CIDR block by incrementing subnet bits. Syntax:
cidrsubnet(prefix, newbits, netnum)
prefix: Base network CIDR (e.g., 10.0.0.0/16)
newbits: Number of additional bits to extend the mask (e.g., 8 to go from /16 to /24)
netnum: Index of the subnet
Example:
cidrsubnet("10.0.0.0/16", 8, 1) # Output: 10.0.1.0/24
Using Loops for Multiple Subnets
Example with for_each
variable "subnet_names" {
default = ["public-a", "public-b", "public-c"]
}
resource "aws_subnet" "public" {
for_each = toset(var.subnet_names)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet("10.0.0.0/16", 8, index(var.subnet_names, each.key))
availability_zone = each.key == "public-a" ? "us-east-1a" :
each.key == "public-b" ? "us-east-1b" : "us-east-1c"
}
This snippet creates three subnets (10.0.0.0/24, 10.0.1.0/24, 10.0.2.0/24) across different availability zones.
More Advanced: Using count and Maps
variable "azs" {
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
resource "aws_subnet" "private" {
count = length(var.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet("10.1.0.0/16", 8, count.index)
availability_zone = var.azs[count.index]
}
This technique scales beautifully in multi-AZ environments and abstracts subnet creation into reusable modules.
Putting It All Together in a Module
Create a reusable subnets module:
modules/subnets/main.tf
resource "aws_subnet" "this" {
for_each = var.subnet_map
vpc_id = var.vpc_id
cidr_block = cidrsubnet(var.base_cidr, var.newbits, each.value.netnum)
availability_zone = each.value.az
}
Root module:
module "subnets" {
source = "./modules/subnets"
vpc_id = aws_vpc.main.id
base_cidr = "10.0.0.0/16"
newbits = 8
subnet_map = {
public-a = { az = "us-east-1a", netnum = 0 }
public-b = { az = "us-east-1b", netnum = 1 }
public-c = { az = "us-east-1c", netnum = 2 }
}
}
Best Practices Summary
Use cidrsubnet to avoid manual CIDR calculation.
Avoid hardcoded subnet blocks – use loops (for_each or count) with indices.
Build reusable modules for subnet logic.
Tag your resources and use variables for flexibility.
Use maps or objects to handle AZ and subnet relationships cleanly.

Comments
Post a Comment