SaveMyCert
Log in
5 of 5 free questions left today·for unlimited practice
Terraform fundamentals

Terraform Providers, Version Constraints, and State Basics

12 min readTerraform 004 · Terraform fundamentalsUpdated

Terraform providers are plugins that translate your configuration into real API calls — the aws provider turns an aws_instance block into EC2 requests, the google provider turns google_compute_instance into Compute Engine requests. Terraform core ships with no platform knowledge at all: you declare which providers a configuration needs in a required_providers block, terraform init downloads them from the Terraform Registry, and a dependency lock file (.terraform.lock.hcl) pins the exact versions so every run uses the same plugins. Alongside providers sits state: the file where Terraform records which real-world resource each configuration block maps to, plus dependency metadata and cached attributes. This lesson covers exam objective 2 end to end — installing and versioning providers, the version-constraint operators including ~>, configuring multiple provider instances with alias, writing multi-provider configurations, and why state exists at all.

What you’ll learn
  • Declare provider requirements with required_providers, including source addresses and version constraints
  • Explain what terraform init installs, where provider plugins are stored, and when to re-run it
  • Apply every version-constraint operator, including the pessimistic ~> operator's two behaviors
  • Describe the role of .terraform.lock.hcl and when to commit or upgrade it
  • Configure multiple instances of one provider with alias and select them per resource
  • Explain why Terraform state exists: resource mapping, dependency metadata, and performance

How Terraform uses providers

Terraform uses providers as its only bridge to the outside world. Terraform core parses configuration, builds the dependency graph, and manages state — but every actual API call is made by a provider plugin, a separate binary that Terraform launches and talks to during plan and apply. Each provider defines a set of resource types and data sources and knows how to create, read, update, and delete them against its platform's API.

You can identify a resource's provider from its type prefix: aws_instance belongs to the aws provider, google_compute_instance to google, azurerm_resource_group to azurerm, random_pet to random. That naming convention is worth internalizing — exam questions use it constantly to imply which provider a snippet requires.

Providers are distributed through the Terraform Registry (registry.terraform.io), which hosts official providers (maintained by HashiCorp, such as aws and kubernetes), partner providers (maintained by the vendor, such as datadog), and community providers. Because providers are versioned and released independently of Terraform itself, a new AWS feature only needs a new aws provider release, not a new Terraform release.

Two consequences the exam probes: without the right provider installed, Terraform cannot manage the resource at all — terraform init fails or plan errors on the unknown type; and because providers are just API clients, anything with an API can be managed, from clouds to DNS to GitHub.

Declaring providers with required_providers

You declare which providers a configuration needs in a required_providers block nested inside the terraform block. Each entry maps a local name to a source address and a version constraint:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    random = {
      source  = "hashicorp/random"
      version = ">= 3.5.0"
    }
  }

  required_version = ">= 1.5.0"
}

The source address has the form [hostname/]namespace/type. The hostname defaults to registry.terraform.io, so hashicorp/aws is shorthand for registry.terraform.io/hashicorp/aws. The local name (the key, aws) is what the rest of the configuration uses in provider blocks and references. If you omit a provider from required_providers, Terraform falls back to assuming hashicorp/<name> for backward compatibility — but explicit declaration is the practice the exam expects, and it is mandatory for any provider outside the hashicorp namespace.

Do not confuse the two version settings visible above: version inside a required_providers entry constrains the provider plugin, while required_version directly inside the terraform block constrains the Terraform CLI itself. The exam likes to swap them in wrong answers.

Installing providers with terraform init

terraform init is the command that installs providers. It reads the configuration's provider requirements, resolves each version constraint against the registry, downloads the chosen plugin binaries into a hidden .terraform/providers directory inside the working directory, and records its selections in the dependency lock file. Providers are never installed at plan or apply time — if a required plugin is missing, those commands tell you to run init first.

Re-run terraform init whenever provider requirements change: you added a provider, changed a version constraint, or pulled teammate changes that did either. Running it again is always safe — it is idempotent and never touches your infrastructure or your state's resource records.

By default, init respects the existing lock file: it reinstalls the exact recorded versions even if newer releases satisfy your constraints. To deliberately move to newer versions within your constraints, run:

terraform init -upgrade

which re-resolves every constraint against the registry and rewrites the lock file with the new selections.

Also part of the same command's job: init downloads modules and configures the state backend, which is why it must be the first command in any new or freshly cloned working directory. For this objective, though, the tested association is direct: installing provider plugins is terraform init's job, the plugins land in .terraform/providers, and that directory is disposable and should never be committed to version control.

Version constraint operators

Version constraints are strings like "~> 5.0" that limit which provider releases Terraform may select. Seven operators exist, and the exam tests all of them — especially ~>:

