TerraformV · The Terraform WorkflowProduction Terraform
terraform init: The Bootstrap
What you'll learn
- Explain what terraform init does and the four artefacts it creates in the working directory
- Choose the right init flag for a given situation: -upgrade, -reconfigure, -backend-config, -plugin-dir, -from-module
- Describe the role of the dependency lock file and the team workflow around it
- Recognise the cost of skipping or partially running init before plan and apply
- Diagnose the four most common init failure modes from their observable symptoms
Prerequisites
Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13
Every terraform plan and terraform apply assumes a working directory that has been initialised. The first command on a fresh checkout, the first command after editing terraform { backend = ... }, the first command after upgrading a provider version, and the first command after a fresh clone into CI is always the same: terraform init. Get init wrong and nothing else in the workflow runs.
What init actually does on disk
terraform init is a one-shot bootstrap that prepares the working directory for plan and apply. It does not call any provider API for resource data; it does not read the state of real resources. It creates four artefacts inside the project directory:
project-root/
├── main.tf
├── variables.tf
├── .terraform/ ← created by init
│ ├── providers/ ← downloaded provider plugin binaries
│ ├── modules/ ← downloaded child modules
│ ├── environment ← serialised backend state pointer
│ └── terraform.tfstate ← local state pointer (NOT the real state)
└── .terraform.lock.hcl ← dependency lock file (committed to VCS)
The four steps init runs, in order, are:
- Backend selection. If
terraform { backend "..." { ... } }is set, init resolves the backend, optionally creates it on first run, and writes the local pointer into.terraform/terraform.tfstate. The real state lives wherever the backend stores it (S3, GCS, Consul, local). - Provider plugin download. Every
providerblock in the configuration, plus every provider required by a module, is resolved against the version constraints and downloaded into.terraform/providers/. Checksums are recorded. - Module download. Every
moduleblock is fetched from its source (Terraform Registry, Git, S3, local path) and unpacked under.terraform/modules/. - Lockfile creation. If no
.terraform.lock.hclexists, init writes one. If one exists, init verifies the recorded checksums against what is downloaded.
The dependency lock file
.terraform.lock.hcl is the file that makes Terraform reproducible across teammates and across CI runs. It records the exact provider version and the checksums of the provider plugin:
# .terraform.lock.hcl
# Manual edits may be lost - proceed with caution!
provider "registry.terraform.io/hashicorp/aws" {
version = "5.55.0"
constraints = "~> 5.55"
hashes = [
"h1:abcd1234...=",
"zh:1234abcd...",
]
}
Commit this file to version control. When a teammate runs init, init reads the lockfile, downloads the same version, verifies the checksums, and refuses to run if the hashes do not match. This is the production contract: “everyone, and every CI run, uses the exact same provider binary.”
The trade-off: a teammate who edits ~> 5.55 to ~> 5.56 in versions.tf will see terraform init resolve the new version and rewrite the lockfile. They must commit the new lockfile as part of the same pull request, or CI will use the old version and produce a confusing plan.
The flags you will actually use
The full flag list runs to dozens. The flags that matter on day one are:
# Default init. Downloads providers and modules, configures backend.
terraform init
# Force init to upgrade provider and module versions to the latest
# matching the configuration constraints. Use after editing versions.tf.
terraform init -upgrade
# Reconfigure the backend without changing the backend type.
# Forces a fresh backend walkthrough (interactive prompts).
terraform init -reconfigure
# Supply or override backend configuration from a file or CLI key=value.
# Useful for keeping secrets out of the repo.
terraform init -backend-config="bucket=prod-tf-state-2026"
terraform init -backend-config=backend.hcl
# Skip the backend entirely (for CI linting and validate-only steps).
terraform init -backend=false
# Install providers from a local directory instead of downloading.
# For air-gapped or mirror-only environments.
terraform init -plugin-dir=/opt/terraform-providers
# Bootstrap a new configuration by copying and applying a template module.
terraform init -from-module=terraform-aws-modules/vpc/aws
The decision tree:
- Fresh clone of an existing repo: plain
terraform init. - Edited
versions.tf:terraform init -upgrade. - Edited the backend block but kept the same backend type:
terraform init -reconfigure. - Switched backend type (e.g. local to S3): plain
terraform initwill offer to migrate. - CI linting job that does not need real state:
terraform init -backend=false. - Air-gapped environment:
terraform init -plugin-dir=....
The CI pipeline pattern
Init appears twice in a typical pipeline: once in the cheap lint step, and once in the plan step:
# CI lint job — no state needed
terraform init -backend=false -input=false
# CI plan job — full init, then plan
terraform init -input=false
terraform plan -input=false -out=tfplan -detailed-exitcode
The -backend=false shortcut saves the CI runner from authenticating to the state backend during a job that will not touch state. It also short-circuits the “this bucket does not exist” failure for ephemeral preview environments.
The cost of skipping init
The cost is paid the first time you run terraform plan on a fresh clone. The errors are fixable but look alarming:
- No init yet:
terraform planexits withError: Backend reinitialisation required. Please run "terraform init". - No init after editing versions.tf:
terraform planexits withError: Provider configuration not present. The fix isterraform init -upgrade. - No init after editing modules:
Error: Module source has not been downloaded. The fix is plainterraform init. - Init but no lockfile committed: CI runs
terraform init, init sees no lockfile, picks a version, writes one — but a different version than your laptop. Plans diverge silently.
None of these are mysterious once you know init is what prepares the directory. They are all mysterious to a new operator who has not internalised that model yet.
Production failure modes
Four failure modes account for most of the tickets you will get.
1. Backend credentials missing or wrong. Symptom: Error: Failed to get existing workspaces: AccessDenied: ... during the backend step. Cause: the IAM role or service account running init does not have permission to read or write the state bucket. Recovery: fix the IAM policy (S3: s3:GetObject, s3:PutObject, s3:ListBucket; DynamoDB lock table: dynamodb:GetItem, dynamodb:PutItem, dynamodb:DeleteItem). Verify with the AWS CLI directly before re-running init.
2. Provider version constraint unsatisfiable. Symptom: Error: Failed to query available provider packages ... no available releases match the given constraints. Cause: a required_providers block pins a version range that no longer has a matching release, often after a registry migration or a private mirror prune. Recovery: relax the constraint, run terraform init -upgrade, or restore the missing version in your private mirror.
3. Lockfile out of sync in CI. Symptom: CI plan fails with Error: Failed to install provider ... locked provider ... does not match the expected checksum. Cause: a teammate changed versions.tf and committed the new constraint but forgot to commit the new .terraform.lock.hcl. Recovery: have the teammate commit the lockfile, or have CI run terraform init -upgrade (which some teams do intentionally, but it should be a deliberate policy, not an accident).
4. Module source unreachable. Symptom: Error: Failed to download module ... Could not download ... 403 Forbidden or connection refused. Cause: the module source points to a private Git repository or registry that the running user cannot reach, or the tag/SHA was deleted. Recovery: verify the source is reachable from the runner (git ls-remote, curl), check the tag or ref still exists, and confirm credentials.
A fifth, rarer failure: partial init where .terraform/providers/ is present but .terraform/terraform.tfstate is missing. This happens if init was killed mid-run (OOM, ctrl-C at the wrong moment). Recovery: delete .terraform/ and .terraform.lock.hcl and re-run init from scratch.
Recovery and rollback
Init is generally safe to re-run. The dangerous variant is switching backends: terraform init will offer to migrate state from the old backend to the new one. If you say yes and the migration is interrupted, the state may end up in both buckets, or in neither. The rule is:
- Same backend type: safe to re-run init any number of times.
- Switching backend type: take a backup of the current state (download from S3, copy the local file) before running init. Run
terraform planafter init to confirm the state matches the configuration before doing anything else.
Security implications
Init downloads and executes provider binaries. A compromised registry, a compromised private mirror, or a man-in-the-middle on the download path can substitute a malicious binary. Mitigations:
- Commit
.terraform.lock.hclso the checksum is verified on every run. - Use a private provider mirror with
--plugin-dirorTF_PLUGIN_CACHE_DIRto decouple CI from the public registry. - For highly regulated environments, audit the lockfile in code review.
The lockfile checksum verification is your contract with the registry. Without it, init trusts whatever the registry serves.
Performance implications
Init is network-bound. The first init on a project with a large dependency closure (say, the AWS provider plus three modules that each pull in their own providers) can take 30-90 seconds. Mitigations:
TF_PLUGIN_CACHE_DIR: a shared on-disk cache so subsequent inits do not re-download.--plugin-dir: install providers once and point every init at the same directory.terraform init -upgradeis slower thanterraform initbecause it re-checks all versions.
Verification
Run through this checklist on a fresh clone to confirm init is healthy:
terraform init -input=false
ls -la .terraform/providers/registry.terraform.io/
test -f .terraform.lock.hcl && echo "lockfile present"
grep -c "hashes" .terraform.lock.hcl # should be > 0
terraform plan -input=false # should not error with "reinit required"
A passing run leaves you with a populated .terraform/ directory, a .terraform.lock.hcl committed to the repo, and a working terraform plan. If any of those three things is wrong, init has not done its job.
What comes next
The next lesson covers terraform fmt and terraform validate — the two cheap gates that should run before every init and every plan in CI. They do not call any provider API and they finish in under a second, which is why they belong at the front of every pipeline.
Knowledge check · 7 questions
Q1. Which artefact does terraform init NOT create?
Q2. You edited required_providers to bump the AWS provider from 5.54.0 to ~> 5.55. Which init flag is correct?
Q3. The .terraform.lock.hcl file should be committed to version control so that every teammate and CI run uses the same provider binary.
Q4. Which of the following are valid reasons to re-run terraform init? (Select all that apply.)
Q5. What does terraform init -backend=false do?
Q6. CI fails with: 'locked provider does not match the expected checksum'. What is the most likely cause?
Q7. Your team uses a private provider mirror served from an internal S3 bucket behind a VPN. CI runs in the cloud outside the VPN. terraform init fails with a network error. What is the minimum-change fix?
Passing score: 75%. Answers are checked in this browser.