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 overiterator— optional custom name (default: block name)content— the block body, with.valuefor current item and.keyfor 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.




