← Back to Learning

Terraform Basics

Terraform describes infrastructure declaratively: you write what you want to exist, not the steps to create it, and Terraform figures out the difference between that and what's actually there. This is the shape of almost everything you'll do with it.

The four commands you actually use

  • terraform init — downloads the providers a configuration needs and sets up its backend. Run it once per directory, and again whenever you add a provider or change backend config.
  • terraform plan — computes the difference between your configuration and real infrastructure, and prints what would change. Read-only, changes nothing.
  • terraform apply — runs a plan, shows it to you, and (after confirmation) actually makes the changes.
  • terraform destroy — the inverse of apply: tears down everything Terraform knows about in that configuration.

plan before apply isn't optional caution, it's the whole safety model. Reading the diff is how you catch "this would replace the resource, not update it" before it happens instead of after.

A minimal real example

terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.4"
    }
  }
}

resource "local_file" "hello" {
  filename = "${path.module}/hello.txt"
  content  = "Hello from Terraform"
}

terraform init, then terraform apply, and a real file appears on disk. Delete the resource block and apply again, and the file goes away. That's the entire model — the config is the source of truth, and Terraform reconciles reality to match it.

Variables and outputs

Hardcoding values works for a five-line example and nothing else. Real configurations use variables for anything that changes between environments, and outputs for anything another piece of config (or a human) needs to read back out.

variable "filename" {
  type        = string
  description = "Where to write the file"
  default     = "hello.txt"
}

resource "local_file" "hello" {
  filename = var.filename
  content  = "Hello from Terraform"
}

output "path" {
  value = local_file.hello.filename
}

Pass a variable at the command line with -var "filename=other.txt", from a file with -var-file, or via an environment variable named TF_VAR_filename. Real projects almost always use a .tfvars file per environment rather than command-line flags, so the actual values used are version-controlled and reproducible.

State: what it actually is

Every time you apply, Terraform writes a state file (by default terraform.tfstate) recording what it created and the real IDs/attributes of each resource. This is how plan knows what already exists without querying every single resource's live API every time, and it's the only way Terraform can tell "this resource is managed by me" from "this looks similar but I've never touched it."

Two rules that matter more than anything else about state:

  • Never hand-edit it. A slightly wrong ID or attribute in state and Terraform's next plan will confidently propose destroying or recreating something it shouldn't.
  • Don't leave it as a local file for anything real. Local state has no locking (two people running apply at once corrupt it) and no backup. A remote backend (S3, GCS, Terraform Cloud, etc.) gets you both, and is the difference between "a tool for one person on one laptop" and something a team can actually run.

Where this goes next

Once the basic loop is comfortable, the next two things worth learning are providers (how Terraform actually talks to AWS, Cloudflare, Kubernetes, or anything else) and the habits that keep a Terraform codebase maintainable once it's not five lines anymore.