Terraform init — What Actually Happens Under the Hood

You Type terraform init. What Actually Happens?

Most people treat terraform init as “the thing you run before the real commands.” That’s underselling it. init is doing several critical operations, and understanding what they are saves you from confusing errors and mysterious behavior.

Here’s the sequence, in order:

1. Read the terraform {} block in config
2. Initialize the backend (where state lives)
3. Download required providers → .terraform/providers/
4. Download referenced modules → .terraform/modules/
5. Create or update .terraform.lock.hcl

Each step has implications. Let’s walk through them.

Step 1: Backend Initialization

If your config has a backend block, terraform init initializes it — connecting to the remote state storage (S3, GCS, HCP Terraform, etc.).

terraform {
  backend "s3" {
    bucket = "my-tf-state"
    key    = "prod/terraform.tfstate"
    region = "us-east-1"
  }
}

This is the step where Terraform establishes the connection to your state file’s location. What happens depends on the situation:

  • No state exists anywhere: Terraform initializes an empty state at the backend location
  • State already exists at the backend: Terraform connects to it and reads the existing state
  • State exists locally AND you’re adding a remote backend for the first time: Terraform detects the migration scenario and asks if you want to copy your local state to the new backend

That last scenario is the one that catches people. You’ve been developing locally with terraform.tfstate in your directory. You add an S3 backend block. You run terraform init. Terraform sees the discrepancy and asks: “Do you want to copy your local state to the new backend?” Answer yes to migrate your existing state. Answer no and your remote backend starts empty — meaning Terraform will try to recreate all your existing resources.

Why does the backend initialize before providers? Because everything Terraform does — plan, apply, import — depends on reading state. If the backend can’t be reached (wrong credentials, bucket doesn’t exist, network issue), nothing else works. It’s the foundation.

Step 2: Provider Installation

Terraform reads required_providers and downloads the specified providers into .terraform/providers/.

.terraform/
└── providers/
    └── registry.terraform.io/
        └── hashicorp/
            └── aws/
                └── 5.31.0/
                    └── darwin_amd64/
                        └── terraform-provider-aws_v5.31.0_x5

The provider binary is platform-specific. darwin_amd64 for macOS Intel, linux_amd64 for Linux. This is why you never commit the .terraform/ directory to Git — the binaries for macOS won’t work on your Linux CI runner, and they’d bloat your repository (the AWS provider binary alone is over 300MB).

If a lock file exists, Terraform installs exactly the locked versions. If no lock file exists, Terraform resolves the latest versions within your constraints and creates the lock file. This is why the first terraform init on a new project takes longer — it’s negotiating with the registry.

Step 3: Module Installation

If your config references modules (either local or remote), terraform init downloads them:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"
}

Remote modules are downloaded into .terraform/modules/. Local modules (with a relative path like source = "./modules/vpc") are referenced directly — no download needed.

This step is why you need to re-run terraform init when you add a new module source. Terraform doesn’t automatically discover modules at plan time — it needs them to be installed first.

The Init Output — Reading It
$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.31.0...
- Installed hashicorp/aws v5.31.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record
the provider selections made above.

Terraform has been successfully initialized!

Key phrases to watch for:
– “Installed” → New download happened
– “Reusing previous version” → Using what’s already cached in .terraform/
– “signed by HashiCorp” → The provider binary’s signature was verified against HashiCorp’s public key
– “Backend configuration changed” → You modified the backend block since the last init

If you see errors during init, they’re almost always about network issues (can’t reach the registry), credential issues (can’t authenticate to the backend), or version constraint conflicts (no version satisfies all constraints).

When Do You Need to Re-Run terraform init?

This is a practical question that comes up constantly:

SituationRe-run init?Why
Added a new provider✅ YesProvider binary needs to be downloaded
Changed provider version constraint✅ YesNeed to resolve and lock the new version
Changed backend configuration✅ YesBackend connection needs to be re-established
Added a new remote module✅ YesModule code needs to be downloaded
Changed a resource argument❌ NoProviders and modules haven’t changed
Added a new resource block (existing provider)❌ NoThe provider is already installed
Changed a variable value❌ NoNo infrastructure tooling changes

Terraform is helpful about this — if you’ve changed something that requires init, terraform plan will error with a clear message like "Required plugins are not installed" or "Backend configuration has changed". When in doubt, just run init. It’s safe to re-run — if everything is already initialized, it’s a no-op.

Key Flags and Their Purposes
# Standard init — downloads providers/modules, connects to backend
terraform init

# Upgrade providers to latest within constraints, update lock file
terraform init -upgrade

# Change backend config without migrating existing state
terraform init -reconfigure

# Copy state from old backend to new backend
terraform init -migrate-state

# Use pre-downloaded providers (for air-gapped environments)
terraform init -plugin-dir=/path/to/local/providers

# Initialize without connecting to a backend (useful for fmt/validate only)
terraform init -backend=false

The difference between -reconfigure and -migrate-state matters:

  • -reconfigure — Tells Terraform to forget about the old backend configuration and use the new one, without moving any state. Use this when you want a clean start — for example, when setting up a new environment that happens to use the same config as an existing one.
  • -migrate-state — Copies your existing state from the old backend to the new backend. Use this when you’re moving state between backends — like migrating from local storage to S3, or from one S3 bucket to another. This is the flag you use for production state migrations.
  • -backend=false — Skips backend initialization entirely. Useful when you just want to run terraform fmt or terraform validate in CI without needing cloud credentials. These commands don’t touch state, so they don’t need a backend.
Design Scenario: The New Team Member

A developer joins the team and clones the Terraform repo. They have terraform installed but nothing else.

git clone [email protected]:company/infrastructure.git
cd infrastructure/environments/prod

terraform init
# Downloads AWS provider 5.31.0 (as pinned in .terraform.lock.hcl)
# Downloads the vpc module from registry
# Connects to S3 backend — reads existing prod state

terraform plan
# Shows current state vs config diff — no surprise changes

Because the lock file was committed to Git, the developer gets identical provider versions to everyone else on the team. The backend is initialized from the config. State is automatically pulled from S3. In under a minute, the new developer has a fully functional Terraform workspace identical to every other team member’s.

This is the power of a properly configured Terraform project. One command — terraform init — sets up everything. No manual downloads, no environment-specific scripts, no “ask Dave how to set up” instructions.

Key Takeaways

terraform init is always safe to re-run. It’s idempotent. If providers and modules are already downloaded and the lock file matches, it’s a no-op. When in doubt — init.

The backend initializes first because state is the foundation of everything else. If Terraform can’t read state, nothing else matters — plan, apply, destroy all depend on knowing what currently exists.

The -backend=false flag is your friend for CI jobs that only need to lint or validate. Format checks and validation don’t touch state, so they don’t need cloud credentials. This speeds up your pipeline and reduces the credential surface area.

Provider binaries are platform-specific and large. Never commit .terraform/. Always commit .terraform.lock.hcl. The lock file is tiny and critical. The provider directory is huge and disposable.