Phase 4 · ORCHESTRATION & IAC
IaC concepts + Terraform first apply
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
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.
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), andplanshows 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
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
Three trip up almost everyone on their first apply. Read the error text slowly — parsing it is the skill.
Common error: Running
terraform plan(orapply) in a fresh directory beforeterraform 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.
initis the step that readsrequired_providers, downloads the named plugins, and caches them under.terraform/. Skip it andplanhas nolocalprovider to load, so it can’t even build the resource’s schema.Fix: Run
terraform initonce in the directory, then re-runplan. Re-runinitany 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/applywith “Could not load plugin” almost always skippedinit, or ran in a clean runner where the.terraform/cache wasn’t restored. The pipeline needs aninitstep (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 wordyes: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(anddestroy) show the plan and then hold at an interactive gate. It’s a deliberate safety catch, so only the exact stringyesproceeds —y, a blank line, or anything else is treated as “no” and Terraform cancels without changing a thing.Fix: Type
yesin full. When you already trust the plan,terraform apply -auto-approveskips the prompt; in automation always pair it with-input=falseso a missing answer errors instead of hanging.How you’d spot it in prod: A pipeline
applystep 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 planand 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:
planfirst 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.tfandapply, 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, useterraform 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
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
Optional extras if you have ~25 more minutes:
- 5 min — Add a
random_petresource and setfilename = "hello-${random_pet.name.id}.txt"; re-runplanand watch it become2 to add, one resource feeding the other’s name. - 10 min — Open
.terraform.lock.hclandterraform.tfstatein 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/localprovider page on the Terraform Registry and note how each resource lists its arguments and attributes — that page is the contractplanchecks 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