Skip to main content
RunBook Academy

TerraformVI · Providers and the Provider EcosystemProduction Terraform

The Provider Plugin Model

Foundation⏱ ~12 minbash

What you'll learn

  • Describe the boundary between Terraform Core and a provider plugin
  • Explain why providers run as separate executables and what protocol they speak
  • Locate the provider cache directory on Linux and macOS hosts
  • Configure a shared plugin cache to reduce downloads and improve integrity
  • Recognise the production failure modes of a stale or mismatched plugin cache

Prerequisites

None — start here.

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

Not yet marked complete on this device.

A Terraform provider is not a library linked into the CLI. It is a separate executable that Terraform Core launches when the configuration is initialised. The boundary is deliberate: it lets HashiCorp, AWS, Azure, GCP, and thousands of third parties ship providers at their own cadence without rebuilding Core, and it lets you pin the version you trust. The lesson teaches what that boundary means in production: where the binaries live, how Core finds them, and what to do when the cache goes wrong.

The boundary: Core and provider as two processes

When you run terraform plan, two processes exist for the duration of the command:

   +--------------------+
   |   terraform CLI    |
   |   (Core binary)    |
   +---------+----------+
             |
             |  Plugin Protocol over stdin/stdout
             |  (gRPC, JSON-encoded)
             |
   +---------v----------+
   |  provider "aws"    |
   |  (separate binary) |
   +--------------------+

Core reads the configuration, builds the resource graph, and delegates every CRUD call against a resource to the provider process. The provider does the API work against AWS, GCP, GitHub, or whatever service it represents.

This model is from Terraform 0.1. The plugin protocol itself has evolved. Terraform 1.x uses a Go-plugin-based RPC. OpenTofu 1.7 diverged in late 2024 to its own protocol iteration but the operational model — Core launches the provider, the provider talks back over a structured protocol — is unchanged.

Where the binaries live

terraform init downloads provider binaries into a working directory under .terraform/providers/. The path encodes the source, version, and operating system:

.terraform/
└── providers/
    └── registry.terraform.io/
        └── hashicorp/
            └── aws/
                └── 5.80.0/
                    └── linux_amd64/
                        └── terraform-provider-aws_v5.80.0_x5

Three pieces of context are encoded in the path:

  1. Hostname — registry.terraform.io is the public registry. A self-hosted private registry or filesystem mirror changes this segment. The next lesson covers sources.
  2. Namespace and type — hashicorp/aws. The source address is the unique key.
  3. Version and platform — 5.80.0/linux_amd64. The lock file records the exact version and the hashes for one or more platforms.

You can list what is on disk:

# READ-ONLY: list the provider binaries Terraform has cached.
find .terraform/providers -type f -name 'terraform-provider-*'
.terraform/providers/registry.terraform.io/hashicorp/aws/5.80.0/linux_amd64/terraform-provider-aws_v5.80.0_x5
.terraform/providers/registry.terraform.io/hashicorp/local/2.5.1/linux_amd64/terraform-provider-local_v2.5.1_x5

If a binary is missing or unreadable, Terraform prints an error on the next init or plan. That error is the only signal the team will get that the cache is corrupt.

The plugin cache

The .terraform/providers/ tree is per working directory. A fresh git clone followed by terraform init re-downloads every provider, which is wasteful on CI runners that build many working trees. The solution is a shared plugin cache, configured once on the host:

# CONFIGURATION: enable a shared, host-wide plugin cache.
mkdir -p /var/cache/terraform-plugin
chmod 0755 /var/cache/terraform-plugin

cat > ~/.terraformrc <<'EOF'
plugin_cache_dir = "/var/cache/terraform-plugin"
EOF

Or, for a CI runner:

# CONFIGURATION: machine-wide CLI config.
sudo tee /etc/terraformrc <<'EOF'
plugin_cache_dir = "/opt/terraform-plugin-cache"
EOF

With plugin_cache_dir set, Terraform still writes provider metadata into .terraform/providers/ under each working directory, but it skips re-downloading the binary if the same version is already present in the shared cache.

The lock file and platform hashes

