Skip to content

Phase 4 · ORCHESTRATION & IAC

IaC concepts + Terraform first apply

Day 76 of 90 ~50 min 0/20 in phase Builds on Day 75

By the end of today

  • Explain declarative desired state versus imperative provisioning scripts
  • Run the init, plan, apply, destroy loop on a local resource
  • Read a plan's + and - symbols and know what state records

Infrastructure as Code: describe what you want, let Terraform build it

Section 1 of 5 · ~3 min

For weeks you’ve provisioned by doing — SSH in, run apt, edit a config, restart a service. That’s imperative: a list of steps a human runs in order, and the only record of what the server looks like is the server itself. Infrastructure as Code flips it. You write a file that declares the desired end state — “one file here, with this content” — and a tool reconciles reality to match. You don’t say how to get there; you say what should exist.

That’s the split at the heart of Terraform. Your .tf files are the desired state; Terraform’s job is to look at what already exists, compare it to what you declared, and make the smallest set of changes to close the gap. Run it twice and the second run does nothing — the state already matches. That property, idempotence, is what a shell script full of apt install never gives you for free.

Terraform reaches real systems through providers — plugins that turn your resource blocks into API calls. There’s one for AWS, one for Kubernetes, one for GitHub, and the two you’ll use today, entirely offline: hashicorp/local writes files and hashicorp/random generates names. init downloads the providers your config names and pins them.

The core loop: init → plan → apply → destroy

Four commands are the whole rhythm. init prepares a working directory: it reads required_providers, downloads them, and writes .terraform.lock.hcl so every machine gets identical plugin versions. plan is a dry run — Terraform shows what it would do, with + to create, ~ to change and - to destroy, and changes nothing. apply executes an approved plan and makes reality match. destroy tears the managed resources back down. You’ll run this loop all day; getting it into your fingers now is the point.

Between runs, Terraform remembers what it manages in state — a JSON file, terraform.tfstate, mapping each resource in your config to the real object it created. State is how plan knows the difference between “create this” and “this already exists, leave it.” Lose the state file and Terraform forgets it ever built anything; corrupt it and it acts on the wrong reality. It’s the single most important artifact in the tool — which is why tomorrow is partly about where it lives.

The Terraform loop: write a .tf file as the desired state, run init to download providers, plan to preview the create and destroy changes, apply to make them real, and destroy to tear them down; state (terraform.tfstate) is the record of what is managed between runs. Write .tf desired state init get providers plan + ~ - preview apply make it real destroy tear it down state (terraform.tfstate) is the record of what Terraform manages between runs
The loop you'll run all day — write desired state, init, plan, apply, destroy; state is the record in between.

Real world: Imperative is handing someone turn-by-turn directions; declarative is giving them the destination address and letting the sat-nav route there. Hit a closed road and the directions strand you — the address still resolves, the nav just recomputes. Terraform is the sat-nav: you commit the address (.tf), and plan shows the route before you drive it.

A named example: the two providers you’ll use today, hashicorp/local and hashicorp/random, are published on the public Terraform Registry (registry.terraform.io) — the same registry that serves the AWS and Kubernetes providers behind millions of real deployments. init pulls them by name and version exactly as it would a cloud provider, so the loop you learn on a local text file is byte-for-byte the loop you’ll run against AWS on Day 79.

One term you’ll hear: OpenTofu is the community, MPL-2.0-licensed fork of Terraform, created in 2023 after HashiCorp relicensed Terraform under the Business Source License; its CLI is a drop-in for everything in this lab.

Hands-On Lab

Section 2 of 5 · ~5 min

Budget about 20 minutes. Open your WSL2 Ubuntu 24.04 terminal. Everything here is 100% local — no cloud account, no credentials, no spend, so nothing can bill you. You need Terraform 1.10+; step 1 installs it. Content hashes and resource ids are derived from the file’s bytes and are shown elided () — yours are reproducible from the same config.

# 1. Install Terraform from HashiCorp's official apt repo, then confirm the version.
# Ensure the tools the next lines use are present (usually already on WSL2 Ubuntu 24.04):
sudo apt-get update && sudo apt-get install -y gnupg software-properties-common wget lsb-release
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 -y terraform
terraform version
# Output (your minor and patch versions will differ — anything satisfying the >= 1.10 constraint works):
# Terraform v1.15.2
# on linux_amd64
# 2. Make a scratch project and write the whole config into main.tf.
mkdir tf-local && cd tf-local
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.10"
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}

