Skip to content
beginner Phase 1 · Infrastructure as Code Foundations and Terraform Setup

Installing and Verifying Terraform

Installing the binary from official sources and validating the local environment.

30m
0 problems
Topic Progress 0%

One Binary, No Runtime: Installing Terraform the Right Way

Terraform distributes as a single archive containing one executable. There is no interpreter to install, no package to sync with a language runtime, no service to register. Unzip it, put terraform somewhere on your PATH, and the tool runs. That simplicity is also a risk: nothing verifies the binary you downloaded unless you do, and a supply-chain-tampered Terraform binary has god-mode access to your cloud accounts. Treat installation as a two-step ritual — download from the official distribution, then verify.

The official source is developer.hashicorp.com/terraform/install (or the releases API, releases.hashicorp.com/terraform/), which publishes a zip per platform plus a SHA256SUMS file and its PGP signature. On macOS or Linux:

curl -LO https://releases.hashicorp.com/terraform/1.9.5/terraform_1.9.5_linux_amd64.zip
curl -LO https://releases.hashicorp.com/terraform/1.9.5/terraform_1.9.5_SHA256SUMS
shasum -a 256 -c terraform_1.9.5_SHA256SUMS --ignore-missing
sudo unzip -o terraform_1.9.5_linux_amd64.zip -d /usr/local/bin
terraform version

On Windows, download terraform_1.9.5_windows_amd64.zip, verify with Get-FileHash .\terraform_1.9.5_windows_amd64.zip -Algorithm SHA256 against the published sum, then extract terraform.exe into a directory on your PATH, such as C:\tools\bin. The verification step is the part people skip; when an interviewer asks "how do you know the tool you installed is trustworthy", the checksum-plus-signature answer is what separates a professional setup from a hope-based one.

A licensing fact worth having in your pocket: since August 2023, Terraform releases after 1.5.x are under the Business Source License 1.1 (BUSL), which restricts competing hosted offerings rather than normal use. That change is what prompted OpenTofu, an MPL-2.0 fork that remains API-compatible — configurations written for Terraform generally run on OpenTofu unchanged, and required_version / provider sources are the same syntax. Teams must decide deliberately which one they standardize on; mixing them against one state backend is how you corrupt state.

For air-gapped or regulated environments, the same files exist on an internal mirror: copy the zip and the SHA256SUMS into an artifact repository, verify there, and point CI at the mirror. Building from source is also possible — the Terraform repository is public Go code and go build produces a working binary — but for most teams checksum verification of the official build is the cheaper, more auditable control, since a self-built binary still has to be trusted by everyone who runs it. Upgrades deserve the same discipline as first installs: read the version's upgrade guide (Terraform's guides call out state or provider protocol changes), bump the pin in .terraform-version and required_version together in one pull request, and let CI prove the plan output is unchanged before the rest of the team pulls the new version.

Once installed, three checks settle every "works on my machine" argument before it starts:

terraform version          # Terraform v1.9.5 on linux/amd64
which terraform            # /usr/local/bin/terraform  (Linux/macOS)
where.exe terraform        # C:\tools\bin\terraform.exe (Windows)

The last two matter more than they look. A leftover binary in /usr/local/bin shadowing a newer one in /opt/homebrew/bin, or an old terraform.exe earlier on the Windows PATH, produces version output that disagrees with your editor's Terraform extension. Confirm exactly one binary exists, at one known path, before you debug anything else. Note also that providers — the plugins that talk to AWS, Azure, and friends — are not in this download: each working directory fetches its own provider binaries during terraform init, which you will do in The init, plan, and apply Workflow.

Pinning Versions So Your Laptop, Your Colleague, and CI All Run the Same Terraform

Terraform configuration is version-sensitive: state formats, provider protocols, and language features all evolve. Two people on different minor versions can produce conflicting plan output from the same commit, and a CI image that silently upgrades Terraform is a production incident waiting for a scheduled apply. Pinning versions is not optional hygiene; it is how you keep plans reproducible.

Pick a version manager for developer machines. On macOS and Linux the two common choices are tfenv and the general-purpose asdf:

git clone --depth 1 https://github.com/tfutils/tfenv.git ~/.tfenv
tfenv install 1.9.5
tfenv use 1.9.5
# or with asdf:
asdf plugin add terraform
asdf install terraform 1.9.5

Both honor a .terraform-version file committed at the repository root, so checking out the repo selects the right Terraform automatically — the toolchain becomes a property of the project, not of whoever set up the laptop. tfenv also walks up parent directories and falls back to ~/.terraform-version, so a machine-wide default can cover everything except the projects that override it; tfenv list shows what is installed and tfenv install --allow-missing will fetch whatever the current directory pins. Package managers also work — brew tap hashicorp/tap && brew install hashicorp/tap/terraform on macOS, the HashiCorp apt/yum repositories on Linux, choco install terraform or scoop install terraform on Windows — but note what they do not do: they install one current version per machine and do not track per-project requirements. That is acceptable for CI images built from an immutable definition, and poor for day-to-day multi-project work.

