Gendi Notes

Terraform

You describe the infrastructure you want in files; Terraform works out the difference from what exists and makes only that change.

Level 2 guide terraformiacinfrastructure

What is it?#

Terraform creates and manages cloud infrastructure from code. You write what you want — a server, a database, a network — and Terraform calls the cloud provider's API to make reality match.

your .tf files Terraform the cloud ────────────── ───────── ───────── "1 server, → compare desired → creates / changes 2GB, in eu-west" vs actual only the difference

Why it exists#

Clicking through a cloud console works once. Then: nobody knows what was changed, staging does not match production, and rebuilding after an outage means remembering forty settings.

Console clickingTerraform
No record of what was doneThe files are the record, in Git
Environments drift apartSame code, different variables
Rebuild = remember everythingterraform apply
Review = "trust me"A pull request with a diff

What it is made of#

PieceWhat it is
.tf filesYour desired infrastructure, written in HCL
ProviderThe plugin that talks to AWS / Azure / GCP
ResourceOne thing to manage — a VM, a bucket, a DNS record
VariableAn input, so the same code works for dev and prod
OutputA value to display or pass to another module
ModuleA reusable group of resources
StateTerraform's record of what it created. The critical piece

State is the part to understand#

YOUR CODE STATE REALITY ───────── ───── ─────── aws_instance.web → id = i-0abc123 → the actual EC2 instance (what you want) (what I made) (what exists)

Terraform's whole job is diffing those three columns. There is no cloud API for "list everything I created" — so without state, Terraform cannot tell "create this" from "this already exists".

How to use it#

A first configuration#

main.tf
terraform {
  required_version = ">= 1.6"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"        # allow 5.x, not 6.0
    }
  }

  # Remote state: shared, versioned, and locked so two people cannot
  # apply at the same time. Set this up before your second engineer.
  backend "s3" {
    bucket         = "my-tf-state"
    key            = "prod/terraform.tfstate"
    region         = "eu-west-1"
    encrypt        = true
    dynamodb_table = "tf-locks"      # the lock table
  }
}

provider "aws" {
  region = var.region
}

variable "region" {
  type    = string
  default = "eu-west-1"
}

variable "instance_type" {
  type        = string
  default     = "t3.micro"
  description = "Size of the web server"
}

resource "aws_instance" "web" {
  ami           = "ami-0abcdef1234567890"
  instance_type = var.instance_type

  tags = {
    Name      = "web-server"
    ManagedBy = "terraform"      # so humans know not to edit it by hand
  }
}

output "public_ip" {
  value = aws_instance.web.public_ip
}

The workflow#

terraform init          # download providers, configure the backend. Run once per checkout
terraform fmt           # format the files
terraform validate      # syntax and type checking. No cloud calls
terraform plan          # WHAT WOULD CHANGE. Always read this
terraform apply         # make the change (asks for confirmation)
terraform destroy       # tear it all down

Reading a plan — the most important skill#

  # aws_instance.web will be updated in-place
  ~ resource "aws_instance" "web" {
      ~ instance_type = "t3.micro" -> "t3.small"
    }

Plan: 0 to add, 1 to change, 0 to destroy.
SymbolMeaning
+Create
~Update in place — no downtime
-Destroy
-/+Destroy then recreate — the dangerous one

Plan then apply, in CI#

terraform plan -out=tfplan      # save the exact plan
# review it, get approval
terraform apply tfplan          # apply EXACTLY that plan, no re-prompt

Without -out, the plan you reviewed and the change that gets applied are two separate calculations — and the world may have changed between them.

Variables per environment#

variables.tf
variable "instance_type" { type = string }
variable "replica_count" { type = number, default = 2 }
prod.tfvars
instance_type = "t3.large"
replica_count = 5
terraform apply -var-file=prod.tfvars

Same code, different inputs. This is how one configuration serves dev, staging and production without copy-paste.

Modules#

module "web_cluster" {
  source = "./modules/web-cluster"      # or a registry / Git URL

  environment   = "production"
  instance_type = "t3.large"
  replicas      = 5
}

Start writing modules when you copy a block for the third time — not before. Premature modules are harder to read than the duplication they replace.

State operations to know#

terraform state list                    # everything Terraform manages
terraform state show aws_instance.web    # one resource in detail

# Adopt an existing resource created outside Terraform. Non-destructive.
# Prefer an `import` block in config (Terraform 1.5+) — it is reviewable in a PR.
terraform import aws_instance.web i-0abc123

