TT Lab
Get started
Learn Learning paths Courses

Terraform/OpenTofu Fundamentals

Hand-Concatenating Values Until the Quotes Won

Continue in TT Lab

Goal

After checking string, collection, conditional, conversion, template, and serialization functions one line at a time in the console, you carry them over as they are into a configuration and make real files. At the end you build a configuration that writes not a single value directly and draws only from variables, and confirm that it holds up for other inputs too.

Why it matters

The reason configuration files get long is usually that the same value is written in several places. If you write the rule for combining names, the values that differ per environment, and the precedence when merging maps as functions, the places to fix when changing a value shrink to one. And functions can be checked on one line in the console, so the cost of repeating applies whenever an expression is confusing disappears. Functions that handle failure matter too. Data that comes in from outside can break at any time, and if the whole apply stops each time, operations are tied to someone else's mistake. If you know the functions that turn failure into a value, you can design a fallback to a default. Finally, serialization functions free a person from being responsible for quotes and escapes — JSON joined by hand will surely break someday.

Steps

  1. Create /root/tfb-func/play and run init with a main.tf that declares only the local provider. Write six lines to /root/tfb-func/strings-in.txt — join("-", ["web", "prod", "01"]), format("%s-%03d", "node", 7), replace("db-prod-01", "prod", "stage"), trimspace(" padded "), substr("abcdefgh", 2, 3), upper(join("_", sort(["b", "a"]))). Pipe that file into tofu console and save the result to /root/tfb-func/strings-out.txt.
  2. In /root/tfb-func/coll/main.tf, put a map(number) variable ports (web 80, api 8080); merge to combine two tag maps into local.tags; sort(keys(...)) to build local.names; lookup to read local.api (an existing key) and local.db (a missing key, default 5432); and distinct to remove duplicates into local.unique; and export them as five outputs. After init and apply, save tofu output -json to /root/tfb-func/collections.json.
  3. In /root/tfb-func/cond/main.tf, put a variable with default prod, env; the expression var.env == "prod" held in local.is_prod; local.replicas (3 or 1), local.log_level (warn or debug), and local.backup (daily or none), chosen by it; and, built with format, local.endpoint (the port is 443 or 8080); and create the file that writes the four values as four lines in the form 키=값 (key=value), namely app.conf. After init and apply, evaluate the same four values with tofu console -var env=dev and save them to /root/tfb-func/cond-dev.txt.
  4. In /root/tfb-func/guard/, create a valid JSON good.json (with name being api and port being 8080) and a broken JSON bad.json. In main.tf, read both files with try(jsondecode(...), {}), pull out an existing key and a missing key each with try(..., "unknown"), and hold the success or failure of the two files with can(jsondecode(...)), exporting them as five outputs (name, miss, bad_keys, ok_good, ok_bad). After apply, save tofu output -json to /root/tfb-func/guard.json.
  5. In /root/tfb-func/tpl/site.tftpl, write a template containing one line for the name, a loop over the list of ports, and a condition based on whether TLS is on, and in main.tf, with templatefile, pass name = "api", ports = [80, 443], and tls = true to make site.conf. The result must be six lines, starting with server {, then name = api, listen 80;, listen 443;, tls on;, and }.
  6. In /root/tfb-func/enc/main.tf, put local.data (name = api, ports = [80, 443], tls = true), and with jsonencode create data.json, and with yamlencode create data.yaml. After apply, save the result read with jq -c . to /root/tfb-func/enc-json.txt and the result read with yq -o=json -I=0 . to /root/tfb-func/enc-yaml.txt. The contents of the two files must be the same.
  7. Write six lines to /root/tfb-func/convert-in.txt — parseint("ff", 16), tonumber("007"), tostring(true), try(tonumber("abc"), -1), can(tonumber("abc")), format("%s has %d ports", "api", length([80, 443])). Pipe it into the console in /root/tfb-func/play and save the result to /root/tfb-func/convert-out.txt.
  8. In /root/tfb-func/compose/main.tf, put a list(string) variable names (default api, web, db) and a list(number) variable ports (default 8080, 80, 5432), use formatlist to build a list of 이름=포트 (name=port) lines, join to concatenate them, and format and length to make a # <개수> services header (the placeholder is the count), and create services.conf. The first line is the header and the service lines follow. Do not write values directly; draw only from variables — the grader applies the same configuration with different inputs.

Notes

Check string functions in the console

Create /root/tfb-func/play and run init with a main.tf that declares only the local provider. Write six lines to /root/tfb-func/strings-in.txt — join("-", ["web", "prod", "01"]), format("%s-%03d", "node", 7), replace("db-prod-01", "prod", "stage"), trimspace(" padded "), substr("abcdefgh", 2, 3), upper(join("_", sort(["b", "a"]))). Pipe that file into tofu console and save the result to /root/tfb-func/strings-out.txt.

