Providers & Versioning
Providers & Versioning
A Terraform provider is a plugin that translates Terraform resource declarations into real API calls against a cloud platform, SaaS service, or on-premises system. Every resource and data block you write belongs to exactly one provider. Understanding how providers are discovered, downloaded, pinned, and locked is not optional knowledge — it is the single most common source of "works on my laptop, breaks in CI" failures on production Terraform codebases.
How Providers Work
Terraform itself has no built-in understanding of AWS, GCP, Azure, or Kubernetes. It is a generic engine that understands the HashiCorp Configuration Language (HCL) and a state file. Providers are independently versioned Go binaries distributed through the Terraform Registry at registry.terraform.io. When you run terraform init, Terraform reads your required_providers block, resolves version constraints, downloads matching binaries into the .terraform/providers/ directory, and writes a lock file.
Providers have a three-part address: <hostname>/<namespace>/<type>. The default hostname is registry.terraform.io, so hashicorp/aws is shorthand for registry.terraform.io/hashicorp/aws. Third-party providers (e.g. grafana/grafana, cloudflare/cloudflare) follow the same pattern but with their own namespace.
The required_providers Block
Provider requirements live in a terraform block inside any .tf file — conventionally versions.tf. Declaring providers explicitly here (rather than relying on implicit detection) is mandatory for reproducible builds:
required_providers block is the contract between your configuration and the outside world. It tells Terraform where to find each provider binary and which versions are acceptable. Omitting it means Terraform guesses — and on a different machine with a different Terraform cache it may resolve a different version, silently producing different infrastructure.Version Constraint Syntax
Terraform uses a constraint syntax borrowed from semantic versioning. Choosing the right operator matters enormously in practice:
= 5.50.0— exact pin. Maximum reproducibility, zero flexibility. Rarely used; makes routine upgrades painful.!= 5.48.0— exclude a specific version. Used to blacklist a known-bad release while still accepting future patches.>= 5.0, < 6.0— range. Accepts any 5.x release. Useful when you know a major version boundary introduces breaking changes.~> 5.50— pessimistic constraint operator (tilde-arrow). Allows rightmost version component to increment freely.~> 5.50means>= 5.50, < 6.0.~> 5.50.0means>= 5.50.0, < 5.51.0. This is the operator used at top-tier companies for most providers.
>= 1.9, < 2.0 to allow patch upgrades without accidentally crossing a major version boundary. For providers, use the tilde-arrow at the minor level (~> 5.50) to accept patch fixes automatically while staying on a tested minor line. Commit the lock file so CI always runs the exact same binary regardless of what the registry returns.Configuring a Provider
Declaring a provider in required_providers tells Terraform which binary to download. Configuring it tells the provider how to authenticate and what defaults to apply. Provider configuration lives in a provider block:
access_key and secret_key directly in HCL means those secrets end up in your git history, in your state file, and in any plan output uploaded to CI logs. Always use environment variables (AWS_ACCESS_KEY_ID), instance profiles (on EC2/ECS), IRSA (on EKS), or a secrets manager integration. Treat the provider block as config-only, never as a credentials store.Provider Aliases — Multiple Configurations of the Same Provider
Sometimes you need to deploy resources into multiple AWS regions or multiple accounts in a single configuration. Provider aliases allow this. You declare a primary provider and one or more aliased providers, then reference them explicitly on resources that need the non-default configuration:
The Dependency Lock File
When terraform init resolves providers for the first time it creates (or updates) .terraform.lock.hcl. This file records the exact provider version selected and its cryptographic hashes for every supported platform. It is the Terraform equivalent of package-lock.json or Pipfile.lock.
A lock file entry looks like this:
The h1: prefix is a hash of the zip archive the binary lives in; zh: hashes cover individual binaries inside. When Terraform downloads a provider, it recomputes these hashes and refuses to proceed if they do not match — protecting you from supply-chain tampering.
terraform init was run (e.g. darwin_arm64 on an Apple Silicon laptop). CI typically runs on linux_amd64. Run terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 once and commit the result so CI does not need network access to the registry just to verify hashes.Upgrading Providers Safely
Routine provider upgrades are a non-negotiable operational discipline. AWS alone ships provider releases every week; patch versions fix bugs and add resource types that mirror new AWS service features. The safe upgrade workflow is:
- Run
terraform init -upgradeto resolve the latest version permitted by your constraints and rewrite the lock file. - Run
terraform planand review the diff carefully — provider upgrades occasionally change defaults or deprecate arguments, producing unexpected resource changes. - Apply in a non-production environment first, then promote through your environment pipeline.
- Commit the updated lock file as a dedicated commit so reviewers can see exactly which provider changed.
terraform init -upgrade without the lock file in git. If your repository does not commit .terraform.lock.hcl, every engineer and every CI run resolves providers independently. On any given day the registry may return a new patch release. Two engineers working on the same PR may be running different provider versions. A silent API behavior difference between 5.50.0 and 5.54.1 will produce a plan diff that has nothing to do with their code change — and may silently recreate resources. Always commit the lock file.Private and Mirror Registries
Large enterprises often cannot allow Terraform runners to reach the public registry. The solutions are:
- Terraform Cloud / HCP Terraform: Has a built-in private registry for internal modules and a provider mirror capability.
- Artifactory / Nexus provider mirror: Configure
~/.terraformrc(or aTF_CLI_CONFIG_FILE) with aprovider_installationblock to redirect resolution to your internal mirror before falling back to the public registry. - filesystem_mirror: For air-gapped environments, pre-download provider zips into a directory structure that Terraform understands and point to it with a
filesystem_mirrorblock.
This lesson focuses on the public registry workflow — the details of enterprise mirror configuration are covered in the Advanced Terraform tutorial.
Anatomy of a Real versions.tf
At a company running a multi-account AWS setup with Kubernetes, Datadog, and Vault, a production versions.tf is a carefully managed artifact, not an afterthought:
Keeping versions.tf as a standalone file (rather than burying required_providers inside main.tf) makes code review faster — a one-line version bump is immediately visible as its own change with a clear purpose.