Every Terraform configuration is a module. The folder where you run terraform plan is the root module; folders it calls with a module block are child modules. A good child module does one job (a network, a container service, a bucket with its policy) and hides the details behind a small interface.
flowchart LR R["Root module envs/dev"] --> N["module.network"] R --> A["module.app"] N -->|"output network_id"| A A --> D["docker_container x2"] V["dev.tfvars"] --> R %% step R: The root module is where you run plan and apply; it owns the backend and the provider settings. %% step V: Values for the root module come from tfvars files, -var flags or TF_VAR_ environment variables. %% step N: A child module receives inputs through variables and creates its own resources. %% step A: Outputs of one module become inputs of another, which also tells Terraform the order. %% step D: Resources inside a module are addressed as module.app.docker_container.web[0].
The standard layout
modules/app/
main.tf # resources
variables.tf # inputs, with type, description and defaults
outputs.tf # values the caller may use
versions.tf # required_version and required_providers
README.md
module "app" {
source = "../../modules/app"
name = "shop-api"
replicas = 2
network = module.network.network_name # output of another module
}
output "app_urls" {
value = module.app.urls
}
Inside the module, the caller's values are var.name, var.replicas. The caller can only read what the module exposes in outputs.tf: module.app.urls. Everything else is private.
Design rules that age well
| Rule | Why |
|---|---|
| One purpose per module | Small modules are easier to test, version and reuse |
| Typed variables with descriptions | Callers get a clear error instead of a broken plan |
| Few required inputs, sensible defaults | The common case needs three lines, not thirty |
No provider blocks inside reusable modules |
The caller configures providers; the module only declares required_providers |
| Output IDs and names others need | Outputs are the module's public API |
| Avoid thin wrappers | A module that only renames one resource's arguments adds indirection without value |
Composition, not nesting
Prefer a flat root module that wires several child modules together over modules that call modules that call modules. Deep nesting makes addresses long, outputs must be passed up level by level, and a change to the inner module ripples everywhere.
Commands
| Command | Meaning |
|---|---|
terraform init |
Downloads providers and installs modules into .terraform/modules |
terraform get -update |
Re-installs modules only (after you changed a source) |
terraform output -json |
All root outputs as JSON for scripts |
Traps: a child module's outputs are not visible from outside the root unless the root re-exports them; and changing a module's source requires terraform init (or terraform get) before plan.