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]
#
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"}
}
#
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.