TT Lab
Get started
Learn Learning paths Courses

Terraform in Practice

Repetition Folded into Values, Repetition Unfolded into Blocks

Continue in TT Lab

In one sentence

Repetition in HCL splits into the side that builds values (for, flatten, setproduct) and the side that builds blocks (dynamic). The former can be folded and unfolded at will, and the latter can only build as much as the provider schema permits.

Why this was needed — what happens when you unroll repetition by hand

Say you deploy three services to three environments. Unrolled by hand, that is nine blocks. At first it is easy to read. The problem comes six months later when "change the health check path in every environment" arrives. You must fix nine places, and the reviewer must compare by eye whether the nine places were changed exactly the same. Even if one place is missed, the plan passes.

So you fold repetition into values. One input, one rule, nine results. Then there is only one place to fix, and the review only needs to look at one thing: "is the rule right?" In exchange, you need practice reading value expressions — where this trade-off is worth accepting is the topic of this module.

How it works

1. Decide the type first. A nested object type serves as documentation.

variable "services" {
  type = list(object({
    name = string
    port = number
    envs = list(string)
  }))
}

If you write the type, a misspelling in tfvars is blocked before apply. If you add validation here, you can also catch rules like "names must not overlap" at the value stage. If names overlap, the for_each keys built later silently merge into one, and this incident shows up in the plan only as "1 deletion," so the cause is hard to find.

2. Turn a list into a map and pass it to for_each. for_each accepts only a set or a map. Unlike count, the instance address is the key, so even if a middle item is removed, the later ones do not shift.

3. Flatten a double repetition into one. There are two ways.

# (가) 안에서 밖으로 — 필요한 것만 만든다
pairs = flatten([
  for s in var.services : [
    for e in s.envs : { env = e, name = s.name, port = s.port }
  ]
])

# (나) 다 만들고 거른다
combos = [
  for pair in setproduct(local.all_envs, var.services) : {
    env = pair[0], name = pair[1].name, port = pair[1].port
  } if contains(pair[1].envs, pair[0])
]

The two Korean comments say that the first approach goes from the inside out and builds only what is needed, and the second builds everything and then filters.

The two can produce the same result but have different properties. The flatten side builds only what is in the input, while the setproduct side builds every combination and then discards some. If the number of combinations is large, the latter does a lot of wasted work. Conversely, if the goal is "finding missing combinations," building all combinations first is the natural way.

4. dynamic builds blocks. It is a device that builds nested blocks, not values, from configuration values.

dynamic "subject" {
  for_each = var.with_subject ? [1] : []
  content {
    common_name = "api.internal"
  }
}

What matters here is that how many you can build depends on the provider schema. If you build two of a block the schema allows at most one of, a plan is made, but value validation blocks it with "at most N allowed, got M." In step 6 of the lab you receive this error yourself.

What it looks like in the field

The place where dynamic is most often used in practice is actually not repetition but optional blocks. Using the property that if you give an empty collection, the block is not created at all, you express "logging configuration only in prod" and "the encryption block only when this option is on." It is much better than the old way of making two copies of the whole resource to imitate a conditional block.

Conversely, there are also clear places not to use it. The official documentation also says not to overuse dynamic because it becomes hard to read. If three blocks differ slightly from each other, building them dynamically hides "what is different" inside data and it is not visible in review. In that case it is better to simply write the three blocks. The criterion is simple — if the count varies with the input, go dynamic; if the count is fixed and only the values differ, write by hand.

Finally, the habit of exporting rendered results to files helps a lot. If you write the unrolled matrix to a file with jsonencode, people can check it by eye and CI can run a diff. From plan output alone, it is hard to trace "why is this combination missing?"

What to do in the next lab

In /root/tfa-dyn, you take a nested object variable and turn it into a map, build the same matrix in two ways, flatten and setproduct, and compare by file whether the results are the same. Next you turn a certificate subject block on and off with dynamic and open the actual certificate to check, receive the error of being blocked by the schema when you try to create two, and then render the unrolled matrix into six files and a summary report.