Terraform Dynamic Blocks and Conditional Logic
Some resources require you to repeat the same nested block multiple times with different values — security group rules, ingress/egress configurations, or tag sets. Writing these by hand does not scale. Dynamic blocks generate repeated nested blocks automatically from a list or map. Combined with conditional expressions, they make configurations powerfully flexible without becoming unreadable.
The Problem: Repetitive Nested Blocks
Consider an AWS security group. Every allowed port requires its own ingress block. Without dynamic blocks:
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"]
}
# Adding a fourth port means adding another whole block manually
}
Three ports, three blocks. Ten ports would mean ten blocks. Any change to the allowed ports means editing the file.
Dynamic Blocks: The Solution
A dynamic block generates nested blocks from a variable or local value. You define the pattern once and Terraform repeats it for every item in a collection.
variable "ingress_rules" {
type = list(object({
port = number
protocol = string
cidr_blocks = list(string)
}))
default = [
{ port = 80, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] },
{ port = 443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] },
{ port = 22, protocol = "tcp", cidr_blocks = ["10.0.0.0/8"] }
]
}
resource "aws_security_group" "web" {
name = "web-sg"
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.port
to_port = ingress.value.port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
Dynamic Block Syntax Breakdown
dynamic "NESTED_BLOCK_NAME" {
for_each = COLLECTION # list or map to iterate over
content {
# The nested block's arguments
# Use NESTED_BLOCK_NAME.value to access each item's value
# Use NESTED_BLOCK_NAME.key for map keys
}
}
Add a new port to var.ingress_rules and Terraform generates the additional ingress block automatically — no code changes in the resource block itself.
Dynamic Blocks with Maps
locals {
tags_to_apply = {
Environment = "production"
Owner = "platform-team"
CostCenter = "engineering"
}
}
resource "some_resource" "example" {
dynamic "tag" {
for_each = local.tags_to_apply
content {
key = tag.key
value = tag.value
}
}
}
When iterating a map, tag.key is the map key and tag.value is the map value. When iterating a list, use tag.value for the item and tag.key for the index.
Conditional Resource Creation
Sometimes a resource should only be created in certain environments. Use the count meta-argument with a conditional expression to toggle resource creation on or off.
variable "enable_bastion" {
type = bool
default = false
}
resource "aws_instance" "bastion" {
count = var.enable_bastion ? 1 : 0
ami = data.aws_ami.linux.id
instance_type = "t3.micro"
}
When enable_bastion = true, count is 1 and Terraform creates the bastion host. When false, count is 0 and the resource does not exist. This is the standard pattern for optional resources in Terraform.
Referencing a Conditionally Created Resource
output "bastion_ip" {
value = var.enable_bastion ? aws_instance.bastion[0].public_ip : "No bastion deployed"
}
Because the instance may not exist, always use a conditional expression before accessing its attributes. Access index [0] since count creates a list.
Conditional Nested Block with Dynamic
Combine dynamic with a conditional expression to include or exclude a nested block based on a variable:
variable "enable_https_redirect" {
type = bool
default = true
}
resource "aws_alb_listener" "http" {
# ...
dynamic "default_action" {
for_each = var.enable_https_redirect ? [1] : []
content {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
}
The trick is for_each = condition ? [1] : []. An empty list produces zero iterations (block absent). A single-item list produces one iteration (block present).
Key Points
- Dynamic blocks generate repeated nested blocks from a list or map — eliminating copy-paste repetition inside resource definitions.
- Use
BLOCK_NAME.valueto access item values andBLOCK_NAME.keyfor map keys or list indexes inside a dynamic block. - Use
count = var.enable ? 1 : 0to conditionally create or skip a resource entirely. - Always guard attribute references to conditionally created resources with a ternary expression to avoid errors when count is 0.
- Use
for_each = condition ? [1] : []to conditionally include or exclude a nested block using a dynamic block.
