terraform init is the very first command you run in any Terraform project. It prepares your working directory so that every other Terraform command (plan, apply, destroy) can work correctly.
Think of it like npm install or pip install, it sets up the dependencies your project needs before you can do anything else.
terraform initWhen you run terraform init, Terraform does three things:
Terraform reads the required_providers block in your configuration and downloads the declared providers from the Terraform Registry (or a private registry):
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
random = {
source = "hashicorp/random"
version = "3.6.0"
}
}
}Running init on this configuration downloads both the AWS and Random providers into .terraform/providers/. This directory can be hundreds of megabytes, always add it to .gitignore.
Sample output:
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Finding hashicorp/random versions matching "3.6.0"...
- Installing hashicorp/aws v5.54.1...
- Installing hashicorp/random v3.6.0...
Terraform has been successfully initialized!
If you have a backend block configured, init connects to it and migrates state if needed:
terraform {
backend "s3" {
bucket = "my-tfstate-bucket"
key = "production/terraform.tfstate"
region = "us-east-1"
}
}When switching from local state to a remote backend, init asks:
Do you want to copy existing state to the new backend?
yes/no:
terraform init writes .terraform.lock.hcl, a file that pins the exact provider versions downloaded:
# .terraform.lock.hcl (auto-generated)
provider "registry.terraform.io/hashicorp/aws" {
version = "5.54.1"
constraints = "~> 5.0"
hashes = [
"h1:abc123...",
]
}Always commit .terraform.lock.hcl to Git. It ensures every team member and every CI/CD pipeline uses exactly the same provider versions, preventing "works on my machine" failures caused by version drift.
After init, your project will have:
my-project/
âââ main.tf
âââ variables.tf
âââ .terraform/ # â created by init, add to .gitignore
â âââ providers/
â â âââ registry.terraform.io/
â â âââ hashicorp/
â â âââ aws/5.54.1/ # downloaded provider binary
â âââ terraform.tfstate # backend config cache
âââ .terraform.lock.hcl # â commit this to Git
Re-download all providers, selecting the newest version that satisfies your constraints:
terraform init -upgradeUse this when you want to update providers to the latest allowed version. Without -upgrade, init uses the versions already in the lock file.
Warning: Always run
terraform planafter-upgradeto check for breaking changes in the new provider version.
Force init to reconfigure the backend without migrating state:
terraform init -reconfigureUseful when you have changed your backend configuration and want to switch without copying existing state.
Skip backend initialisation entirely:
terraform init -backend=falseUseful in CI pipelines where you only want to validate the configuration without connecting to a remote backend.
Pass backend configuration values at init time instead of hardcoding them in the backend block, great for keeping bucket names or credentials out of source code:
terraform init \
-backend-config="bucket=my-tfstate-bucket" \
-backend-config="key=production/terraform.tfstate" \
-backend-config="region=us-east-1"You must re-run terraform init whenever you:
| Change | Why |
|---|---|
Add or remove a required_providers entry | New provider needs to be downloaded |
Change provider version constraints | Different version needs to be installed |
Change the backend block | Backend connection must be re-established |
| Clone a repo for the first time | Providers are not committed to Git |
Add or update a module source | Module code needs to be downloaded |
It is always safe to re-run init, it never modifies your infrastructure.
In a CI/CD pipeline, always run init before plan or apply. Use the -input=false flag to prevent interactive prompts:
# GitLab CI example
terraform-plan:
script:
- terraform init -input=false
- terraform plan -out=tfplan<MCQExercise exercise={{ id: 'tf-init-q1', title: 'Question 1: Lock File Purpose', description: 'After running terraform init, you see a .terraform.lock.hcl file. A teammate asks if they should commit it. What do you tell them?', options: [ { id: 'a', text: 'No, add it to .gitignore, it is a local file like .terraform/' }, { id: 'b', text: 'Yes, commit it so the whole team and CI use the same provider versions' }, { id: 'c', text: 'It does not matter, Terraform ignores it if it already exists' }, { id: 'd', text: 'Only commit it after running terraform apply for the first time' }, ], correctId: 'b', explanation: 'Commit .terraform.lock.hcl to Git. It pins the exact provider versions (with checksums) so every team member and every CI/CD run uses identical providers. Without it, different people might get different provider versions, causing inconsistent behaviour. The .terraform/ directory (the actual downloaded binaries) goes in .gitignore, not the lock file.', wrongExplanations: { a: '.terraform/ belongs in .gitignore, but .terraform.lock.hcl should be committed. They serve different purposes.', c: 'Terraform actively uses the lock file. On subsequent inits, it installs exactly the versions recorded there.', d: 'Commit the lock file immediately after init, before any apply. Its purpose is to lock versions from the start.', }, }} />
<MCQExercise exercise={{ id: 'tf-init-q2', title: 'Question 2: When to Re-run init', description: 'You add a new provider to required_providers in your main.tf. You run terraform plan immediately. What happens?', options: [ { id: 'a', text: 'Terraform downloads the new provider automatically during plan' }, { id: 'b', text: 'Terraform errors: "Required plugins are not installed. Run terraform init."' }, { id: 'c', text: 'Terraform skips the new provider and plans without it' }, { id: 'd', text: 'Terraform uses the cached provider from a previous project' }, ], correctId: 'b', explanation: 'terraform plan does not download providers. Only terraform init does. If you add a new provider and run plan without running init first, Terraform will error telling you to run terraform init. This is by design, init is the explicit step for installing dependencies.', wrongExplanations: { a: 'Only terraform init downloads providers. terraform plan only reads state and calculates diffs.', c: 'Terraform does not silently skip missing providers. It errors clearly.', d: 'Provider plugins are not shared between projects. Each project's .terraform/ directory has its own copies.', }, }} />
<MCQExercise exercise={{ id: 'tf-init-q3', title: 'Question 3: terraform init -upgrade', description: 'Your lock file pins AWS provider at v5.0.0. A new version v5.54.1 is available. You want to update. What command do you run?', options: [ { id: 'a', text: 'terraform init (no flags needed)' }, { id: 'b', text: 'terraform upgrade' }, { id: 'c', text: 'terraform init -upgrade' }, { id: 'd', text: 'terraform providers upgrade aws' }, ], correctId: 'c', explanation: 'terraform init -upgrade re-downloads all providers, selecting the newest version that satisfies your version constraints. Without -upgrade, terraform init uses the versions already recorded in .terraform.lock.hcl and skips downloading if they are already installed. After upgrading, run terraform plan to verify no breaking changes were introduced.', wrongExplanations: { a: 'Without -upgrade, terraform init respects the lock file and uses the pinned version.', b: 'terraform upgrade is not a valid Terraform command.', d: 'terraform providers upgrade is not a valid command either.', }, }} />
Whenever the things it sets up change: after adding or changing a provider or its version constraint, after adding or updating a module source, after changing the backend configuration, and after cloning a repository for the first time since .terraform/ is not committed. Terraform tells you when it is needed with an error saying the configuration has changed since initialisation. Running it again when it is not needed is harmless.
Plain init respects .terraform.lock.hcl and installs exactly the provider versions recorded there, which is what makes builds reproducible. -upgrade ignores the locked versions, resolves the newest releases allowed by your version constraints, and rewrites the lock file. Use -upgrade deliberately when you intend to move provider versions, then review the lock file diff like any other dependency bump, never as a reflex when something is failing.
No. It contains downloaded provider binaries which are large, platform-specific and reproducible from the lock file, so committing them bloats the repository for no benefit. Add .terraform/ to .gitignore. Do commit .terraform.lock.hcl though, that is the small text file recording which versions to install, and it is what makes everyone's init produce the same result.
Run it at the start of every job, since CI workspaces start empty. Use -input=false so it never waits for interactive input, and -backend-config to supply backend settings from environment variables or secrets rather than committing them. Caching the .terraform directory between runs speeds things up noticeably on large provider sets, but key the cache on the lock file so a provider change invalidates it.
Usually network access or a version constraint that cannot be satisfied. In a restricted network the registry may be unreachable, in which case a provider mirror or a network policy change is needed. If the error names a version, your constraints conflict. Perhaps a module requires a provider version your root configuration excludes. Read the constraint chain in the error, which names each source, and reconcile them in required_providers.
Read terraform plan symbols correctly, save a plan file with -out, target resources, detect drift and run plan in CI/CD without surprises on apply.
Use terraform apply safely: review the plan, apply a saved plan file, tune parallelism, use -auto-approve in CI/CD and recover from common apply errors.
Run terraform destroy safely: read the destroy plan, preview it first, remove single resources with -target, and keep code tidy with fmt and validate.