Dynamic Blocks & HCL Built-in Functions
Generate repeated child blocks (like security group rules or tags) dynamically from data.
Use 'cidrsubnet()' and 'cidrhost()' for calculated, collision-free IP subnetting.
Render external bash scripts and configuration templates with variable injection.
The Big Idea
Certain cloud resources contain repeating nested blocks within their body. For example, an AWS Security Group contains nested ingress and egress blocks. Copy-pasting ten ingress blocks for various ports is unmaintainable and violates clean architecture standards.
Dynamic Blocks allow you to generate repeated nested blocks programmatically from a list or map of objects. Combined with an iterator, you can translate an array of port definitions into fully configured firewall rules in six lines of clean code.
Furthermore, HCL includes dozens of Built-in Functions for strings, collections, math, and networking. Functions like cidrsubnet() eliminate manual IP subnet calculations, while templatefile() lets you pass structured variables into external EC2 user-data bash scripts with zero string-concatenation bugs.
Jargon Buster: Plain-English Translations
- Dynamic Block: An HCL construct that iterates over a collection to dynamically generate nested configuration blocks inside a resource.
- Iterator: A custom name given to the current item being processed inside a dynamic block (defaults to the block type name).
- cidrsubnet(prefix, newbits, netnum): A mathematical function that calculates a subnet address within a given IP network address prefix.
- templatefile(path, vars): A function that reads an external file, evaluates it as a template, and substitutes variables.
Dynamic Security Group Rules & CIDR Subnet Math
locals {
# Define firewall rules as structured data
inbound_rules = [
{ port = 80, description = "HTTP access", cidr = "0.0.0.0/0" }
{ port = 443, description = "HTTPS access", cidr = "0.0.0.0/0" }
{ port = 8080, description = "Internal API", cidr = "10.0.0.0/8" }
]
}
resource "aws_security_group" "web" {
name = "production-web-security-group"
vpc_id = aws_vpc.core.id
description = "Managed web security group"
# Dynamic nested block generates ingress rules automatically
dynamic "ingress" {
for_each = local.inbound_rules
content {
description = ingress.value.description
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = [ingress.value.cidr]
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Calculated subnets using cidrsubnet():
# 10.0.0.0/16 with +8 bits yields 10.0.1.0/24, 10.0.2.0/24, etc.
resource "aws_subnet" "calculated" {
count = 3
vpc_id = aws_vpc.core.id
cidr_block = cidrsubnet("10.0.0.0/16", 8, count.index)
availability_zone = "us-east-1a"
}
Beginner Traps & Gotchas
1. Overusing dynamic blocks when dedicated resources exist
The Gotcha: Using dynamic blocks inside aws_security_group instead of standalone 'aws_security_group_rule' resources.
The Fix: Standalone resources (like aws_security_group_rule) are often more flexible and prevent monolithic lock contention when adding rules.
2. Hardcoding user-data bash scripts inside main.tf strings
The Gotcha: Writing 200 lines of bash inside a raw HCL string with escaped quotes and broken indentation.
The Fix: Keep scripts in separate files (e.g. 'templates/user-data.sh.tftpl') and load them via the 'templatefile()' function.