Cloud  /  Terraform

IaC Terraform 50 guides · updated 2026

Infrastructure as code done right — providers, state, reusable modules, and the workflow patterns that keep multi-cloud deployments sane in 2026.

Dynamic Blocks in Terraform

Dynamic blocks generate repeated nested configuration blocks from a collection (list, set, or map). Instead of hardcoding multiple ingress, lifecycle_rule, or statement blocks, you iterate over a variable to produce as many blocks as needed — making configurations flexible without duplication.


Basic Dynamic Block Syntax

dynamic "<block_type>" {
for_each = <collection>
content {
# Access the current element with <block_type>.value (or iterator.value)
<attribute> = <block_type>.value.<field>
}
}

Dynamic Security Group Rules

The most common use case — variable numbers of ingress/egress rules:

variable "ingress_rules" {
type = list(object({
port = number
protocol = string
description = string
cidr_blocks = list(string)
}))
default = [
{ port = 80, protocol = "tcp", description = "HTTP", cidr_blocks = ["0.0.0.0/0"] },
{ port = 443, protocol = "tcp", description = "HTTPS", cidr_blocks = ["0.0.0.0/0"] },
{ port = 8080, protocol = "tcp", description = "App", cidr_blocks = ["10.0.0.0/8"] }
]
}
resource "aws_security_group" "app" {
name = "app-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 = ingress.value.protocol
description = ingress.value.description
cidr_blocks = ingress.value.cidr_blocks
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}

Custom Iterator Label

The default iterator label is the block type name. Use iterator to rename it for clarity or to avoid conflicts:

variable "rules" {
type = list(object({
port = number
sources = list(string)
}))
}
resource "aws_security_group" "app" {
vpc_id = aws_vpc.main.id
dynamic "ingress" {
for_each = var.rules
iterator = rule # Use "rule" instead of "ingress"
content {
from_port = rule.value.port
to_port = rule.value.port
protocol = "tcp"
cidr_blocks = rule.value.sources
}
}
}

Dynamic S3 Lifecycle Rules

variable "lifecycle_rules" {
type = list(object({
id = string
prefix = string
transition_days = number
expiration_days = optional(number)
}))
default = [
{ id = "move-to-ia", prefix = "logs/", transition_days = 30 },
{ id = "move-to-glacier",prefix = "backups/", transition_days = 90, expiration_days = 365 }
]
}
resource "aws_s3_bucket_lifecycle_configuration" "main" {
bucket = aws_s3_bucket.main.id
dynamic "rule" {
for_each = var.lifecycle_rules
content {
id = rule.value.id
status = "Enabled"
filter {
prefix = rule.value.prefix
}
transition {
days = rule.value.transition_days
storage_class = "STANDARD_IA"
}
dynamic "expiration" {
for_each = rule.value.expiration_days != null ? [rule.value.expiration_days] : []
content {
days = expiration.value
}
}
}
}
}

Dynamic IAM Policy Statements

variable "s3_permissions" {
type = list(object({
actions = list(string)
bucket = string
prefix = string
}))
}
data "aws_iam_policy_document" "app" {
dynamic "statement" {
for_each = var.s3_permissions
content {
effect = "Allow"
actions = statement.value.actions
resources = [
"arn:aws:s3:::${statement.value.bucket}/${statement.value.prefix}*"
]
}
}
statement {
effect = "Allow"
actions = ["s3:ListAllMyBuckets"]
resources = ["*"]
}
}

Dynamic ECS Container Definitions

variable "environment_variables" {
type = list(object({
name = string
value = string
}))
default = []
}
variable "secrets" {
type = list(object({
name = string
valueFrom = string
}))
default = []
}
resource "aws_ecs_task_definition" "app" {
family = "app"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 256
memory = 512
container_definitions = jsonencode([{
name = "app"
image = var.image
environment = [for e in var.environment_variables : {
name = e.name
value = e.value
}]
secrets = [for s in var.secrets : {
name = s.name
valueFrom = s.valueFrom
}]
}])
}

Conditional Dynamic Blocks

Use a conditional collection ([] = no blocks, [value] = one block) to conditionally include a nested block:

variable "enable_logging" {
type = bool
default = false
}
resource "aws_lb" "main" {
name = "main-alb"
internal = false
load_balancer_type = "application"
subnets = var.subnet_ids
# Include access_logs block only when logging is enabled
dynamic "access_logs" {
for_each = var.enable_logging ? [1] : []
content {
bucket = aws_s3_bucket.alb_logs[0].id
prefix = "alb"
enabled = true
}
}
}

The pattern for_each = condition ? [1] : [] is idiomatic for optional nested blocks — [1] produces one block iteration, [] produces none.