TT Lab
Get started
Learn Learning paths Courses

Terraform in Practice

Three Services by Three Environments Became Twenty Blocks by Hand

Continue in TT Lab

Goal

You take a nested object variable and turn it into a map, unroll a double repetition in two ways, flatten and setproduct, to produce the same result, build an optional block with a dynamic block and confirm its limit through an error, and then render the unrolled matrix as files and a report.

Why it matters

The real reason infrastructure code gets long is not that there are many resources but that the same shape repeats. If you unroll that repetition by hand, there are twenty places to fix, and the reviewer must compare by eye whether the twenty blocks are really the same. HCL solves this on two tracks — the side that builds values and the side that builds blocks. The value side (for expressions, flatten, setproduct, object types) can be folded and unfolded as much as you like, and the block side (dynamic) can build only within the range the provider schema permits. If you do not know this difference, you try to write everything with dynamic and end up with unreadable configuration. The place where dynamic is most valuable in practice is actually the choice of 'include this block or not,' not repetition, and repetition is usually handled by for_each and value expressions.

Steps

  1. In /root/tfa-dyn/variables.tf, declare a services variable of type list(object({ name = string, port = number, envs = list(string) })) and attach a validation that checks names do not overlap. In /root/tfa-dyn/terraform.tfvars, write three entries: api (8080, dev and prod), web (8081, dev), and batch (9090, dev, stage, and prod). In /root/tfa-dyn/main.tf, put the provider declaration and a local_file.services that writes jsonencode(var.services) to out/services.json, then init and apply.
  2. In /root/tfa-dyn/svc.tf, create local.by_name (a map keyed by name), and declare local_file.svc that iterates for_each over it and writes /root/tfa-dyn/out/svc/<이름>.conf for each service (where the placeholder is the service name). The file contents are the two lines name=<이름> and port=<번호> (the service name and the port number). After applying, see what the instance keys in the state are.
  3. In /root/tfa-dyn/flat.tf, build with flatten a local.pairs that unrolls every service × that service's environments (each item is an object with env, name, and port), and jsonencode a local.by_key map keyed by "<env>/<name>" and write it to /root/tfa-dyn/out/flat.json. There must be six items.
  4. In /root/tfa-dyn/product.tf, create local.all_envs (a deduplicated list of all environments), keep only the environments the service actually uses from all combinations of setproduct(local.all_envs, var.services) to build local.combo_by_key, and write it to /root/tfa-dyn/out/product.json. It must be exactly the same as the step 3 result.
  5. In /root/tfa-dyn/cert.tf, put var.with_subject (bool, default true), an ED25519 private key, and a tls_self_signed_cert.c that turns the subject block on and off with dynamic "subject". The subject is common_name = "api.internal" and organization = "LabHub". The certificate is written to /root/tfa-dyn/out/cert.pem. First apply with -var with_subject=false and save the result of openssl x509 -noout -subject to /root/tfa-dyn/out/no-subject.txt, then apply again with the default and save the result of the same command to /root/tfa-dyn/out/subject.txt.
  6. Briefly edit the dynamic "subject" in /root/tfa-dyn/cert.tf so that it builds two blocks, apply, and save the failure output to /root/tfa-dyn/out/limit-error.txt. Then revert it to the original, apply again, and finish with a clean plan.
  7. In /root/tfa-dyn/rendered.tf, iterate for_each over the step 3 local.by_key to create six files /root/tfa-dyn/rendered/<env>-<name>.conf (where the placeholders are the environment and the service name). The contents are the three lines env=, name=, and port=.
  8. In /root/tfa-dyn/summary.tf, create /root/tfa-dyn/out/summary.json containing three things: the number of items per environment (by_env), a sorted list of names (services), and the sum of service ports (total_ports). All three values are computed with HCL expressions.

Notes

Accept a nested object as a variable type

In /root/tfa-dyn/variables.tf, declare a services variable of type list(object({ name = string, port = number, envs = list(string) })) and attach a validation that checks names do not overlap. In /root/tfa-dyn/terraform.tfvars, write three entries: api (8080, dev and prod), web (8081, dev), and batch (9090, dev, stage, and prod). In /root/tfa-dyn/main.tf, put the provider declaration and a local_file.services that writes jsonencode(var.services) to out/services.json, then init and apply.

