TerraformPilot

DevOps

Terraform Dynamic Blocks and For Expressions with Examples

Learn Terraform dynamic blocks and for expressions. Generate repeated blocks from variables, filter lists, transform maps, and build DRY security groups

LLuca Berton1 min read

Dynamic Blocks

#

Dynamic blocks generate repeated nested blocks from a variable. Instead of writing the same block 10 times, you write it once with dynamic.

The Problem: Repetitive Blocks

#
# Without dynamic — 5 ingress blocks, lots of repetition
resource "aws_security_group" "web" {
  name = "web-sg"
 
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
 
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
 
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8"]
  }
  # ... more rules
}

The Solution: Dynamic Block

#
variable "ingress_rules" {
  default = [
    { port = 80,  cidr = "0.0.0.0/0",   description = "HTTP" },
    { port = 443, cidr = "0.0.0.0/0",   description = "HTTPS" },
    { port = 22,  cidr = "10.0.0.0/8",  description = "SSH internal" },
    { port = 8080, cidr = "10.0.0.0/8", description = "App internal" },
    { port = 3306, cidr = "10.0.1.0/24", description = "MySQL" },
  ]
}
 
resource "aws_security_group" "web" {
  name   = "web-sg"
  vpc_id = aws_vpc.main.id
 
  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = "tcp"
      cidr_blocks = [ingress.value.cidr]
      description = ingress.value.description
    }
  }
 
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Dynamic Block Syntax

#
dynamic "BLOCK_NAME" {
  for_each = COLLECTION
  iterator = OPTIONAL_NAME   # defaults to BLOCK_NAME
  content {
    # Use BLOCK_NAME.value or ITERATOR.value
    attribute = BLOCK_NAME.value.something
  }
}
  • BLOCK_NAME — the nested block to generate (ingress, setting, tag, etc.)
  • for_each — list or map to iterate over
  • iterator — optional custom name (default: block name)
  • content — the block body, with .value for current item and .key for index/key

More Examples

#

AWS IAM Policy Statements:

variable "policy_statements" {
  default = [
    {
      effect    = "Allow"
      actions   = ["s3:GetObject", "s3:ListBucket"]
      resources = ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"]
    },
    {
      effect    = "Allow"
      actions   = ["dynamodb:GetItem", "dynamodb:PutItem"]
      resources = ["arn:aws:dynamodb:*:*:table/my-table"]
    }
  ]
}
 
data "aws_iam_policy_document" "app" {
  dynamic "statement" {
    for_each = var.policy_statements
    content {
      effect    = statement.value.effect
      actions   = statement.value.actions
      resources = statement.value.resources
    }
  }
}

Azure Network Security Group Rules:

variable "nsg_rules" {
  default = {
    ssh  = { priority = 100, port = 22,  access = "Allow" }
    http = { priority = 200, port = 80,  access = "Allow" }
    rdp  = { priority = 300, port = 3389, access = "Deny" }
  }
}
 
resource "azurerm_network_security_group" "web" {
  name                = "web-nsg"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name
 
  dynamic "security_rule" {
    for_each = var.nsg_rules
    content {
      name                       = security_rule.key
      priority                   = security_rule.value.priority
      direction                  = "Inbound"
      access                     = security_rule.value.access
      protocol                   = "Tcp"
      source_port_range          = "*"
      destination_port_range     = security_rule.value.port
      source_address_prefix      = "*"
      destination_address_prefix = "*"
    }
  }
}

Optional Blocks with Conditional:

variable "enable_encryption" {
  type    = bool
  default = true
}
 
resource "aws_s3_bucket_server_side_encryption_configuration" "example" {
  bucket = aws_s3_bucket.example.id
 
  dynamic "rule" {
    for_each = var.enable_encryption ? [1] : []
    content {
      apply_server_side_encryption_by_default {
        sse_algorithm = "aws:kms"
      }
    }
  }
}

The for_each = var.enable_encryption ? [1] : [] pattern creates 0 or 1 blocks based on a boolean.

For Expressions

#

For expressions transform and filter lists and maps inline.

Basic Syntax

#
# List → List
[for item in list : transform(item)]
 
# List → Map
{for item in list : key => value}
 
# With filter
[for item in list : transform(item) if condition]

Transform a List

#
variable "names" {
  default = ["alice", "bob", "charlie"]
}
 