resource "local_file" "hello" {
  filename = "hello.txt"
  content  = "Hello from Terraform — this file is managed as code.\n"
}
EOF
ls
# Output:
# main.tf
# 3. Initialize: Terraform downloads the local provider and writes the lock file.
terraform init
# Output (your minor and patch versions and the exact hashes will differ — anything satisfying the ~> 2.5 constraint works):
# Initializing the backend...
#
# Initializing provider plugins...
# - Finding hashicorp/local versions matching "~> 2.5"...
# - Installing hashicorp/local v2.9.1...
# - Installed hashicorp/local v2.9.1 (signed by HashiCorp)
#
# Terraform has created a lock file .terraform.lock.hcl to record the provider
# selections it made above. Include this file in your version control repository
# so that Terraform can guarantee to make the same selections by default when
# you run "terraform init" in the future.
#
# Terraform has been successfully initialized!
# 4. See what init created: a .terraform cache dir and the .terraform.lock.hcl lock file.
ls -a
# Output:
# .  ..  .terraform  .terraform.lock.hcl  main.tf
# 5. Preview the change. + means "create"; Terraform touches nothing yet.
terraform plan
# Output (trimmed):
# Terraform used the selected providers to generate the following execution
# plan. Resource actions are indicated with the following symbols:
#   + create
#
# Terraform will perform the following actions:
#
#   # local_file.hello will be created
#   + resource "local_file" "hello" {
#       + content              = "Hello from Terraform — this file is managed as code.\n"
#       + content_base64sha256 = (known after apply)
#       + content_base64sha512 = (known after apply)
#       + content_md5          = (known after apply)
#       + content_sha1         = (known after apply)
#       + content_sha256       = (known after apply)
#       + content_sha512       = (known after apply)
#       + directory_permission = "0777"
#       + file_permission      = "0777"
#       + filename             = "hello.txt"
#       + id                   = (known after apply)
#     }
#
# Plan: 1 to add, 0 to change, 0 to destroy.
# 6. Apply it. Terraform re-shows the plan, then waits for you to type yes.
terraform apply
# Output (the plan prints again first, then):
# Do you want to perform these actions?
#   Terraform will perform the actions described above.
#   Only 'yes' will be accepted to approve.
#
#   Enter a value: yes
#
# local_file.hello: Creating...
# local_file.hello: Creation complete after 0s [id=… (SHA1 of the content)]
#
# Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
# 7. The resource is real: Terraform wrote the file with exactly the content you declared.
cat hello.txt
# Output:
# Hello from Terraform — this file is managed as code.
# 8. Ask Terraform what it now manages — this reads terraform.tfstate.
terraform state list
# Output:
# local_file.hello
# 9. Tear it back down. - means "destroy"; again you confirm with yes.
terraform destroy
# Output (trimmed):
#   # local_file.hello will be destroyed
#   - resource "local_file" "hello" {
#       - content              = "Hello from Terraform — this file is managed as code.\n" -> null
#       - content_base64sha256 = "…" -> null
#       - directory_permission = "0777" -> null
#       - file_permission      = "0777" -> null
#       - filename             = "hello.txt" -> null
#       - id                   = "…" -> null
#     }
#
# Plan: 0 to add, 0 to change, 1 to destroy.
#
# Do you really want to destroy all resources?
#   Terraform will destroy all your managed infrastructure, as shown above.
#   There is no undo. Only 'yes' will be accepted to confirm.
#
#   Enter a value: yes
#
# local_file.hello: Destroying... [id=…]
# local_file.hello: Destruction complete after 0s
#
# Destroy complete! Resources: 1 destroyed.
# 10. hello.txt is gone; the state files remain (state now records zero resources).
ls
# Output:
# main.tf  terraform.tfstate  terraform.tfstate.backup

Read that back as one loop: you declared a file in code, init fetched the provider, plan previewed a single + create, apply made it real, and destroy removed it with a single - destroy. Swap the local_file block for an aws_instance and every command — and every symbol — is identical. That is the whole of Terraform; the rest of Phase 4 is bigger resource blocks and better places to keep the state.

Common Errors & Fixes

Section 3 of 5 · ~3 min

Three trip up almost everyone on their first apply. Read the error text slowly — parsing it is the skill.

Common error: Running terraform plan (or apply) in a fresh directory before terraform init:


│ Error: Could not load plugin

│ Plugin reinitialization required. Please run "terraform init".

│ Plugins are external binaries that Terraform uses to access and manipulate
│ resources. The configuration provided requires plugins which can't be
│ located, don't satisfy the version constraints, or are otherwise
│ incompatible.

│ Failed to instantiate provider "registry.terraform.io/hashicorp/local" to
│ obtain schema: unavailable provider "registry.terraform.io/hashicorp/local"

Why: Terraform ships no providers of its own — a bare CLI can’t talk to files, AWS or anything else. init is the step that reads required_providers, downloads the named plugins, and caches them under .terraform/. Skip it and plan has no local provider to load, so it can’t even build the resource’s schema.

Fix: Run terraform init once in the directory, then re-run plan. Re-run init any time you add a provider or change a version constraint; the message literally tells you to.

How you’d spot it in prod: A CI job that fails at plan/apply with “Could not load plugin” almost always skipped init, or ran in a clean runner where the .terraform/ cache wasn’t restored. The pipeline needs an init step (or a cached plugin dir) before it plans.

Common error: Typing y, pressing Enter, or hitting Ctrl-C at the apply confirmation instead of the full word yes:

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: y

Apply cancelled.

