lesson ── terraform-intermediate ── ~15 мин ── 4 шагов
Many AWS resources have subblocks that repeat: lifecycle rules in S3,
ingress/egress in a security group, statement in an IAM policy. When
there are two or three, you write them by hand. When the count depends
on a variable, without a dynamic block you would have to duplicate
resources.
In this lesson you build an S3 bucket with a variable number of lifecycle rules. One input, a list of objects, turns into N subblocks of a resource.
интерактивный sandbox
Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.
stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя
Create main.tf in /home/student/tf-dynamic/:
resource "random_id" "suffix" {byte_length = 4
}
variable "lifecycle_rules" { type = list(object({id = string
enabled = bool
prefix = string
expiration_days = number
}))
default = [
{id = "archive-old-logs"
enabled = true
prefix = "logs/"
expiration_days = 30
},
{id = "remove-tmp"
enabled = true
prefix = "tmp/"
expiration_days = 7
},
]
}
The type is list(object(...)) with an explicit schema for each
element. This lets Terraform check at the plan stage that every
element has the required fields.
✓ The input structure is described. Now: a resource with a dynamic block.
Add to main.tf:
resource "aws_s3_bucket" "demo" { bucket = "linuxlab-dynamic-${random_id.suffix.hex}"}
resource "aws_s3_bucket_lifecycle_configuration" "demo" {bucket = aws_s3_bucket.demo.id
dynamic "rule" {for_each = var.lifecycle_rules
content {id = rule.value.id
status = rule.value.enabled ? "Enabled" : "Disabled"
filter {prefix = rule.value.prefix
}
expiration {days = rule.value.expiration_days
}
}
}
}
A breakdown:
dynamic "rule", the name of the subblock in the parent resource.
Here aws_s3_bucket_lifecycle_configuration takes several rule { }.for_each = var.lifecycle_rules, you iterate over the list.content { }, the body of one subblock.rule.value, the current element. rule.value.id, rule.value.prefix.rule.key exists too, but for a list it is the index (0, 1, 2);
for a map, the key.✓ The dynamic block is written. Init + apply.
cd /home/student/tf-dynamic
terraform init
terraform plan
In the plan you will see the two expanded rules inside aws_s3_bucket_lifecycle_configuration:
+ rule {+ id = "archive-old-logs"
+ status = "Enabled"
+ filter { prefix = "logs/" } + expiration { days = 30 }}
+ rule {+ id = "remove-tmp"
+ status = "Enabled"
+ filter { prefix = "tmp/" } + expiration { days = 7 }}
dynamic expanded into two real subblocks. At the resource
level the result is identical to writing two rule { } blocks by
hand.
terraform apply -auto-approve
✓ Lifecycle configuration created with two rules.
OpenTofu keeps its CLI and state compatible with Terraform for the
commands in this step: migration usually goes through mv .terraform .terraform.bak; tofu init -upgrade. But on the first
switch, back up your state and run it on a feature branch, the
differences concentrate in the newer features (variables in the
backend, state encryption, OCI registry-backed modules). See
tf-opentofu-parity for the full matrix.
Change the default of the variable, make it empty:
variable "lifecycle_rules" { type = list(object({id = string
enabled = bool
prefix = string
expiration_days = number
}))
default = []
}
Run a plan:
terraform plan
This is a common pattern: dynamic with an empty list = "no rules". It is especially useful in modules: if the user of the module did not pass any rules, the resource is created without them, it does not fail.
You can apply (apply) or restore the previous value in the
editor, this is not critical right now.
If you want it back the way it was: change default back to a list with two elements.
✓ An empty list: a valid state. dynamic survives the edge case.
Sometimes a dynamic block can be replaced outright with a for
expression:
# the dynamic variant
resource "aws_security_group" "web" { dynamic "ingress" {for_each = var.ports
content {from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
}
}
}
vs
# the for-inside-an-attribute variant (if the resource takes a list attribute)
#, some resources can do this, for example aws_vpc_security_group_ingress_rule
# takes a list directly.
Not every AWS resource allows this. List attributes are available
where there are attributes, not subblocks. If in HCL you write
xxx { ... } without =, it is a subblock and you need
dynamic.
A rule of thumb:
If the resource complains "expected block, got expression", try dynamic. If "expected expression", a for / list literal.
dynamic "X" { for_each = ..., content { ... } } expands into N
X blocks inside the parent resource. Use it when the number of
subblocks depends on an input. Do not confuse it with for_each on the
resource itself: dynamic works inside one resource, for_each
creates N resources.
команды
terraform planyou see the expanded subblocks in the outputterraform consolecheck the structure of var before you plug it into dynamicконцепции