Day 16: Dynamic Blocks for Flexible Resources

Welcome to Day 16! Today we’ll master dynamic blocks - a powerful feature that allows you to dynamically generate nested configuration blocks. This makes your resources incredibly flexible and reduces repetition.
🎯 Today’s Goals
Understand dynamic blocks and their purpose
Create dynamic security group rules
Use for_each in dynamic blocks
Handle complex nested blocks
Build flexible, data-driven resources
🧩 What is a Dynamic Block in Terraform?
A dynamic block allows you to generate nested blocks dynamically inside a resource — especially useful when the number of nested blocks changes based on variables or lists.
Normally, in Terraform, you would define multiple ingress or egress rules like this:
resource "aws_security_group" "example" {
name = "example-sg"
description = "Example security group"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
But what if your ingress rules are dynamic, and you want to loop over them instead of writing each one manually?
That’s where dynamic blocks come in.
🚀 Syntax of a Dynamic Block
A dynamic block has this format:
dynamic "<block_label>" {
for_each = <collection>
content {
# configuration for each generated block
}
}
Explanation:
block_label→ the nested block name (e.g.,ingress,egress)for_each→ the list or map to iterate overcontent→ defines the inside of each block
🛠 Example: AWS Security Group with Dynamic Ingress Rules
Let’s make it practical. 👇
variables.tf
variable "ingress_rules" {
type = list(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
default = [
{
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
},
{
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
},
{
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
]
}
main.tf
provider "aws" {
region = "ap-southeast-1"
}
resource "aws_security_group" "web_sg" {
name = "web-sg"
description = "Security group with dynamic ingress rules"
vpc_id = "vpc-1234567890abcdef0" # Replace with your VPC ID
# Dynamic ingress rule generation
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
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "web-sg"
}
}
🧠 What’s Happening Here?
Terraform will loop through each item in var.ingress_rules and create one ingress block for each.
So the final plan will be equivalent to manually writing:
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
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"]
}
🧩 Optional — Using Maps Instead of Lists
You can also use a map, which gives you access to both the key and value.
provider "aws" {
region = "ap-southeast-1"
}
variable "ingress_rules_map" {
type = map(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
default = {
ssh = {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
http = {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}
resource "aws_security_group" "web_sg_map" {
name = "web-sg-map"
vpc_id = "vpc-1234567890abcdef0"
dynamic "ingress" {
for_each = var.ingress_rules_map
content {
description = ingress.key
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
This version adds a description for each ingress rule based on the map key (e.g., "ssh", "http").
✅ Benefits of Using Dynamic Blocks
| Benefit | Description |
| 💡 Reusable | You can reuse the same code for many environments (dev, staging, prod) |
| 🧱 Clean | Avoid repeating similar nested blocks |
| 🧮 Data-driven | Easily control rules from variables or JSON files |
| ⚙️ Flexible | Works great with for_each, locals, and even count |
Let’s go deep into nested dynamic blocks — like how to dynamically generate tags, or even deeper structures inside a resource.
🧩 What Is a Nested Dynamic Block?
A nested dynamic block means a dynamic block inside another dynamic block or a nested configuration.
In simple words:
You use a dynamic block inside another block (static or dynamic) to generate multiple levels of configuration dynamically.
🧠 Why Do We Need Nested Dynamic Blocks?
Some Terraform resources (like aws_lb_listener, aws_ecs_task_definition, etc.) have deeply nested configurations — e.g.:
multiple rules
each rule has multiple
cidr_blocksortagseach tag or rule may differ depending on input
In those cases, you can nest dynamic blocks.
⚙️ Example 1: Dynamic + Nested Dynamic Block for ALB listener
Goal: Create an AWS Application Load Balancer (ALB) listener
with multiple actions (dynamic)
each action can have multiple target groups (nested dynamic).
You’ll see a real, supported nested dynamic → dynamic pattern.
🗂️ variables.tf
variable "listener_actions" {
description = "List of listener actions with nested target groups"
type = list(object({
type = string
order = number
target_groups = list(object({
arn = string
weight = number
}))
}))
# Example demo data
default = [
{
type = "forward"
order = 1
target_groups = [
{ arn = "arn:aws:elasticloadbalancing:ap-southeast-1:111122223333:targetgroup/app1-tg/aaaa", weight = 1 },
{ arn = "arn:aws:elasticloadbalancing:ap-southeast-1:111122223333:targetgroup/app2-tg/bbbb", weight = 2 }
]
},
{
type = "redirect"
order = 2
target_groups = []
}
]
}
🗂️ main.tf
provider "aws" {
region = "ap-southeast-1"
}
# Mock Load Balancer just for structure demo (you can skip or replace)
resource "aws_lb" "demo_lb" {
name = "demo-alb"
internal = false
load_balancer_type = "application"
subnets = ["subnet-aaa111", "subnet-bbb222"] # replace with yours
}
resource "aws_lb_listener" "demo_listener" {
load_balancer_arn = aws_lb.demo_lb.arn
port = 80
protocol = "HTTP"
# ----------------------------------- #
# Dynamic Block: default_action #
# ----------------------------------- #
dynamic "default_action" {
for_each = var.listener_actions
content {
type = default_action.value.type
order = default_action.value.order
# ----------------------------------- #
# Nested Dynamic Block: forward block #
# ----------------------------------- #
dynamic "forward" {
for_each = (
default_action.value.type == "forward" ?
[default_action.value] : []
)
content {
# ------------------------------------------------- #
# Nested Dynamic Block inside forward: target_group #
# ------------------------------------------------- #
dynamic "target_group" {
for_each = forward.value.target_groups
content {
arn = target_group.value.arn
weight = target_group.value.weight
}
}
}
}
# Example: redirect block for second action
dynamic "redirect" {
for_each = (
default_action.value.type == "redirect" ?
[default_action.value] : []
)
content {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
}
}
🗂️ outputs.tf
output "listener_actions_result" {
value = var.listener_actions
}
Run & Test
terraform init
terraform validate
terraform plan
✅ Expected result:
Terraform will successfully validate and show dynamically generated nested blocks:
Plan: 2 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ listener_actions_result = [
+ {
+ order = 1
+ target_groups = [
+ {
+ arn = "arn:aws:elasticloadbalancing:ap-southeast-1:111122223333:targetgroup/app1-tg/aaaa"
+ weight = 1
},
+ {
+ arn = "arn:aws:elasticloadbalancing:ap-southeast-1:111122223333:targetgroup/app2-tg/bbbb"
+ weight = 2
},
]
+ type = "forward"
},
+ {
+ order = 2
+ target_groups = []
+ type = "redirect"
},
]
🏗 Example 2: Simple Dynamic Tags for Any Resource
If your resource supports a tags block (not just a map), you can use:
dynamic "tags" {
for_each = var.tags
content {
key = tags.key
value = tags.value
}
}
But—most AWS resources already accept tags = {} maps directly, so you usually don’t need a dynamic block for tags.
✅ Key Takeaways
| Concept | Meaning |
| Dynamic block | Used to generate one or more nested blocks dynamically |
| Nested dynamic block | A dynamic block inside another dynamic or block |
| When to use | When you have nested repeating structures (like network interfaces, rules, tags) |
| Not for | Simple attributes (like cidr_blocks = []) — only for real sub-blocks |
| Common usage | AWS launch_template, listener_rule, security_group_rule, etc. |
🧪 Hands-On Lab: Dynamic Blocks Mastery
Let’s build flexible infrastructure using dynamic blocks!
Step 1: Create Project
mkdir terraform-dynamic-blocks-lab
cd terraform-dynamic-blocks-lab
Step 2: Create variables.tf
# variables.tf
variable "aws_region" {
type = string
default = "us-east-1"
}
variable "project_name" {
type = string
default = "dynamic-demo"
}
variable "environment" {
type = string
default = "dev"
}
variable "security_groups" {
description = "Security group configurations"
type = map(object({
description = string
ingress_rules = list(object({
description = string
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
egress_rules = list(object({
description = string
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
}))
default = {
web = {
description = "Security group for web servers"
ingress_rules = [
{
description = "HTTP from anywhere"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
},
{
description = "HTTPS from anywhere"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
]
egress_rules = [
{
description = "All traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
]
}
app = {
description = "Security group for app servers"
ingress_rules = [
{
description = "App port from VPC"
from_port = 8080
to_port = 8080
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
]
egress_rules = [
{
description = "HTTPS to internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
},
{
description = "Database access"
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
]
}
db = {
description = "Security group for databases"
ingress_rules = [
{
description = "PostgreSQL from app tier"
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
]
egress_rules = []
}
}
}
variable "instances" {
description = "Instance configurations with dynamic volumes"
type = map(object({
instance_type = string
ami_filter = string
volumes = list(object({
device_name = string
size = number
type = string
}))
tags = map(string)
}))
default = {
web = {
instance_type = "t2.micro"
ami_filter = "amzn2-ami-hvm-*"
volumes = [
{ device_name = "/dev/sdf", size = 30, type = "gp3" }
]
tags = {
Tier = "Web"
Role = "Frontend"
}
}
app = {
instance_type = "t2.small"
ami_filter = "amzn2-ami-hvm-*"
volumes = [
{ device_name = "/dev/sdf", size = 50, type = "gp3" },
{ device_name = "/dev/sdg", size = 100, type = "gp3" }
]
tags = {
Tier = "App"
Role = "Backend"
}
}
}
}
variable "s3_buckets" {
description = "S3 bucket configurations with dynamic lifecycle rules"
type = map(object({
versioning = bool
lifecycle_rules = list(object({
id = string
enabled = bool
prefix = string
expiration_days = number
}))
}))
default = {
logs = {
versioning = true
lifecycle_rules = [
{
id = "delete-old-logs"
enabled = true
prefix = "logs/"
expiration_days = 90
},
{
id = "delete-temp"
enabled = true
prefix = "temp/"
expiration_days = 7
}
]
}
data = {
versioning = true
lifecycle_rules = [
{
id = "archive-old-data"
enabled = true
prefix = "archive/"
expiration_days = 365
}
]
}
}
}
Step 3: Create main.tf
# main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "${var.project_name}-vpc"
}
}
# Subnets
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "${var.project_name}-public-${count.index + 1}"
}
}
data "aws_availability_zones" "available" {
state = "available"
}
# Security Groups with Dynamic Blocks
resource "aws_security_group" "groups" {
for_each = var.security_groups
name = "${var.project_name}-${each.key}-sg"
description = each.value.description
vpc_id = aws_vpc.main.id
# Dynamic ingress rules
dynamic "ingress" {
for_each = each.value.ingress_rules
iterator = rule
content {
description = rule.value.description
from_port = rule.value.from_port
to_port = rule.value.to_port
protocol = rule.value.protocol
cidr_blocks = rule.value.cidr_blocks
}
}
# Dynamic egress rules
dynamic "egress" {
for_each = each.value.egress_rules
iterator = rule
content {
description = rule.value.description
from_port = rule.value.from_port
to_port = rule.value.to_port
protocol = rule.value.protocol
cidr_blocks = rule.value.cidr_blocks
}
}
tags = {
Name = "${var.project_name}-${each.key}-sg"
Tier = title(each.key)
}
}
# AMI Data Source
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
# EC2 Instances with Dynamic EBS Volumes
resource "aws_instance" "servers" {
for_each = var.instances
ami = data.aws_ami.amazon_linux.id
instance_type = each.value.instance_type
subnet_id = aws_subnet.public[0].id
vpc_security_group_ids = [
aws_security_group.groups[each.key].id
]
# Dynamic EBS volumes
dynamic "ebs_block_device" {
for_each = each.value.volumes
iterator = volume
content {
device_name = volume.value.device_name
volume_size = volume.value.size
volume_type = volume.value.type
encrypted = true
tags = {
Name = "${var.project_name}-${each.key}-volume-${volume.key}"
}
}
}
tags = merge(
each.value.tags,
{
Name = "${var.project_name}-${each.key}"
Environment = var.environment
}
)
}
# S3 Buckets with Dynamic Lifecycle Rules
resource "random_id" "bucket" {
for_each = var.s3_buckets
byte_length = 4
}
resource "aws_s3_bucket" "buckets" {
for_each = var.s3_buckets
bucket = "${var.project_name}-${each.key}-${random_id.bucket[each.key].hex}"
tags = {
Name = "${var.project_name}-${each.key}"
Type = each.key
}
}
resource "aws_s3_bucket_versioning" "buckets" {
for_each = var.s3_buckets
bucket = aws_s3_bucket.buckets[each.key].id
versioning_configuration {
status = each.value.versioning ? "Enabled" : "Disabled"
}
}
resource "aws_s3_bucket_lifecycle_configuration" "buckets" {
for_each = var.s3_buckets
bucket = aws_s3_bucket.buckets[each.key].id
dynamic "rule" {
for_each = each.value.lifecycle_rules
iterator = lifecycle
content {
id = lifecycle.value.id
status = lifecycle.value.enabled ? "Enabled" : "Disabled"
filter {
prefix = lifecycle.value.prefix
}
expiration {
days = lifecycle.value.expiration_days
}
}
}
}
Step 4: Create outputs.tf
# outputs.tf
output "security_groups" {
description = "Security group details"
value = {
for key, sg in aws_security_group.groups :
key => {
id = sg.id
name = sg.name
ingress_count = length(var.security_groups[key].ingress_rules)
egress_count = length(var.security_groups[key].egress_rules)
}
}
}
output "instances" {
description = "Instance details"
value = {
for key, instance in aws_instance.servers :
key => {
id = instance.id
private_ip = instance.private_ip
volume_count = length(var.instances[key].volumes)
tags = var.instances[key].tags
}
}
}
output "s3_buckets" {
description = "S3 bucket details"
value = {
for key, bucket in aws_s3_bucket.buckets :
key => {
name = bucket.id
versioning = var.s3_buckets[key].versioning
lifecycle_rule_count = length(var.s3_buckets[key].lifecycle_rules)
}
}
}
output "dynamic_block_summary" {
description = "Summary of resources created with dynamic blocks"
value = {
security_groups = {
total = length(aws_security_group.groups)
total_ingress = sum([for sg in var.security_groups : length(sg.ingress_rules)])
total_egress = sum([for sg in var.security_groups : length(sg.egress_rules)])
}
instances = {
total = length(aws_instance.servers)
total_volumes = sum([for inst in var.instances : length(inst.volumes)])
}
s3_buckets = {
total = length(aws_s3_bucket.buckets)
total_lifecycle_rules = sum([for bucket in var.s3_buckets : length(bucket.lifecycle_rules)])
}
}
}
Step 5: Deploy and Test
# Initialize
terraform init
# Plan
terraform plan
# Apply
terraform apply -auto-approve
# View outputs
terraform output
# Check security group rules
terraform output security_groups
# Check instance volumes
terraform output instances
# Check S3 lifecycle rules
terraform output s3_buckets
# Clean up
terraform destroy -auto-approve
📝 Best Practices
✅ DO:
Use dynamic blocks for repeated nested blocks
Keep dynamic blocks simple and readable
Document complex dynamic structures
Validate input data structure
❌ DON’T:
Don’t use dynamic blocks for single blocks
Don’t nest too many levels of dynamic blocks
Don’t create overly complex conditions in dynamic blocks
Don’t use dynamic blocks when static blocks are clearer
📝 Summary
Today you learned:
✅ Dynamic block syntax and structure
✅ Creating dynamic security group rules
✅ Nested dynamic blocks
✅ Dynamic blocks with for_each
✅ Real-world dynamic block patterns
🚀 Tomorrow’s Preview
Day 17: Introduction to Modules
Tomorrow we’ll:
Understand Terraform modules
Learn module structure
Use public modules
Pass data between modules
Organize code with modules
← Day 15: Built-in Functions | Day 17: Introduction to Modules →
Remember: Dynamic blocks eliminate repetition and make resources data-driven!



