Terraform describes infrastructure as code and applies changes to match
that description. The core workflow — init, plan, apply — is the same
whether you're managing a single file on disk or a thousand cloud
resources. This guide runs through it with zero cloud account required, so
you can build the muscle memory first.
Install Terraform
macOS:
brew tap hashicorp/tap
brew install hashicorp/tap/terraformLinux:
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 terraformWindows:
winget install -e --id Hashicorp.TerraformVerify:
terraform -versionIf you'll be switching between projects pinned to different Terraform
versions, install tfenv instead of a
single global binary — same idea as nvm for Node or pyenv for Python.
Your first configuration
Create a directory with one file:
mkdir tf-demo && cd tf-demo# main.tf
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
resource "local_file" "hello" {
filename = "${path.module}/hello.txt"
content = "Hello from Terraform, managed as code.\n"
}This uses the local provider — no cloud credentials needed — so you can
run the real workflow end to end before ever touching AWS, GCP, or Azure.
The core workflow
terraform initDownloads the provider plugin and sets up the .terraform/ working
directory. You run this once per project (and again if you add providers
later).
terraform planShows exactly what Terraform would do — in this case, create
hello.txt — without touching anything yet. Reading the plan output
carefully, every time, is the single most important Terraform habit: it's
your last chance to catch an unintended change before it happens.
terraform applyRe-runs the plan and asks for confirmation before applying it. Type yes.
Check hello.txt now exists with the content you specified.
Change the content value and run terraform plan again — Terraform
diffs the real state against your config and shows only what changed.
terraform destroyRemoves everything Terraform created. Safe to run in this local example; think twice before running it against anything real.
Variables and outputs
Hardcoding values works for a demo, not for a real project. Pull them out:
# variables.tf
variable "message" {
type = string
default = "Hello from Terraform, managed as code."
}# main.tf
resource "local_file" "hello" {
filename = "${path.module}/hello.txt"
content = "${var.message}\n"
}
output "file_path" {
value = local_file.hello.filename
}Override a variable at apply time without editing the file:
terraform apply -var="message=Overridden at apply time"State: the part that trips people up
Terraform tracks what it created in a terraform.tfstate file. That file
is how it knows what "already exists" on the next plan.
Never commit terraform.tfstate to Git. It often contains sensitive
values in plain text (database passwords, keys) and creates merge
conflicts the moment two people apply at different times. Add
*.tfstate* and .terraform/ to .gitignore from the start.
For solo/local learning, the default local state file is fine. The moment more than one person (or a CI pipeline) applies the same configuration, move to remote state — an S3 bucket with DynamoDB locking, or Terraform Cloud — so state lives in one shared place instead of on someone's laptop.
Moving to a real cloud provider
Swapping the local provider for a real one is mostly changing the
required_providers block and the resource types — the init / plan /
apply workflow you just practiced doesn't change at all:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_s3_bucket" "example" {
bucket = "my-unique-bucket-name"
}This needs AWS CLI credentials configured first — see the AWS CLI setup guide for that piece.
Common gotchas
"Error: Inconsistent dependency lock file" after pulling a teammate's
changes. Someone added or upgraded a provider. Run terraform init -upgrade to reconcile .terraform.lock.hcl.
Plan shows changes to a resource you didn't touch. Someone (or
something) changed it outside Terraform — the AWS console, a manual kubectl edit, another tool. Terraform is diffing reality against your config and
reality moved. Bring the resource back under Terraform's control by
updating your config to match, or accept the drift with terraform apply.
Once init → plan → apply is reflexive, reading a Terraform diff
before applying it becomes exactly as normal as reading a git diff
before committing.