Objective 2a of the Terraform Associate (004) exam is a single line: install and version Terraform providers. Most candidates prepare for it by learning the required_providers block, version constraint operators and the fact that .terraform.lock.hcl should be committed. Our providers and provider block guide covers exactly that, including terraform init -upgrade and the one-paragraph mention that mirrors and a plugin cache exist for offline use.
This article is about the plumbing that paragraph skipped. Where does terraform init look for a provider, in what order, and what stops it from using a cached copy? Why does a lock file created on a Mac break a Linux CI run only when the team uses a mirror? What does dev_overrides actually bypass? The exam asks these as scenario questions (“the team must install providers without internet access”, “a developer wants to test an unreleased provider build”, “init fails with a checksum error on a new platform”), and the answers are all in the CLI configuration file and the lock file’s hash scheme.
What terraform init Actually Does With Providers
When you run terraform init, the provider part of the job is a pipeline:
- Discover requirements. Terraform walks the root module and every child module for
required_providersentries and implied provider references, and merges the constraints.terraform providersprints this tree without installing anything, which is the fastest way to find out which module dragged in an unexpected provider. - Select a version. If the lock file already records a version for that provider, Terraform reselects it even when newer versions satisfy the constraints. If there is no entry, Terraform picks the newest version that satisfies every constraint.
-upgradethrows away the recorded selection and chooses the newest again. - Pick an installation method. This is governed by the CLI configuration file. With no configuration, Terraform checks a set of implied local mirror directories and then falls back to the provider’s origin registry (the
directmethod). With aprovider_installationblock, it uses whatever you configured. - Fetch the package from that method, or reuse a copy from the plugin cache if one is allowed.
- Verify checksums against the lock file. A package that matches none of the recorded hashes fails
initwith a “doesn’t match the checksums recorded in the lock file” error. - Write the package under the working directory’s
.terraformdata directory (relocatable withTF_DATA_DIR) and update the lock file, printing a reminder to review and commit it if anything changed.
Every exam scenario about installation is a question about which of those six steps is being changed.
The CLI Configuration File
The CLI configuration file holds per-user settings that apply to every working directory. It is not Terraform configuration: nothing in it goes in a .tf file, and nothing in a .tf file can set it.
| Platform | Default location |
|---|---|
| Windows | terraform.rc in the %APPDATA% directory |
| macOS, Linux and other systems | .terraformrc in the home directory |
| Any | Override with the TF_CLI_CONFIG_FILE environment variable (use a *.tfrc name) |
The file uses HCL. The settings you should recognise:
| Setting | What it controls |
|---|---|
credentials "hostname" block with token | API token for HCP Terraform or Terraform Enterprise; must be a user or team token, one block per host |
TF_TOKEN_<hostname> environment variable (v1.2+) | Same purpose, dots in the hostname become underscores, takes precedence over the file |
credentials_helper "name" block | External program consulted for hosts with no credentials block; at most one, Terraform ships none |
disable_checkpoint | Turns off upgrade and security bulletin checks |
disable_checkpoint_signature | Keeps the checks but drops the anonymous de-duplication ID |
plugin_cache_dir | Enables the provider plugin cache at the given, pre-existing directory |
plugin_cache_may_break_dependency_lock_file | Lets the cache be used even when the lock file has no matching hash |
provider_installation block | Replaces the default installation methods |
A providers block in this file is the Terraform 0.12 mechanism and is deprecated; provider_installation replaced it in 0.13.
Implied Behaviour: What Happens With No Configuration
With no provider_installation block, Terraform behaves as if you had written an implied configuration with a set of filesystem mirrors first and direct last. The implied mirror directories are:
| Platform | Implied filesystem mirror directories |
|---|---|
| Windows | %APPDATA%/terraform.d/plugins and %APPDATA%/HashiCorp/Terraform/plugins |
| macOS | $HOME/.terraform.d/plugins, ~/Library/Application Support/io.terraform/plugins, /Library/Application Support/io.terraform/plugins |
| Linux and Unix | $HOME/.terraform.d/plugins plus terraform/plugins under the XDG data directories (defaults ~/.local/share, /usr/local/share, /usr/share) |
| All | terraform.d/plugins in the current working directory (the launch directory when using -chdir) |
Any provider found in an implied mirror is automatically excluded from the implied direct method, so dropping a provider package into ~/.terraform.d/plugins is enough to make Terraform stop contacting the registry for it. That automatic exclusion does not apply when you write an explicit block; there you must add exclude yourself.
Explicit Installation Methods: provider_installation
An explicit block lists one or more methods. Terraform tries all matching methods for a provider and selects the newest version available across them that satisfies the configuration’s constraints.
# ~/.terraformrc
provider_installation {
filesystem_mirror {
path = "/opt/terraform/providers"
include = ["registry.terraform.io/hashicorp/*", "example.com/*/*"]
}
network_mirror {
url = "https://mirror.example.com/providers/"
exclude = ["example.com/*/*"]
}
direct {
exclude = ["registry.terraform.io/hashicorp/*", "example.com/*/*"]
}
}
| Method | Argument | Purpose |
|---|---|---|
direct | none | Download from each provider’s origin registry |
filesystem_mirror | path | Read packages from a local directory in the mirror layout; multiple blocks allowed |
network_mirror | url (must be https: and end in a slash) | Fetch from a server implementing the provider network mirror protocol |
dev_overrides | map of source address to local directory | Use a locally built provider binary, bypassing version and checksum checks |
Every method except dev_overrides accepts include and exclude lists of source-address patterns. Exclusions take priority over inclusions. A pattern may omit the registry.terraform.io/ prefix, so */* means every provider on the public registry. Terraform warns against untrusted network mirrors because a mirror can serve modified packages.
Pattern you will see on the exam: air-gapped installation
“The security team blocks outbound internet from the CI runners. How should the team install providers?” The answer is a filesystem mirror (or an internal network mirror) declared in the CLI configuration, populated with terraform providers mirror, with direct either omitted or excluded for every provider. Omitting direct entirely makes the registry unreachable by design.
provider_installation {
filesystem_mirror {
path = "/opt/terraform/providers"
include = ["*/*"]
}
}
A one-off alternative is terraform init -plugin-dir=/opt/terraform/providers, which treats the directory as if it were the only filesystem_mirror and ignores the configured methods for that run. It is meant for testing, not for a standing policy.
Mirror Directory Layouts
A filesystem mirror holds providers in one of two layouts, and both may be mixed in one directory:
| Layout | Path pattern | Notes |
|---|---|---|
| Packed | HOSTNAME/NAMESPACE/TYPE/terraform-provider-TYPE_VERSION_TARGET.zip | The original distribution zip from the registry; Terraform always extracts it |
| Unpacked | HOSTNAME/NAMESPACE/TYPE/VERSION/TARGET/ | The extracted contents; Terraform may symlink to it instead of copying |
VERSION is a plain version such as 5.70.0; TARGET is the platform, such as linux_amd64 or darwin_arm64. The hostname is the provider’s origin hostname (for example registry.terraform.io), not the mirror’s, which is why one mirror can serve providers from several registries.
Building the mirror with terraform providers mirror
terraform providers mirror \
-platform=linux_amd64 \
-platform=darwin_arm64 \
/opt/terraform/providers
The command reads the current configuration’s requirements, downloads the matching packages for each -platform (defaulting to the platform you run it on) and writes them in the packed layout. It also writes the index.json and VERSION.json index files that the network mirror protocol needs, so the same directory can be uploaded to any static HTTPS host and used as a network_mirror; when used as a filesystem mirror the JSON files are ignored. Re-running it against the same directory adds packages and merges indexes rather than wiping them, and running it from several configurations into one directory produces one combined mirror. By default (-lock-file=true) it respects the versions in the lock file if the directory has been initialised.
The network mirror protocol in one paragraph
Terraform makes two kinds of request under the mirror’s base URL: HOSTNAME/NAMESPACE/TYPE/index.json to list available versions, and HOSTNAME/NAMESPACE/TYPE/VERSION.json to list the archives per platform. Each archive entry has a url (relative to the JSON file) and an optional hashes list of h1: values. If hashes are present Terraform verifies the download against them; if not, it installs unverified, which is why mirrors must be trusted. Credentials configured for the mirror’s hostname are sent to the index requests but not to the archive downloads. The protocol has existed since Terraform 0.13.2, and it is deliberately static-file friendly.
dev_overrides: Testing a Provider You Built Locally
provider_installation {
dev_overrides {
"hashicorp/null" = "/home/dev/go/bin"
}
direct {}
}
Rules that turn into exam distractors:
dev_overridesmust be the first method in the block and wins over every other method for the providers it lists.- The directory must contain an executable named
terraform-provider-TYPE(optionally with a version suffix). - For an overridden provider Terraform disables version constraint checking and checksum verification. You get a warning on every command.
terraform initstill selects and records a published version in the lock file; it is the other commands (plan,apply, and so on) that ignore the lock entry and use the local binary. So “dev_overrides lets me skip init” is false, and “the lock file still shows the registry version” is expected.- If you omit
direct {}from the block, only the overridden providers are installable. - It works in Terraform 0.14 and later, is documented as a temporary measure, and should never appear in shared CI configuration.
The Plugin Cache and Its Lock-File Rule
Without a cache, every working directory downloads its own copy of each provider. The cache is a shared directory that init copies from (or symlinks to) instead of hitting the network:
plugin_cache_dir = "$HOME/.terraform.d/plugin-cache"
or, for one shell session, export TF_PLUGIN_CACHE_DIR="$HOME/.terraform.d/plugin-cache". The directory must already exist, must not also be a filesystem mirror directory, is never cleaned by Terraform, and is not guaranteed to be safe for concurrent init runs.
The behaviour that trips people in CI is the interaction with the lock file. Terraform uses a cached package only if it matches at least one checksum already recorded in the lock file for that provider. On the first init of a configuration with no lock entry, Terraform downloads from upstream even though the cache has the package, so that it can record the registry’s signed checksums for all platforms. If you want the cache used regardless, set plugin_cache_may_break_dependency_lock_file = true (or the TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE environment variable to anything other than empty or 0). The cost is in the name: the resulting lock entry may contain only an h1: hash for the current OS and architecture, which breaks the next teammate on a different platform. The documentation reserves it for unusual situations.
Lock-File Hashes: zh: vs h1: and Trust on First Use
.terraform.lock.hcl lives in the root of the configuration, is created and updated only by init (and providers lock), and tracks providers only; module versions are never locked, so an exact module constraint is the only way to pin a module. Our state management guide explains why the lock file belongs in version control next to the backend configuration.
provider "registry.terraform.io/hashicorp/aws" {
version = "5.70.0"
constraints = "~> 5.70"
hashes = [
"h1:3b2p6LqG3nQ8rCzQy1vRq6eC3dD1cQ9mK2wE7hL5xYo=",
"zh:0b1a2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5",
"zh:1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
]
}
| Field | Meaning |
|---|---|
version | The exact version init selected |
constraints | The constraints in force when it was selected; informational only |
hashes | Every checksum Terraform will accept for that version, across platforms |
Two hash schemes coexist:
| Scheme | Computed from | Can verify |
|---|---|---|
zh: (“zip hash”) | SHA-256 of the official .zip from the registry | Packages installed from a registry, which still publishes this scheme |
h1: (“hash scheme 1”) | SHA-256 over the package contents | The official zip, an unpacked mirror directory, or a re-zipped copy; the preferred scheme going forward |
The security model is trust on first use. When a provider is added, Terraform pre-populates hashes from the developer’s signed checksums published by the registry, which are mostly zh: values for every platform the developer released. From then on, any package that matches none of the recorded hashes is rejected. init prints the signing key ID so you can verify who signed what, and h1: hashes are added opportunistically whenever a newly installed package already matches a recorded hash.
Why a mirror breaks cross-platform lock files
Registry installs record signed zh: hashes for all platforms. A mirror install cannot do that: a filesystem mirror in the unpacked layout has no official zip to hash, and no mirror can supply the developer’s signature. So an init against a mirror records only the h1: hash for the platform where it ran. The next init on another platform finds a package whose hash is not in the file and fails with the checksum error. The fix is to pre-populate the lock file from the origin registry for every platform the team uses:
terraform providers lock \
-platform=linux_amd64 \
-platform=darwin_arm64 \
-platform=windows_amd64
terraform providers lock adds selection and checksum information without installing anything. Run with no provider addresses it covers every provider in the configuration; name specific addresses to update only those entries. -fs-mirror=PATH and -net-mirror=URL make it read from a mirror instead of the registry (recording only the mirror’s hashes for the requested platforms, with no signed checksums), which is how you lock an in-house provider that exists only in your mirror. -enable-plugin-cache is off by default because the cache is not an authoritative source.
init flags that touch the lock file
| Flag | Effect |
|---|---|
terraform init -upgrade | Ignore recorded selections, pick the newest versions matching the constraints, rewrite the lock file |
terraform init -lockfile=readonly | Verify checksums against the lock file but never modify it; conflicts with -upgrade; the right flag for CI so a run cannot silently change dependencies |
terraform init -plugin-dir=PATH | Install only from that directory, as a one-time filesystem_mirror |
terraform init on Terraform 1.1+ | Removes lock entries for providers no longer referenced by configuration or state; v1.0 and earlier left stale entries, which produce a “missing or corrupted provider plugins” error until a newer init cleans them |
Removed providers that are later re-added are treated as new, so the selected version and hashes may differ.
Decision Table: Scenario to Mechanism
| Requirement | Mechanism |
|---|---|
| Stop re-downloading the same provider for every project on a laptop | plugin_cache_dir or TF_PLUGIN_CACHE_DIR |
Cache is populated but init still downloads on a fresh project | Expected: no lock entry yet; or set plugin_cache_may_break_dependency_lock_file and accept a single-platform lock |
| No outbound internet from runners | terraform providers mirror into a directory, filesystem_mirror (or network_mirror) in the CLI config, no direct |
| Internal providers from an internal mirror, public ones from the registry | filesystem_mirror or network_mirror with include, direct with matching exclude |
| Test an unreleased provider build locally | dev_overrides first in provider_installation, keep direct {} for everything else |
| Lock file fails checksum on Linux after being created on macOS via a mirror | terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 |
| CI must fail rather than mutate the lock file | terraform init -lockfile=readonly |
| Move to newer provider versions deliberately | terraform init -upgrade, review the diff, commit the lock file |
| Lock a provider that exists only in an internal mirror | terraform providers lock -fs-mirror=PATH or -net-mirror=URL |
| Install from a directory just once without editing config | terraform init -plugin-dir=PATH |
| See which module requires which provider | terraform providers |
| Machine-readable resource and attribute schemas | terraform providers schema -json after init |
Commands Cheat Sheet
# Show provider requirements by module (no installation)
terraform providers
# Record versions and signed checksums for three platforms without installing
terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 -platform=windows_amd64
# Lock an internal provider from a filesystem mirror
terraform providers lock -fs-mirror=/opt/terraform/providers example.com/platform/internal
# Populate a mirror (packed zips + network-mirror JSON indexes)
terraform providers mirror -platform=linux_amd64 /opt/terraform/providers
# One-off install from a directory, ignoring configured methods
terraform init -plugin-dir=/opt/terraform/providers
# CI: verify against the lock file, never change it
terraform init -lockfile=readonly
# Deliberately move to newer versions within constraints
terraform init -upgrade
# Enable the plugin cache for this shell
export TF_PLUGIN_CACHE_DIR="$HOME/.terraform.d/plugin-cache"
# Point Terraform at a non-default CLI config file
export TF_CLI_CONFIG_FILE="$HOME/ci.tfrc"
The rest of the day-to-day command set is in our Terraform commands cheat sheet, and the init to apply sequence these flags modify is covered in the core workflow guide.
Exam Scenario Drill
| Scenario | Correct answer | Trap answer |
|---|---|---|
| Team wants every developer to use the same provider versions | Commit .terraform.lock.hcl | Pin exact versions in required_providers only (works, but the lock file is the mechanism Terraform provides) |
init on a Linux runner fails with a checksum mismatch after a developer created the lock on macOS using a mirror | terraform providers lock -platform=linux_amd64 … and commit | Delete the lock file |
| Runners have no internet | provider_installation with a mirror populated by terraform providers mirror | Put the provider binary in the module directory |
A developer needs to run plan against a provider build from source | dev_overrides in the CLI configuration | Edit required_providers to point at a local path (not valid) |
| After enabling the plugin cache, a brand-new project still downloads | Normal: nothing in the lock file matches yet | The cache path is wrong |
| Where is the CLI configuration file on Linux? | ~/.terraformrc (or TF_CLI_CONFIG_FILE) | terraform.tfvars, backend.tf |
| What does the lock file record for modules? | Nothing; only providers | Module versions and hashes |
What does -upgrade do to the lock file? | Re-selects newest versions within constraints and rewrites entries | Deletes it |
What does -lockfile=readonly do? | Checks hashes, refuses to change the file | Skips hash verification |
| Which hash scheme can a mirror in unpacked layout produce? | h1: only | zh: |
A network_mirror URL starts with http:// | Rejected; must be https: with a trailing slash | Accepted with a warning |
dev_overrides listed after direct {} | Invalid; it must come first | Fine |
Common Mistakes
- Putting installation settings in
.tffiles.plugin_cache_dir,provider_installationandcredentialsonly exist in the CLI configuration file or their environment variables. - Expecting the cache to be used on first
init. It is skipped until the lock file holds a matching hash, unless you opt into the “may break” setting. - Treating
dev_overridesas a way to skipinit.initstill runs and still records a registry version; only later commands use the local binary. - Assuming a mirror install produces a portable lock file. It records one platform’s
h1:hash; runterraform providers lock -platform=…for the others. - Deleting the lock file to “fix” a checksum error. That discards trust on first use; add the missing platform hashes instead.
- Omitting
excludein an explicit block. Automatic exclusion fromdirectonly happens for implied mirrors. - Using an
http:network mirror or forgetting the trailing slash. Both are rejected. - Running
-upgradein CI. Use-lockfile=readonlyso dependency changes only happen through a reviewed commit. - Believing the lock file pins modules. It does not; use exact module version constraints.
- Sharing a plugin cache directory as a mirror. The documentation requires them to be separate directories.
Frequently Asked Questions
Is the CLI configuration file the same as a .tfvars or backend file?
No. .tfvars files set input variables for a configuration; the backend block lives in .tf files; the CLI configuration file (~/.terraformrc or %APPDATA%\terraform.rc) sets per-user CLI behaviour such as credentials, the plugin cache and provider installation methods. It is HCL, but it is not Terraform configuration and is never read as part of a module.
Do I still need terraform init when using dev_overrides?
Yes. init selects a published version and records it in the lock file as usual. The override is honoured by plan, apply and the other commands, which ignore the lock entry for that provider and run the local binary, printing a warning.
Why does my lock file have many zh: lines but only one h1: line?
Registry installs record the developer’s signed checksums, which are zh: hashes for every platform they released. Terraform computes an h1: hash only for the package it actually installed on your platform, and adds more h1: hashes as other platforms install and match. Running terraform providers lock -platform=… records both schemes for each platform you name.
Can terraform providers mirror build a network mirror?
Yes. Besides the packed zips, it writes the index.json and per-version JSON files the provider network mirror protocol expects, so the directory can be served from any static HTTPS host and referenced as a network_mirror.
Does terraform init -upgrade also upgrade modules?
Yes. It re-selects providers to the newest versions within constraints and also refreshes modules to the newest version their source constraints allow, since module versions are never locked.
What is the difference between -plugin-dir and filesystem_mirror?
-plugin-dir is a one-run flag on init that behaves like a single filesystem_mirror and ignores the configured methods for that run. filesystem_mirror is persistent configuration in the CLI configuration file, can be combined with include and exclude, and is the right tool for a standing air-gapped policy.
Conclusion
Objective 2a looks small until you list what terraform init actually decides: which version, from which source, verified against which hash, written to which directory, recorded in which file. The CLI configuration file controls the source (implied mirrors and direct by default, or an explicit provider_installation block with filesystem and network mirrors and dev_overrides), the plugin cache controls whether the network is touched at all, and the lock file’s zh: and h1: hashes with trust on first use control whether the result is accepted. Knowing that a mirror install records a single-platform h1: hash, that terraform providers lock -platform=… fixes it, that the cache is skipped until the lock file matches, and that dev_overrides bypasses checksums but not init, answers most of the scenario questions in this area.
For the rest of the exam, Sailor.sh’s HashiCorp Terraform Associate (004) Mock Exam Bundle gives you eight full-length, 60-minute practice exams with explanations that spell out why the -upgrade answer, the “delete the lock file” answer or the .tf-file answer is wrong. Pair it with our Terraform Associate exam guide and the 30-day study plan, then run through the Terraform Associate practice questions to confirm the installation mechanics have stuck.