If you write the type as object, a misspelling in tfvars is blocked before apply. The condition of a validation passes when true — it is enough to check whether the count of names equals the count after deduplication. jsonencode turns an HCL value into JSON as is.

Turn the list into a map and pass it to for_each

In /root/tfa-dyn/svc.tf, create local.by_name (a map keyed by name), and declare local_file.svc that iterates for_each over it and writes /root/tfa-dyn/out/svc/<이름>.conf for each service (where the placeholder is the service name). The file contents are the two lines name=<이름> and port=<번호> (the service name and the port number). After applying, see what the instance keys in the state are.

for_each does not accept a list — it must be a set or a map. If you use count with a list, when you delete a middle item everything after it shifts and is recreated, but if you use map keys, only that item is deleted. Check that the state address is written with the key inside square brackets.

Flatten a double repetition into one layer with flatten

In /root/tfa-dyn/flat.tf, build with flatten a local.pairs that unrolls every service × that service's environments (each item is an object with env, name, and port), and jsonencode a local.by_key map keyed by "<env>/<name>" and write it to /root/tfa-dyn/out/flat.json. There must be six items.

The inner for returns a list, so the result of the outer for becomes a list of lists. flatten peels off that one layer. If you turn it into a map, you can pass it straight to for_each in the next step, and the key becomes the name people read.

Build the same matrix again with setproduct

In /root/tfa-dyn/product.tf, create local.all_envs (a deduplicated list of all environments), keep only the environments the service actually uses from all combinations of setproduct(local.all_envs, var.services) to build local.combo_by_key, and write it to /root/tfa-dyn/out/product.json. It must be exactly the same as the step 3 result.

setproduct does not filter — it builds all combinations and then you choose what to keep with the if of a for expression. Put the two files side by side and judge which is easier to read: the approach that builds all combinations and discards some, or the approach that unrolls only what is needed from the start.

Make a block present or absent with dynamic

In /root/tfa-dyn/cert.tf, put var.with_subject (bool, default true), an ED25519 private key, and a tls_self_signed_cert.c that turns the subject block on and off with dynamic "subject". The subject is common_name = "api.internal" and organization = "LabHub". The certificate is written to /root/tfa-dyn/out/cert.pem. First apply with -var with_subject=false and save the result of openssl x509 -noout -subject to /root/tfa-dyn/out/no-subject.txt, then apply again with the default and save the result of the same command to /root/tfa-dyn/out/subject.txt.

If you give dynamic's for_each an empty collection, that block is not created at all. This is the most common reason to use dynamic in practice — it is an optional block, not repetition. Whether the certificate's subject is really empty should be checked by opening the created certificate, not by looking at the configuration.

dynamic builds only as much as the schema permits

Briefly edit the dynamic "subject" in /root/tfa-dyn/cert.tf so that it builds two blocks, apply, and save the failure output to /root/tfa-dyn/out/limit-error.txt. Then revert it to the original, apply again, and finish with a clean plan.

dynamic is not a device that builds any number of blocks but a device that 'builds blocks from configuration values.' How many are allowed is decided by the provider's schema. See whether the error message tells you the allowed range and the actual count together.

Render the unrolled matrix into files

In /root/tfa-dyn/rendered.tf, iterate for_each over the step 3 local.by_key to create six files /root/tfa-dyn/rendered/<env>-<name>.conf (where the placeholders are the environment and the service name). The contents are the three lines env=, name=, and port=.

The key already holds the environment and name, so you can assemble each file name by taking it from each.value. See whether, when one service later uses one more environment, fixing just one line of tfvars creates one more file — that is the reason for using this structure.

Fold the unrolled values back up into a report

In /root/tfa-dyn/summary.tf, create /root/tfa-dyn/out/summary.json containing three things: the number of items per environment (by_env), a sorted list of names (services), and the sum of service ports (total_ports). All three values are computed with HCL expressions.

For by_env, filter pairs for each environment and count the length, and get the port sum with the sum function. The reason to sort here is not that it looks nice to people but that without sorting, the same input would come out in a different order on each run and comparison would be impossible.