Terraform/OpenTofu Fundamentals
Hand-Concatenating Values Until the Quotes Won
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
- Create
/root/tfb-func/playand run init with amain.tfthat 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 intotofu consoleand save the result to/root/tfb-func/strings-out.txt. - In
/root/tfb-func/coll/main.tf, put amap(number)variableports(web 80, api 8080);mergeto combine two tag maps intolocal.tags;sort(keys(...))to buildlocal.names;lookupto readlocal.api(an existing key) andlocal.db(a missing key, default 5432); anddistinctto remove duplicates intolocal.unique; and export them as five outputs. After init and apply, savetofu output -jsonto/root/tfb-func/collections.json. - In
/root/tfb-func/cond/main.tf, put a variable with defaultprod,env; the expressionvar.env == "prod"held inlocal.is_prod;local.replicas(3 or 1),local.log_level(warn or debug), andlocal.backup(daily or none), chosen by it; and, built withformat,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), namelyapp.conf. After init and apply, evaluate the same four values withtofu console -var env=devand save them to/root/tfb-func/cond-dev.txt. - In
/root/tfb-func/guard/, create a valid JSONgood.json(withnamebeingapiandportbeing 8080) and a broken JSONbad.json. Inmain.tf, read both files withtry(jsondecode(...), {}), pull out an existing key and a missing key each withtry(..., "unknown"), and hold the success or failure of the two files withcan(jsondecode(...)), exporting them as five outputs (name,miss,bad_keys,ok_good,ok_bad). After apply, savetofu output -jsonto/root/tfb-func/guard.json. - 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 inmain.tf, withtemplatefile, passname = "api",ports = [80, 443], andtls = trueto makesite.conf. The result must be six lines, starting withserver {, thenname = api,listen 80;,listen 443;,tls on;, and}. - In
/root/tfb-func/enc/main.tf, putlocal.data(name= api,ports= [80, 443],tls= true), and withjsonencodecreatedata.json, and withyamlencodecreatedata.yaml. After apply, save the result read withjq -c .to/root/tfb-func/enc-json.txtand the result read withyq -o=json -I=0 .to/root/tfb-func/enc-yaml.txt. The contents of the two files must be the same. - 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/playand save the result to/root/tfb-func/convert-out.txt. - In
/root/tfb-func/compose/main.tf, put alist(string)variablenames(default api, web, db) and alist(number)variableports(default 8080, 80, 5432), useformatlistto build a list of이름=포트(name=port) lines,jointo concatenate them, andformatandlengthto make a# <개수> servicesheader (the placeholder is the count), and createservices.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
- The console reads the current directory's variables and locals, so you must run it in a directory where init is done.
- The console stops at the first error and ends with a non-zero code. When you pipe in several lines, the later lines are evaluated only if the earlier ones succeed.
- Common mistake: fixing the broken JSON in step 4. That file must be left broken for the absorption to be confirmed.
- Common mistake: writing the service count as 3 in step 8. If the input changes, only the header will be wrong.
- Function list · templatefile · try · can · jsonencode · yamlencode · Conditional expressions
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.