How to Install Terraform and Run Your First Apply
Install Terraform 1.17 on Linux, macOS or Windows, verify the download, set up cloud credentials, and run init, plan, apply and destroy on a first project.
TLDR: On Ubuntu or Debian, add HashiCorp's apt repository and run sudo apt install terraform. On macOS, run brew install hashicorp/tap/terraform. On Windows, run winget install --id Hashicorp.Terraform -e. Or download the zip from releases.hashicorp.com, check its SHA-256 sum and put the terraform binary on your PATH. Then create a directory with a main.tf file and run terraform init, terraform plan, terraform apply and terraform destroy. Your first project in this part needs no cloud account.
Terraform is a single binary with no runtime dependencies. This part installs it, checks the download, runs the full workflow on a project that only creates local files, sets up credentials for the major clouds, and then shows the same workflow with a real cloud server.
Prerequisites
- A Linux, macOS or Windows machine where you can install software.
- A terminal and a text editor. The HashiCorp Terraform extension for VS Code (or the Terraform language server in any editor) gives you syntax highlighting, completion and formatting.
- Part 1 explains the concepts used here.
Which Version to Install
Install the current release, Terraform 1.17.0 (released on 21 October 2026). HashiCorp ships a minor release every few months and patch releases in between; the previous minor line still gets fixes for a while (1.16.7 came out the day after 1.17.0). Within 1.x, HashiCorp promises that configurations written for an earlier 1.x release keep working, so upgrading is usually safe, but read the upgrade notes of each minor release.
If you prefer the open source fork, install OpenTofu 1.13 instead. Its command is tofu, and you can replace terraform with tofu in nearly every command in this guide. The guide overview explains the differences.
Install Terraform
Pick your platform. Every option installs the same terraform binary.
Notes per Platform
Ubuntu and Debian. The commands store HashiCorp's key in its own keyring file and reference it with signed-by, so the key is trusted for this repository only. Older tutorials use apt-key add. apt-key is deprecated and newer releases no longer ship it, so use the commands above. The same repository also provides packer, vault and HashiCorp's other tools.
macOS. After the license change in 2023, Homebrew's main repository froze Terraform at 1.5.7 and has since removed the formula. Install from HashiCorp's tap with the full hashicorp/tap/terraform name. OpenTofu, which is open source, is still in Homebrew's main repository as opentofu.
Windows. winget and Chocolatey add Terraform to your PATH. Open a new terminal afterwards so the change takes effect. The winget package sometimes lags a few days behind a new release.
Several versions side by side. If different projects need different Terraform versions, use a version manager such as tenv, which installs Terraform, OpenTofu and Terragrunt versions and picks the right one from a .terraform-version file or the required_version setting. The post Upgrade Terraform to a Specific Version shows the options.
Manual Install With Checksum Verification
Every release has a zip file per platform and a SHA256SUMS file. Check the sum before you run a binary you downloaded. This run is on an arm64 Linux machine; on most x86 servers, use linux_amd64 instead:
sha256sum -c must print OK. For a stronger check, also verify the SHA256SUMS.sig signature with HashiCorp's PGP key, as described on the HashiCorp security page. If ~/.local/bin is not on your PATH yet, add export PATH="$HOME/.local/bin:$PATH" to your ~/.bashrc or ~/.zshrc.
Verify the Installation
Whichever method you used, check the version and the list of commands:
The -help output above is cut after the first few commands; the full list continues with output, providers, state, test, workspace and more. -install-autocomplete adds tab completion for Bash or Zsh to your shell's startup file; open a new shell to use it. Every subcommand has its own help, for example terraform plan -help.
Your First Configuration (No Cloud Account Needed)
You learn the workflow faster when a mistake costs nothing. This first project uses two small providers: random, which generates random names, and local, which manages files on your own machine. Create a directory and a file called main.tf:
terraform {
required_version = ">= 1.17"
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.9"
}
random = {
source = "hashicorp/random"
version = "~> 3.9"
}
}
}
# A random two-word name, such as "brave-otter"
resource "random_pet" "name" {
length = 2
}
# A file whose content uses that name
resource "local_file" "hello" {
filename = "${path.module}/hello.txt"
content = "Hello from ${random_pet.name.id}!\n"
file_permission = "0644"
}
output "pet_name" {
value = random_pet.name.id
}
What each block does:
- The
terraformblock sets the minimum Terraform version and lists the providers this configuration needs.sourceis the provider's registry address (hashicorp/localmeansregistry.terraform.io/hashicorp/local), andversionis a constraint:~> 2.9means "2.9 or newer, but below 3.0". random_pet.namecreates a random name. It is stored in the state, so it stays the same on every run until you replace it.local_file.hellowrites a file.path.moduleis the directory of this configuration. The referencerandom_pet.name.idmakes Terraform create the name first.- The
outputprints the name after apply.
terraform init
init downloads the providers and creates a dependency lock file:
Terraform picked the newest versions that match your constraints and recorded them, with their checksums, in .terraform.lock.hcl. Commit this file to Git. It guarantees that your teammates and your CI system install exactly the same provider builds, and that a tampered download fails the checksum check. You change the recorded versions on purpose with terraform init -upgrade. The .terraform directory holds the downloaded provider binaries; never commit it. The post Should .terraform.lock.hcl Be in .gitignore? goes deeper.
terraform fmt and terraform validate
Two quick checks before you plan anything. fmt rewrites files into the standard style (it printed nothing because the file is already formatted), and validate checks syntax, references and argument types without touching any real infrastructure:
terraform plan
Both resources show + (create). Values the provider only knows after creation, such as the file's checksums and the pet name itself, appear as (known after apply). The plan reads like a contract: if it is not what you expect, stop here and fix the code.
terraform apply
apply makes a new plan, shows it again and waits for you to type yes:
Terraform created the name first, then the file, because the file refers to the name. Check the result and what Terraform recorded:
terraform.tfstate is the state file from Part 1. Run terraform plan again and it reports that nothing needs to change:
Change Something
Edit the content line of local_file.hello so it reads "Hello again from ${random_pet.name.id}!\n" and plan again:
The local provider cannot edit a file in place, so changing content forces replacement (-/+): Terraform deletes the file and writes a new one. The pet name is unchanged, so the same name appears in the new content. Cloud resources behave the same way: some arguments update in place (~), others replace the object, and the plan always tells you which.
Apply it without the prompt this time. -auto-approve skips the question, so only use it when you have just read the plan or in automation that applies a reviewed, saved plan:
terraform destroy
When you are done, remove everything this configuration manages:
The file is gone, and the state file is now empty of resources (Terraform keeps it, plus a terraform.tfstate.backup copy of the previous state). For a cloud project, destroy is how you stop paying for what you created while learning.
Set Up Cloud Credentials
Providers find credentials the same way the platform's own CLI does, so you rarely put them in Terraform code. Never write a secret into a .tf file: it ends up in Git. Pick the platform you use:
Some rules that apply to every platform:
- Prefer short-lived credentials. SSO sessions,
gclouduser logins and OIDC tokens in CI expire on their own. Long-lived access keys and service account key files are the credentials that leak. - Use environment variables, not provider arguments, for anything secret. A
token = "..."line in a provider block is a secret in Git. - In CI, use OpenID Connect (OIDC) so the pipeline gets temporary cloud credentials without stored keys. Part 9 shows a GitHub Actions example.
- Start with broad permissions in a sandbox account, then narrow them. The post Determining Minimum AWS Permissions for Terraform shows how.
Your First Cloud Server
The same workflow creates real infrastructure. This example creates a DigitalOcean Droplet (a virtual server) with 1 vCPU and 1 GB of memory. At list price it costs $6 a month. DigitalOcean bills Droplets per second while they exist (at least 60 seconds or $0.01 per Droplet), up to that monthly price, so destroy it when you are done.
If you do not have a DigitalOcean account, you can sign up through our referral link. A new account gets DigitalOcean's standard signup credit ($5 that expires 90 days after sign-up), and you must add a payment method before you can create a Droplet. If you later spend $25 on DigitalOcean, DevOps Daily gets $25 of credit (referral program).
Create a new directory, droplet, with this main.tf:
terraform {
required_version = ">= 1.17"
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.106"
}
}
}
# Reads the token from the DIGITALOCEAN_TOKEN environment variable
provider "digitalocean" {}
variable "ssh_key_fingerprint" {
description = "Fingerprint of an SSH key already added to your DigitalOcean account"
type = string
}
resource "digitalocean_droplet" "web" {
name = "web-1"
image = "ubuntu-24-04-x64"
region = "fra1"
size = "s-1vcpu-1gb"
ssh_keys = [var.ssh_key_fingerprint]
monitoring = true
tags = ["learn-terraform"]
}
output "droplet_ip" {
value = digitalocean_droplet.web.ipv4_address
}
The provider block is empty because the provider reads DIGITALOCEAN_TOKEN from the environment. The Droplet logs in with your SSH key instead of a password: add your public key under Settings > Security in the control panel first, and pass its fingerprint when Terraform asks for the variable. ssh-keygen -l -E md5 -f ~/.ssh/id_ed25519.pub prints the key size, the fingerprint and the comment: copy only the fingerprint and remove its leading MD5:.
The plan below is a real run of this configuration with a placeholder token. Planning a new Droplet does not call the DigitalOcean API, so it works without an account; with a real token, the output is the same:
With a real token and your own fingerprint, terraform apply creates the server. This part of the output is an example, because it needs a real account:
digitalocean_droplet.web: Creating...
digitalocean_droplet.web: Still creating... [00m10s elapsed]
digitalocean_droplet.web: Still creating... [00m20s elapsed]
digitalocean_droplet.web: Still creating... [00m30s elapsed]
digitalocean_droplet.web: Creation complete after 33s [id=512345678]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
droplet_ip = "203.0.113.25"
Connect with ssh root@$(terraform output -raw droplet_ip), then run terraform destroy when you are done. The Deploy a DigitalOcean Droplet with Terraform exercise walks through a slightly bigger version with a firewall.
Files to Keep Out of Git
Add a .gitignore to every Terraform project before your first commit:
# Provider binaries and module downloads
.terraform/
# State can contain secrets; keep it in a remote backend (Part 9)
*.tfstate
*.tfstate.*
# Saved plans also contain secrets
*.tfplan
tfplan
# Crash logs and local CLI configuration
crash.log
crash.*.log
.terraformrc
terraform.rc
# Variable files that hold secrets; name them so this pattern catches them
*.secret.tfvars
*.secret.tfvars.json
Keep per-environment settings such as env/prod.tfvars in Git, because reviewers need to see them and CI needs them (Part 8). Keep secrets out of those files: pass them as TF_VAR_ environment variables from a secret store or your CI's secrets, or in a *.secret.tfvars file that the pattern above keeps out of Git.
Commit .terraform.lock.hcl. Older guides told you to ignore it; that is wrong for any project you share, because the lock file is what makes provider installs repeatable.
Common Setup Problems
terraform: command not found. The directory with the binary is not on your PATH, or you need a new terminal after installing.
Failed to query available provider packages or a timeout during init. Your network blocks registry.terraform.io or releases.hashicorp.com. Behind a corporate proxy, set HTTPS_PROXY. In air-gapped networks, use a provider mirror (terraform providers mirror and a provider_installation block in the CLI configuration).
the cached package ... does not match any of the checksums recorded in the dependency lock file. The lock file was created on another operating system or CPU and only has checksums for that platform, or a download was corrupted. Add the missing platforms with terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 -platform=windows_amd64. See How to Fix Terraform Provider Checksum Mismatch Errors.
No valid credential sources found or 401 Unauthorized. The provider did not find credentials. Check the environment variables in the shell where you run Terraform, run the platform's "who am I" command (aws sts get-caller-identity, doctl account get, gcloud auth list) and check that the session has not expired.
Error acquiring the state lock. Another Terraform process is running in the same directory or against the same remote state, or a previous run crashed. Part 5 explains what to do.
Something else. Run the command again with TF_LOG=DEBUG set to see every API call, and TF_LOG_PATH=terraform.log to write the log to a file. Debug logs can contain secrets, so do not paste them anywhere public.
Summary
- Install Terraform 1.17 from HashiCorp's package repositories, HashiCorp's Homebrew tap, winget, or a verified zip.
initdownloads providers and writes.terraform.lock.hcl; commit the lock file, ignore.terraform/.fmt,validate,plan,applyanddestroyare the commands you use every day.- Credentials come from the environment and the platform's CLI login, never from
.tffiles. - The workflow for a cloud server is the same as for a local file; only the provider changes.
In Part 3 you learn the configuration language in detail.
Keep Learning
Tags
Found an issue?