# FORGET a resource. It keeps running and keeps billing; Terraform stops
# managing it. For handing a resource to another configuration.
terraform state rm aws_instance.web

Drift#

Someone changed something in the console:

terraform plan -refresh-only     # show differences without proposing changes
terraform plan -detailed-exitcode  # exit 2 if there is a diff — scriptable in CI

A scheduled drift check is one of the highest-value CI jobs you can add.

Blast radius: split your state#

One state file = one blast radius. Everything in it can be destroyed by one mistaken apply, and every apply locks all of it.

ONE BIG STATE SPLIT BY RATE OF CHANGE ┌─────────────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ network │ │ network│ │ data │ │ apps │ │ database │ │ (rare) │ │ (rare) │ │(hourly)│ │ 40 applications │ └────────┘ └────────┘ └────────┘ └─────────────────┘ · 10-minute plans · small, fast plans · one typo destroys all · small blast radius · every apply locks everything · teams work independently

Split by how often things change and who owns them, not by resource type. A VPC that changes twice a year does not belong in the same state as applications that deploy hourly.

Common problems#

SymptomCauseFix
Error acquiring the state lockSomeone is applying, or a crashed runWait; force-unlock <id> only after verifying
Plan wants to recreate everythingWrong backend/workspace — it sees empty stateCheck terraform init and the state key
Resource already existsCreated outside Terraformimport it
Plan is never emptyA provider computes a value each runOften benign; lifecycle { ignore_changes = [...] }
State file lostDeleted, or bucket versioning offRestore from versioning; else re-import each resource
Two engineers overwrite each otherNo state lockingAdd the lock table. This is why locking exists

Interview corner · الأسئلة اللي بتتسأل#

What is Terraform state, why does it exist, and what happens if you lose it?

State is Terraform's record mapping each resource in your code to the real object it created in the cloud (aws_instance.web → i-0abc123).

Why it must exist: no cloud API can answer "list everything Terraform created". Without that mapping, Terraform cannot distinguish "create this" from "this already exists", so every plan would propose recreating your entire estate.

If you lose it:

AvailableRecovery
Bucket versioning onRestore the previous version. Minutes
A local .tfstate.backupRestore it; may be slightly stale
Nothingterraform import every resource by hand — days, and error-prone

The real answer to the last row is that it should never happen: remote backend, encryption, versioning, and locking, configured before the first apply.

Someone changed a security group in the console. Terraform state is now out of date. What do you do?

Two halves — the fix, and the reason. Only giving the first reads as junior.

The fix:

terraform plan                    # see the drift. Do NOT apply blindly
terraform apply -refresh-only     # reconcile state with reality, changes nothing real
terraform import ...              # if they created something new

Then make a deliberate decision:

DecisionAction
The console change should stayWrite it into the code, then apply — code and reality agree
The console change should goLet apply revert it

The reason it happened: console write access existed. The durable fix is process, not commands:

  • Remove human write permissions in production; the pipeline's identity is the only thing that can apply.
  • Add a scheduled drift check — terraform plan -detailed-exitcode exits 2 on a diff, so it is a one-line CI job that finds this in an hour rather than in six weeks.
  • Tag resources ManagedBy = terraform so a human in the console can see they should not touch it.
Error acquiring the state lock. What is it protecting, and when is force-unlock safe?

The lock prevents two applies running at once. Without it, two engineers reading the same state, each computing a plan from it, and each writing back their result produces a state file that describes neither — with real resources orphaned or duplicated.

no lock: engineer A read state ──→ plan ──→ apply ──→ write state engineer B read state ──→ plan ──→ apply ──→ write state └─ overwrites A's A's resources now exist but are untracked

Legitimate causes: someone is genuinely applying right now; or a previous run crashed (CI runner killed, laptop closed) and left the lock behind.

force-unlock is safe only when you have verified no apply is running. Check who holds it first — the error message names the user, the operation and the timestamp:

terraform force-unlock <LOCK_ID>

Force-unlocking while an apply is genuinely in progress is how you get the corrupted state the lock exists to prevent.

Key takeaways#

  • Code → plan → apply. Read the plan every time.
  • -/+ means destroy and recreate. On a database that is data loss.
  • State maps your code to real resources, holds plaintext secrets, and must never be committed.
  • Remote state with locking from the moment a second person is involved.
  • One state = one blast radius. Split by rate of change.
  • state rm does not delete the resource — it abandons it, still billing.
  • Bucket versioning today. It is the difference between an inconvenience and a disaster.

Last updated 2026-09-08