Why: apply (and destroy) show the plan and then hold at an interactive gate. It’s a deliberate safety catch, so only the exact string yes proceeds — y, a blank line, or anything else is treated as “no” and Terraform cancels without changing a thing.

Fix: Type yes in full. When you already trust the plan, terraform apply -auto-approve skips the prompt; in automation always pair it with -input=false so a missing answer errors instead of hanging.

How you’d spot it in prod: A pipeline apply step that hangs until it times out — or exits with “Apply cancelled.” — is waiting on a prompt with no TTY to answer it. Add -input=false -auto-approve (or feed an approved saved plan) to non-interactive applies.

Common error: Editing or deleting the generated file by hand, then running terraform plan and being surprised it wants to put the file back:

Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the
last "terraform apply" which may have affected this plan:

  # local_file.hello has been deleted

Terraform will perform the following actions:

  # local_file.hello will be created
  + resource "local_file" "hello" {
      + filename = "hello.txt"
      ...
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Why: plan first refreshes state against reality. It sees the file it recorded no longer matches — you deleted or edited it — reports the change “outside of Terraform,” and because your config still declares the file, plans to recreate it so reality matches the code again. That’s reconciliation working exactly as designed, not a bug.

Fix: Don’t hand-edit resources Terraform owns. Change the content in main.tf and apply, keeping code the source of truth. If you changed the real object on purpose and want to keep it, update the config to match; to pull real-world changes into state without altering anything, use terraform apply -refresh-only.

How you’d spot it in prod: A plan that reports “changes made outside of Terraform,” or unexpected creates/replaces on resources nobody touched in code, is classic drift — someone clicked in a console or edited a resource by hand. The fix is to move that change into code, not to keep editing by hand.

Terraform Interview Questions

Section 4 of 5 · ~1 min

IaC and the init-plan-apply loop are Phase-4 screening staples — cover each answer, say your own version out loud first, then compare. The four questions and answers render right after this note.

Go Deeper

Section 5 of 5 · ~1 min

Optional extras if you have ~25 more minutes:

  • 5 min — Add a random_pet resource and set filename = "hello-${random_pet.name.id}.txt"; re-run plan and watch it become 2 to add, one resource feeding the other’s name.
  • 10 min — Open .terraform.lock.hcl and terraform.tfstate in an editor: the lock file pins the provider version and its hashes, the state file is the JSON record of your file. Read them, don’t hand-edit either.
  • 10 min — Read the hashicorp/local provider page on the Terraform Registry and note how each resource lists its arguments and attributes — that page is the contract plan checks your config against.
What's the difference between declarative and imperative infrastructure? Both

Imperative means I write the exact steps — install this, edit that, restart — and run them in order; the only record of the result is the box itself. Declarative means I describe the end state I want and let a tool work out the steps. Terraform is declarative: my .tf files say what should exist, and terraform plan computes the diff between that and reality. The big win is idempotence — run it twice and the second run is a no-op because state already matches. A shell script has no idea what it did last time, so re-running it can double-apply or fail halfway. Declarative code is also reviewable and diffable like any other code in the repo.

Walk me through the core Terraform workflow. Both

Four commands. terraform init prepares the directory — it reads which providers my config needs, downloads them, and writes a lock file so every machine uses the same versions. terraform plan is a dry run: it shows what it would create, change or destroy, marked with plus, tilde and minus, and touches nothing. terraform apply executes that plan after I confirm with 'yes' and makes reality match the config. terraform destroy tears the managed resources back down. In between, Terraform keeps state — a file mapping each resource in my config to the real object it made — so plan can tell 'create this' from 'already exists'. Init, plan, apply is the loop I run all day.

What is Terraform state and why does it matter? Product

State is Terraform's record of what it manages — a JSON file, terraform.tfstate, mapping every resource in my config to the actual object it created, plus that object's current attributes. It's how plan knows the difference between something it must create and something that already exists, so it can compute a minimal diff. It matters because it's the tool's source of truth: lose it and Terraform forgets it ever built anything and tries to recreate everything; let it drift from reality and plans act on the wrong picture. On a team you never keep it on one laptop — it goes in a shared remote backend with locking so two applies can't corrupt it. That's tomorrow's topic.

What's the difference between Terraform and OpenTofu? Both

They're the same tool, forked. Terraform is HashiCorp's, and in 2023 HashiCorp relicensed it from the open-source MPL to the Business Source License, which restricts some competing commercial uses. In response the community forked the last MPL version and created OpenTofu, now under the Linux Foundation and still MPL-2.0. For everything in this course the two are interchangeable — same HCL, same init-plan-apply loop, same providers from a compatible registry; you'd just type 'tofu' instead of 'terraform'. Which one a job uses is mostly a licensing and governance choice, not a day-to-day workflow difference. Knowing both exist, and why, is enough for an interview.

Mark Day 76 complete

Tomorrow you stop hard-coding values — variables and outputs make a config reusable, and you'll see exactly where Terraform keeps its state.

Stuck on today’s lab? Ask in Mission 90 Q&A