4.2 Initialize a Working Directory
Key Takeaways
- terraform init is the first command after you write a new configuration or clone one from version control; plan and apply cannot run until it succeeds
- Init initializes the backend, installs child modules, downloads provider plugins, and creates or updates .terraform.lock.hcl
- Re-running init is safe and idempotent: it never deletes configuration or state, and it only fetches modules or providers that are new unless you pass -upgrade
- When backend settings change, -migrate-state copies existing state to the new backend and -reconfigure ignores the old backend configuration without migrating
- Commit .terraform.lock.hcl; do not commit the .terraform/ cache (modules, provider binaries, current workspace name)
4.2 Initialize a Working Directory
Quick Answer:
terraform initprepares the working directory. It initializes the backend, installs modules, downloads provider plugins into.terraform/, and writes.terraform.lock.hcl. It is safe to run again. You cannotplanorapplyuntil it succeeds. Commit the lock file. Do not commit.terraform/.
Objective 3b on Terraform Associate (004) is operational: initialize a Terraform working directory on Terraform 1.12. HashiCorp's command reference calls init the first command you should run after writing a new configuration or cloning an existing one. HCP Terraform still runs an equivalent init on each remote run so that workspace gets the same plugins the lock file names.
Official reference: terraform init command and the Initialize Terraform configuration tutorial.
Why this objective appears on 004
init is the command people skip, and it is the command whose flags people mix up. 004 asks whether you know what init installs, that you can re-run it, which flags change backend vs plugin behavior, and what belongs in Git. If you answer "init creates the VPC" or "commit the .terraform folder so CI is faster," you have the wrong mental model.
What terraform init actually does
Usage is terraform init [options]. In a typical root module it performs several initialization steps:
| Step | What Terraform does | What you see |
|---|---|---|
| Backend initialization | Reads the backend or cloud block (or selects the local backend) and prepares state storage | Initializing the backend... |
| Child module installation | Finds module blocks and retrieves source code from local paths, the public registry, Git, or other sources | Initializing modules... |
| Provider plugin installation | Resolves required_providers (root and modules), downloads plugins, verifies checksums | Initializing provider plugins... |
| Dependency lock file | Creates or updates .terraform.lock.hcl with selected provider versions and hashes | A note to review and commit lock-file changes |
$ terraform init
Initializing the backend...
Initializing modules...
- vpc in modules/vpc
Downloading registry.terraform.io/terraform-aws-modules/security-group/aws 5.1.0 for sg...
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Installing hashicorp/aws v5.70.0...
Terraform has been successfully initialized!
After that message, HashiCorp's own output tells you to try terraform plan and to re-run init if you change modules or backend configuration.
init does not create your aws_instance. It does not refresh remote objects. It does not write application infrastructure into terraform.tfstate beyond whatever the backend itself needs. Those jobs belong to plan and apply.
Providers and modules, briefly
Provider install is the same mechanism chapter 3 covered: required_providers plus the lock file decide the package, and binaries land under .terraform/providers/<hostname>/<namespace>/<type>/<version>/<os>_<arch>/.
Module install is parallel. A local source = "./modules/vpc" is recorded. A registry module such as source = "terraform-aws-modules/vpc/aws" with version = "~> 5.0" is downloaded into .terraform/modules/. Change a module source or version and you must run init again (or terraform get) before plan can see the new code. The lock file tracks provider selections, not remote module versions — pin modules with an exact version argument when you need a freeze.
Idempotent re-runs
HashiCorp states that init is always safe to run multiple times to bring the directory up to date. Subsequent runs may error (bad backend credentials, a constraint that cannot be satisfied), but the command never deletes your existing configuration or state.
Default re-run behavior:
- New modules added since last init are installed.
- Already-installed modules are left alone unless you pass
-upgrade. - Providers already recorded in
.terraform.lock.hclare reused, even if a newer version matches~> 5.0. - Backend settings that have not changed are reused.
That is why a teammate can clone the repo, run plain terraform init, and get the same provider bits the lock file names. It is also why "I added a module and plan cannot find it" is fixed by re-running init, not by re-running apply.
Exam-level flags: -upgrade, -reconfigure, -migrate-state
These three flags are the 3b discriminators. They do different jobs.
terraform init -upgrade
-upgrade opts to upgrade modules and provider plugins during their install steps.
- Providers: ignore versions recorded in
.terraform.lock.hcland select the newest version that still satisfies every module'srequired_providersconstraint. Then rewrite the lock file. - Modules: update already-installed modules to the latest source that matches each
versionconstraint, instead of leaving the previously downloaded tree in place.
-upgrade does not upgrade the Terraform CLI. It does not ignore your constraints (~> 5.0 still will not jump to 6.x). It does not migrate state. To move a major provider line you edit the constraint first, then run init -upgrade.
Backend change: -reconfigure vs -migrate-state
Re-running init after you change backend settings will not silently pick a new backend. Official docs: you must supply either -reconfigure or -migrate-state to update the backend configuration.
| Flag | State data | When you use it |
|---|---|---|
-migrate-state | Attempts to copy existing state to the new backend; may prompt per workspace | You are moving from local state to S3, or from one remote backend to another, and you want the same objects tracked |
-force-copy | Same copy, but answers "yes" to migration prompts; implies -migrate-state | Automation that must not wait on a TTY |
-reconfigure | Disregards existing backend configuration; does not migrate state | You intend to start using the new backend as-is, or you are correcting settings without copying |
-backend=false | Skips backend configuration | Already-initialized directory; useful so validate can run without touching remote state |
-backend-config=... | Partial backend settings (bucket name, key, credentials) that should not live in .tf | Dynamic or sensitive backend values |
terraform {
backend "s3" {
bucket = "tfstate-prod-004"
key = "network/terraform.tfstate"
region = "us-east-1"
}
}
After replacing a local backend with that block:
terraform init -migrate-state # copy existing state into S3
# or
terraform init -reconfigure # use S3 from scratch; do not copy
Mixing these up is a classic 004 trap. -upgrade is not a backend flag. -reconfigure is not "the safe migrate." -migrate-state is the copy path.
What .terraform/ contains vs what you commit
A successful init creates two different artifacts.
.terraform/ (usually do not commit)
providers/— downloaded plugin binaries for this OS/arch.modules/— installed module source (plusmodules.json).environment— the currently selected CLI workspace name, so two engineers can select different workspaces without fighting a shared file.- With an HCP Terraform
cloudblock, additional metadata such as a state pointer for that remote workspace.
HashiCorp's init tutorial says Terraform automatically manages this directory: do not check it into version control, and do not edit it by hand. HCP Terraform and CI runners create their own .terraform/ on each run.
.terraform.lock.hcl (do commit)
This file sits next to the root .tf files, not inside .terraform/. It records exact provider versions and package checksums (trust-on-first-use). Terraform 1.12 still uses it for providers only. Include it in VCS so every laptop and every HCP Terraform run installs the same plugin packages. If init rewrites it, review the diff like any other dependency bump.
# commit
*.tf
*.tfvars.example
.terraform.lock.hcl
# do not commit
.terraform/
*.tfplan
terraform.tfstate
terraform.tfstate.d/
You cannot plan or apply until init succeeds
terraform plan and terraform apply require an initialized working directory. If plugins or modules are missing, those commands stop and tell you to run init. terraform validate has the same requirement — it needs the provider schemas and the module tree (see 4.3). The escape hatch for validate-only CI is terraform init -backend=false, which installs plugins and modules without talking to the configured backend.
Scenario: new hire, clean laptop
Jordan clones the network repo. The clone has main.tf, a modules/ path, and .terraform.lock.hcl. It does not have .terraform/. terraform plan fails. terraform init initializes the local or HCP backend, installs modules, downloads the locked hashicorp/aws package, and leaves the lock file unchanged. Then plan works. If Jordan had committed their .terraform/darwin_arm64 plugins, the Linux CI runner still could not use them — that is why the lock file, not the cache, is the portable pin.
Later the team bumps required_providers to allow AWS 5.80. Jordan runs terraform init -upgrade, reviews the lock-file diff, and opens a PR. Nobody runs -reconfigure for that change, because the backend did not move.
004 traps for objective 3b
initprepares the directory; it does not apply infrastructure.- Re-running
initis expected, not a smell. -upgraderefreshes modules and providers against constraints; it does not migrate state.-migrate-statecopies state;-reconfiguredoes not.- Commit
.terraform.lock.hcl. Do not commit.terraform/. - Plan/apply/validate wait until init has succeeded.
You clone a Terraform 1.12 root module and run terraform plan immediately. The command fails. What did terraform init still need to do first?
After a successful terraform init, which artifacts belong in Git for a team using Terraform 1.12 and HCP Terraform?
You changed the backend block from local state to an HCP Terraform cloud block. Which terraform init flags describe the two official ways to handle that change?