OperatorMeaningExampleAllows
=Exactly this version (also the default when no operator is written)= 5.31.0Only 5.31.0
!=Any version except this one!= 5.31.0Everything but 5.31.0
>Greater than> 5.0.05.0.1 and up, not 5.0.0
>=Greater than or equal>= 5.0.05.0.0 and up
<Less than< 6.0.0Anything below 6.0.0
<=Less than or equal<= 6.0.06.0.0 and below
~>Pessimistic: only the rightmost stated component may increase~> 5.15.1, 5.2, ... 5.x — never 6.0

The pessimistic operator's behavior depends on how many components you write. ~> 5.1 means at least 5.1 and below 6.0 (minor releases may increase). ~> 5.1.0 means at least 5.1.0 and below 5.2.0 (only patch releases may increase). That distinction is a reliable exam question.

Constraints combine with commas — ">= 3.5.0, < 4.0.0" — and every listed condition must hold. Recommended practice, and the phrasing the exam rewards: pin providers with a pessimistic constraint so you receive compatible updates without silently jumping a major version that may contain breaking changes.

The dependency lock file: .terraform.lock.hcl

.terraform.lock.hcl records exactly which provider versions terraform init selected, so every subsequent run — on your machine, a teammate's, or CI — installs identical plugins. A version constraint like ~> 5.0 can match many releases; the lock file removes that ambiguity by pinning the one that was actually chosen, together with checksums used to verify each downloaded plugin has not been tampered with.

Terraform creates or updates the file automatically during terraform init, in the root of the working directory. It locks provider dependencies only — module versions are not recorded in it. On later runs, init obeys the lock file even when newer matching versions exist; selections change only when you edit constraints so the locked version no longer qualifies, or when you explicitly run terraform init -upgrade.

Commit the lock file to version control. That is HashiCorp's explicit recommendation and a frequent exam answer: committing it makes provider selection reproducible across the team and makes upgrades deliberate, reviewable diffs rather than surprises. The .terraform directory, by contrast, is a disposable local cache and stays out of version control.

One operational wrinkle worth knowing: the lock file stores checksums per platform. If your team spans architectures (say Apple Silicon laptops and Linux CI runners), terraform providers lock with -platform flags records hashes for additional platforms so init verifies cleanly everywhere.

Configuring providers, and multiple instances with alias

A provider block supplies the settings a provider needs to reach its API — region, credentials, project, endpoints:

provider "aws" {
  region = "us-east-1"
}

This is the default provider configuration for the local name aws: every aws_* resource that says nothing else uses it. Note the division of labor — required_providers says which plugin and version; the provider block says how to connect. Credentials should come from environment variables or CLI/instance credentials, not hard-coded arguments.

Sometimes you need the same provider configured twice — the classic case is deploying to two AWS regions. Additional configurations get an alias:

provider "aws" {
  region = "us-east-1"
}

provider "aws" {
  alias  = "west"
  region = "us-west-2"
}

resource "aws_instance" "primary" {
  ami           = "ami-0c02fb55956c7d316"
  instance_type = "t3.micro"
}

resource "aws_instance" "replica" {
  provider      = aws.west
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.micro"
}

A resource opts into an aliased configuration with the provider meta-argument, referencing it as <local_name>.<alias> — here aws.west, unquoted. Resources without the meta-argument (like primary) fall through to the default, unaliased configuration. Modules receive alternate configurations through a providers map on the module block. Exam triggers: "two regions", "two accounts", or "same provider, different settings" all point at alias.

Writing configuration with multiple providers

Using several different providers in one configuration takes no special mechanism: declare each in required_providers, add a provider block for any that need settings, and write resources. One terraform init installs all the plugins; one plan and apply covers every resource:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
  }
}

provider "aws" {
  region = "eu-west-1"
}

resource "random_pet" "name" {
  length = 2
}

resource "aws_s3_bucket" "logs" {
  bucket = "logs-${random_pet.name.id}"
}

The bucket's name references random_pet.name.id, an attribute of a resource from a different provider. Terraform's dependency graph does not care which provider owns which node: it sees that the bucket depends on the pet name, creates the pet first, and feeds its result into the bucket — cross-provider references are ordinary references.

Note that random needs no provider block at all: providers with no required settings work with an implied empty configuration. And each provider authenticates independently with its own credentials — there is no shared login. This composability is exactly how the multi-cloud story from objective 1 is realized in code: an azurerm or google block would slot into this same file the same way.

How Terraform uses and manages state

State is Terraform's record of what it manages: a JSON document — by default a local file named terraform.tfstate — that binds each resource address in your configuration to the real object it created. Configuration alone cannot do this: nothing about the block aws_instance.web says which of your hundred EC2 instances it is. State stores that binding (aws_instance.web is instance i-0abc123), which is how Terraform knows what to update or destroy instead of creating duplicates on every apply.

