Lesson 1 of 7
Core Concepts
Declare the infrastructure you want and let Terraform's providers do the API work — plan previews every change before apply makes it real.
① Watch
Speed
📖 Read this walkthrough — every command, and why
Terraform Core Concepts — declare, plan, apply, track
Mental model: Terraform is declarative infrastructure as code. You describe the end
state you want in HCL; Terraform figures out how to reach it. You don't write step-by-step
commands — you declare resources, and each resource maps to a real thing that a provider
knows how to manage. Providers are plugins that make the real API calls. The core loop is:
write HCL -> init (fetch providers) -> plan (preview the diff) -> apply (make it real) ->
state (track what exists).
The config for this walkthrough declares two resources — the desired state, not the steps:
resource "local_file" "note" {
filename = "./note.txt"
content = "..."
}
resource "random_pet" "name" {
length = 2
}
1 · terraform init — reads the config and downloads the provider plugins
terraform init
Initializing provider plugins found in the configuration...
- Installing hashicorp/local v2.9.0...
- Installing hashicorp/random v3.9.0...
Initializing the backend...
Terraform has been successfully initialized!
What each part does:
- init inspects your config, sees it needs the local and random providers, and installs them.
- Providers are versioned plugins (hashicorp/local v2.9.0, hashicorp/random v3.9.0). They
are the code that actually creates, reads, updates, and deletes real resources.
- It also initializes the backend — where state is stored (local by default).
2 · terraform plan — the execution plan, a preview diff. It changes NOTHING.
terraform plan
Plan compares your desired config against current state and the real world, then prints
the diff. Everything is marked + because nothing exists yet:
# random_pet.name will be created
+ id = (known after apply)
+ length = 2
# local_file.note will be created
+ filename = "./note.txt"
+ content = (known after apply)
+ content_sha256 = (known after apply)
+ id = (known after apply)
Plan: 2 to add, 0 to change, 0 to destroy.
What each part does:
- "# random_pet.name will be created" — the resource address (type.name) and the action.
- + is create, ~ is update in place, - is destroy (you'll see those on later runs).
- (known after apply) means the value is computed by the provider at apply time, so plan
can't show it yet (an id, a hash, generated content).
- "Plan: 2 to add, 0 to change, 0 to destroy" is the summary. Read this line first.
3 · terraform apply — execute the plan; providers make the real API calls
terraform apply
Apply builds the dependency graph, then walks it. Independent resources run in parallel;
dependent ones wait. Watch each resource go Creating... then Creation complete:
random_pet.name: Creating...
random_pet.name: Creation complete after 0s [id=close-glider]
local_file.note: Creating...
local_file.note: Creation complete after 0s [id=fe3768923dd35f1ef683189ae035f90a2034dac6]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
What each part does:
- The provider translates each declared resource into a real API call and creates it.
- [id=close-glider] is the real resource id assigned at creation — that's the pet name the
random provider generated. Values that were "known after apply" now have concrete values.
- "Apply complete! Resources: 2 added, 0 changed, 0 destroyed" is the outcome summary.
4 · state — Terraform records everything it manages; state is the source of truth
terraform state list
local_file.note
random_pet.name
terraform output
filename = "./note.txt"
pet = "close-glider"
What each part does:
- Every managed resource is written to the state file by its address (local_file.note,
random_pet.name). State is how Terraform knows what already exists.
- The next plan reads state (not a fresh discovery from scratch) to compute the diff, so it
already knows these two resources are there and shows "No changes" until config drifts.
Providers as plugins
hashicorp/local v2.9.0 manages files on disk (local_file.note -> ./note.txt)
hashicorp/random v3.9.0 generates values (random_pet.name -> close-glider)
Providers are downloaded by init, pinned to a version, and do all the real work at apply.
The dependency graph
Terraform doesn't run resources top-to-bottom in file order. It builds a graph from the
references between resources, creates independent ones in parallel, and orders dependent
ones automatically. Here random_pet.name and local_file.note are independent, so both are
created without you sequencing them.
Notes
- plan is safe: it never mutates real infrastructure. Only apply does.
- apply re-runs plan and asks for confirmation before making changes (unless -auto-approve).
- Resource address = <type>.<name> (random_pet.name). That address is stable across init,
plan, apply, and state — it's how every command refers to the resource.
Gotchas
- Don't hand-edit the file Terraform manages. Editing note.txt out-of-band is drift: the
next plan sees the file no longer matches state and plans to restore it to the configured
content. Change the config, not the managed resource.
- Never edit terraform.tfstate by hand — it's Terraform's source of truth. Use terraform
state commands (or import) instead.
- (known after apply) in a plan is normal for computed values (ids, hashes); it is not an
error and doesn't mean something is missing.
- Re-running init after adding a provider or changing versions is required — a missing or
stale plugin surfaces at init, not apply.
Verify
terraform plan
No changes. Your infrastructure matches the configuration.
A clean plan right after apply confirms real world, config, and state all agree.
Takeaway
Declare in HCL -> init to fetch providers -> plan to preview the diff -> apply to make it
real -> state to track it. Declare, plan, apply — that's infrastructure as code.