What belongs in a reusable Terraform module's examples/ directory, and how do you keep the input and output tables in its README accurate?
answer
- each example is its own root module
- subdirectories are never loaded by consumers
- init -backend=false, then validate
- descriptions are the documentation source
- CI diff-check, not just a hook
basics
~20 sexamples/ holds complete, runnable root modules — one per usage pattern — that CI can init, validate and plan, so a broken interface fails the build. README tables are generated from the description fields on variables and outputs by a tool such as terraform-docs, with the generation checked in CI rather than done by hand.
solid answer
~50 sEach subdirectory of `examples/` is a small **root module**: its own `provider` block, a `module` block pointing at `source = "../../"`, and the minimum arguments for one realistic usage — a minimal case, and one or two showing the interesting options. Because Terraform does not load subdirectories, these files are inert for anyone consuming the module, yet a CI job can `terraform -chdir=examples/simple init -backend=false` and `validate` (or plan against a sandbox) so an interface change that breaks callers fails your own build before it fails theirs. For the README, write a `description` on every `variable` and `output` and let a generator such as terraform-docs build the tables from them; run it in a pre-commit hook and add a CI check that fails when the committed README differs from freshly generated output. That way the documentation cannot silently drift from the interface, which hand-written tables always eventually do.
code
bash · 7 linesset -euo pipefail
# Every examples/ subdirectory is a root module; validate them all in CI
for dir in examples/*/; do
terraform -chdir="$dir" init -backend=false
terraform -chdir="$dir" validate
donego deeper
Know that examples/ holds runnable root modules showing how to call the module, and that every variable and output should carry a description because that is what the documentation is built from.
Explain the mechanics that make it work — subdirectories are not loaded by consumers, init -backend=false plus validate needs no credentials, and terraform-docs generates the tables from description fields.
Show how this becomes enforcement rather than good intentions: examples validated in CI catch interface breakage before consumers do, and a docs diff-check stops the published contract drifting from the declared one.
Own the standard across the module library — which checks are mandatory before publishing, who pays for sandbox plan runs, and how a consistent README shape lets teams evaluate an unfamiliar module without reading its implementation.
## Why a module needs examples at all A module's `variables.tf` tells a consumer what arguments exist. It does not tell them which combination actually works, which arguments are meaningful together, or what a realistic call looks like. That gap is where `examples/` lives — and unlike prose documentation, an example is code that can be checked. ## What an example directory actually is Each subdirectory under `examples/` is a complete **root module** in its own right: it declares its providers, calls the module by relative path, and supplies real values. ```hcl # examples/simple/main.tf provider "aws" { region = "eu-west-1" } module "network" { source = "../../" name = "example" cidr_block = "10.0.0.0/16" } ``` Two properties make this work. First, Terraform loads only the `.tf` files in the module directory itself and never recurses into subdirectories, so `examples/` is completely invisible to a consumer who calls your module — it adds no resources and no variables to their configuration. Second, because an example *is* a root module, all the normal commands work inside it. A good set is small and purposeful: a minimal example showing the required inputs only, and one or two showing the options that people get wrong. A directory with fifteen near-identical examples rots faster than no examples at all. ## Making examples executable in CI The reason to use examples rather than a README snippet is that they can be run. The cheap tier costs no cloud credentials at all: ```bash set -euo pipefail for dir in examples/*/; do terraform -chdir="$dir" init -backend=false terraform -chdir="$dir" validate done ``` `init -backend=false` installs providers and the local module without configuring state; `validate` then checks syntax, references, types and required arguments against the real provider schemas. If you delete a variable that an example passes, or rename an output an example reads, this fails — which is the point. Your own build breaks before a consumer's does. The expensive tier is a `plan` (or a full apply-and-destroy) against a sandbox account, which catches provider-level errors that `validate` cannot see. That belongs on a slower schedule, not on every commit. ## Generated documentation Hand-maintained input and output tables always drift; someone adds a variable and does not touch the README. The fix is to generate them from the source of truth, which means the `description` field on every declaration: ```hcl variable "cidr_block" { description = "IPv4 CIDR range for the VPC, e.g. 10.0.0.0/16" type = string } ``` `terraform-docs` reads a module directory and emits a Markdown table of inputs (name, description, type, default, required) and outputs (name, description). It can inject that table into an existing README between marker comments, so the prose you wrote stays and only the generated section is replaced. Generation alone is not enough — it has to be enforced, or someone will edit `variables.tf` and forget to regenerate. Two mechanisms, ideally both: - A **pre-commit hook** that regenerates the README on every commit touching `.tf` files. - A **CI check** that regenerates into a temporary file and fails if it differs from what is committed. This is the one that actually holds, because hooks are per-developer and easy to skip. ## What documentation is for here All of this serves the leaf idea that a module's inputs and outputs are its API. Generated tables mean the published contract is mechanically identical to the declared contract. Descriptions stop being optional politeness and become the documentation itself, which changes how they should be written — say what the value is *for* and what units or format it takes, not merely restate the name. "IPv4 CIDR range for the VPC, e.g. 10.0.0.0/16" is documentation; "The cidr block" is noise that will be published verbatim. And a README should still carry what a generator cannot produce: what the module is for, what it deliberately does not do, the prerequisites a caller must already have, and a pointer at the examples. Generation replaces the tables, not the thinking.
- Does an examples/ directory inside a module affect consumers who call that module?No. Terraform loads only the .tf files in the module directory itself and never recurses into subdirectories, so examples/ contributes no resources, variables or providers to a caller's configuration. That is exactly why examples can be full root modules with their own provider blocks without interfering with anyone.
- What does terraform validate catch in an example, and what does it miss?It checks syntax, references, types, and required arguments against the installed provider schemas, so a removed variable or renamed output fails immediately. It does not contact any API, so it cannot catch invalid values a provider would reject, quota limits, or IAM problems — those need a plan or apply against a real account.
- Why enforce generated docs in CI rather than relying on a pre-commit hook?Hooks are installed per developer and trivially bypassed with --no-verify, so a hook is a convenience, not a guarantee. A CI job that regenerates the tables and fails on any difference is the enforcement point every contributor passes through, which is what keeps the published contract identical to the declared one.
saying these in an interview costs you the question
- Thinks examples/ resources get created for consumers
- Maintains README input tables by hand
- Writes descriptions that only restate the variable name
- Only runs terraform fmt and calls the module documented
- Assumes validate contacts the cloud provider API