← Cheatsheets

CHEATSHEET · DEVOPS · BEFORE THE INTERVIEW

Terraform — The Interview Cheatsheet.

terraform iac devops interview-prep
Terraform is declarative IaC: you write desired infrastructure in HCL, it diffs against state (a JSON record of what it manages) and makes the real world match. The loop is init → plan → apply. Almost everything hard is about state.

1. Core concepts

TermWhat
ProviderPlugin for a platform (aws, google, azurerm, kubernetes). Exposes resources + data sources.
ResourceA managed infra object (aws_instance.web) — type + local name = address.
Data sourceRead-only lookup of existing infra (data.aws_ami.x).
StateJSON mapping config → real resource IDs + cached attributes. Terraform's source of truth.
ModuleReusable folder of .tf with inputs (variables) + outputs.
BackendWhere state lives (local, S3+DynamoDB lock, GCS, Terraform Cloud).
PlanThe computed diff (create/update/replace/destroy) before applying.

How a run works: Terraform reads config + refreshes state against real infra, builds a dependency graph, computes a diff, and applies it in dependency order (parallel where possible). Dependencies are implicit when one resource references another's attribute.

2. Workflow & CLI

terraform init        # download providers, configure backend, install modules
terraform fmt -recursive    # canonical formatting
terraform validate          # syntax + internal consistency
terraform plan              # preview diff (read this EVERY time)
terraform plan -out=tfplan  # save a plan
terraform apply tfplan      # apply exactly that plan (CI-safe)
terraform apply -auto-approve
terraform destroy           # tear down everything in state
terraform apply -target=aws_instance.web   # narrow (use sparingly — hides drift)
terraform output            # show outputs ; -json for machines
terraform console           # REPL to test expressions
terraform graph | dot -Tsvg > graph.svg
terraform providers ; terraform version

3. HCL essentials

terraform {
  required_version = ">= 1.6"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}
provider "aws" { region = var.region }

variable "env"    { type = string  default = "dev" }
variable "tags"   { type = map(string)  default = {} }
locals { name = "app-${var.env}" }

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]
  filter { name = "name"  values = ["ubuntu/images/*-24.04-*"] }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id     # implicit dependency
  instance_type = "t3.micro"
  tags          = merge(var.tags, { Name = local.name })
}

output "ip" {
  value     = aws_instance.web.public_ip
  sensitive = false
}

Types: string, number, bool, list, set, map, object, tuple. Common functions: merge, lookup, coalesce, try, for expressions, jsonencode, templatefile.

Variable precedence (high → low): CLI -var / -var-file > *.auto.tfvars > terraform.tfvars > env TF_VAR_* > default.

4. Meta-arguments

ArgUse
countN copies by index. Address res[0]. Good for identical resources.
for_eachOne per map/set key. Address res["key"] — stable. Preferred.
depends_onExplicit ordering when there's no attribute reference.
providerPick a non-default provider alias (e.g. multi-region).
lifecyclecreate_before_destroy, prevent_destroy, ignore_changes, replace_triggered_by.
resource "aws_iam_user" "u" {
  for_each = toset(["alice", "bob"])
  name     = each.key
}
resource "aws_instance" "web" {
  lifecycle {
    create_before_destroy = true
    ignore_changes        = [tags["LastModified"]]
  }
}
count vs for_each count keys by index — remove the middle item and everything after shifts and gets recreated. for_each keys by name — stable, no churn. Prefer for_each for anything you'll add to/remove from.

5. State management

terraform state list                 # what's managed
terraform state show           # full attributes of one resource
terraform import      # adopt an existing resource into state
terraform state mv             # rename/move without recreating
terraform state rm             # stop managing (does NOT delete real infra)
terraform refresh                    # (or plan -refresh-only) sync state to reality
terraform force-unlock      # release a stuck lock (only if sure)
# remote, locked, versioned backend (the right setup for teams)
terraform {
  backend "s3" {
    bucket         = "my-tf-state"
    key            = "prod/app.tfstate"
    region         = "us-east-1"
    dynamodb_table = "tf-locks"     # state locking
    encrypt        = true
  }
}
state is sensitive + must be locked State can contain secrets in plaintext — never commit it to git; encrypt the backend. Local state breaks teams; use a remote backend with locking so two applies can't corrupt it.

