In Terraform, which parts of a resource can a dynamic block NOT generate, and why?
answer
- blocks, not arguments
- tags is a map, use merge
- meta-arguments are processed first
- graph is built before values are known
- no dynamic lifecycle, no dynamic provisioner
basics
~20 sA dynamic block can only generate repeatable nested blocks defined by the resource, data source, provider or provisioner schema. It cannot produce plain arguments such as tags, nor meta-argument blocks like lifecycle, which Terraform must process before evaluating expressions.
solid answer
~50 sTwo limits. First, `dynamic` generates **blocks**, not arguments: `tags` on most AWS resources is a map argument, so you build it with `merge()` or a `for` expression, not a dynamic block. Second, it can only generate blocks that belong to the thing being configured — the resource, data source, provider or provisioner schema — which excludes Terraform's own meta-arguments. You cannot make `lifecycle` or `provisioner` dynamic, and `count`, `for_each`, `depends_on` and `provider` must be given literally. The reason is evaluation order: Terraform has to process meta-arguments to build the dependency graph and decide what to create before it is safe to evaluate arbitrary expressions, so those cannot themselves depend on expressions. In practice this bites people trying to make `ignore_changes` conditional — the answer there is usually two resources or a restructured module, not a dynamic block.
go deeper
Know the headline rule: dynamic makes repeated nested blocks only. Arguments like tags are built with expressions such as merge().
Explain the evaluation-order reason — meta-arguments shape the dependency graph, and the graph must exist before expressions can be resolved.
Show the workaround instinct: when graph shape must vary, vary it with separate resources, count, or the module interface rather than fighting the language.
Take the position that configuration whose graph shape depends on inputs is a module-boundary problem, and set the convention for how variants are expressed across the estate.
## Two separate limits, often confused ### 1. Blocks, not arguments A `dynamic` block generates *repeatable nested blocks*. It has no effect on arguments. The confusion is understandable because HCL writes both inside the same braces, but a provider's schema distinguishes them sharply: ```hcl resource "aws_security_group" "web" { name = "web" # argument -> use an expression tags = { Env = "dev" } # argument (a map) -> use an expression ingress { ... } # nested block -> a dynamic block can generate it } ``` So the answer to "how do I build tags dynamically?" is never a dynamic block; it is `merge(var.common_tags, { Name = local.name })`, or a `for` expression producing a map. Conversely, `ingress`, and any other repeatable nested block the provider declares, is exactly what `dynamic` is for. A useful way to tell them apart without guessing: look at how the provider documentation writes it. `tags - (Optional) A map of tags` is an argument. `ingress - (Optional) Configuration block ... can be specified multiple times` is a repeatable block. ### 2. Schema blocks, not meta-arguments The second limit is stricter and more interesting. Terraform's documentation states that a dynamic block can only generate arguments that belong to the resource type, data source, provider or provisioner being configured — it cannot generate meta-argument blocks such as `lifecycle`, and it cannot generate `provisioner` blocks, because Terraform must process these before it is safe to evaluate expressions. The same applies to the meta-arguments that are not blocks: `count`, `for_each`, `depends_on` and `provider` are read while Terraform builds the resource graph. That graph is what decides evaluation order in the first place, so those values cannot themselves be the result of arbitrary evaluation. `lifecycle` sits in the same bucket: `prevent_destroy` and `ignore_changes` steer how the plan is constructed, before per-instance expressions have results. ## What people try, and what to do instead **"Make ignore_changes conditional on an input."** Not possible via `dynamic`, and `ignore_changes` takes attribute references rather than arbitrary expressions. The realistic answers are: accept ignoring the attribute always; split into two resources selected by `count`, each with its own literal lifecycle; or move the branch up into the module interface so a caller picks the behaviour. **"Generate provisioners for a variable list of scripts."** Also not possible. Provisioners are meta-blocks. Loop *inside* one command, or push the work to the machine image or a configuration-management run, which is where it belongs anyway. **"Attach depends_on dynamically."** No. `depends_on` is fixed configuration; pass the dependency through an attribute reference so the implicit dependency does the work, which is stronger and cheaper than an explicit one. **"Make the provider conditional."** No. Provider selection is graph-time; the pattern is an aliased provider chosen by the caller. ## Why the restriction is the right design It is tempting to read the limit as an implementation gap, but it falls straight out of how Terraform works. The dependency graph must exist before values are known, because the graph determines the order in which values *become* known. Anything that shapes the graph — how many instances, what depends on what, which provider, whether a resource may be destroyed — has to be resolvable up front. Everything that merely fills in a resource's body can wait, and that is exactly the ground `dynamic` covers. Knowing where the line sits keeps you from burning an afternoon on a construction the language will never accept, and it makes the alternative obvious: when configuration genuinely must vary in graph shape, you vary it with separate resources, separate modules or `count`, not with generated syntax.
- A colleague wants ignore_changes to apply only in production. How would you handle that?Not with a dynamic block — `lifecycle` cannot be generated. Practical routes: accept the ignore everywhere, since ignoring an attribute rarely needs to differ; or declare two resources selected by `count` on the environment, each with its own literal lifecycle block; or lift the choice into the module interface so the caller instantiates the variant they want. Duplication here is cheaper than cleverness.
- How do you decide whether something in provider documentation is an argument or a repeatable block?Read how it is described. "A map of tags" or "a list of strings" is an argument, filled by an expression. "Configuration block" — especially with "can be specified multiple times" or "detailed below" — is a nested block, and only those are candidates for a dynamic block. If an assignment with `=` works in the schema, it is an argument.
saying these in an interview costs you the question
- Tries to generate a lifecycle block with dynamic
- Thinks dynamic can produce the tags argument
- Claims depends_on can be built from a variable list
- Says the limit is arbitrary rather than graph-ordering
- Believes any brace-delimited construct is a nested block