Inside the configuration, the terraform block records what the code itself demands:

terraform {
  required_version = ">= 1.6.0, < 2.0.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.40"
    }
  }
}

required_version makes a too-old binary fail fast during terraform init with a clear message instead of failing halfway through an apply with a cryptic parse error. The ~> 5.40 operator means "5.40 or higher within 5.x" — the standard way to accept patch fixes while blocking a major-minor jump. Together with the .terraform-version pin, you get defense in depth: the file picks the binary, the constraint rejects a wrong one.

Finally, read terraform version output like an instrument panel. Bare, it prints the CLI version and target platform. Inside an initialized directory it also lists every provider and its exact version — which is how you answer "what did this apply actually run with" after an incident, months later.

CI should assert rather than assume:

- run: terraform version
- run: terraform fmt -check -recursive
- run: terraform init -backend=false
- run: terraform validate

Fail the pipeline if the asserted minimum is missing. The five seconds of checking replaces the hour of debugging a plan that behaves differently from everyone else's. A concrete failure you are thereby avoiding: terraform init on a CLI older than required_version dies immediately with "Error: Unsupported Terraform Core version" and names the required range — an excellent, early error, as long as the init step runs before anything can apply. The constraint operators are the same set you will use for providers: >= 1.6.0, < 2.0.0 spelled out, or ~> 1.6.0 meaning "1.6.x or newer below 1.7" — note the pessimism applies to the last segment you wrote, so ~> 1.6 allows 1.x up to (but not including) 2.0, while ~> 1.6.0 holds you to the 1.6 series. Choosing which one you mean is a real decision: allow your team's patch upgrades, or lock the exact patch and upgrade deliberately. Where runs execute also matters: if your team uses HCP Terraform (formerly Terraform Cloud) for remote execution, the runner's Terraform version is selected per workspace, so the CLI version pinned on laptops and the version running applies are two pins that must agree — one more reason to keep required_version as the single written source of truth everyone's tooling reads.

Shell Completion and First-Run Behavior: Making the CLI Bearable

Terraform has over one hundred subcommands and flags across them — -target, -out, -var-file, -parallelism, -refresh=false. Discovering them by typing terraform --help every time is slow and error-prone. The CLI ships its own completion generator: run terraform -install-autocomplete once per shell environment and the binary writes the appropriate completion registration into your shell's configuration.

terraform -install-autocomplete    # bash, zsh, or PowerShell
# restart the shell, then:
terraform plan -<TAB>              # completes -target, -out, -var, ...
terraform aws_<TAB>                # nothing: resource names live in configs, not the CLI

Details bite here. On macOS the zsh integration assumes the standard Homebrew zsh paths; on Windows, PowerShell's execution policy can silently block the generated profile script — Get-ExecutionPolicy -List shows whether the profile is allowed to run. The registration is machine- and shell-specific, so a teammate running the same command on their machine is not covered by yours. And terraform -uninstall-autocomplete reverses it if the shell profile line causes trouble in dotfile setups.

Understand what completion can and cannot do. It completes Terraform's own verbs — subcommands, flags, backend types, and so on. It cannot complete resource types or names, because those come from the configuration in your working directory and are resolved by the language server your editor runs, not by the CLI binary. Knowing that boundary explains a common beginner puzzle: "why does my editor autocomplete aws_instance in .tf files but the terminal does not?"

Completion only removes friction; the knowledge it should accelerate is knowing which command answers which question. Build the mental map early:

You need to... Command
See every subcommand terraform -help
See a subcommand's flags terraform <subcommand> -help
Evaluate expressions interactively terraform console
List everything managed terraform state list
Adopt an existing object terraform import <addr> <id>
Show what a directory manages (read-only) terraform show
Switch the set of variables applied terraform workspace list

Two of these are worth flagging now. terraform console gives you a REPL over your configuration — type var.environment, aws_instance.web[0].id, or length(var.cidrs) and see the evaluated value, which turns "why is this string wrong?" into a ten-second experiment. terraform import attaches an already-existing object to a state address, the standard path for bringing hand-created infrastructure under management — an operation you will practice once the basics are solid.

The last habit to build is knowing where to look when something goes wrong. terraform -help lists subcommands; terraform <subcommand> -help lists its flags — for example, terraform init -help reveals -reconfigure (skip backend migration) and -upgrade (refresh provider pins within constraints), flags that rescue two of the most common beginner jams. Combined with the version checks from the previous chapter, a healthy environment is: exactly one verified binary, a pinned version matching the repo's .terraform-version, required_version in the configuration, and completion enabled so flags stop being guessed from memory. Next you will run that binary for real in The init, plan, and apply Workflow.

Quiz

1. You download terraform_1.9.5_linux_amd64.zip from a search-ad result and unzip it to /usr/local/bin. What is the primary risk you accepted, and what is the control that addresses it?

Question 1 options

2. Two engineers check out the same commit but run Terraform 1.5 and 1.9, and their plan outputs disagree. Which change best prevents this class of problem?

