Hands-on Terraform training, driven by the
dsoxlab CLI. This repository is the
lab catalog behind the Terraform track of
www.stephane-robert.info,
geared toward the Terraform Associate (004) and Terraform Authoring and
Operations Professional certifications.
terraform-dsoxlab-training is a content repository, not an application. It
provides:
- guided labs whose brief describes a situation, never a set of steps;
- challenges with no step-by-step, to test autonomy;
- capstones aligned with the six objectives of the Professional exam;
- automated validation that proves the state the configuration computes, not that a command was typed;
- scoring with cost-weighted hints.
The dsoxlab CLI is the single entry point: it sets a lab up, shows the brief,
validates, scores and reports. It lives in its own repository and is
installed separately.
- Python 3.11+ and
uv gitterraform(ortofu) on the PATH: every lab.libvirtdand thedmacvicar/libvirtprovider: thefirst-infrasection only, which provisions real local resources.- Docker and Floci,
a local MIT-licensed AWS emulator (
floci/floci:1.6.0): theawssection and a few AWS-targeting labs. Thehashicorp/awsprovider points at Floci through anendpointsblock: no AWS account, no bill. Those labs declare Floci inruntime.services, and dsoxlab starts it for you. - Docker, for the
vault-secretslab: it starts ahashicorp/vault:1.21server in development mode, declared the same way. - An HCP Terraform token: exactly one lab out of 88 requires one, and it
is optional. Without a token its tests skip instead of failing. See
docs/hcp-token.md.
No VM, no dsoxlab provision: unlike the Ansible and Linux catalogs, Terraform
runs on the learner's own machine. Every lab is runtime: shell.
dsoxlab is published on PyPI. Install it
as a standalone tool:
# 1. Install the dsoxlab CLI (external tool, stays out of this repo)
uv tool install dsoxlab # or: pipx install dsoxlab
# 2. Clone this lab catalog
git clone https://github.com/stephrobert/terraform-dsoxlab-training.git
cd terraform-dsoxlab-training
# 3. Check the contract is valid
dsoxlab validate-structuredsoxlab list-labs # browse the catalog
dsoxlab run getting-started-terraform-workflow # set the starting state
dsoxlab challenge getting-started-terraform-workflow # read the mission
# ... you work in challenge/work ...
dsoxlab check getting-started-terraform-workflow # validate and scorerun creates the lab's work directory and copies the declared fixtures into it.
Everything then happens in challenge/work: it is the only place you change,
and dsoxlab clean removes it.
Stuck? dsoxlab hint <id> reveals a hint, whose cost is deducted from the score.
git pull # the catalog
uv tool upgrade dsoxlab # the engineThe two evolve separately. A lab that fails after a Terraform upgrade is a defect
in the catalog: open an issue, and dsoxlab support --issue arrives pre-filled.
The catalog is described by data, not code, which keeps the dsoxlab engine
domain-agnostic:
meta.ymlat the root declares the repository identity and the order of the sections shown bylist-labs;lab.yamlper lab declares itsskills,level,runtime(type, fixtures, services),distros,doc_urland avalidationblock. Alab.fr.yamloverridestitleanddescriptionin French, and nothing else.
dsoxlab validate-structure checks the whole contract: meta.yml is well
formed, every referenced lab exists with a valid lab.yaml, every declared test
and fixture file is really there, and every relative link in a README leads
somewhere.
dsoxlab list-labs # browse the catalog
dsoxlab show <id> # metadata and status of one lab
dsoxlab run <id> # set the starting state
dsoxlab challenge <id> # read the mission, no step-by-step
dsoxlab hint <id> # reveal a hint (deducted from the score)
dsoxlab check <id> # run the tests, compute and record the score
dsoxlab clean <id> # remove the work directory
dsoxlab progress # per-section progress, average score| Runtime | What the lab asks for |
|---|---|
shell |
a terminal and terraform. The tests read the state your configuration computes, on your own machine. |
shell + floci |
Docker as well: dsoxlab starts the AWS emulator declared in runtime.services, and stops it with the session. |
shell + HCP account |
a single, optional lab, which runs a real remote run on HCP Terraform. |
Validation proves the state, it does not trust the learner. Each lab ships
pytest tests under challenge/tests/ that query terraform show -json and
terraform output -json: what the configuration COMPUTES, never what a file
contains.
And a lab is proven in both directions: the tests must fail before the work
and pass after it. A test that is green before the work is not a test, it is an
assumption about the setup. The root conftest.py replays the reference solution
before the tests in instructor mode, to prove the solution itself is correct;
dsoxlab check goes through the learner's path instead.
Reference solutions live under solution/, encrypted with ansible-vault: a
solution shipped in clear text spoils the lab, and git keeps it forever.
check records a score (tests passed out of total, minus the cost of any hints
revealed). Hints are base64-encoded in challenge/hints.yaml, so that
opening the file does not give them away, and their cost grows with their
precision. History lives in a SQLite database local to this repository.
Labs live under labs/ and are ordered by meta.yml. The table below is
generated from the real lab.yaml files: run python3 scripts/gen_catalog.py
to refresh it.
| Lab (id) | Title | Level | Certif | Runtime | Companion guide |
|---|---|---|---|---|---|
getting-started-terraform-overview |
Prove that Terraform has a memory | l1 | TF-ASSOCIATE | shell | guide |
getting-started-declarative-vs-imperative |
Prove idempotence where the imperative script diverges | l1 | TF-ASSOCIATE | shell | guide |
getting-started-terraform-vs-opentofu |
Prove Terraform / OpenTofu compatibility, and where it stops | l1 | TF-ASSOCIATE | shell | guide |
getting-started-install-terraform |
Pin the CLI version and lock the providers | l1 | TF-ASSOCIATE | shell | guide |
getting-started-cli-terraform |
Make fmt, validate and the outputs agree | l1 | TF-ASSOCIATE | shell | guide |
getting-started-terraform-workflow |
Read a plan before applying it | l1 | TF-ASSOCIATE | shell | guide |
getting-started-providers-resources-data-sources |
What Terraform manages, and what it merely reads | l1 | TF-ASSOCIATE | shell | guide |
getting-started-terraform-project-structure |
Split a monolith without moving the plan | l1 | TF-ASSOCIATE | shell | guide |
| Lab (id) | Title | Level | Certif | Runtime | Companion guide |
|---|---|---|---|---|---|
first-infra-first-infrastructure |
First infrastructure: the full cycle, proven | l1 | TF-ASSOCIATE | shell | guide |
first-infra-variables-outputs |
Variables, locals and the real precedence order | l1 | TF-ASSOCIATE | shell | guide |
first-infra-virtual-network |
The dependency Terraform cannot guess | l1 | TF-ASSOCIATE | shell | guide |
first-infra-vm-libvirt |
Update in place or replacement: read it in the plan | l1 | TF-ASSOCIATE | shell | guide |
first-infra-ansible |
Produce an Ansible inventory from the state | l1 | TF-ASSOCIATE | shell | guide |
first-infra-debug-apply |
Resume after a failed apply, without redoing the work | l1 | TF-ASSOCIATE | shell | guide |
first-infra-clean-destroy |
Destroying cleanly, and the four things it covers | l1 | TF-ASSOCIATE | shell | guide |
| Lab (id) | Title | Level | Certif | Runtime | Companion guide |
|---|---|---|---|---|---|
write-code-providers |
Explicit source and provider alias | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-declare-resources |
Read a resource's lifecycle in the plan | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-variables |
Variables typing, validation and precedence | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-outputs |
The secret the output does not hide | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-locals |
The local that is not resolved at plan time | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-data-sources |
When exactly does Terraform read a data source? | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-expressions |
What expressions really compute | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-functions |
Compose values with HCL functions | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-provider-defined-functions |
Provider-defined functions | l2 | TF-PROFESSIONAL | shell | guide |
write-code-conditionals |
The configuration that refuses absurd values | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-validation-check-preconditions |
Custom conditions: precondition, postcondition and check blocks | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-count |
count indexes by position, and the position lies | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-for-each |
Add an instance without destroying the others | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-for-loops |
Transform a catalog with for expressions | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-dynamic-blocks |
Generate blocks, and know when not to | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-depends-on |
depends_on belongs only where a reference cannot go | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-lifecycle |
The lifecycle block decides the order, not you | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-tfvars-files |
The typo that breaks nothing | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-version-constraints |
Version constraints and the lock file | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-style-guide |
The config that works but no CI accepts | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-sensitive-data-sensitive-values |
When sensitivity breaks for_each | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-sensitive-data-ephemeral-values |
The value that never touches the state | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
write-code-sensitive-data-write-only-arguments |
Write-only arguments: a secret that never lands in the state | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell + floci | guide |
write-code-sensitive-data-vault-secrets |
Read secrets from Vault | l2 | TF-PROFESSIONAL | shell + vault | guide |
| Lab (id) | Title | Level | Certif | Runtime | Companion guide |
|---|---|---|---|---|---|
state-understand-state |
Take over a secret already in service, without regenerating it | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
state-backends |
Migrate the state without breaking its lineage | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
state-state-locking |
State locking, what it really blocks | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
state-terraform-state-list |
terraform state list, the address is the identity | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
state-terraform-state-show |
terraform state show, and what the summary hides | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
state-terraform-state-mv |
terraform state mv and the moved block, refactor without destroying | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
state-terraform-state-rm |
terraform state rm and the removed block, stop managing without destroying | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
state-removed-block |
The removed block: bequeath an infrastructure without destroying it | l2 | TF-PROFESSIONAL | shell | guide |
state-backup-restore-state |
Restore an amputated state: pick the right backup, prove nothing was recreated | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
state-diagnose-state |
Diagnose a drift and adopt an orphan resource, without losing its value | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
| Lab (id) | Title | Level | Certif | Runtime | Companion guide |
|---|---|---|---|---|---|
modules-create-modules |
A reusable module configures no provider of its own | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
modules-module-structure |
Refactor a monolith into the standard module structure | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
modules-module-variables-outputs |
A module interface is a contract, not just types | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
modules-module-local |
A local module is read in place, not installed | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
modules-module-registry |
A registry module is downloaded, versioned, and never locked | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
modules-version-modules |
Publish and consume module versions with Git tags | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
modules-test-module |
Prove a module with terraform test, mutations included | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
modules-module-best-practices |
Make a module composable, and prove it from the plan JSON | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
modules-module-anti-patterns |
Refactor a copy-pasted project without destroying anything | l2 | TF-ASSOCIATE · TF-PROFESSIONAL | shell | guide |
| Lab (id) | Title | Level | Certif | Runtime | Companion guide |
|---|---|---|---|---|---|
environments-organize-terraform-repo |
Split a monolithic configuration, and prove the plan did not move | l3 | TF-PROFESSIONAL | shell | guide |
environments-separate-environments |
Two roots, two states, one shared module | l3 | TF-PROFESSIONAL | shell | guide |
environments-per-environment-variables |
Which value wins, and how to prove it | l3 | TF-ASSOCIATE | shell | guide |
environments-workspace |
One directory, three states that never see each other | l3 | TF-ASSOCIATE | shell | guide |
environments-when-to-use-workspaces |
Workspaces or separate configurations, and what the split costs | l3 | TF-PROFESSIONAL | shell | guide |
environments-monorepo-vs-repo-per-stack |
Split a monorepo into two stacks that talk to each other | l3 | TF-PROFESSIONAL | shell | guide |
environments-terraform-in-automation |
Run Terraform in automation (CI/CD) | l3 | TF-PROFESSIONAL | shell | guide |
| Lab (id) | Title | Level | Certif | Runtime | Companion guide |
|---|---|---|---|---|---|
aws-provider-aws-first-ec2 |
AWS provider: authentication, endpoints and default tags | l3 | TF-ASSOCIATE | shell + floci | guide |
aws-sg-subnet-instance |
Security group: dedicated rules, for_each and a deterministic subnet | l3 | TF-ASSOCIATE | shell + floci | guide |
aws-iam-role-policy-instance-profile |
Compose the IAM chain, and name its two policies correctly | l3 | TF-PROFESSIONAL | shell + floci | guide |
aws-backend-s3-remote-state |
A remote state, locked, and read by another stack | l3 | TF-PROFESSIONAL | shell + floci | guide |
aws-launch-template-autoscaling |
Read a replacement in the plan, before it happens | l3 | TF-PROFESSIONAL | shell | guide |
aws-import-moved-drift |
Import, moved and drift: the three traps a tutorial never shows | l3 | TF-PROFESSIONAL | shell + floci | guide |
| Lab (id) | Title | Level | Certif | Runtime | Companion guide |
|---|---|---|---|---|---|
hcp-terraform-hcp-terraform-overview |
The run workflow: played in two steps, then qualified | l3 | TF-PROFESSIONAL | shell | guide |
hcp-terraform-hcp-workspaces |
Workspaces: one word, two meanings, two attachment strategies | l3 | TF-PROFESSIONAL | shell | guide |
hcp-terraform-remote-runs |
The stream a run sends back, and the three ways to start one | l3 | TF-PROFESSIONAL | shell | guide |
hcp-terraform-variable-sets |
Fifteen precedence levels, and one inversion | l3 | TF-PROFESSIONAL | shell | guide |
hcp-terraform-shared-credentials |
Credentials: neither in the code, nor in the state | l3 | TF-PROFESSIONAL | shell + floci | guide |
hcp-terraform-projects-teams |
Permissions add up, they do not override | l3 | TF-PROFESSIONAL | shell | guide |
hcp-terraform-policy-as-code |
Policy as code: what blocks a run, and who can override it | l3 | TF-PROFESSIONAL | shell | guide |
hcp-terraform-premier-run-distant |
The first remote run, for real (optional, needs an account) | l3 | TF-PROFESSIONAL | shell + HCP account | guide |
| Lab (id) | Title | Level | Certif | Runtime | Companion guide |
|---|---|---|---|---|---|
certifications-associate-essential-commands |
The commands the exam expects, done rather than recited | l4 | TF-ASSOCIATE | shell | guide |
certifications-associate-mock-004 |
Associate 004: mock exam | l4 | TF-ASSOCIATE | shell | guide |
| Lab (id) | Title | Level | Certif | Runtime | Companion guide |
|---|---|---|---|---|---|
certifications-professional-capstone1-resource-lifecycle |
Pro · Objective 1: resource lifecycle, import and drift reconciliation | l4 | TF-PROFESSIONAL | shell + floci | guide |
certifications-professional-capstone2-dynamic-config |
Pro · Objective 2: dynamic configuration and troubleshooting | l4 | TF-PROFESSIONAL | shell | guide |
certifications-professional-capstone3-collaborative-workflows |
Pro · Objective 3: collaborative workflows | l4 | TF-PROFESSIONAL | shell + floci | guide |
certifications-professional-capstone4-modules |
Pro · Objective 4: create, maintain and use modules | l4 | TF-PROFESSIONAL | shell | guide |
certifications-professional-capstone5-providers |
Pro · Objective 5: configure and use providers | l4 | TF-PROFESSIONAL | shell + floci | guide |
certifications-professional-capstone6-hcp |
Pro · Objective 6: HCP Terraform, where the sub-objectives cross | l4 | TF-PROFESSIONAL | shell | guide |
certifications-professional-mock-pro |
Pro · Integrative mock exam: the six objectives in one sitting | l4 | TF-PROFESSIONAL | shell | guide |
| Lab (id) | Title | Level | Certif | Runtime | Companion guide |
|---|---|---|---|---|---|
level-exams-write-provision |
Level A exam: write and provision a demo environment | l2 | - | shell | guide |
level-exams-operate-state |
Level B exam: take over the operation of a platform | l3 | - | shell | guide |
level-exams-modularize |
Level C exam: turn copy-pasted code into a versioned module | l3 | - | shell | guide |
91 labs, table generated by scripts/gen_catalog.py.
Contributions are welcome: read CONTRIBUTING.md, which describes the real anatomy of a lab in this repository and the golden rule, a lab is proven in both directions. The code of conduct applies to every exchange, and vulnerabilities are reported privately: SECURITY.md.
This content is published under Creative Commons Attribution 4.0 International (CC BY 4.0). You may share and adapt it, including commercially, provided you credit the source.