The .terraform.lock.hcl records the version and the hashes Terraform verified when it first downloaded the binary. The hashes are platform-specific:

provider "registry.terraform.io/hashicorp/aws" {
  version = "5.80.0"
  hashes = [
    "h1:abcd1234...",  # linux_amd64
    "h1:efgh5678...",  # darwin_amd64
    "h1:ijkl9012...",  # darwin_arm64
  ]
}

A team with mixed Linux and macOS workstations ends up with multiple hashes per provider. The terraform providers lock -platform=<list> command records additional platforms explicitly; terraform init records the platform it was run on. Both should be committed to Git.

Discovery: how Core finds the binary

When terraform plan runs, Core performs the following:

  1. Read the lock file. Determine the resolved version.
  2. Read required_providers. Determine the source and constraint.
  3. Locate the binary. Look first in .terraform/providers/, then in plugin_cache_dir, then download from the source.
  4. Hash the binary. If the computed hash is not in the lock file, Terraform re-downloads and updates the lock file. If it is, it proceeds.
  5. Launch the provider. Core spawns the binary as a child process and communicates over the plugin protocol.

The discovery path is short. It is also the source of several production failure modes.

Failure modes in the plugin model

Five failure modes recur in production:

1. Stale plugin cache after a provider upgrade. A developer upgrades the constraint in required_providers, but init -upgrade fails because the cache directory is read-only or the hash in the cache does not match. The plan errors out with “provider hash mismatch”. The fix is to clear the cache for that provider or run init -upgrade with a writable cache.

2. Missing platform hash. A developer on macOS Apple Silicon commits a lock file that only contains linux_amd64 hashes. CI on Linux runs terraform init and Terraform downloads a new binary and updates the lock file. The diff is the new hash. The fix is to commit hashes for every platform the team uses: terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 -platform=darwin_amd64.

3. Plugin binary deleted mid-run. A script cleans up ~/.terraform.d/ or /tmp/.terraform/ on a shared runner. The next plan errors with “plugin not found”. The fix is to configure a persistent plugin_cache_dir outside the ephemeral filesystem.

4. Provider signature mismatch on a private registry. A self-hosted registry is upgraded and signs binaries with a new GPG key not present in the CLI configuration. init fails with “untrusted signature”. The fix is to update provider_installation in the CLI config to point at the new signing key.

5. Wrong OS arch pulled in. A macOS engineer commits a hash that Terraform never computed — perhaps from a manual edit to the lock file. CI rejects the plan. The fix is to never hand- edit the lock file. Regenerate it.

Operational guidance

For a production Terraform estate:

  • Commit the lock file. It is the reproducibility contract. Without it, two engineers running init on the same day can resolve different provider versions.
  • Pin a shared plugin cache on CI runners. Set plugin_cache_dir to a directory that survives between workflow runs. A path under /var/cache or /opt/terraform-plugin-cache is typical.
  • Pin a shared plugin cache on developer hosts. Same file. Reduces the time to first plan and reduces the surface for inconsistent hashes.
  • Record all platforms the team uses. Run terraform providers lock -platform=<list> once on each supported OS and commit the union.
  • Upgrade deliberately. An init -upgrade followed by a plan is the standard pattern; the plan is the audit.

What comes next

The next lesson is on provider sources — the registry, the private registry, and the local filesystem mirror that determines where the binaries are downloaded from.

Verification

Knowledge check · 6 questions

  1. Q1. How does Terraform Core invoke a provider plugin?

  2. Q2. Which directory contains the provider binaries downloaded by `terraform init`?

  3. Q3. The shared plugin cache (`plugin_cache_dir`) replaces the per-working-directory `.terraform/providers/` tree.

  4. Q4. A macOS Apple Silicon engineer commits a lock file. CI on Linux rejects it because the hash is missing. What is the fix?

  5. Q5. Which of the following are production failure modes of the plugin cache? (Select all that apply.)

  6. Q6. A CI runner cleans `/tmp/.terraform` between workflow runs. Apply jobs fail with `plugin not found`. What is the right mitigation?

Passing score: 75%. Answers are checked in this browser.