Terraform Guide

How to document a Terraform repository: four layers, three of them generated

By the engineer who builds Datadef, from client work on data platforms · Reviewed August 21, 2026

Terraform documentation fails in a predictable way. Someone writes a good README on day one, the repo grows three stacks and nine modules, and two quarters later the only sentence still true is the title. The fix is not more discipline. It is deciding, layer by layer, what has to be written by a human and what should be produced from the .tf files every time they change.

7 min readFor the maintainer who inherited both the repo and its README

See it as a diagram

Everything below, as a diagram you can edit. Describe yours and see it in seconds.

153/20003 credits left
Try:

No account needed · Editable canvas, not a picture

The four layers, and who writes each one

Layer one is intent: why this platform exists, why the module boundaries fall where they do, which conventions are deliberate. No parser can recover intent, because it was never written into the code. This layer is human, and it should be short, because short things get re-read and corrected.

Layer two is module reference: inputs, outputs, providers, required versions. terraform-docs generates this from the source on commit, and hand-maintaining it is wasted effort.

Layer three is architecture: what the repository builds, how the modules wire together, how the environments differ. This is the layer everyone needs and nobody maintains, because writing it by hand means re-reading forty directories.

Layer four is operations: how to apply, which backend holds which state, who approves what. Partly generated (backends and stacks are in the source), partly human (approval rules are not).

Write down only what the code cannot say

The highest-value paragraphs in a Terraform README are the ones the source contradicts nobody about because the source is silent. There are usually five of them, and they are worth more than a page of prose about what Terraform is.

Name the resources that are deliberately not managed by Terraform. Every real platform has some: a DNS zone created before the repo existed, a subscription-level policy owned by another team, a bucket someone imported and never finished. Anyone reading the code alone will assume the repo is the whole story, and be wrong at the worst moment.

Name which environment is authoritative when they diverge, name the one naming convention that is load-bearing, and name the module boundary you would move if you had a week. That last one saves the next maintainer from re-deriving your regret.

# Platform infrastructure

<!-- generated diagram, updates on every sync -->
![Architecture](https://datadef.io/api/embed/my-platform-a1b2c3d4)

## What this repo owns
One sentence per stack.

## What this repo does NOT own
- dns zone example.com (created 2021, owned by IT)
- prod subscription policies (platform-governance repo)

## Conventions that matter
- every resource name carries the env suffix; module keys do not

## Applying
backend, approval, order between stacks

Generate the layers that rot

Layers two, three, and four have one property in common: they are derivable from the files. Connect the repository read-only to Datadef from GitHub, GitLab, or Azure DevOps, pick a branch or tag, and every .tf and .tfvars file is parsed. No terraform init, no plan, no state file, no cloud credentials.

What comes back is an architecture doc grounded in that parse plus a diagram of the same model, so the two cannot disagree with each other. The doc ends with a module reference table composed from the parse rather than written by the model: one row per module, columns Stack, Module, Source, Resources, Notes. A local module shows a resource count; a registry module shows external, because its contents are not in your repository. See the generator page for what that table looks like next to terraform-docs output.

The sync runs daily and on demand. Each run hashes the parsed model, meaning the curated node list with its counts and its wiring, into a structure fingerprint. A commit that changes a comment, a test, or the README moves the commit sha and leaves that hash alone, and the sync stops before generation, so the doc history stays quiet when the architecture does.

## Module reference

| Stack | Module | Source | Resources | Notes |
| --- | --- | --- | --- | --- |
| platform | (root) | - | 7 | backend azurerm |
| platform | network | `./modules/network` | 12 | |
| platform | databricks | `./modules/databricks` | 21 | per project (DEV 2, QUAL 1) |
| platform | vpc | `terraform-aws-modules/vpc/aws` | external | |

Environments: DEV, QUAL, PROD (tfvars).

Put the picture at the top

A README that opens with a current diagram gets read. One markdown line renders a live embed that follows every sync, and viewers need no account.

FAQ

What should a Terraform repository README contain?

One sentence per stack on what it owns, an explicit list of resources the repo does not manage, the naming conventions that are load-bearing, the apply procedure and backend per stack, and a current architecture diagram. Input and output tables belong in per-module docs generated by terraform-docs rather than in the top-level README.

Should I write Terraform documentation by hand or generate it?

Split it. Intent and operational rules must be written, because they are not in the code. Module reference tables, the architecture overview, and the diagram are derivable from the .tf files and should be generated, because those are exactly the parts that go stale between reviews.

Do I need terraform init or a state file to generate repository documentation?

No. Datadef parses the .tf and .tfvars files of the connected repository directly: no init, no plan, no state access, no cloud credentials. Read access to the repository is the entire requirement, which is what makes it usable on production and client repositories.

How do I document infrastructure that exists but is not in Terraform?

List it explicitly in the README under a heading that says the repo does not own it, with the reason and the owner. Source parsing can only see what is declared, so unmanaged resources are invisible to any code-derived diagram and have to be stated by a human once.

How often does generated Terraform documentation update?

On a daily sync of the chosen branch or tag, on demand from the interface, or when a coding agent triggers a refresh through the MCP server. There are two skip points: a head commit that has not moved is never fetched, and a commit that moved without changing the parsed structure is fetched, parsed, and then left alone. Unrelated commits therefore do not churn the doc.