TT Lab
Get started
Learn Learning paths Courses

Terraform/OpenTofu Fundamentals

The Review Diff Was All Whitespace

Continue in TT Lab

Goal

You run the formatting verdict, the structure check, and the expression console yourself, and confirm the boundary of what each catches and what it cannot. At the end you build by hand a gate that bundles the three and runs before commit.

Why it matters

The theme of this module is separating what you learn only by applying from what you can know before applying. For formatting, the tool has fixed one right answer, so people do not need to argue — there is a separate verdict-only mode and the exit code differs, so it becomes a gate as it is. The structure check reads only the configuration file and looks at whether references exist, whether required arguments are present, and whether types match the schema. It sees neither the state nor the real object, so it cannot catch a wrong value. If you do not know that boundary, two accidents happen. One is believing it is safe because it passed, and the other is dragging something a static check could have caught all the way to a plan and wasting time. The console fills in between — instead of applying repeatedly over one confusing expression, you check the value in a single line.

Steps

  1. Create /root/tfb-fmt/src/main.tf and /root/tfb-fmt/src/modules/net/main.tf, but write the indentation and the equals-sign alignment deliberately misaligned. In /root/tfb-fmt/src, run tofu fmt -check -diff -recursive, save the output to /root/tfb-fmt/fmt-before.txt, and append exit=<종료 코드> (the exit code) on the last line.
  2. In /root/tfb-fmt/src, actually fix it with tofu fmt -recursive and save the list of fixed file names to /root/tfb-fmt/fmt-files.txt. Then run tofu fmt -check -recursive again and save the output and exit=<종료 코드> to /root/tfb-fmt/fmt-after.txt.
  3. In /root/tfb-fmt/broken/main.tf, break the syntax by putting an argument with a missing value (for example, nothing after filename =). In that directory, run tofu fmt -check -recursive and save the output and exit=<종료 코드> to /root/tfb-fmt/fmt-broken.txt. Do not fix this file; leave it as it is.
  4. In /root/tfb-fmt/invalid/main.tf, put together a resource that references an undeclared variable and a resource that puts a list where a string belongs, and run init. Save the output of tofu validate -json to /root/tfb-fmt/validate.json, and with the values read from it, write two lines to /root/tfb-fmt/validate-summary.txt: errors=<오류 개수> and summaries=<요약들을 사전순으로 콤마로 이은 것> (the placeholders are the number of errors and the summaries joined by commas in dictionary order).
  5. In /root/tfb-fmt/blind/main.tf, put a variable seed_path with no default and a resource that reads that path with file() and makes a file, and run init. tofu validate passes, but tofu plan fails for two reasons. Run a plan that gives no value and a plan that gives a nonexistent path, save the latter output to /root/tfb-fmt/blind-plan.txt, and write three lines to /root/tfb-fmt/blind-report.txt: validate=<종료 코드>, plan_novar=<종료 코드>, and plan_missing=<종료 코드> (the placeholders are exit codes). For the nonexistent path, give ./inputs/seed.txt, and do not create that file.
  6. Run init in /root/tfb-fmt/src and write four lines of expressions to /root/tfb-fmt/console-in.txt — join("-", [var.env, var.app, "01"]), length(toset(["a", "a", "b"])), "5" + 5, 1 == "1". Pipe that file into tofu console and save the result to /root/tfb-fmt/console-out.txt.
  7. In /root/tfb-fmt/src/name.tf, define local.name exactly the same as the first expression of step 6, and with local_file.name, which writes that value on one line, make name.txt. The new file must also be correctly formatted (fmt -check -recursive gives 0), validate must pass, and you must finish through apply.
  8. Create /root/tfb-fmt/precheck.sh. For the directory given as the first argument, it does the formatting verdict first and, if it passes, the structure check. It must finish by printing the single word fmt and a non-zero code if it is caught on formatting, printing validate and a non-zero code if it is caught on structure, and printing ok and 0 if both pass. Save the result of running it on /root/tfb-fmt/src to /root/tfb-fmt/precheck-src.txt and the result of running it on /root/tfb-fmt/broken to /root/tfb-fmt/precheck-broken.txt, and append exit=<종료 코드> (the exit code) at the end of each file.

Notes

Find a tree with misaligned formatting

Create /root/tfb-fmt/src/main.tf and /root/tfb-fmt/src/modules/net/main.tf, but write the indentation and the equals-sign alignment deliberately misaligned. In /root/tfb-fmt/src, run tofu fmt -check -diff -recursive, save the output to /root/tfb-fmt/fmt-before.txt, and append exit=<종료 코드> (the exit code) on the last line.

