Authoring guide¶
How this workshop is built. Read before authoring a section, a lab, or a module. Contribution workflow (issues, PRs, the small-fix fast path) lives in CONTRIBUTING; this page is the authoring contract itself.
Non-negotiable guardrails¶
- Vendor-neutral. No employer, customer, or corporate names anywhere — slides, labs, code, assets, commits, docs. Product names (OpenTofu, Terraform, LocalStack, Spacelift, …) are fine when technically relevant.
- No tooling attribution. Never name editors, generators, or AI assistants in
commits, code, or docs. No
Co-Authored-Bytrailers. - Label AI imagery. Every AI-generated image carries a visible
AI generatedfooter (thesection-coverlayout does this automatically). - OpenTofu-first. Teach the
tofuCLI. HCL's top-level block is stillterraform {}, but prose, commands, and output saytofu. Note Terraform compatibility; don't run a parallel Terraform track. - Stay current. Track current OpenTofu behaviour and versions. Verify version claims against upstream release notes at authoring time.
Published site (GitHub Pages)¶
The live site is MkDocs Material at / plus hash-routed Slidev under
/deck/ (superset), /deck/3day/, and /deck/templates/. Build/preview locally
with task pages:build / task pages:preview (needs
python3 -m pip install -r docs/requirements-docs.txt). Contract tests:
pnpm test:pages / task test:pages.
Repository map¶
versions.env— toolchain pin SSoT (OpenTofu, Go, LocalStack, Terramate). Bump versions here;scripts/verify.shsection 10 catches consumer skew (Taskfile, docker-compose, Terratest Dockerfile, CI workflow literals, bootstrap).scripts/deck-manifest.mjs— section metadata SSoT (id, slug, title, tier, day, canonical cut, status, timings, Day-1 fit-plan markers). Syllabus and runbook tables are validated against it in CI.scripts/generate-decks.mjs— emits generated root decks from the manifest. Runpnpm decks:generateafter manifest edits;pnpm decks:checkfails on drift.- Facilitator launcher:
pnpm deck -- --day 1,--section S05, or--range S05-S09(task deck -- …) starts a deterministic selection.pnpm deck -- --listdiscovers choices. With a TTY,gumadds a menu; without it, explicit flags work everywhere. A noninteractive invocation with no selector fails instead of opening the superset.--dry-runwrites gitignored.deck-selection.mdwithout starting Slidev. slides.md/slides-3day.md/slides-day-1.md/slides-day-2.md/slides-day-3.md— generated root decks (header:Generated by scripts/generate-decks.mjs … Do not edit.). Mostly frontmatter +src:import blocks. Do not hand-edit tier tokens or hide flags here — changescripts/deck-manifest.mjsand regenerate.slides-templates.md— hand-authored templates gallery (not manifest-generated).pages/SNN-topic/index.md— one self-contained section: asection-coverdivider + content slides. Never reference another section's slide numbers, and never embed a lab body — reference labs by path.labs/day-N/NN-topic.md— standalone labs (see contract below).modules/+examples/— the runnable OpenTofu (see the module DoD below).variant/ovh/— the OVHcloud Public Cloud variant track, a self-contained lane outside the base discovery globs; its own entry isvariant/ovh/README.md.theme/— the local Slidev theme:layouts/,components/(IacIcon,KwCard,KwChip,CodeNote,CodeCallout,ArchBox),styles/theme.css.components/— animated Vue teaching diagrams (stepprop bound to$clicks; clamp out-of-rangestep— never throw or blank a slide). Auto-imported into every deck from the repo root. Current set:PlanApplyFlow— the core workflow lit stage-by-stage: config → plan → apply → state. Props:step?: number(0–4, bind:step="$clicks"; clamped). step 0 lights nothing, step 4 lights all four.TestPyramid— the testing pyramid built bottom-up (static → unit → integration → e2e). Props:step?: number(0–4, base-first, clamped) plus per-layer label listsstaticTools/unitTools/integrationTools/e2eTools(string[], default[]→ bare bands). Reused by S12 and S18 with their own tool sets.StateEncryptionFlow— client-side state encryption lit stage-by-stage: plaintext state → PBKDF2 key provider → AES-GCM method → ciphertext. Props:step?: number(0–4, bind:step="$clicks"; clamped). step 0 lights nothing, step 4 lights all four. S05's headline visual.DependencyGraph— a resource/module DAG revealed in dependency (topological) order:random_pet→module.svc_a/module.svc_b(one module consumed twice) →local_file. Props:step?: number(0–8, bind:step="$clicks"; clamped) — a source node, then its edge, then the dependent node, per click. step 0 lights nothing, step 8 lights all. The S02 (references between blocks) / S07 (module composition) visual.StateReconcile— the desired / state / actual reconciliation model lit stage-by-stage: desired → state → actual → refresh → reconcile. Props:step?: number(0–5, bind:step="$clicks"; clamped). step 0 lights nothing, step 5 lights all five. S04's headline visual; reused in S09 when reading a plan diff.MockProviderFlow— converting an apply-shaped test into a mocked plan contract lit stage-by-stage: apply run → mock_provider → plan run → mock_resource → override_resource. Props:step?: number(0–5, bind:step="$clicks"; clamped). step 0 lights nothing, step 5 lights all five. S17's headline visual.TerramateOrchestration— the Day-3 Terramate pipeline lit phase-by-phase: discover → generate → order → filter. Props:step?: number(0–4, bind:step="$clicks"; clamped), optionalphase?: 'discover' | 'generate' | 'order' | 'filter'orhighlight?: number(0-based) to emphasise one stage without suppressing others. step 0 lights nothing, step 4 lights all four. S20 shows the full loop; S21–S24 passphaseto highlight one verb.scripts/— the gates and generators beyond the SSoT trio above:verify.sh(drift/fmt/validate/test),lab-contract.mjs,lab-inventory.mjs,link-check.mjs, deck/pages/quiz/supply-chain test suites, export tooling.tests/shell/— bats suites for the shell plane (lab.bats,ci-contract.bats, stubs) coveringsetup/*.shand the CI contract.setup/— learner/facilitator environment:bootstrap.sh(toolchain install),lab.sh+docker-compose.ymlglue, LocalStack notes (localstack.md,localstack-k8s.yaml), and the Terratest container image (setup/terratest/Dockerfile).docs/_generated/lab-inventory.json— generated lab-inventory snapshot;node scripts/lab-inventory.mjs --checkfails on drift,--writerefreshes.quiz/— portable participant question bank (questions.json+questions.schema.json;pnpm quiz:validate,pnpm test:quiz).supply-chain/exceptions.json— dependency-audit exception list consumed by the npm-audit gate (pnpm dep-audit,pnpm test:supply-chain).public/— static deck assets served at the site root:branding/,covers/(AI-labelled section art),icons/, favicon.docs/— MkDocs site sources (syllabus, setup, runbook, this guide) plus tracked references that are not published (e.g.decisions/).docs/decisions/— tracked ADRs.docs/facilitator-runbook.md— delivery guide for facilitators (timing cuts, morning checklist, known traps)..github/— CI workflows (ci.yml,release.yml, …), PR template, issue templates.
Gate-name legend (US-*)¶
Gate and check names like US-F-TIERS or US-P-PINS in Taskfile.yaml,
scripts/verify.sh, and .github/workflows/ci.yml are the IDs of the internal
backlog stories that introduced them — kept as stable labels so a gate can be
traced to its origin. The backlog itself is untracked planning material; treat
the IDs as opaque gate names.
Design system¶
Reuse the layouts; never invent a per-slide layout. Available: cover,
section-cover, agenda, statement, code-walkthrough, code-annotated,
comparison, two-cols-code, topology, lab, recap. Patterns:
magic-move— grow an HCL manifest field-by-field, or morph HCL → plan → shell.CodeNote— click-synced side rail explaining highlighted lines ({none|1-2|...}line steps sync withat="N").CodeCallout— floating overlay that labels a risky line in place.IacIcon— one badge per HCL construct (kind="resource|module|state|test|…"). Use a glyph where a slide names a specific construct; keep emoji for conceptual/decorative cards. Over-conversion is a defect.
Section headers & tiers¶
Every section sets section: 'NN', day: Day N,
tier: core|recommended|optional in its pages/SNN-*/index.md frontmatter. Tier,
day, title, and cut membership live in scripts/deck-manifest.mjs; generated
decks import sections under # SNN · Title · tier · Day comments produced by
pnpm decks:generate. Tiers must match frontmatter and obey
hide:true ⟺ optional in slides-3day.md — pnpm decks:check and
task verify (US-F-TIERS) enforce both.
Rationale: tier meanings in ADR 0008;
manifest model in ADR 0014.
Lab authoring contract¶
Flat file labs/day-N/NN-topic.md, one per section. Every lab:
- Opens with a header table: Section, Environment (
localstack ✓ / mock ✓ / real-aws (optional)), Estimated time. - Has Objective, Prerequisites, Files used, numbered Steps, Expected observations, Cleanup / panic reset, optional Stretch.
- Idiot-proof: every task and question ships a
<details><summary>spoiler with the exact command / expected output. - Break → fix: show the failure, then the fix (e.g. enforced-plaintext error →
add
fallback). - Single source of truth: the HCL a slide teaches is the file the lab applies. The next lab continues the same project — by carrying its spine addresses forward, not by being a file superset (see below).
- Panic reset is always safe:
task lab:down+tofu destroyleaves no residue.
The evolving project: service-manifest¶
Every hands-on stage grows one project, service-manifest — the child
module already at labs/day-1/07-modules/modules/service-manifest/ is the
name's source of truth (svc-manifest is informal shorthand). The published
stage→section map is canonical in
docs/syllabus.md · The evolving project — edit it there
first; the rule below is the authoring-side copy.
A shared mutating directory is impossible here for two physical reasons: every
lab must run standalone from its own tracked workdir (task lab:validate
DIR=labs/day-N/NN-topic, below), and scripts/verify.sh byte-compares an
annotated block against the whole source file, so a directory that mutates
between sections has no stable snapshot to cite. Continuity is therefore carried
by addresses:
- Project spine — carried forward, never renamed, never silently dropped:
local_file.manifest,variable "service",variable "environment",output "manifest_path". Once introduced, every later Day-1 stage declares it — except stage 8, where S07 extracts the spine into./modules/service-manifestand the root reaches it through the instances (module.checkout.local_file.manifest, each instance'smanifest_path,service/environmentpassed as module arguments). Names unchanged, prefix moved; the extraction is the lesson, so stage 8 is framing only — never a structural edit. - Auxiliary demonstration resources (e.g.
local_file.summary, which only gives the dependency-graph beat a second node) may be retired — but only explicitly, with the lab preamble naming what was retired and why. A silent disappearance is a defect. - A stage conforms when its diff from the previous stage reads as spine + an explicit auxiliary delta.
- Where the spine's first
variablelands — decided (US-C-STAGE-D1a). The spine arrives in two instalments:local_file.manifestandoutput "manifest_path"at stage 1,variable "service"andvariable "environment"at stage 4 (labs/day-1/06-variables/), where S06 teaches typed inputs. Stage 2 teaches thevariableblock type withvariable "owner"— auxiliary, non-spine name, retired explicitly at stage 3, which still declares no variables. Do not "fix" that by adding spine inputs to stage 3: the alternative was rejected because it forces S06 to re-type a spine address fromstringtoobject({…}). Rationale and revert live in the lane's decision note; the canonical statement is indocs/syllabus.md.
Do not author to a file superset. "Stage N is stage N−1 plus a delta" is
contradicted by the tree at six of the seven Day-1 transitions — 2→3 drops
variable "owner", locals, data.local_file.motd and module "greeting";
3→4 drops random_pet.env and local_file.summary; 4→5 drops
variable "api_token" and the outputs effective_environment and api_token
(and brings random_pet.env back); 5→6 drops both guard variables and the
precondition/postcondition/check blocks they fed; 6→7 drops
random_pet.env, output "db_password" and the explicit backend "local"
block, introducing variable "state_passphrase"; 7→8 drops
variable "state_passphrase" and random_password.session, moving the spine
into ./modules/service-manifest. Only 1→2 retires nothing. S04 and S05 also
teach deliberately against a small config. Judge a stage by the spine rule,
never by counting files. Every one of those drops is named in the receiving
lab's ### Continuity preamble — that is where a learner's "where did it go?"
is answered, and an unnamed drop is a defect.
Showing the transition on a slide: use the drift-checked pattern the repo
already ships — a code-walkthrough whose magic-move container holds
consecutive annotated fences, each byte-checked against its own whole file.
Reference: slides-templates.md:123-172 over
labs/fixtures/templates-demo/naming-step-{1,2,3}.tf. Step snapshots belong
under labs/fixtures/ (carve-out below) — never as an excerpt of a lab workdir
file, which the whole-file gate rejects.
Lab workdir & drift contract¶
A lab's runnable HCL lives as tracked files in a sibling workdir, not as
heredocs the learner pastes into $HOME. For a lab labs/day-N/NN-topic.md,
put its config under labs/day-N/NN-topic/ (e.g. labs/day-1/09-best-practices/main.tf)
and reference those files by path from the prose. The heredoc-into-$HOME
pattern is never the primary flow — a learner should be able to
task lab:validate DIR=labs/day-N/NN-topic against real, tofu fmt-clean files.
The lab:* tasks all take DIR=labs/day-N/NN-topic (see Taskfile.yaml).
Carve-out:
labs/fixtures/is reserved forscripts/verify.shdrift fixtures (e.g.labs/fixtures/drift-demo/,labs/fixtures/templates-demo/). It is an intentional exception to thelabs/day-N/NN-topicconvention and is not a workshop section — never number it into the section namespace.
To make "slide↔lab single source of truth" CI-verifiable, tie a fenced
hcl block in labs/** or pages/** to its source file with an HTML-comment
marker on the line immediately above the fence (shown indented so the inner
fences render):
<!-- source: labs/fixtures/drift-demo/main.tf -->
```hcl
terraform {
required_version = ">= 1.9"
}
```
scripts/verify.sh scans labs/**/*.md, pages/**/*.md, and
slides-templates.md, diffs each annotated block body against its source file,
and fails the build, naming the file, on any drift (or if the file is
missing). Rules:
- Annotated block → diffed against its source; drift is a build failure.
- Unannotated
hclblock → ignored (scratch/inline teaching HCL, or a partially-authored lab). A lab with only unannotated blocks warns, never fails, so in-flight work never blocks unrelated lanes. - Line endings must be LF. Lab
.mdand.tffiles are enforced to LF by the repo-root.gitattributes; the drift check also strips\rupstream so a stray CRLF cannot silently disarm detection. Only the block-body comparison additionally normalises a lone trailing newline — it is not a licence to author CRLF. - The marker
<!-- source: … -->and the opening fence are expected at column 0 (top level). Bare```hcland magic-move fences with metadata (```hcl {none|…}) are both accepted — only thehcllanguage tag matters; the{…}info string is ignored. A block written inside a list/indented context keeps its indentation in the body, so it will diff against the raw file only if the file is indented identically — author drift-checked blocks at top level. - The block body must be byte-identical to the file. Generate the block
from the file — never hand-sync the two.
labs/fixtures/drift-demo/is the reference example.
Definition of Done (per section)¶
- Slides authored in
pages/SNN-topic/index.md, matching the outline beats. - Lab authored, idiot-proof with spoilers, break→fix.
- Manifest single-source-of-truth: slide HCL ↔ lab HCL identical.
- Components reused, not re-invented.
pnpm decks:checkgreen; generated root decks build (pnpm build,build:day1–day3,build:3day,build:templates).- PDF/PNG export clean (magic-move + components render without overflow).
task verifygreen —tofu fmt -check,validate, andtofu testpass (plan/mock lane needs no cloud; integration lane uses LocalStack). Thefmtgate scans git-tracked*.tf(minus the deliberate S13 messy fixture), so it ignores sibling worktrees and caches — but equally, a new.tfyou have notgit added yet is not checked. Stage it before trusting a green gate. CI checkouts are fully tracked, so CI always sees it.- Presenter notes on every content slide (see convention below) so anyone can deliver the deck, not just its author.
- Facilitator runbook row in
docs/facilitator-runbook.mdfor the section (timing cues from README fit plan / labduration:, one checkpoint question, known failure modes, and the live cut-order note). Stub sections must not claim a shipped row until this lands with the authored content. - Cleanup safe; no guardrail violations.
- Conventional Commit + gitmoji.
Presenter-notes convention¶
Every content slide carries Slidev presenter notes so any facilitator can
deliver it. Slidev treats the last HTML comment in a slide as its presenter
notes: place it at the very end of the slide's markdown, after all content
(including any ::notes:: slot / CodeNote rail — that slot is a layout region,
not presenter notes) and immediately before the --- separator. Notes show
in presenter mode and never render on the slide.
Each note contains three things: what to say (the beat's teaching point in
2–4 sentences), a timing cue (e.g. ~3 min; on lab slides match the
duration: frontmatter), and the transition line into the next slide (on
recap slides echo the next: frontmatter). Anchor claims to what the slide
actually teaches — don't invent facts. Pure cover/divider slides with no
teaching content may be skipped; a divider with a spoken framing line is a beat
and gets a note.
# Some slide title
- point one
- point two
<!--
Say: what this beat teaches, in 2-4 sentences. (~3 min)
Then: one transition line into the next slide.
-->
Commits¶
<emoji> <type>(<scope>): <subject> — Conventional Commits + gitmoji. Types:
feat ✨ · fix 🐛 · docs 📝 · refactor ♻️ · test ✅ · chore 🔧 · ci 👷.
Scopes: deck, labs, theme, modules, repo, ci. Imperative, lowercase,
no trailing period.
Build & verify¶
task verify / scripts/verify.sh require Bash ≥4 (shopt globstar from
US-X-DRIFT2). macOS /bin/bash 3.2 fails if it is first on PATH; Homebrew
bash 5 and CI Ubuntu are fine.
pnpm install
pnpm deck -- --list # discover day / section / range choices
pnpm deck -- --day 1 # facilitator launcher (or task deck -- …)
pnpm decks:generate && pnpm decks:check # after manifest / section metadata edits
pnpm build && pnpm build:3day && pnpm build:templates # decks
pnpm lint # markdownlint (labs + variant/ovh docs)
task verify # tofu fmt/validate/test