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.