6. Drift & imports

Drift = real infra changed outside Terraform (a console click). Detect: terraform plan -refresh-only. Reconcile by updating config to match reality, or let Terraform revert the manual change. Import existing resources so TF manages them:

# import block (TF 1.5+) — reviewable, in code
import {
  to = aws_s3_bucket.logs
  id = "my-existing-bucket"
}
# then: terraform plan -generate-config-out=generated.tf

7. Modules

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.5.0"
  name    = "main"
  cidr    = "10.0.0.0/16"
  azs     = ["us-east-1a", "us-east-1b"]
}
# consume outputs elsewhere:
resource "aws_instance" "web" { subnet_id = module.vpc.public_subnets[0] }
  • Module sources: local path, Terraform Registry, Git, S3.
  • Inputs = variable blocks; returns = output blocks.
  • Pin version on registry modules; keep modules small + composable.

8. Environments & workspaces

terraform workspace list / new staging / select prod
# terraform.workspace is available in config

Workspaces give multiple states from one config — fine for light dev/staging splits. For real prod isolation prefer separate state files / directories per environment (and often separate backends/accounts) so a mistake in dev can't touch prod.

9. Provisioners (last resort)

local-exec / remote-exec run scripts during create/destroy. Avoid them — they're not tracked in state, break idempotency, and fail unpredictably. Prefer cloud-init / user_data, configuration management, or baked images (Packer). If you must, pair with null_resource + triggers.

10. Best practices

  • Remote, locked, versioned state backend; one state per environment.
  • Pin required_version, provider versions, and module versions.
  • Never commit secrets or state to git; mark sensitive outputs sensitive = true.
  • plan in CI on every PR; gated apply on merge (apply the saved plan).
  • Prefer data sources + for_each over copy-paste; small modules.
  • Use -target and force-unlock rarely — they hide problems.
  • terraform fmt + validate + a linter (tflint) + a policy gate (OPA/Sentinel) in CI.

11. Troubleshooting

ProblemFix
Error acquiring the state lockAnother apply running, or a crashed one. Confirm, then force-unlock <id>.
Plan wants to change untouched thingsDrift — plan -refresh-only; reconcile config or accept reality.
AlreadyExists on applyResource exists outside state — terraform import.
Cycle errorBad depends_on / mutual reference — let implicit deps order it.
Forces replacement unexpectedlyAn immutable attribute changed; check the plan's "# forces replacement". Use create_before_destroy.
Need verbose logsTF_LOG=DEBUG terraform apply (provider API calls).

12. Rapid-fire Q&A

  • What is state and why does it matter?JSON mapping config to real resource IDs. It's how TF knows what exists, computes diffs, and avoids recreating things. Lose/corrupt it and TF loses track of your infra.
  • count vs for_each?count = index-keyed (shifts on removal → recreation); for_each = key-keyed (stable). Prefer for_each.
  • plan vs apply?plan computes/preview the diff; apply executes it. In CI, save a plan and apply exactly that.
  • How do you manage existing infra?terraform import (or import blocks) to bring it under state, then plan should be a no-op.
  • How do teams share state safely?Remote backend with locking (S3+DynamoDB / TF Cloud) so applies serialize; encrypted, versioned.
  • local vs variable vs output vs data?local = computed reuse; variable = input; output = exposed/module return; data = read existing infra.
  • What is drift?Real infra changed outside Terraform. Detect with plan -refresh-only; reconcile.
  • create_before_destroy?Lifecycle rule to make the replacement before destroying the old — avoids downtime on replace.
  • Why avoid provisioners?Not in state, not idempotent, fail unpredictably. Prefer user_data / images / config management.
  • How is dependency order decided?Implicitly, by attribute references between resources; depends_on only when there's no reference.
← prev: Kubernetes next: Linux →
© cvam — written in plaintext, served warm