skip to content

Does Terraform care whether your configuration is split into main.tf, variables.tf and outputs.tf, and why is that layout worth keeping anyway?

level: juniorimportance: should knowfreq 55%

answer

  1. all .tf files, one configuration
  2. the graph decides order, not the file
  3. convention for humans, not the parser
  4. interface in two predictable files
  5. tfvars and override.tf are the exceptions

basics

~20 s

Terraform does not care. It loads every .tf file in the directory and orders work from the dependency graph, so file names are purely a human convention. The convention survives because it puts a module's interface — its inputs and outputs — where a consumer can find it without reading the implementation.

solid answer

~50 s

Terraform loads *all* `.tf` and `.tf.json` files in the module directory and merges them into one configuration, so a `resource` could sit in `zebra.tf` and work identically. Execution order comes from the dependency graph Terraform builds by following references, not from file or line order, so an `output` may reference a resource declared "later". The `main.tf` / `variables.tf` / `outputs.tf` / `versions.tf` split is a community convention with a real payoff: `variables.tf` and `outputs.tf` together *are* the module's public API, and keeping them in fixed, predictable files means a consumer can read the contract in two files instead of grepping the whole module. It also makes documentation generators and diffs legible — an interface change shows up as a change to those two files. Two filename patterns *are* special and are not conventions: `terraform.tfvars` / `*.auto.tfvars` are loaded automatically as variable values, and `override.tf` / `*_override.tf` files are merged as overrides.

code

hcl · 10 lines
hcl
# outputs.tf — references a resource declared in another file, later
output "bucket_arn" {
  description = "ARN of the log bucket"
  value       = aws_s3_bucket.logs.arn
}

# main.tf
resource "aws_s3_bucket" "logs" {
  bucket = "acme-logs"
}

go deeper

for a junior

Be able to say plainly that Terraform reads every .tf file in the directory and works out order from references, and that main.tf/variables.tf/outputs.tf is a convention you follow so others can navigate your code.

for a middle

Explain the graph-based ordering behind it, and note the filenames that are not conventions — terraform.tfvars and *.auto.tfvars supply root variable values, override.tf merges over other blocks.

for a senior

Argue the convention from the consumer's side: variables.tf plus outputs.tf is the module's readable contract, and an interface diff should be visible as such in review rather than buried in a large main.tf.

for a principal

Own it as an estate standard — a consistent layout across dozens of modules is what makes documentation generation, automated interface checks and engineer mobility between repositories possible at all.

## What Terraform actually loads When Terraform evaluates a module directory it reads every file ending in `.tf` (native HCL syntax) and `.tf.json` (JSON syntax) in that directory, and concatenates their contents into a single configuration. It does **not** recurse into subdirectories — a nested folder is only loaded if some `module` block names it as a `source`. That single fact explains both halves of the answer: - You may name files anything you like; the split is for readers. - Directories like `examples/` and `test/` inside a module are inert. They sit next to the module without affecting anyone who calls it. ## Why order does not matter HCL configuration is declarative. Terraform parses all blocks, then walks every expression looking for references (`aws_subnet.private.id`, `var.name`, `module.network.vpc_id`) and builds a directed graph from them. Operations are ordered by that graph, so this is completely valid even though the output appears first: ```hcl # outputs.tf output "bucket_arn" { value = aws_s3_bucket.logs.arn } # main.tf resource "aws_s3_bucket" "logs" { bucket = "acme-logs" } ``` The common junior error is to assume `main.tf` runs top to bottom like a script, and then to "fix" a perceived ordering problem by moving blocks around. Moving blocks changes nothing. If you truly need an ordering Terraform cannot infer — because the dependency is invisible to it, such as an IAM policy that must exist before a service can assume a role — the tool for that is `depends_on`, not file layout. ## The conventional layout The layout that has settled across the ecosystem is roughly: - `main.tf` — the resources and data sources: the implementation. - `variables.tf` — every `variable` block, with a `description` and a `type`. - `outputs.tf` — every `output` block, with a `description`. - `versions.tf` (sometimes `terraform.tf`) — the `terraform` block with `required_version` and `required_providers`. - `locals.tf` — computed intermediate values, when there are enough to be worth separating. - `README.md` — what the module does, plus generated input/output tables. - `examples/` — one or more runnable root modules showing how to call it. Larger modules split the implementation further by concern: `network.tf`, `iam.tf`, `alarms.tf`. That is fine and still conventional — what stays fixed is that variables and outputs live in their own files. ## Why the convention earns its keep The convention is not tidiness for its own sake. A module's `variable` and `output` declarations are its public interface; everything in `main.tf` is implementation a consumer must never depend on. Putting the interface in two predictable filenames means: 1. **Discoverability.** A consumer evaluating your module opens `variables.tf` and `outputs.tf` and knows what they can pass and what they get back, without reading a line of resource code. 2. **Reviewable interface changes.** In a pull request, a diff touching `variables.tf` or `outputs.tf` is a *contract* change and deserves different scrutiny from a diff that only touches `main.tf`. 3. **Tooling.** Documentation generators such as terraform-docs build input and output tables from the `description` fields in those files, so a well-kept convention feeds directly into the README. 4. **Onboarding.** Any engineer who has seen one Terraform module can navigate yours instantly. Consistency across an estate of dozens of modules is worth more than any individual arrangement. ## The filenames that are not conventions Two patterns carry real meaning and are worth knowing so you do not create one by accident: - `terraform.tfvars`, `terraform.tfvars.json` and any `*.auto.tfvars` file are loaded automatically to supply **root module** variable values. Naming a file `prod.auto.tfvars` inside a module directory changes behaviour. - `override.tf` and files ending `_override.tf` are loaded last and *merge over* matching blocks defined elsewhere in the directory. It is a rarely-needed escape hatch for machine-generated configuration; using it for normal work makes a module very hard to read, because a block no longer means what it says. Everything else is convention — and precisely because it is convention rather than enforcement, a module that breaks it is a module a consumer has to read in full.

  • If Terraform ignores file names, how do you force one resource to be created before another?
    You let the dependency graph do it: reference the first resource's attribute in the second, and Terraform infers the edge. When the dependency is real but invisible to Terraform — an IAM policy that must exist before a service can use it — add an explicit `depends_on`. Rearranging files or blocks has no effect at all.
  • Does Terraform load .tf files from subdirectories of the module?
    No. Only files directly in the module directory are loaded; subdirectories are ignored unless a `module` block names one as its `source`. That is why an `examples/` or `test/` folder inside a module is harmless to consumers — it is never part of their configuration.

saying these in an interview costs you the question

  • Thinks main.tf executes top to bottom like a script
  • Believes variables must live in variables.tf to be recognised
  • Reorders blocks hoping to change creation order
  • Assumes subdirectories of a module are loaded automatically
  • Treats terraform.tfvars as just another naming convention

context