skip to content

In Terraform, what is the difference between the root module and a child module, and what can only the root module do?

level: middleimportance: must knowfreq 70%

answer

  1. the directory you happen to run in
  2. roles, not file types
  3. backend and providers live at the top
  4. tfvars never reaches a child module
  5. child outputs go to the caller only

basics

~20 s

The root module is the directory Terraform is run in; a child module is a directory called from a module block. Only the root takes backend configuration, tfvars and CLI variable values, and the provider configuration for the run.

solid answer

~50 s

Every Terraform configuration is a module. The **root module** is whatever directory you run `terraform init`/`plan`/`apply` in; a **child module** is a directory pulled in by a `module` block via its `source`. Root and child are roles, not file formats — the same directory can be a child in one repo and a root when you `cd` into it. The difference is what the root is allowed to own: the `backend` (or `cloud`) block that decides where state lives, the provider configurations for the run, and the entry points for values — `terraform.tfvars`, `*.auto.tfvars`, `-var`, and `TF_VAR_` env vars only ever set root variables. A child module's variables are set exclusively by the arguments in its `module` block, and its outputs are visible only to its caller, not to `terraform output`. That is why a reusable child module should contain no backend block and no provider block.

code

hcl · 23 lines
hcl
# environments/prod/main.tf — the ROOT module of this run
terraform {
  required_version = ">= 1.5.0"

  backend "s3" {
    bucket = "acme-tfstate"
    key    = "prod/network.tfstate"
    region = "eu-west-1"
  }
}

provider "aws" {
  region = "eu-west-1"
}

module "network" {
  source     = "../../modules/network" # a CHILD module
  cidr_block = "10.20.0.0/16"          # the only way to set its variables
}

output "vpc_id" {
  value = module.network.vpc_id # re-exported so terraform output shows it
}

go deeper

for a junior

Know that a module is just a directory of .tf files, that the directory you run Terraform in is the root, and that a module block pulls in a child by its source path.

for a middle

Be ready to list precisely what only the root can hold — the backend block, provider configurations, and tfvars/-var/TF_VAR_ inputs — and to explain that a child's variables come solely from its module block arguments.

for a senior

Show the judgment behind it: reusable modules ship no backend and no provider block, roots stay thin, and you can spot an environment directory that was mistakenly filed under modules/ before it reaches production.

for a principal

Own the estate-level shape this implies — how many roots exist, where the boundary between environment-specific configuration and reusable module code sits, and what your team's convention costs when a module must later serve a second account or region.

## Every configuration is a module Terraform has no separate "program" and "library" concept. A module is simply a directory of `.tf` files. When you run `terraform plan` in a directory, that directory becomes the **root module** of the run. If any of its files contain a `module` block, the directory named by that block's `source` is loaded as a **child module**, and so on recursively. So "root" and "child" describe a *position in one run's tree*, not a property baked into the code. The same `modules/network` directory is a child when your production config calls it, and a root module when you `cd modules/network && terraform plan` (which is exactly what a test or an `examples/` directory does). ## What only the root can do Three things are reserved to the root module, and each has a reason. **State and backend.** The `backend` block (or `cloud` block for HCP Terraform) tells Terraform where to read and write state. There is one state per run, so there is one backend per run, and it must be declared in the root. A `backend` block inside a child module is a configuration error. This is the single most common structural mistake: someone copies a working environment directory into `modules/` and ships the backend with it. ```hcl # root only terraform { backend "s3" { bucket = "acme-tfstate" key = "prod/network.tfstate" region = "eu-west-1" } } ``` **Provider configuration.** `provider` blocks belong in the root. Child modules inherit the caller's default provider configurations automatically; when a child needs something other than the default (a second region, a second account) the caller passes it in explicitly. A child module that declares its own `provider` block hard-codes credentials and region choices into something meant to be reused, and it cannot be removed cleanly later. **Variable inputs from outside.** All the familiar ways of feeding values into Terraform — `terraform.tfvars`, `*.auto.tfvars`, `-var` and `-var-file` on the command line, and `TF_VAR_name` environment variables — apply *only to root module variables*. There is no mechanism to set a child module's variable from a tfvars file. A child gets its values from one place: the arguments written in its `module` block. ```hcl module "network" { source = "../../modules/network" cidr_block = var.cidr_block # the only way in } ``` This is what makes the child's `variable` declarations a genuine contract. The caller must name every input explicitly at the call site, which is visible in review, rather than a value appearing from an environment variable nobody can see. ## Outputs run the other way A child module's `output` values are returned to its caller and referenced as `module.<name>.<output>`. They are *not* printed by `terraform output` and are not part of the user-facing result of the run unless the root re-exports them: ```hcl output "vpc_id" { value = module.network.vpc_id } ``` Root outputs are stored in state, which is also what makes them readable by another configuration through a remote-state data source. Child outputs are an internal wiring detail of the run. ## What this means for how you lay out a repository The practical shape that falls out is: **thin roots, fat children.** A root module (often one per environment) holds the backend, the providers, a handful of environment-specific values, and `module` blocks. The child modules hold the actual resources and expose a deliberate set of variables and outputs. A child that contains a backend, a provider, or a `terraform.tfvars` is a root module that got filed in the wrong folder — it will work when run directly and break the moment someone tries to call it. ## Version notes The root/child distinction has been stable since Terraform 0.11. What has changed around it: since 0.13, `count`, `for_each` and `depends_on` can be used on `module` blocks, and `required_providers` inside a child module declares *which* providers it needs (not how they are configured), which stays correct and belongs in the child's `versions.tf`.

  • Can you run terraform apply directly inside a directory that is normally used as a child module?
    Yes — it becomes the root module of that run. That is how `examples/` directories and `terraform test` work. It will use whatever provider configuration and backend it can find there, so a child module with no backend block gets local state in that directory. It is a useful smoke test, but never point it at production state.
  • If a child module declares required_providers, is that the same as configuring a provider?
    No. `required_providers` inside a child declares which provider source addresses and version constraints the module needs, so Terraform can install them and record them in the lock file. It says nothing about region, credentials or endpoints. Configuration stays in the root and is inherited, or passed explicitly by the caller.
  • Why can't a caller set a child module's variable with a TF_VAR_ environment variable?
    Because `TF_VAR_` and tfvars files are root-variable input mechanisms only. Terraform deliberately gives a child exactly one input path — the arguments in its `module` block — so the full set of values a module receives is visible at the call site and in code review, rather than depending on ambient environment state.

The root module is the executable's main() and the child modules are its libraries: only main decides where the output file goes and reads the command-line arguments, and a library that hard-codes both cannot be reused.

saying these in an interview costs you the question

  • Puts a backend block inside a reusable child module
  • Thinks terraform.tfvars sets variables in child modules
  • Believes a child module's outputs appear in terraform output
  • Says root and child are different file formats
  • Declares provider blocks in every module for safety

context