Your First apply: From Five Lines of HCL to a Real Object
The fastest honest first deployment uses the local provider — it needs no cloud account, no credentials, and creates files on the machine running Terraform, which makes the mechanics impossible to miss. Write this in a new directory as main.tf:
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
resource "local_file" "hello" {
filename = "${path.module}/hello.txt"
content = "Deployed by Terraform at ${timestamp()}\n"
}
Then run the full cycle:
terraform init # downloads the local provider, writes .terraform.lock.hcl
terraform plan # Plan: 1 to add, 0 to change, 0 to destroy.
terraform apply # confirm with "yes"
Walk through what happens, because every apply in your career is a bigger version of this. The resource block declares an object of type local_file with the local name hello; together they form the address local_file.hello, the permanent identifier Terraform uses in state. content and filename are arguments the provider schema accepts; anything else is rejected at validate time with "Unsupported argument". During apply, the provider's create operation runs, hello.txt appears on disk, and Terraform records the object's attributes in state so the next plan can compare them against the configuration.
The apply output is a narration of exactly that, and it is worth reading rather than skipping:
local_file.hello: Creating...
local_file.hello: Creation complete after 0s [id=7c2a2c1c94a0ba67...]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
The id is the provider's unique identifier for the object — for this provider a content hash, for cloud resources usually a generated ID like i-0abc123def456. Remember that ID: it is what references and debugging all flow through.
Now change one thing — edit the content string — and re-run terraform plan. You get "1 to change" with a ~ marker and a diff of the content. Apply it, and the file updates in place. This loop — edit, plan, read the diff, apply — is the daily rhythm of infrastructure work, and it only works because the state file remembers what the previous apply created. For a version-controlled example that provisions real cloud objects, the same shape applies to, say, an aws_s3_bucket with a bucket argument: declaration, init, plan, apply, provider-created object, recorded state.
One discipline to form immediately: never edit objects Terraform manages by hand, including these practice files. Every hand edit becomes drift that the next plan flags and offers to revert, and you have just learned why declarative tools re-assert the declared state.
The first apply also creates your state file, and it is worth opening once just to see the shape of the model — you will spend the rest of your career trusting it:
terraform state list # local_file.hello
terraform show # renders the recorded attributes of local_file.hello
Inside, the record maps the address local_file.hello to the provider's id and every attribute the provider reported after creation. From now on, plans compare configuration against this record after refreshing it from reality.
One gotcha in the practice configuration is deliberately planted: timestamp() returns the current time on every evaluation, so its value can never match what was recorded before. Each plan will propose updating the content even though nothing you care about changed — an excellent, harmless demonstration that any non-deterministic value in configuration produces a perpetual diff. Real-world offenders include generated passwords and uuid() calls; the fix in practice is to write such values to state exactly once (a separate mechanism covered later) or remove them from configuration. Now you will meet the human-facing side of state: outputs and inspection.
Outputs and terraform show: Seeing What You Built Without Opening State
After the first apply you inevitably need a value — the public IP of the instance, the bucket domain name, the database endpoint — and state is where it lives. State files, though, are large, JSON-heavy, contain every sensitive value in cleartext, and are meant to be machine-managed. Outputs are the human-facing interface for exactly this: values you publish deliberately.
output "hello_file_path" {
description = "Absolute path of the file managed by local_file.hello"
value = local_file.hello.filename
}
output "content_hash" {
description = "Provider-generated identifier of the managed file"
value = local_file.hello.id
sensitive = false
}
Outputs print after every apply, so the values you need appear right where you are working:
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
Outputs:
content_hash = "7c2a2c1c94a0ba67..."
hello_file_path = "./hello.txt"
They are also addressable from the CLI — the form scripts and CI actually use:
terraform output # all outputs, human-readable
terraform output -raw hello_file_path # single value, no quotes — safe for shell use
terraform output -json # everything machine-readable
-raw matters in pipelines: without it, string outputs are quoted and escaped, so cat $(terraform output -raw hello_file_path) behaves while $(terraform output hello_file_path) passes stray quotes into your command.
For anything deeper than the values you published, terraform show reads state and renders it for humans — and it takes an address to focus on one object:
terraform state list # every managed address: local_file.hello, ...
terraform state show local_file.hello # full attribute record as Terraform stores it
terraform show # everything, rendered
terraform show -json # complete JSON state — for tooling, not eyeballs
The distinction worth memorizing: terraform show reads state, the recorded model of reality; it does not re-query the provider. When you need the live answer to "what does the object look like right now", that is what a plan's refresh step does. Confusing the two produces the classic "the state says 5 subnets but the console shows 6" panic, which a fresh plan resolves in seconds.
Two outputs habits pay off from day one: give every output a description, because that is what teammates and future-you read in terraform output -json documentation; and mark anything secret sensitive = true, which keeps it out of CLI listings — though the value still sits in state, so protection belongs on the state backend, not on the display.
One structural detail becomes important the moment you split a directory into modules: outputs declared in a child module are not printed after an apply and are not reachable with terraform output. They exist only as inputs to whatever module called them, addressable inside the root as module.network.private_subnets-style references. Values become visible to operators only when the root module re-declares them as outputs. That rule — child outputs flow up, root outputs flow out — is why mature configurations carry a tidy block of root-level outputs mirroring the values their consumers need, each with a description, forming a documented interface between the infrastructure and the humans and pipelines that use it.
terraform destroy: Tearing Down Cleanly and What That Really Means
The counterpart to apply is destroy, and running it once on purpose is the best education in the model you have been building:
terraform destroy # interactive: plan of destruction + confirmation
# or preview only:
terraform plan -destroy
The output is a plan like any other, only inverted — every managed object carries a - marker:
# local_file.hello will be destroyed
- resource "local_file" "hello" {
- filename = "./hello.txt" -> null
}
Plan: 0 to add, 0 to change, 1 to destroy.
Do you really want to destroy all resources?
The question is deliberately blunt because destroy acts on everything the working directory manages — there is no "destroy just this one" unless you target, and targeting brings its own traps. Confirm, and the provider's delete operations run, the file disappears, and state drops to empty. A second destroy reports "No objects need to be destroyed."
Two facts about destroy are interview gold. First, destroy removes objects from state and calls the provider's delete API; it does not delete the configuration or your state history setup. After destroying, the same .tf files can apply the whole environment again — which is precisely why destroying a practice stack is safe and rebuilding a disposable environment is a Tuesday-afternoon operation, not an expedition. Second, destroy runs in reverse dependency order: things that were created last (which depended on earlier things) are deleted first, so the subnet goes before the VPC it lived in. The graph Terraform built for creation is traversed backwards for teardown.
Destroy also fails in informative ways. With prevent_destroy set on a resource, destroy aborts with an error demanding you remove the guard deliberately first — a pattern you will use on production databases in The lifecycle Meta-argument. And if an object was already deleted out-of-band, destroy still removes its state entry, so the operation completes and the record is cleaned up.
Destroy is also not the same operation as detaching. terraform state rm local_file.hello removes the object from state without deleting it: the file stays on disk, but the next plan no longer knows it exists, so configuration and reality diverge deliberately. That is the move for "this object now belongs to another team's state", or for releasing an individually named resource so a fresh one can take its place — and its inverse is terraform import, covered later. The distinction to hold onto: destroy changes the world; state commands change Terraform's memory of the world. Confusing them produces either accidental deletions or ghost resources, and interviewers use both as discriminator questions.
For real environments, prefer managed demolition over archaeology: destroy only stacks that were built from a known directory, never point a fresh terraform destroy at a directory whose state you did not just inspect — the command destroys what the state says exists, and a wrong or stale state means deleting the wrong things. Check terraform state list first, every time. Teams routinely use exactly this property in reverse to run disposable preview environments: one pipeline applies a throwaway stack per pull request, another destroys it when the review closes, and cost stays proportional to open reviews.
With the complete lifecycle under your belt — declare, init, plan, apply, inspect, destroy — you can now step back and choose tools deliberately: Terraform Versus Other IaC Tools compares it with the alternatives you will meet in interviews.
Quiz
1. You apply a configuration creating a local_file, then delete hello.txt manually and run terraform plan. What happens and why?
2. After an apply, a value you need (an instance public IP) is only inside terraform.tfstate. What is the idiomatic way to make it available to humans and scripts?
3. What does terraform destroy actually do?
4. A teammate claims terraform show displays the live cloud state of your resources. What is the correct rebuttal?
Flashcards
Question
What are the four elements of a resource declaration, using resource "local_file" "hello" { ... } as the example?
Click to reveal answer
Answer
The block type `resource`; two labels — the provider's resource type (`local_file`) and your local name (`hello`); a body of arguments (`filename`, `content`) that must match the provider schema; and the resulting address `local_file.hello`, which is how the object is referenced in configuration and named in state.
Question
What is printed after apply, and what do terraform output -raw and -json add?
Click to reveal answer
Answer
Declared outputs print with the apply summary, so published values (addresses, IDs) appear without opening state. `terraform output -raw <name>` returns a single unquoted value for scripts; `-json` returns all outputs machine-readable, including which are sensitive.
Question
State three commands for inspecting what exists and what each reads.
Click to reveal answer
Answer
`terraform state list` — every managed address from state; `terraform state show <addr>` — one object's full recorded attributes; `terraform show [-json]` — the whole state rendered for humans or tooling. All three read state only; none re-queries providers.
Question
In what order does destroy operate, and what does it change in state?
Click to reveal answer
Answer
Reverse dependency order: dependents are deleted before their dependencies, so subnets go before the VPC they reference. Each successful delete removes the object's entry from state; afterwards the directory reports "No objects need to be destroyed" and can apply the same environment again.
Question
A resource has lifecycle { prevent_destroy = true } and you run terraform destroy. What happens?
Click to reveal answer
Answer
The destroy aborts before deleting anything, with an error naming the resource and the prevent_destroy setting. You must explicitly remove or flip the setting in configuration and apply that change before destruction is allowed — a deliberate two-step guard for critical objects.
Revision Notes
Key Takeaways
- 1. A resource block's two labels (type + local name) form the address used in references and state; arguments are validated against the provider schema at plan time
- 2. Outputs are the deliberate, documented interface to computed values — print after apply, query with `terraform output -raw` in scripts, mark secrets `sensitive` (but remember state still holds them)
- 3. `terraform show` and `state` commands read the state record; only a plan refreshes from provider APIs, so "show says it exists" and "the cloud says it exists" are different claims
- 4. Destroy deletes every managed object in reverse dependency order and empties state, while leaving configuration intact — disposable environments are rebuilt, not hand-restored
- 5. Practice destroy deliberately; the confidence it builds is what makes large teardowns and migrations routine instead of terrifying
Interview Tips
- • "Show me you understand apply." — narrate the loop: plan compares config/state/reality, apply executes the reviewed diff, provider create runs, state records the object under its address and provider ID, outputs publish the values you declared
- • "How do scripts get the endpoint of a resource you just created?" — a declared output read via `terraform output -raw <name>`; mention `-json` for multiple values and warn that outputs marked sensitive are still present in state
- • "terraform state and the cloud console disagree — what do you do?" — run a plan (it refreshes from the provider APIs); the diff shows reality-vs-config, then decide whether to update config or investigate the out-of-band change; do not hand-edit state
- • "How do you safely tear down an environment?" — `terraform state list` to confirm scope, `terraform plan -destroy` to review, then destroy; explain reverse-order deletion and that prevent_destroy resources abort the run by design
Cheat Sheet
First deployment lifecycle
Minimal working configuration:
terraform {
required_providers {
local = { source = "hashicorp/local", version = "~> 2.5" }
}
}
resource "local_file" "hello" {
filename = "${path.module}/hello.txt"
content = "hello\n"
}
output "hello_file_path" {
description = "Path of the managed file"
value = local_file.hello.filename
}
The loop: terraform init → terraform plan → read the diff → terraform apply → outputs print. Edit, then repeat; never hand-edit managed objects.
Inspecting:
| Command | Reads | Use |
|---|---|---|
terraform output [-raw|-json] |
outputs | published values for humans/scripts |
terraform state list |
state | every managed address |
terraform state show <addr> |
state | one object's recorded attributes |
terraform show [-json] |
state | everything rendered / for tooling |
Destroy: terraform plan -destroy to preview; terraform destroy to execute everything the directory manages, in reverse dependency order. State entries are removed; the configuration stays and can re-apply. Output markers: + add · ~ update · - destroy.