locals {
  # Uppercase
  upper_names = [for name in var.names : upper(name)]
  # Result: ["ALICE", "BOB", "CHARLIE"]
 
  # Add prefix
  prefixed = [for name in var.names : "user-${name}"]
  # Result: ["user-alice", "user-bob", "user-charlie"]
}

Filter a List

#
variable "instances" {
  default = [
    { name = "web-1",  type = "t3.micro",  env = "prod" },
    { name = "web-2",  type = "t3.small",  env = "prod" },
    { name = "dev-1",  type = "t3.micro",  env = "dev" },
  ]
}
 
locals {
  # Only prod instances
  prod_instances = [for i in var.instances : i if i.env == "prod"]
  # Result: [{name="web-1",...}, {name="web-2",...}]
 
  # Just the names
  prod_names = [for i in var.instances : i.name if i.env == "prod"]
  # Result: ["web-1", "web-2"]
}

List to Map

#
variable "users" {
  default = [
    { name = "alice", role = "admin" },
    { name = "bob",   role = "developer" },
    { name = "carol", role = "admin" },
  ]
}
 
locals {
  # Create a map keyed by name
  user_map = {for user in var.users : user.name => user.role}
  # Result: {alice = "admin", bob = "developer", carol = "admin"}
}

Map Transformations

#
variable "tags" {
  default = {
    Environment = "prod"
    Team        = "platform"
    Project     = "api"
  }
}
 
locals {
  # Prefix all tag keys
  prefixed_tags = {for k, v in var.tags : "app:${k}" => v}
  # Result: {"app:Environment" = "prod", "app:Team" = "platform", ...}
 
  # Filter tags
  non_env_tags = {for k, v in var.tags : k => v if k != "Environment"}
}

Flatten Nested Lists

#
variable "vpc_subnets" {
  default = {
    "us-east-1a" = ["10.0.1.0/24", "10.0.2.0/24"]
    "us-east-1b" = ["10.0.3.0/24", "10.0.4.0/24"]
  }
}
 
locals {
  all_subnets = flatten([
    for az, cidrs in var.vpc_subnets : [
      for cidr in cidrs : {
        az   = az
        cidr = cidr
      }
    ]
  ])
  # Result: [{az="us-east-1a", cidr="10.0.1.0/24"}, ...]
}
 
resource "aws_subnet" "all" {
  for_each          = {for s in local.all_subnets : "${s.az}-${s.cidr}" => s}
  vpc_id            = aws_vpc.main.id
  availability_zone = each.value.az
  cidr_block        = each.value.cidr
}

Combining Dynamic Blocks with For Expressions

#
variable "services" {
  default = {
    web = { port = 80,  public = true }
    api = { port = 8080, public = false }
    db  = { port = 5432, public = false }
  }
}
 
resource "aws_security_group" "app" {
  name   = "app-sg"
  vpc_id = aws_vpc.main.id
 
  # Only create ingress rules for public services
  dynamic "ingress" {
    for_each = {for k, v in var.services : k => v if v.public}
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = ingress.key
    }
  }
 
  # Internal services accessible from VPC only
  dynamic "ingress" {
    for_each = {for k, v in var.services : k => v if !v.public}
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = "tcp"
      cidr_blocks = [aws_vpc.main.cidr_block]
      description = "${ingress.key} (internal)"
    }
  }
}

When NOT to Use Dynamic Blocks

#

Dynamic blocks add complexity. Use them when you have 3+ repeated blocks driven by a variable. For 1-2 blocks, write them explicitly — it's more readable.

# Don't do this for 2 rules — just write them out
dynamic "ingress" {
  for_each = [80, 443]
  content {
    from_port   = ingress.value
    to_port     = ingress.value
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
 
# Better: explicit blocks for 2 rules
ingress {
  from_port   = 80
  to_port     = 80
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
}
 
ingress {
  from_port   = 443
  to_port     = 443
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
}

Hands-On Courses

#

Learn by doing with interactive courses on CopyPasteLearn:

Conclusion

#

Dynamic blocks replace repeated nested blocks with a single dynamic block that iterates over a variable. For expressions transform and filter lists and maps inline. Use them together to build flexible security groups, IAM policies, and multi-AZ subnets from variables. Keep it simple — if you only have 1-2 blocks, write them explicitly.

#Terraform#HCL#Dynamic Blocks#Infrastructure as Code#Tips

Share this article