Lesson 12 of 16
Section 3: Variables, Expressions, Loops & Meta-Arguments35 min read & walkthrough

Dynamic Blocks & HCL Built-in Functions

01 / DYNAMIC
Nested Blocks

Generate repeated child blocks (like security group rules or tags) dynamically from data.

02 / NETWORKING
CIDR Math

Use 'cidrsubnet()' and 'cidrhost()' for calculated, collision-free IP subnetting.

03 / TEMPLATES
Templatefile

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.