git clone --filter=blob:none --sparse https://github.com/calmict/book_labs.git cd book_labs git sparse-checkout set terraform-opentofu/ed1
Before learning the syntax, you need to *feel* the problem. In this exercise you build two "servers" by hand, the way it used to be done (and sadly still is) with click-ops: you will find they diverge immediately. Then you describe them as code: the same snapshot of the result for both. From there on you torture the infrastructure — you hand-edit it in the middle of the night, you delete a piece of it, you raze it to the ground — and every time a single command brings it back exactly to the model. By the end, a "server" is no longer a pet with a name and a personal history: it is a head of cattle with a tag, replaceable at any moment.
The "servers" here are plain configuration files on your disk: zero cloud, zero cost, but the concepts — drift, idempotence, convergence, immutable identity — are exactly the ones you will meet in production.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap01/solution ./run.sh
A recipe lists the steps: crack the eggs, heat the pan, pour. A photograph shows the finished dish. In chapter 1 you *felt* the drift; here you put your hands on the split that generates it: you actually write an imperative provisioning script, watch it blow up on the second run, repair it by adding guards ("if it exists, skip") — and then discover its fatal flaw: guards make the script re-runnable, but *blind*. A vandalised file walks past the guard undisturbed, because the guard checks that the file *exists*, not that it is *right*.
Then you photograph the same fleet in a main.tf and torture it from four different starting points — empty yard, half-built, vandalised, already finished — always with the same identical command. You no longer write the steps: the tool computes them, every time, by comparing reality with the model.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap02/solution ./run.sh
Facing a building that must change, the architect has two roads: renovate (the building stays up, one system gets changed) or demolish and rebuild (a new building takes the old one's place). Infrastructure works the same way, and the remarkable thing is that *you do not decide* which road is taken: the provider knows it, attribute by attribute — and the plan always announces it in advance, with precise signage that this exercise teaches you to read.
Here the "server" is for the first time a living thing: a Docker container running nginx. You change its memory and watch it remain the same object (renovation, in-place). Then you change the image version and watch it *die and be reborn* (reconstruction, replace): nobody ever stepped inside that container to upgrade nginx — this is, literally, immutability. Finally you take the lifecycle block in hand and govern the replacement: first you flip the order (build the new one, then demolish the old one), then you engage the safety catch that blocks any demolition — and discover it blocks more than you thought.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap03/solution ./run.sh
Across the last three chapters one question stayed open: when resources are many and linked, who decides *in which order* to build them? Not you — you never wrote an order anywhere. An invisible foreman decides: the dependency graph, which the tool builds by reading your code.
In this exercise you make it visible with the most honest instrument there is: a stopwatch. You build three floors of 5 seconds each *without telling the model they are a tower*: they all go up together, 5 seconds total — physically absurd, but the model does not know one floor rests on another until the code says so. Then you chain the floors with references and look at the stopwatch again: 15 seconds, one at a time. Same three resources, no "order" written anywhere: only edges born from references. Finally you look the graph in the face with tofu graph, watch the demolition proceed backwards, and try to build the one thing the foreman refuses: the cycle — the chicken born from the egg that is laid by the chicken.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap04/solution ./run.sh
Across the first four chapters you *read* HCL, guided by the comments. From this chapter on you *write* it. No more torturing the infrastructure: this is architect's desk work — filling in a skyscraper's technical datasheet using, one by one, every data type in the language: the primitives for the records, a list for the materials (where a deliberately planted duplicate will show you the difference from a set), a map for the areas, an object for the address, a tuple for the coordinates. Then you assemble everything into a text block with interpolation: the datasheet itself, which the apply deposits into a file.
The chapter closes with the humblest and most used tool of the trade: tofu fmt, put to the test on a file written by a sloppy colleague — valid but unreadable. You will discover what fmt always fixes (the form) and what it never touches (the meaning).
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap05/solution ./run.sh
Five chapters of concepts and guided exercises: now you lay your own first stone. In this exercise you write from scratch — line by line, no more placeholders — your first complete configuration: the terraform block that declares the translators, the provider block that configures them, the resources, an output. The result is not a file on disk: it is a real web service, reachable with a browser, switched on from code.
And above all you live the lifecycle in slow motion, looking where so far you rushed past: what init *really* downloads (you will go and weigh the provider binary inside .terraform: surprise), what a saved plan is and why executing it asks for no confirmation, which everyday questions are answered by state list, show and output, and what destroy demolishes — and what it leaves standing.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap06/solution ./run.sh
A serious construction site has a specification book: which standards apply, which suppliers are admitted, and a register of *exactly* which materials were chosen. In a project, the specification book is the terraform block — and in this exercise you put it to the test by breaking and repairing it: an impossible required_version slams the gate in your face (and that is a good thing: you will discover what it protects from), an exact pin shows you the birth of the lock file, and then the game gets subtle — you widen the constraint with the ~> operator and discover that *nothing changes*, until you ask for it yourself with init -upgrade.
It is the separation of powers that holds teamwork together: the *constraint* in the code is the fence (what would be acceptable), the *lock* is the choice (what we all actually use, today). The exercise closes with the July colleague: he deletes the register, re-runs init, and gets a different translator from yours — same code, months later, different provider. It is the drift of chapters 1 and 2, climbed up from the world of servers to the world of tools.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap07/solution ./run.sh
In chapter 6 you weighed the translator: a binary of tens of megabytes inside .terraform. But the binary alone is not enough: you must tell it *which world* to talk to — and that is the provider block's trade. Here you discover it in the most concrete way possible: you build a second datacenter on your machine (a Docker inside Docker: a real, separate engine, reachable over the network) and configure *two instances of the same translator* — the default line towards the Milan site and an aliased line towards the Frankfurt one. Then you place the same nginx in both, deciding the destination resource by resource with a single line: provider =.
The second half of the chapter is to be read, not executed: a gallery of real provider blocks — AWS with roles, vSphere with the password in the wrong place and then in the right one — leading to the golden rule: the code says *where* and *how* to connect, never *who you are*: secrets live outside the code, always.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap08/solution ./run.sh
The resource is the brick everything is made of, and this chapter puts it on the operating table. The first discovery is that it has two faces: the *arguments* — what you write, the input — and the *attributes* — what the resource gives back once born, the output: the id, the IP address Docker assigned it, the values nobody knew before the apply. You build a dossier that consumes precisely those outputs, and in the plan you see the trail they leave: (known after apply), the declared unknown travelling along the graph.
The second discovery completes chapter 3's lifecycle with its subtlest piece: ignore_changes. The night team changes a setting of your container by hand; the plan — faithful to chapters 1 and 2 — wants to converge it back. But this time the change is *legitimate*: that knob belongs to another process. You will sign the blind-eye contract, and learn when it is wisdom and when it is just a patch.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap09/solution ./run.sh
No construction site starts on virgin ground: there is the neighbourhood's network, the waterworks, the land registry recording what exists. So far everything in your model was created by you; in this chapter you learn to *consult* — to read what exists, belongs to others, and is not yours to manage.
The platform team created a Docker network (by hand: Phase 0, you play them). You read it with a data block — the scouts announced in chapter 9's gallery — and lean your container on it: you build *on* what you do not own. Along the way you discover chapter 9's reverse: a data source's attributes are known *already at plan* — the existing is consulted right away, there is nothing to wait for — except when the data depends on a resource yet to be born: then the read slips to the apply, and the unknown returns. You will see the two cases side by side, in the same plan. And at destroy, the proof that closes the chapter: what you read is not yours — the platform's network survives intact.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap10/solution ./run.sh
For ten chapters you have used plan and apply, and one character has been working in the shadows at every command: the notebook where the tool records what it built and what it was called in the code. In this chapter you open it and read it: terraform.tfstate, the mapping between the model's addresses and the real objects — with even the graph's edges inside.
Then three discoveries, in crescendo. The first one burns: you create a password marked sensitive, the output hides it — and the notebook keeps it *in plain text*: whoever reads the state reads every secret. The second is the game of the three sources of truth: code, memory, reality — you delete a container behind the model's back and learn the command that syncs *only the memory* (plan and apply -refresh-only), separating "update the notebook" from "touch the world". The third is the finale that sets up chapter 12: a colleague clones your code but not your memory — his plan wants to rebuild everything, his apply crashes into the reality that already exists, and his notebook is left half-written. Same code, two memories, one reality: it is the problem that only *shared* state solves.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap11/solution ./run.sh
Chapter 11 closed on an incident: two colleagues, two notebooks, one contested reality. The solution has a name — backend — and it is the answer to the question "where does the state live?". In this exercise you build the answer: the platform team (you again, helmet on) switches on the site's noticeboard — a Consul in a container, a real remote backend — and you *move* your notebook in there with the official manoeuvre: the backend block plus init -migrate-state. You will verify in person that the local file emptied and that the state now lives in the backend (with, still in plain text, what you know from chapter 11 inside: the house changed, the custody rules did not).
Then the colleague returns — and this time the story is different: he attaches to the same backend and his first plan says No changes: *he sees your resources*, because he reads your very memory. And the grand finale: while one of your applies is running, he tries to work — and the lock stops him, with a full name tag: Error acquiring the state lock, stating who holds it and for which operation. Chapter 11's chaos has become an orderly queue.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap12/solution ./run.sh
A building without fire doors burns all at once. A project with a single state does too: in this exercise you first build the *monolith* — network and application in the same notebook — and measure the blast radius: the network team renames its own network, and the plan shows the fire spreading all the way to the app's container; meanwhile, a single lock queues everyone, whatever they are working on.
Then you install the fire doors: two configurations, two notebooks (in the Consul you know from chapter 12), and the official channel to make them talk — terraform_remote_state, the data source that reads another state's *outputs*. You close with the two containment proofs: the most destructive command in existence, launched in the app's room, cannot even see the network; and a slow app apply no longer blocks the network's plan — two queues, two locks, two teams genuinely working in parallel.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap13/solution ./run.sh
So far you have written configurations with the values *hard-coded* inside: the container name, the port, all fixed in the file. That works for a single building. But the same project must serve dev, staging and prod — and rewriting the file for each is the photocopy chapter 1 taught us to fear.
This chapter installs three doors in the configuration. The front door — *variables* — lets values in from outside: whoever uses the module chooses environment and external_port without touching the code. The service door — *outputs* — shows the outside only what it promises: here, the URL where the service answers. And the internal kitchen — *locals* — has no door onto the world: it is where the container name is *derived* once (cap14-web-${var.environment}) and reused everywhere.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap14/solution ./run.sh
So far every resource was a one-off, written by hand. But a neighbourhood has a hundred identical houses, and nobody writes them one by one. This chapter gives you the two ways to *multiply* a resource — and shows you why the choice between them is one of the most important you will make.
The first way counts by number: count. You give a number, and get that many copies, indexed [0], [1], [2]. Handy, immediate — and with a hidden trap. Each copy's identity is its *position*: house number 2. Remove a house in the middle of the row, and every house after it *shifts down a number*: 3 becomes 2, 4 becomes 3. Terraform, which ties identity to position, thinks you renamed half the fleet — and rebuilds it. You will see it in your own plan: a single removal, and the fire spreads down the tail.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap15/solution ./run.sh
The last chapter closed with a promise: the collections you hand to count and for_each often have to be *prepared* first — cleaned, transformed, filtered. This chapter gives you the tools to prepare them, and a bench to test the tools before you fit them.
The tools are the functions: HCL has about a hundred, ready-made (you cannot write your own in the classic way — you take what is there). They transform a string (lower, trimspace, split), count and combine collections (length, merge, keys), convert them (toset, tolist, jsonencode). A function takes input in parentheses and returns a value: nothing else, no side effects.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap16/solution ./run.sh
For sixteen chapters you have written the same pattern: variables at the top, resources in the middle, outputs at the bottom. And every time you rewrite it from scratch, folder after folder. A real architect does not redraw the same building for every lot: they design it once as a prefab, then drop it into the city as many times as needed, each with its own finishing.
The prefab, here, is the module: a folder with .tf files, but seen as a *box with doors*. The input doors are its variables (name, environment, port); the machinery inside is the resources (image and container); the output doors are its outputs (the URL where it answers). Whoever uses the box does not look inside: they pass inputs to the input doors, and read results from the output doors. They are exactly chapter 14's variables and outputs — but promoted to the *interface* of a reusable component.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap17/solution ./run.sh
Chapter 17 closed with a trap: by wrapping resources in a module you changed their *address*, and chapter 15 warned us the address *is* the identity. Rename a resource in the code, and Terraform does not see a new nameplate: it sees a resource gone and a new one born — it demolishes and rebuilds. For a container that is an annoyance; for a production database it is a disaster.
But chapter 11's notebook — the state — is only a *map* between addresses in the code and real objects. And a map can be corrected without touching the territory. This chapter gives you four ways to change the paperwork without touching the buildings:
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap18/solution ./run.sh
Dev, staging, prod: the same design, three different cities. Copy-pasting the configuration three times is the photocopy chapter 1 taught us to fear — but an environment is not just "the same code with a different name". An environment is an *isolated copy* of the same infrastructure, with its own settings and — above all — its own state. And chapter 13 already shouted it: environments must never share the blast radius. A mistake in dev must not be able to queue, corrupt or destroy prod.
This chapter compares the two main strategies, and puts both in your hands.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap19/solution ./run.sh
For nineteen chapters we have said "one language, two binaries": every tofu command has its terraform twin. This chapter keeps the promise and draws the boundary. Because the two binaries *are* twins — born from the same code — but from a certain point on they took different roads.
The history in brief (20.1): in 2023 HashiCorp changed Terraform's licence, from open source to a restrictive one (the BSL). The community reacted with a *fork* — a copy of the code that sets off on its own — placed under the Linux Foundation and renamed OpenTofu, with an open licence (MPL). From there, two twin binaries that share almost everything and diverge on a little.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap20/solution ./run.sh
An architect does not hand over a design because it "looks right": they run it through a ladder of checks, from the cheapest and most frequent to the most expensive and rare. It is the validation pyramid, and it has four floors.
At the *base*, wide and instant, two checks you run constantly: fmt verifies the drawing is legible (the form), validate that it is internally consistent (references resolve, types match). They cost milliseconds; you run them on every save.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap21/solution ./run.sh
For twenty-one chapters you ran the commands: plan, apply, test. This chapter — the last — takes them out of your hands and puts them on a conveyor belt. In one end goes a commit; out the other comes infrastructure in production. And along the belt, automatically, chapter 21's pyramid fires: form, consistency, security, behaviour. Nobody applies by hand; nobody forgets a check.
The belt has two stretches. The first is CI (Continuous Integration): on every *proposed* change — a pull request — the belt runs the checks and produces a *plan*, the broadcast of what would change. It is the gate: if a check fails, the door stays shut, and nobody argues with the machine. The second is CD (Continuous Delivery/Deployment): when the change is *approved and merged* into the main branch, the belt runs the apply. Proposing and delivering become two separate, automatic acts — plan on the PR, apply on the merge.
Fill in the TODOs in the starter files, then run the solution's test:
cd terraform-opentofu/ed1/cap22/solution ./run.sh