dynamic Blocks: Generating Repeated Nested Blocks
Scenario
You are managing a security group that needs 8 different ingress rules — one for SSH, HTTP, HTTPS, a monitoring port, and four application ports. Writing 8 separate ingress {} blocks is verbose, error-prone, and hard to maintain. When a new port needs to be added, you want to just add it to a variable list, not write another 6-line block.
dynamic blocks let you generate repeated nested blocks (like ingress) from a list or map — DRY and data-driven.
- Define a list(object(...)) variable for ingress rules.
- Use a dynamic "ingress" block to generate one ingress rule per list entry.
- Access attributes inside the dynamic block using ingress.value.port (or a custom iterator name).
- Add a custom iterator name to the dynamic block.
- Output the security group ID and the number of rules created.
Additional Context
dynamic blocks are syntactic sugar for generating repeated nested blocks. They replace patterns like "write 8 ingress blocks" with "loop over a list and generate ingress blocks dynamically."
The content {} block inside a dynamic is where you define the actual attributes of the nested block. By default, the iterator name matches the label (e.g., dynamic "ingress" → ingress.value). You can set a custom iterator name for clarity.
HashiCorp recommends using dynamic blocks sparingly — they reduce readability when overused. Use them when you have a genuinely variable number of nested blocks driven by input data.
variables.tf
variable "aws_region" { type = string; default = "us-east-1" }
# Each object defines one ingress rule
variable "ingress_rules" {
description = "List of ingress rules for the security group."
type = list(object({
port = number
protocol = string
cidr_blocks = list(string)
description = string
}))
default = [
{ port = 22, protocol = "tcp", cidr_blocks = ["10.0.0.0/8"], description = "SSH from internal" },
{ port = 80, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"], description = "HTTP from anywhere" },
{ port = 443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"], description = "HTTPS from anywhere" },
{ port = 8080, protocol = "tcp", cidr_blocks = ["10.0.0.0/8"], description = "App port from internal" },
{ port = 9090, protocol = "tcp", cidr_blocks = ["10.0.0.0/8"], description = "Monitoring from internal" },
]
}main.tf
# main.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
provider "aws" { region = var.aws_region }
resource "aws_security_group" "app" {
name = "app-sg-dynamic"
description = "Security group with dynamic ingress rules"
# ─── DYNAMIC BLOCK: generates one ingress {} per list entry ───
dynamic "ingress" {
for_each = var.ingress_rules # iterate over the list
iterator = rule # custom iterator name (optional)
content {
from_port = rule.value.port # rule.value = current list element
to_port = rule.value.port
protocol = rule.value.protocol
cidr_blocks = rule.value.cidr_blocks
description = rule.value.description
}
}
# Static egress — allow all outbound
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "All outbound"
}
tags = { Name = "app-sg", ManagedBy = "terraform" }
}outputs.tf
output "security_group_id" { value = aws_security_group.app.id }
output "ingress_rule_count" {
description = "Number of ingress rules generated."
value = length(var.ingress_rules)
}Workflow Commands
terraform init && terraform apply
# Add a new rule — just add to the list variable, no code change needed
terraform apply -var='ingress_rules=[{"port":22,"protocol":"tcp","cidr_blocks":["10.0.0.0/8"],"description":"SSH"},{"port":80,"protocol":"tcp","cidr_blocks":["0.0.0.0/0"],"description":"HTTP"},{"port":3000,"protocol":"tcp","cidr_blocks":["10.0.0.0/8"],"description":"New dev port"}]'
terraform destroyApply complete! Resources: 1 added, 0 changed, 0 destroyed. Outputs: ingress_rule_count = 5 security_group_id = "sg-0abc1234def56789"

