What does `terraform init` actually do, and what ends up inside the `.terraform` directory after it runs?
answer
- turns source code into a working directory
- backend, providers, modules
- binaries are platform-specific
- gitignore the directory, commit the lock file
- validate needs the schemas init installs
basics
~20 sterraform init prepares a working directory: it wires up the backend, installs the providers the configuration requires, and fetches remote modules. Plugins land in .terraform/providers and module copies in .terraform/modules, while .terraform.lock.hcl is written beside your code.
solid answer
~40 s`terraform init` is the setup step that has to run before plan or apply in a fresh checkout. It does three things: initialises the backend so Terraform knows where state lives, downloads every provider the configuration declares, and retrieves modules from their remote sources. The artefacts go into a local `.terraform` directory — provider binaries under `.terraform/providers`, module copies under `.terraform/modules` with a `modules.json` manifest, and a small `.terraform/terraform.tfstate` recording the backend configuration. That whole directory is machine-local, regenerable and belongs in `.gitignore`; the dependency lock file `.terraform.lock.hcl` is written next to your `.tf` files and is committed. `init` is safe to re-run, and `-upgrade` lets it move providers and modules within their allowed version ranges.
code
bash · 8 linesterraform init
# lint job: enough setup to validate, no state credentials needed
terraform init -backend=false -input=false
terraform validate
# move providers forward inside their version constraints
terraform init -upgradego deeper
Know that init runs first in any fresh checkout and that it downloads the providers and modules the configuration needs before plan or apply can work.
Explain the three jobs — backend, providers, modules — and say precisely what lands in .terraform versus what is written beside your code and committed.
Bring the operational angle: plugin caches or mirrors for constrained runners, why -reconfigure and -migrate-state are different answers, and how initialisation behaves in an air-gapped pipeline.
Own supply-chain and repeatability policy: where providers are allowed to come from, whether an internal mirror is mandatory, and how initialisation cost shapes pipeline design across many working directories.
## Why a separate init step exists A Terraform configuration is not self-contained. It names providers that are separate binaries published elsewhere, and it may call modules that live in a registry or a git repository. It also declares where its state is stored. None of that can be resolved from the `.tf` files alone, so `terraform init` exists to turn a directory of source code into a working directory Terraform can actually run in. Every other core command assumes it has been done — running `terraform plan` in a fresh clone fails immediately with a message telling you to initialise first. ## The three jobs **Backend initialisation.** Terraform reads the `backend` (or `cloud`) settings and establishes where state is read from and written to. The result is cached in `.terraform/terraform.tfstate`. That file trips people up: despite the name, with a remote backend it does not hold your infrastructure state — it is a small local record of which backend this directory is bound to. If you change the backend settings later, `init` will not continue silently and asks you to choose: `-migrate-state` to copy the existing state to the new location, or `-reconfigure` to bind to the new backend and ignore the old state. **Provider installation.** Terraform determines every provider the configuration needs, resolves versions against your constraints, and downloads the plugin binaries. They land in a nested layout under `.terraform/providers`, keyed by registry host, namespace, type, version and platform: ``` .terraform/providers/registry.terraform.io/hashicorp/aws/5.100.0/darwin_arm64/ ``` The platform segment is the reason a `.terraform` directory copied from a laptop is useless on a Linux runner — the binaries are native executables for one OS and architecture. If you set the `TF_PLUGIN_CACHE_DIR` environment variable, Terraform keeps one shared copy of each plugin there instead of re-downloading it for every working directory. **Module installation.** Remote modules are fetched from the registry, git, HTTP or wherever their `source` points, and copied under `.terraform/modules`, with `modules.json` mapping each module call to its local path. Local modules referenced by relative path are not copied — they are read in place. ## What is *not* in `.terraform` The dependency lock file, `.terraform.lock.hcl`, is written in the working directory itself, alongside your configuration, and it is meant to be committed so that everyone and every pipeline installs the same provider versions. This split confuses people constantly. The rule is simple: `.terraform/` is a build output, disposable and gitignored; `.terraform.lock.hcl` is source, reviewed and committed. ## Re-running init `init` is safe to run repeatedly — pipelines run it in every job because every job starts from an empty checkout. Useful variants: - `-upgrade` — re-resolve providers and modules to the newest versions the constraints allow, and update the lock file accordingly. Without it, init sticks to what the lock file already records. - `-backend=false` — skip backend initialisation entirely. This is how a static-check job installs just enough to run `terraform validate` without needing credentials for the state store. - `-input=false` — never prompt; fail instead. Standard in automation. ## Its relationship to validate `terraform validate` requires an initialised directory, and candidates are often surprised by that. Validation is not a text-level syntax check: to know that an argument is valid on a resource type, Terraform needs the provider's schema, and the schema comes from the installed plugin. That is why a lint job still runs `terraform init -backend=false` first. ## The failure modes worth naming A CI runner with no network cannot init at all — hence plugin caches and mirrors. A `.terraform` directory committed by accident bloats the repository with binaries and breaks on every other platform. And a run that skipped `init` after someone added a new provider fails at plan time with a missing-plugin error rather than anything about the resource itself. Knowing where the artefacts live turns each of those into a thirty-second diagnosis.
- Why can't you just copy the `.terraform` directory into a CI runner to skip init?Because provider plugins are native binaries stored under a platform-specific path such as `darwin_arm64`; a Linux runner cannot execute a macOS build, and the backend record inside may not match the runner's credentials either. The directory is a build output. If download time is the problem, use a shared `TF_PLUGIN_CACHE_DIR` or an internal provider mirror instead.
- Why does `terraform validate` insist that you run init first?Validation checks the configuration against the provider schemas — argument names, types, required fields — and those schemas ship inside the provider plugins that init downloads. Without them Terraform can only see HCL syntax, not whether a resource is configured correctly. `terraform init -backend=false` gives validate what it needs without touching the state backend.
saying these in an interview costs you the question
- Thinks .terraform/terraform.tfstate holds the infrastructure state
- Says the lock file lives inside the .terraform directory
- Commits .terraform to git to speed up CI
- Believes init is only needed once per repository
- Thinks validate works without init because it is offline