Terraform for platform teams: printable course notes
Run Terraform as a platform team: the plan and apply loop, inputs and validation, remote state with a lock and secrets kept out of it, a module for every team, dev and prod in separate directories, tests, checks, policy and CI, import, drift and provider upgrades, five debugging labs, and a capstone.
These pages are the same content as the interactive course, on one page per module, for reading and printing.
Four pieces
| Piece | What it is |
|---|---|
| See the end result | Not the first step: a 30-minute preview with few explanations. A cluster, a locked remote state, one module for two teams, and a plan that would destroy, stopped. Useful to know where you are heading. |
| Full course | 10 modules from an empty laptop to five teams on one module: state and locking, modules, dev and prod, tests and CI, import, drift, upgrades, five debugging labs and a capstone. |
| Reference repo | The finished implementation: the module, both environments, the state store, the policy, the workflows and the docs. You copy from it. |
| Your repo | tf-platform: the module, envs/dev and envs/prod. You create it in module 00. |
Your setup
Commands in these notes use these values (shared with the interactive course, stored only in this browser).
Outline
| Module | Part | Time | You are | You will |
|---|---|---|---|---|
| 00 Orientation: what a platform team keeps in Terraform | Orientation | 20 min | Platform and product | Create the repo and the cluster, and initialise an empty Terraform configuration in envs/dev. |
| 01 Write, plan, apply: a namespace from code | The loop | 35 min | Platform team | Create team-checkout's namespace, quota and default limits from envs/dev/main.tf, with the provider pinned and locked. |
| 02 Inputs, locals, outputs and validation | The loop | 35 min | Platform team | Replace every hard-coded value in envs/dev with a typed, validated variable, a local or an output. |
| 03 State: a shared backend, a lock, and the secrets inside | State, modules and environments | 45 min | Platform team | Move dev's state into a locked remote backend and keep the one secret you create out of it. |
| 04 One module, two teams | State, modules and environments | 45 min | Platform team | Package the tenant kit as a module with typed inputs, move checkout into it without destroying anything, and use it for a second team. |
| 05 Dev and prod without copy and paste | State, modules and environments | 35 min | Platform team | Run the same module for dev and prod from two directories with separate, locked state, and say who reviews each. |
| 06 Tests, checks and a pipeline on every pull request | Safe change | 45 min | Platform and product | Make every pull request prove that the code is formatted, valid, tested and within policy, with no access to the cluster or the state. |
| 07 Import, drift and provider upgrades | Safe change | 45 min | Platform team | Bring a hand-made namespace under Terraform, detect and undo a hand change, and upgrade a provider with an empty plan as the proof. |
| 08 Debugging labs: five plans that go wrong | Safe change | 45 min | Platform team | Diagnose five common Terraform failures from their messages and fix each without destroying anything. |
| 09 Capstone: onboard team-payments on your own | Safe change | 35 min | Platform and product | Onboard a fifth team end to end, extending the module without replacing anything that exists. |
The change loop
- Write: Describe what should exist, in `.tf` files in Git.
- Plan: Terraform compares code, state and the real objects and proposes the steps. Nothing changes.
- Review: A pull request with checks, and a person who reads the plan's last line.
- Apply: The steps run, and the result is written to state.
- State: The record of what Terraform manages. Locked while someone writes it.
Terraform or OpenTofu
Every module works with both. These notes show terraform; on OpenTofu type tofu instead. Only module 06 (the workflow file) and module 07 (Dependabot's ecosystem name) differ.
Between you and production
- State in a real bucket: S3 (or GCS, azurerm) with versioning, encryption and `use_lockfile`; read access limited to the people and pipelines that apply. See `docs/cloud.md`.
- Plans in the pipeline: A `plan -out` job on every pull request that can reach the state, the plan posted for review, and an approved job that applies that exact file.
- Short-lived credentials: GitHub OIDC to a role per environment; prod's pipeline cannot read dev's state and the other way round. No long-lived keys anywhere.
- Branch protection: `main` moves only through pull requests; the three jobs and a CODEOWNER review are required.
- Nightly drift detection: `plan -refresh-only -detailed-exitcode` on a schedule, alerting on exit code 2.
- Secrets from a store: Values that must not be in state come from a secret store (write-only arguments or a controller in the cluster), never from Terraform outputs.
- Tagged module releases: The module published with Git tags, so dev and prod can be on different versions while a change is proven.
- A linter and plan-level policy: tflint, and the policy run on the JSON of a real plan as well as on the files.
- Upgrade cadence: Dependabot pull requests read and merged weekly; the CLI version bumped with them in `versions.tf` and the workflows.
- Hand-off to GitOps: Terraform makes the namespaces and access; a controller such as [Argo CD](/learn/argocd/) deploys what runs inside them.