Terraform Dynamic Blocks – T6
Hey there! Welcome back to our Terraform journey. In our last post, you learned how to create multiple resources with count and for_each. Today, we’re going to learn about dynamic blocks – a way to make the inside of your resources super flexible.
Think of it this way: count and for_each help you create multiple houses, but dynamic blocks help you add multiple rooms to each house!
The Problem: Repeating Configuration Inside Resources
Let’s say you want to create a security group (like a firewall) that allows multiple ports. The old way would be:
resource "aws_security_group" "web" {
name = "web-server"
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 = ["0.0.0.0/0"]
}
}
See how we’re repeating the same ingress
block over and over? That’s a lot of typing! And what if you need to allow 10 ports? Or 20?
Enter Dynamic Blocks
Dynamic blocks let you create multiple nested blocks inside a resource without repeating yourself.
Here’s the magic version:
variable "allowed_ports" {
type = list(number)
default = [80, 443, 22]
}
resource "aws_security_group" "web" {
name = "web-server"
dynamic "ingress" {
for_each = var.allowed_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}
What just happened?
dynamic "ingress"
creates multiple ingress blocksfor_each = var.allowed_ports
loops through the list of portscontent { }
defines what goes inside each ingress blockingress.value
gets the current port number
This creates the exact same security group but with much less code!
Understanding Dynamic Block Parts
Let’s break down the parts of a dynamic block:
dynamic "BLOCK_NAME" {
for_each = LIST_OR_MAP
content {
# What goes inside each block
}
}
The parts:
dynamic "BLOCK_NAME"
– what type of block to create (like “ingress”, “tag”, etc.)for_each
– what to loop throughcontent
– the template for each block
Your First Simple Dynamic Block
Let’s start with something super simple – creating multiple tags:
variable "server_tags" {
type = map(string)
default = {
Environment = "production"
Team = "web"
Project = "blog"
}
}
resource "local_file" "server_info" {
filename = "server-tags.txt"
content = "Server configuration with tags"
# This doesn't actually work with local_file,
# but shows the concept
}
Wait, local_file doesn’t have nested blocks like tags. Let me use a better example:
variable "backup_folders" {
type = list(string)
default = ["documents", "photos", "videos"]
}
variable "folder_info" {
type = list(object({
name = string
size = string
}))
default = [
{ name = "documents", size = "500MB" },
{ name = "photos", size = "2GB" },
{ name = "videos", size = "10GB" }
]
}
resource "local_file" "backup_report" {
filename = "backup-report.txt"
content = <<-EOF
Backup Report
=============
Folders to backup:
%{for folder in var.folder_info}
- ${folder.name}: ${folder.size}
%{endfor}
Total folders: ${length(var.folder_info)}
EOF
}
What’s %{for ... %{endfor}
? This is Terraform’s template syntax for loops inside strings. It’s like a mini dynamic block for text!
Real Dynamic Blocks with AWS
Let’s use a real example with AWS security groups:
variable "web_ports" {
type = list(object({
port = number
description = string
}))
default = [
{ port = 80, description = "HTTP traffic" },
{ port = 443, description = "HTTPS traffic" },
{ port = 22, description = "SSH access" }
]
}
resource "aws_security_group" "web_server" {
name = "web-server-sg"
description = "Security group for web server"
dynamic "ingress" {
for_each = var.web_ports
content {
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = ingress.value.description
}
}
# Allow all outbound traffic
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "web-server-security-group"
}
}
This creates a security group with 3 ingress rules, each with its own port and description!
Dynamic Blocks with Different Data
Let’s try something more complex – creating multiple disk attachments:
variable "extra_disks" {
type = list(object({
device_name = string
volume_type = string
volume_size = number
encrypted = bool
}))
default = [
{
device_name = "/dev/sdb"
volume_type = "gp3"
volume_size = 100
encrypted = true
},
{
device_name = "/dev/sdc"
volume_type = "gp3"
volume_size = 200
encrypted = true
}
]
}
resource "aws_instance" "web_server" {
ami = "ami-12345678"
instance_type = "t3.medium"
dynamic "ebs_block_device" {
for_each = var.extra_disks
content {
device_name = ebs_block_device.value.device_name
volume_type = ebs_block_device.value.volume_type
volume_size = ebs_block_device.value.volume_size
encrypted = ebs_block_device.value.encrypted
}
}
tags = {
Name = "web-server-with-extra-disks"
}
}
What’s happening:
dynamic "ebs_block_device"
creates multiple disk attachments- Each disk gets its own device name, type, size, and encryption setting
ebs_block_device.value.device_name
gets the device name from the current disk
Using the Iterator Name
By default, you reference values with the block name (like ingress.value
or ebs_block_device.value
). But you can choose your own name:
variable "network_rules" {
type = list(object({
port = number
protocol = string
source = string
}))
default = [
{ port = 80, protocol = "tcp", source = "0.0.0.0/0" },
{ port = 443, protocol = "tcp", source = "0.0.0.0/0" },
{ port = 3306, protocol = "tcp", source = "10.0.0.0/8" }
]
}
resource "aws_security_group" "database" {
name = "database-sg"
dynamic "ingress" {
for_each = var.network_rules
iterator = rule # Custom name instead of "ingress"
content {
from_port = rule.value.port
to_port = rule.value.port
protocol = rule.value.protocol
cidr_blocks = [rule.value.source]
description = "Allow ${rule.value.protocol} on port ${rule.value.port}"
}
}
}
Now you use rule.value
instead of ingress.value
. This makes the code easier to read!
Conditional Dynamic Blocks
Sometimes you only want to create blocks under certain conditions:
variable "environment" {
type = string
default = "dev"
}
variable "monitoring_enabled" {
type = bool
default = false
}
variable "backup_schedules" {
type = list(object({
name = string
time = string
}))
default = [
{ name = "daily", time = "02:00" },
{ name = "weekly", time = "03:00" }
]
}
resource "local_file" "server_config" {
filename = "${var.environment}-server.conf"
content = <<-EOF
# Server Configuration for ${var.environment}
environment = ${var.environment}
monitoring_enabled = ${var.monitoring_enabled}
%{if var.monitoring_enabled}
# Monitoring Configuration
monitoring {
enabled = true
level = "${var.environment == "prod" ? "detailed" : "basic"}"
}
%{endif}
%{if length(var.backup_schedules) > 0}
# Backup Schedules
%{for schedule in var.backup_schedules}
backup "${schedule.name}" {
time = "${schedule.time}"
enabled = ${var.environment == "prod" ? "true" : "false"}
}
%{endfor}
%{endif}
EOF
}
What’s new:
%{if condition}
only includes content if condition is true%{for item in list}
loops through items- You can use conditions inside the loops too
Nested Dynamic Blocks
You can even put dynamic blocks inside other dynamic blocks! (But don’t go crazy with this):
variable "applications" {
type = map(object({
ports = list(object({
number = number
protocol = string
description = string
}))
environment_vars = map(string)
}))
default = {
web = {
ports = [
{ number = 80, protocol = "tcp", description = "HTTP" },
{ number = 443, protocol = "tcp", description = "HTTPS" }
]
environment_vars = {
NODE_ENV = "production"
PORT = "3000"
}
}
api = {
ports = [
{ number = 8080, protocol = "tcp", description = "API" }
]
environment_vars = {
DATABASE_URL = "postgresql://localhost:5432/api"
JWT_SECRET = "super-secret"
}
}
}
}
resource "local_file" "app_configs" {
for_each = var.applications
filename = "${each.key}-config.yaml"
content = <<-EOF
# Configuration for ${each.key} application
name: ${each.key}
ports:
%{for port in each.value.ports}
- number: ${port.number}
protocol: ${port.protocol}
description: ${port.description}
%{endfor}
environment:
%{for key, value in each.value.environment_vars}
${key}: ${value}
%{endfor}
EOF
}
This creates separate config files for each application with their specific ports and environment variables!
Practical Example: Complete Infrastructure Setup
Let’s build something real – a complete web application setup:
variable "app_config" {
type = object({
name = string
environment = string
ports = list(number)
ssl_enabled = bool
backup_files = list(string)
team_access = map(string)
})
default = {
name = "blogapp"
environment = "production"
ports = [80, 443, 22]
ssl_enabled = true
backup_files = ["database", "uploads", "config"]
team_access = {
developers = "read-write"
devops = "admin"
support = "read-only"
}
}
}
# Create security group with dynamic rules
resource "local_file" "security_rules" {
filename = "${var.app_config.name}-security.tf"
content = <<-EOF
# Security configuration for ${var.app_config.name}
resource "aws_security_group" "${var.app_config.name}_sg" {
name = "${var.app_config.name}-${var.app_config.environment}-sg"
%{for port in var.app_config.ports}
ingress {
from_port = ${port}
to_port = ${port}
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow port ${port}"
}
%{endfor}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
EOF
}
# Create backup configuration
resource "local_file" "backup_config" {
filename = "${var.app_config.name}-backup.json"
content = jsonencode({
application = var.app_config.name
environment = var.app_config.environment
backup_enabled = var.app_config.environment == "production"
backup_items = [
for file in var.app_config.backup_files : {
name = file
path = "/app/data/${file}"
frequency = var.app_config.environment == "production" ? "hourly" : "daily"
}
]
})
}
# Create team access configuration
resource "local_file" "team_access" {
filename = "${var.app_config.name}-access.yaml"
content = <<-EOF
# Team access configuration for ${var.app_config.name}
application: ${var.app_config.name}
environment: ${var.app_config.environment}
team_permissions:
%{for team, permission in var.app_config.team_access}
${team}:
level: ${permission}
ssl_required: ${var.app_config.ssl_enabled}
%{endfor}
EOF
}
# Create application summary
resource "local_file" "app_summary" {
filename = "${var.app_config.name}-summary.md"
content = <<-EOF
# ${var.app_config.name} Application Summary
## Basic Information
- **Application**: ${var.app_config.name}
- **Environment**: ${var.app_config.environment}
- **SSL Enabled**: ${var.app_config.ssl_enabled}
## Network Configuration
### Allowed Ports
%{for port in var.app_config.ports}
- Port ${port}
%{endfor}
## Backup Configuration
### Files to Backup
%{for file in var.app_config.backup_files}
- ${file}
%{endfor}
## Team Access
%{for team, permission in var.app_config.team_access}
- **${team}**: ${permission} access
%{endfor}
## Files Generated
- Security rules: ${var.app_config.name}-security.tf
- Backup config: ${var.app_config.name}-backup.json
- Access config: ${var.app_config.name}-access.yaml
*Generated by Terraform on ${timestamp()}*
EOF
}
output "application_summary" {
value = {
app_name = var.app_config.name
environment = var.app_config.environment
ports_count = length(var.app_config.ports)
backup_items = length(var.app_config.backup_files)
teams = length(var.app_config.team_access)
ssl_enabled = var.app_config.ssl_enabled
files_created = [
"${var.app_config.name}-security.tf",
"${var.app_config.name}-backup.json",
"${var.app_config.name}-access.yaml",
"${var.app_config.name}-summary.md"
]
}
}
This creates a complete application setup with:
- Security group configuration
- Backup settings
- Team access rules
- A nice summary document
When to Use Dynamic Blocks
Use Dynamic Blocks when:
- You need multiple similar nested blocks in a resource
- The number or content of blocks might change
- You want to avoid repeating the same configuration
Good examples:
- Security group rules (multiple ports)
- Server disk attachments (multiple disks)
- Load balancer listeners (multiple protocols)
- Database parameter groups (multiple settings)
Don’t use Dynamic Blocks when:
- You have just one or two blocks (not worth the complexity)
- The blocks are very different from each other
- You’re creating multiple resources (use count/for_each instead)
Common Patterns
Pattern 1: Port Lists
variable "ports" {
default = [80, 443, 22]
}
dynamic "ingress" {
for_each = var.ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
}
}
Pattern 2: Complex Objects
variable "rules" {
type = list(object({
port = number
protocol = string
source = string
}))
}
dynamic "ingress" {
for_each = var.rules
iterator = rule
content {
from_port = rule.value.port
to_port = rule.value.port
protocol = rule.value.protocol
cidr_blocks = [rule.value.source]
}
}
Pattern 3: Conditional Blocks
dynamic "backup" {
for_each = var.enable_backup ? var.backup_configs : []
content {
# backup configuration
}
}
Quick Reference
Basic Dynamic Block:
dynamic "BLOCK_NAME" {
for_each = LIST_OR_MAP
content {
# block configuration using BLOCK_NAME.value
}
}
With Custom Iterator:
dynamic "BLOCK_NAME" {
for_each = LIST_OR_MAP
iterator = custom_name
content {
# block configuration using custom_name.value
}
}
Template Loops in Strings:
content = <<-EOF
%{for item in var.list}
- ${item}
%{endfor}
EOF
Conditional Templates:
content = <<-EOF
%{if var.condition}
This appears when condition is true
%{endif}
EOF
Practice Challenge
Try creating this yourself:
- Create a variable with different database configurations (name, port, backup_enabled)
- Use dynamic blocks to create multiple configuration sections
- Use template loops to create a formatted report
- Add conditional sections based on environment
Here’s a starter:
variable "databases" {
type = list(object({
name = string
port = number
backup_enabled = bool
storage_size = number
}))
default = [
# Add your database configs here
]
}
# Your dynamic blocks here
What’s Next?
Fantastic work! You’ve mastered another powerful Terraform feature:
Dynamic Blocks – Making resources super flexible:
- ✅ Creating multiple nested blocks without repetition
- ✅ Using for_each with complex data structures
- ✅ Custom iterators for cleaner code
- ✅ Template loops and conditionals in strings
- ✅ When and how to use dynamic blocks effectively
Advanced Patterns:
- ✅ Conditional dynamic blocks
- ✅ Nested dynamic blocks
- ✅ Complex configuration generation
- ✅ Real-world application examples
In our next post, we’ll explore Terraform Functions – the built-in tools that help you manipulate data, format strings, work with lists and maps, and do calculations. You’ll learn:
- String functions for text manipulation
- List and map functions for data processing
- Math functions for calculations
- Date and time functions
- How to combine functions for powerful data transformations
The dynamic block knowledge you gained today will work perfectly with functions to create incredibly flexible configurations!
Ready to supercharge your Terraform with built-in functions? The next post will show you some amazing data manipulation tricks!