Question 2 options

3. Terraform 1.9.8 is installed and a repository pins `version = "~> 5.40"` for the AWS provider. What does ~> allow?

Question 3 options

4. After `terraform -install-autocomplete`, the terminal still does not complete terraform flags. Which cause is most likely?

Question 4 options

Flashcards

Question

What is the official way to verify a downloaded Terraform binary, and why does the open-source nature of the project not suffice?

Answer

Download the matching SHA256SUMS (and its PGP signature) from releases.hashicorp.com and compare with `shasum -a 256 -c` or `Get-FileHash`; verify the signature against HashiCorp's published key. Public source code proves what the build should be, not that a given downloaded artifact came from that build.

Question

Which two mechanisms pin the Terraform CLI version for a project, and what does each protect?

Answer

`.terraform-version` (read by tfenv/asdf) selects the binary per checkout on developer machines; `required_version` in the `terraform` block makes `terraform init` fail fast if the running CLI is outside the allowed range, which also protects CI images that were built independently.

Question

How do `~> 5.40` and `>= 5.40, < 6.0.0` differ in a provider version constraint?

Answer

They are equivalent: both mean 5.40 or newer within the 5.x series, excluding 6.0. `~>` is shorthand that drops the last specified segment for the upper bound; `= 5.40` would pin exactly, and a bare `5.40` means only that exact version.

Question

After terraform -install-autocomplete, what are the three usual reasons completion still does not work?

Answer

The shell session predates the registration (restart it), PowerShell execution policy blocks the generated profile script, or on Linux/macOS the rc file line was written for a different shell than the one in use. The CLI cannot complete resource names regardless — those come from the working directory's configuration via the editor's language server.

Question

What is the difference between the Terraform CLI binary and providers, and when are providers obtained?

Answer

The CLI is the single engine that reads configuration, builds the graph, and manages state; providers are separate plugin binaries implementing each platform's API. They are not bundled with the CLI — each working directory downloads exact provider versions during `terraform init` and records them in .terraform.lock.hcl.

Revision Notes

Key Takeaways

  • 1. Terraform is one self-contained binary with no runtime dependencies; authenticity, not availability, is the installation risk, so verify against SHA256SUMS before trusting it
  • 2. The toolchain version should be a property of the repository: `.terraform-version` for humans, `required_version` for enforcement at init, `~> ` constraints for providers
  • 3. `terraform version` in an initialized directory also lists provider versions — the post-incident record of what actually ran
  • 4. Completion is generated by the CLI into your shell profile; it completes flags, never resource or attribute names, which come from the working directory's configuration
  • 5. Providers are per-directory plugins pinned in `.terraform.lock.hcl`; the CLI binary and provider binaries are versioned independently

Interview Tips

  • • "How do you secure your toolchain installs?" — checksum verification with signature check, then pinning via version manager and required_version; mention that the control targets artifact tampering, not the project's source code
  • • "What changed about Terraform licensing?" — post-1.5.x releases are BUSL 1.1, restricting competing hosted services rather than your usage; OpenTofu forked as MPL-2.0, and configurations largely run on either — name this as a deliberate team decision
  • • "Terraform behaves differently on my laptop than in CI. How do you debug it?" — compare `terraform version` outputs and each environment's provider lockfile (.terraform.lock.hcl); the version pinning and init -upgrade flags explain nearly every divergence
  • • "What does terraform init actually install?" — provider binaries for the working directory, written under .terraform/providers and locked in .terraform.lock.hcl; the CLI itself ships no providers, which is why two projects can use different AWS provider versions side by side

Cheat Sheet

Install & Verify Terraform

Install + verify (Linux/macOS):

curl -LO https://releases.hashicorp.com/terraform/1.9.5/terraform_1.9.5_linux_amd64.zip
curl -LO https://releases.hashicorp.com/terraform/1.9.5/terraform_1.9.5_SHA256SUMS
shasum -a 256 -c terraform_1.9.5_SHA256SUMS --ignore-missing
sudo unzip -o terraform_1.9.5_linux_amd64.zip -d /usr/local/bin
terraform version

Windows: Get-FileHash .\terraform_1.9.5_windows_amd64.zip -Algorithm SHA256, extract terraform.exe to a PATH directory.

Version pinning layers:

Layer Mechanism Guards against
Developer machine .terraform-version + tfenv/asdf "works on my machine" version skew
Configuration required_version = ">= 1.6.0, < 2.0.0" init/apply with an unsupported CLI
Providers version = "~> 5.40" + .terraform.lock.hcl unplanned provider upgrades
CI image baked version + terraform version assert silent image drift

Completion: terraform -install-autocomplete / -uninstall-autocomplete — per shell, per machine; restart shell; check PowerShell execution policy. Completes CLI flags only, never resource names.

Also remember: BUSL 1.1 licensing since post-1.5.x releases; OpenTofu is the API-compatible MPL-2.0 fork; providers are fetched per-directory at terraform init, not shipped with the CLI.

Continue Learning