Does Terraform care whether your configuration is split into main.tf, variables.tf and outputs.tf, and why is that layout worth keeping anyway?
answer
- all .tf files, one configuration
- the graph decides order, not the file
- convention for humans, not the parser
- interface in two predictable files
- tfvars and override.tf are the exceptions
basics
~20 sTerraform 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 sTerraform 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# 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
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.
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.
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.
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