02 · Installing Terraform & Providers¶
Terraform ships as a single self-contained binary — no runtime to install separately. This module covers getting it installed, verifying the version, and understanding how providers are downloaded and pinned.
Installing the CLI¶
HashiCorp publishes Terraform for macOS, Linux, and Windows. The three common paths:
# macOS (Homebrew) — HashiCorp maintains its own tap
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
# Linux (apt, Debian/Ubuntu) — add HashiCorp's repo, then install
wget -O- https://apt.releases.hashicorp.com/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
# Manual (any OS) — download the matching zip from releases.hashicorp.com,
# unzip, and place the `terraform` binary somewhere on PATH.
Whichever route, confirm it worked:
The version manager option¶
Because different projects can require different Terraform versions, many teams use tfenv (or tfswitch) instead of a single pinned install:
brew install tfenv
tfenv install 1.9.5
tfenv use 1.9.5
tfenv list # shows installed versions, marks the active one
This mirrors tools like nvm for Node or pyenv for Python — useful once
you work across several repositories with different required_version
constraints (covered next).
Pinning the Terraform version in a project¶
A terraform block's required_version documents (and enforces) which CLI
versions are acceptable for a given configuration:
Running terraform plan with an incompatible CLI version fails immediately
with a clear error rather than proceeding and hitting a confusing syntax or
behavior mismatch later.
Providers: plugins Terraform downloads, not part of the core binary¶
Terraform's core binary knows the language (HCL) and the workflow (plan/apply/state), but it has no built-in knowledge of AWS, Azure, GCP, or any other API. That knowledge lives in providers — plugins Terraform downloads separately, one per API you want to manage.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}
source is <namespace>/<name> on the Terraform Registry (module 04 covers
the registry itself); version is a constraint, not an exact pin — ~> 5.0
allows 5.1, 5.2, ... but not 6.0.
terraform init: the command that fetches providers¶
init reads every required_providers block in the configuration,
resolves a version for each that satisfies all constraints, downloads the
matching plugin binaries into a local .terraform/ directory, and records
the exact versions it chose in a dependency lock file,
.terraform.lock.hcl:
# .terraform.lock.hcl (excerpt — generated by `terraform init`, do not hand-edit)
provider "registry.terraform.io/hashicorp/aws" {
version = "5.60.0"
constraints = "~> 5.0"
hashes = [
"h1:abc123...",
]
}
That lock file should be committed to version control. It's the
Terraform equivalent of a package-lock.json or Gemfile.lock: without it,
two different machines running terraform init against ~> 5.0 on
different days could resolve two different patch versions and behave
subtly differently. terraform init -upgrade deliberately re-resolves to
the newest versions satisfying the constraints and rewrites the lock file.
.terraform/ should never be committed¶
terraform init also creates a .terraform/ directory holding the
downloaded provider binaries and (for module 03's remote backends) cached
configuration. It's large, machine-specific, and fully regenerable by
re-running init — add it to .gitignore:
(Whether .terraform.lock.hcl itself is ignored or committed is the one
exception worth calling out explicitly — HashiCorp's guidance is to commit
it for applications, so every contributor and CI run resolves identical
provider versions.)
Verifying everything is wired up¶
mkdir demo && cd demo
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.6.0"
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}
provider "random" {}
EOF
terraform init
# Initializing the backend...
# Initializing provider plugins...
# - Finding hashicorp/random versions matching "~> 3.6"...
# - Installing hashicorp/random v3.6.3...
# Terraform has been successfully initialized!
The random provider is a good first one to experiment with because it
needs no cloud account or credentials at all — it just generates values
like random strings, IDs, and passwords locally.
How It Actually Works: what terraform init actually does¶
terraform init looks like one command, but it performs several distinct,
independently-observable steps — none of these require a real cloud account,
just the local filesystem and the public registry:
- Backend initialization. It reads the
terraform { backend "..." {} }block (or defaults tolocal) and prepares wherever state will be read/written — for the local backend this is just confirmingterraform.tfstateis writable in the current directory. - Provider requirement resolution. It walks every
required_providersblock across every.tffile in the directory (Terraform merges them all into one configuration, as covered in module 01), builds a combined version constraint per provider (e.g.~> 5.0from one file and>= 5.2from another must both be satisfiable), and resolves that to one concrete version. - Registry protocol handshake. For
hashicorp/aws, Terraform queries the registry'sservice discoverydocument atregistry.terraform.io/.well-known/terraform.json, then hits/v1/providers/hashicorp/aws/versionsto list available versions and their platform-specific download URLs (a separate binary per OS/architecture — this is why a provider install on Linux vs. macOS downloads different.zipfiles). - Download, verify, and cache. The provider
.zipis fetched, its SHA256 checked against the registry's published checksums (and a GPG signature over those checksums, verified against HashiCorp's or the publisher's public key), then unpacked into.terraform/providers/registry.terraform.io/.... - Lock file write. The exact resolved version and checksums for every
supported platform are written to
.terraform.lock.hcl, which — unlike.terraform/— is meant to be committed, so that everyone on a team (and CI) resolves to the byte-identical provider binary rather than "whatever satisfies~> 5.0today."
The provider binary itself is not a library Terraform links against — it is a standalone executable that Terraform Core spawns as a subprocess and talks to over a local RPC channel each time a resource in your config needs to be read, planned, or applied.
Exercise¶
Install Terraform (or a version manager) locally, then create a directory
with a terraform block requiring hashicorp/random at ~> 3.6 and run
terraform init. Open .terraform.lock.hcl afterward and identify: the
exact version it resolved, and the registry hostname it came from.