Terraform Dynamic Blocks Prompt
Generate nested blocks dynamically — security group rules, tags, conditional blocks, complex iteration.
- Target user
- Terraform engineers writing DRY configurations
- Difficulty
- Intermediate
- Tools
- Claude, ChatGPT
The prompt
You are a senior Terraform engineer who has used dynamic blocks extensively for generating nested resource configurations.
I will provide:
- The use case
- Current attempt
- Symptom (block not appearing, wrong values)
Your job:
1. **Dynamic basics**:
- `dynamic "<block_name>" { for_each = ..., content { ... } }`
- Generates multiple instances of nested block
- Inside resource, data source, provisioner
2. **For iteration syntax**:
- `for_each` — map, set, or list
- `each.key`, `each.value` inside content
- `iterator` for renaming default
3. **For conditional**:
- Empty list → no blocks generated
- `for_each = condition ? [{...}] : []`
4. **For complex inputs**:
- Map of objects
- Each provides config to block
5. **For combining with static**:
- Static blocks + dynamic
- All evaluated; combined
6. **For nested dynamic**:
- Dynamic inside dynamic
- Use iterators for clarity
7. **For common pitfalls**:
- Forgetting `content {}`
- Iterator name collision
- Wrong attribute access
8. **For when NOT to use**:
- Static blocks simpler when no iteration
- Don't dynamic everything
Mark DESTRUCTIVE: dynamic block generating wrong configs, missing required attributes, accidentally producing thousands of blocks.
---
Use case: [DESCRIBE]
Current attempt: [PASTE]
Symptom: [DESCRIBE]
Why this prompt works
Dynamic blocks are powerful but tricky. This prompt walks them.
How to use it
- Use when iteration needed.
- for_each appropriate.
- Static for simple.
- Test with small inputs first.
Patterns
Security group with dynamic rules
variable "ingress_rules" {
description = "Map of ingress rules"
type = map(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
description = optional(string)
}))
default = {
https = {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS from anywhere"
}
ssh = {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
description = "SSH from internal"
}
}
}
resource "aws_security_group" "web" {
name = "web"
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
description = lookup(ingress.value, "description", ingress.key)
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
Conditional dynamic
resource "aws_s3_bucket" "data" {
bucket = "myorg-data"
dynamic "lifecycle_rule" {
for_each = var.enable_lifecycle ? [1] : []
content {
id = "archive"
enabled = true
transition {
days = 90
storage_class = "GLACIER"
}
}
}
dynamic "logging" {
for_each = var.enable_logging ? [1] : []
content {
target_bucket = aws_s3_bucket.logs.id
target_prefix = "data-access/"
}
}
}
Nested dynamic with iterator
resource "aws_iam_policy" "complex" {
name = "complex"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
for k, v in var.permissions : {
Effect = v.effect
Action = v.actions
Resource = v.resources
}
]
})
}
# Alternative: dynamic block inside resource
resource "aws_appautoscaling_policy" "ecs" {
name = "policy"
target_tracking_scaling_policy_configuration {
target_value = var.target_value
dynamic "customized_metric_specification" {
for_each = var.use_custom_metric ? [var.custom_metric] : []
content {
metric_name = customized_metric_specification.value.metric_name
namespace = customized_metric_specification.value.namespace
statistic = customized_metric_specification.value.statistic
dynamic "dimensions" {
for_each = customized_metric_specification.value.dimensions
iterator = dim # rename to avoid confusion
content {
name = dim.key
value = dim.value
}
}
}
}
}
}
Dynamic tags
locals {
base_tags = {
Environment = var.environment
ManagedBy = "Terraform"
}
resource_tags = merge(local.base_tags, var.additional_tags)
}
resource "aws_instance" "web" {
ami = "ami-..."
instance_type = "t3.medium"
tags = merge(
local.resource_tags,
{
Name = "web-${var.environment}"
}
)
}
Static + dynamic combined
resource "aws_security_group" "mixed" {
name = "mixed"
# Static ingress (always present)
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS"
}
# Dynamic additional ingress (configurable)
dynamic "ingress" {
for_each = var.additional_ingress
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
Common findings this catches
- Dynamic block missing → for_each empty.
- Wrong attribute →
each.value.xvseach.key. - Iterator collision in nested → rename.
- Required block missing when for_each is empty → use static.
- Performance with large inputs → profile.
- Hard to read → static blocks where possible.
- Conditional dynamic showing in plan even when empty.
When to escalate
- Complex dynamic logic — refactor.
- Performance issues at scale — profile.
- Cross-module dynamic blocks — design.
Related prompts
-
Terraform `for_each` vs `count` Prompt
Choose between for_each and count for resource iteration — key stability, addition/removal behavior, when to use each.
-
Terraform Module Composition Prompt
Design Terraform modules — input/output contracts, composition, versioning, public vs private registry, when to abstract.