The exam wants the reasons state exists, so keep the official list ready:

  • Mapping to the real world — the config-address-to-remote-ID binding above.
  • Metadata — state records resource dependencies, so when you delete a resource from configuration, Terraform still knows the correct destroy order even though the config no longer describes it.
  • Performance — state caches every resource's attribute values. For small setups Terraform refreshes them from real APIs before planning, but at scale that is slow, and teams can plan against the cached state (-refresh=false).
  • Syncing — with remote state, a team shares one source of truth so runs do not collide.

During plan, Terraform compares desired configuration against state (refreshed from reality) and proposes the difference; apply executes it and writes the results back. Two warnings that recur on the exam: state can contain sensitive data in plain text — database passwords, private keys — so protect and never commit it; and never edit the file by hand — inspect and modify it only through Terraform commands. Remote backends, locking, and drift handling get their own domain (objective 6); here you need the what and the why.

Tip. This objective is heavily code-driven: expect snippets asking what terraform init will install given a required_providers block, which versions a constraint like ~> 5.1 permits, and how to place a resource in a second region (the answer involving alias and provider = aws.west). The lock file appears as a should-you-commit-it question and as the reason init ignores newer releases. State questions ask why state exists — mapping to real resources, dependency metadata, performance — and flag that state may contain sensitive data. Trigger words: "pin provider versions", "same provider different regions", "where are plugins installed", "source of truth for what Terraform manages".

Key takeaways
  • Providers are plugins that map resource types to platform API calls; Terraform core makes no cloud API calls itself
  • required_providers (inside the terraform block) pins each provider's source and version; hashicorp/aws is shorthand for registry.terraform.io/hashicorp/aws
  • terraform init downloads provider plugins into .terraform/providers and records selections in the lock file — providers are never installed during plan or apply
  • ~> 5.1 allows 5.x releases from 5.1 upward but never 6.0; ~> 5.1.0 allows only 5.1.x patch releases — the rightmost stated component is the one that may increase
  • Commit .terraform.lock.hcl to version control; terraform init -upgrade is the deliberate way to change locked provider versions
  • alias creates additional configurations of the same provider; a resource selects one with provider = aws.west, and resources without it use the default unaliased configuration
  • version in required_providers constrains the provider plugin; required_version constrains the Terraform CLI — do not swap them
  • State exists to map configuration to real resource IDs, store dependency metadata, and cache attributes for performance — it can hold sensitive data in plain text, so protect it and never edit it by hand

Frequently asked questions

What does terraform init actually download and where does it go?

terraform init resolves the configuration's required_providers constraints against the Terraform Registry, downloads the selected provider plugin binaries into the hidden .terraform/providers directory in your working directory, and records the chosen versions and checksums in .terraform.lock.hcl. It also downloads any referenced modules and configures the state backend. The .terraform directory is a local cache — never commit it.

Should I commit .terraform.lock.hcl to Git?

Yes — HashiCorp explicitly recommends committing the dependency lock file to version control. It pins the exact provider versions and checksums that terraform init selected, so teammates and CI install identical plugins instead of whatever latest release happens to match your constraints. Provider upgrades then show up as reviewable diffs, made deliberately with terraform init -upgrade.

What does the ~> version constraint mean in Terraform?

The pessimistic constraint operator ~> allows only the rightmost version component you specify to increase. So ~> 5.1 accepts 5.1, 5.2, and any later 5.x, but never 6.0; while ~> 5.1.0 accepts only patch releases 5.1.x, never 5.2.0. It is the recommended way to pin providers: you receive compatible updates while blocking potentially breaking major (or minor) version jumps.

How do I deploy to two AWS regions in one Terraform configuration?

Write two provider blocks for aws: one default (no alias) with the first region, and one with alias = "west" (or any name) and the second region. Resources use the default configuration automatically; to place a resource in the second region, add the meta-argument provider = aws.west to its block. Modules receive alternate configurations via a providers map on the module block.

Why does Terraform need a state file at all?

Because configuration alone cannot tell Terraform which real-world objects it already manages. State maps each resource address to a real resource ID (so applies update rather than duplicate), stores dependency metadata (so removed resources are destroyed in the correct order), and caches attribute values for performance on large infrastructures. Without state, Terraform could not compute the difference between what you declared and what exists.

Is the Terraform state file sensitive?

Treat it as sensitive, yes. State stores all resource attributes in plain text, which can include database passwords, private keys, and other secrets a provider returns. Do not commit terraform.tfstate to version control, restrict access to wherever it is stored, and prefer remote backends that support encryption and access control. Also never edit the file by hand — manipulate state only through Terraform commands.

Test yourself on this topic
Practice questions with full explanations.
Practice now

Sign up free to mark lessons complete, bookmark topics and track your exam readiness.