If you pipe a file into the console, it evaluates one line at a time and prints only the result. The placeholders of format follow the format string rules as they are, and the second argument of substr is not the length but the start position.

Merge and pick from maps and lists

In /root/tfb-func/coll/main.tf, put a map(number) variable ports (web 80, api 8080); merge to combine two tag maps into local.tags; sort(keys(...)) to build local.names; lookup to read local.api (an existing key) and local.db (a missing key, default 5432); and distinct to remove duplicates into local.unique; and export them as five outputs. After init and apply, save tofu output -json to /root/tfb-func/collections.json.

In merge, the map that comes later wins. The third argument of lookup is the default for when the key is missing, and if you leave it out, a missing key raises an error. keys does not guarantee ordering, so wrap it in sort.

Choose per-environment values with conditional expressions

In /root/tfb-func/cond/main.tf, put a variable with default prod, env; the expression var.env == "prod" held in local.is_prod; local.replicas (3 or 1), local.log_level (warn or debug), and local.backup (daily or none), chosen by it; and, built with format, local.endpoint (the port is 443 or 8080); and create the file that writes the four values as four lines in the form 키=값 (key=value), namely app.conf. After init and apply, evaluate the same four values with tofu console -var env=dev and save them to /root/tfb-func/cond-dev.txt.

In a conditional expression, the types for true and false must be the same. If you give a variable value to the console, you can see the result of the other branch without fixing the configuration, so the check is done without repeating applies.

Absorb broken input as a value

In /root/tfb-func/guard/, create a valid JSON good.json (with name being api and port being 8080) and a broken JSON bad.json. In main.tf, read both files with try(jsondecode(...), {}), pull out an existing key and a missing key each with try(..., "unknown"), and hold the success or failure of the two files with can(jsondecode(...)), exporting them as five outputs (name, miss, bad_keys, ok_good, ok_bad). After apply, save tofu output -json to /root/tfb-func/guard.json.

try gives the next argument if evaluating the earlier argument fails, and can turns success or failure into true or false. Both catch only errors that arise at run time — they cannot catch static errors such as a reference that is not even declared.

Stamp out a configuration file with a template

In /root/tfb-func/tpl/site.tftpl, write a template containing one line for the name, a loop over the list of ports, and a condition based on whether TLS is on, and in main.tf, with templatefile, pass name = "api", ports = [80, 443], and tls = true to make site.conf. The result must be six lines, starting with server {, then name = api, listen 80;, listen 443;, tls on;, and }.

The repetition and conditions of a template are directives that begin with a percent sign. To keep the directive lines from remaining as blank lines in the result, attach a whitespace-strip marker before the closing brace. The template file extension is free, but there is a convention.

Export the same data in two formats

In /root/tfb-func/enc/main.tf, put local.data (name = api, ports = [80, 443], tls = true), and with jsonencode create data.json, and with yamlencode create data.yaml. After apply, save the result read with jq -c . to /root/tfb-func/enc-json.txt and the result read with yq -o=json -I=0 . to /root/tfb-func/enc-yaml.txt. The contents of the two files must be the same.

The two functions only convert the same value into different notations, so when read back the same data comes out. If you use these functions instead of joining configuration files by hand, the tool takes responsibility for quotes and escapes.

Check the conversion functions and their failures

Write six lines to /root/tfb-func/convert-in.txt — parseint("ff", 16), tonumber("007"), tostring(true), try(tonumber("abc"), -1), can(tonumber("abc")), format("%s has %d ports", "api", length([80, 443])). Pipe it into the console in /root/tfb-func/play and save the result to /root/tfb-func/convert-out.txt.

A conversion function raises an error when it meets a value that cannot be converted. What turns that error into a value is try, and what turns it into true or false is can. For strings whose base is not 10, there is a dedicated function.

Chain functions together into one configuration file

In /root/tfb-func/compose/main.tf, put a list(string) variable names (default api, web, db) and a list(number) variable ports (default 8080, 80, 5432), use formatlist to build a list of 이름=포트 (name=port) lines, join to concatenate them, and format and length to make a # <개수> services header (the placeholder is the count), and create services.conf. The first line is the header and the service lines follow. Do not write values directly; draw only from variables — the grader applies the same configuration with different inputs.

formatlist applies the same format while walking several lists side by side. The count in the header must be computed so that it stays right even when the input changes. By the time you get this far, you no longer have to fix several places in the file to change one value.