Terraform modules
A module is a black box you call, and the module block is its whole interface
A module is a container for a group of resources that are used together, and you use one by writing a module block that calls it. That block is the module's entire interface, and it captures two things about the module: where its code comes from, given by the required source argument, and what data crosses its boundary. Values go in as input arguments and come back out as module.NAME.OUTPUT, so it helps to read a module as a function whose parameters are its inputs and whose return values are its outputs. The trap this model dodges is assuming a child can reach outside its box: it cannot read the caller's var.* or local.*, the caller cannot read the child's internal resources, and the only sanctioned flow is inputs down and declared outputs up. Provider configurations are the one deliberate exception to that isolation, flagged where it matters below.
The domain unfolds in four steps, one per subtopic
Each subtopic owns one facet of that module block, and they build in order. How Terraform sources modules comes first, because the source string's shape is the master switch: a leading ./ or ../ is a local path, a <NAMESPACE>/<NAME>/<PROVIDER> triple is a Terraform Registry address, and a git::, s3::, or archive URL is a remote package whose // marker selects a sub-directory inside it. Variable scope within modules then draws the boundary: inputs are local to the module that declares them, outputs are the only way a result escapes, and default (unaliased) providers are inherited automatically while aliased ones must be handed over with the providers meta-argument. Using modules in configuration covers the call itself: the module block, its input arguments, terraform init to install a module before you plan or apply, and count or for_each to stamp out many instances. Managing module versions closes the loop by locking the code, and how you pin follows straight from the source type.
When two answers both work, trust the source type and prefer the explicit interface
The reliable instinct across this whole domain is to read the source first, because the source type dictates what is even legal downstream. Only a registry address accepts the version argument; a Git source pins with a ?ref= tag that can be a branch, a tag, or a full commit SHA; and a local path cannot be versioned at all, because it always shares the exact version of the caller that references it. When two answers both look valid, the exam rewards the explicit interface over the implicit one: declare and pass an input rather than expecting a child to inherit it, expose an output rather than reaching into a child's resources, and pin a version rather than letting the next terraform init quietly install the newest release.
The four facets of a module block, one per subtopic
| Facet of the module block | What decides it | Key mechanism | Drill into |
|---|---|---|---|
| Where its code comes from | The `source` address shape | Local `./` or `../`, registry `NS/NAME/PROVIDER`, or remote `git::`/`s3::` (with `//` for a sub-directory) | How Terraform sources modules |
| What crosses its boundary | Module encapsulation | Inputs in as arguments, outputs up as `module.NAME.OUTPUT`; providers the one exception | Variable scope within modules |
| How you write the call | The `module` block | `source` plus input args, `terraform init` to install, `count`/`for_each` for many instances | Using modules in configuration |
| How you pin its version | The source type | Registry `version`, Git `?ref=`, local unversioned; `get -update` / `init -upgrade` | Managing module versions |