This command does not fix files but only gives a verdict. That the exit code differs from 0 when there is something to fix is why it can be used as a gate. -diff shows what would be fixed and how.

Fix it and check again

In /root/tfb-fmt/src, actually fix it with tofu fmt -recursive and save the list of fixed file names to /root/tfb-fmt/fmt-files.txt. Then run tofu fmt -check -recursive again and save the output and exit=<종료 코드> to /root/tfb-fmt/fmt-after.txt.

The fixing mode prints only the names of the files it changed, one per line. Without the option that goes into subdirectories, it looks only at the files in the root — both must be in the list.

For a file with broken syntax, the exit code differs from the formatting verdict

In /root/tfb-fmt/broken/main.tf, break the syntax by putting an argument with a missing value (for example, nothing after filename =). In that directory, run tofu fmt -check -recursive and save the output and exit=<종료 코드> to /root/tfb-fmt/fmt-broken.txt. Do not fix this file; leave it as it is.

To fix the formatting, it first has to be able to read the file. The exit code when it cannot be read differs again from "there is something to fix." Think about whether it is fine to treat the two as one lump when you build a gate.

What validate catches — references, required arguments, types

In /root/tfb-fmt/invalid/main.tf, put together a resource that references an undeclared variable and a resource that puts a list where a string belongs, and run init. Save the output of tofu validate -json to /root/tfb-fmt/validate.json, and with the values read from it, write two lines to /root/tfb-fmt/validate-summary.txt: errors=<오류 개수> and summaries=<요약들을 사전순으로 콤마로 이은 것> (the placeholders are the number of errors and the summaries joined by commas in dictionary order).

validate looks not at values but at structure. It is whether a reference exists, whether required arguments are present, and whether the type of the value put in matches the schema. If you use -json, it can be read by CI rather than a person.

What validate cannot catch — values

In /root/tfb-fmt/blind/main.tf, put a variable seed_path with no default and a resource that reads that path with file() and makes a file, and run init. tofu validate passes, but tofu plan fails for two reasons. Run a plan that gives no value and a plan that gives a nonexistent path, save the latter output to /root/tfb-fmt/blind-plan.txt, and write three lines to /root/tfb-fmt/blind-report.txt: validate=<종료 코드>, plan_novar=<종료 코드>, and plan_missing=<종료 코드> (the placeholders are exit codes). For the nonexistent path, give ./inputs/seed.txt, and do not create that file.

A static check reads only the configuration file. What value will come into the variable and whether a file exists at that path are facts outside the configuration file, so they cannot be known. That is why passing validate does not mean "it is okay to apply."

Check an expression in one line in the console

Run init in /root/tfb-fmt/src and write four lines of expressions to /root/tfb-fmt/console-in.txt — join("-", [var.env, var.app, "01"]), length(toset(["a", "a", "b"])), "5" + 5, 1 == "1". Pipe that file into tofu console and save the result to /root/tfb-fmt/console-out.txt.

The console reads the current directory's variables and locals as they are. The last two lines are deliberately confusing — the conversion that happens in addition and the conversion that does not happen in an equality comparison differ.

Put the checked expression into the configuration

In /root/tfb-fmt/src/name.tf, define local.name exactly the same as the first expression of step 6, and with local_file.name, which writes that value on one line, make name.txt. The new file must also be correctly formatted (fmt -check -recursive gives 0), validate must pass, and you must finish through apply.

If you carry over the expression you checked in the console as it is, you do not have to worry again about whether the value is right. You created a new file, so the formatting verdict must pass again too — it is an order to get used to before building the gate.

Build a static-check gate that runs before commit

Create /root/tfb-fmt/precheck.sh. For the directory given as the first argument, it does the formatting verdict first and, if it passes, the structure check. It must finish by printing the single word fmt and a non-zero code if it is caught on formatting, printing validate and a non-zero code if it is caught on structure, and printing ok and 0 if both pass. Save the result of running it on /root/tfb-fmt/src to /root/tfb-fmt/precheck-src.txt and the result of running it on /root/tfb-fmt/broken to /root/tfb-fmt/precheck-broken.txt, and append exit=<종료 코드> (the exit code) at the end of each file.

The order matters. If a file that cannot be read is mixed in, the structure check is meaningless, so the formatting verdict comes first. You must use the option that only gives a verdict and does not fix, so that the gate does not change the repository.