Terraform has exactly one workflow, and almost everything else on the HashiCorp Terraform Associate exam hangs off it. Providers, state, variables, modules — they all exist to serve a single repeating loop: write configuration, init the directory, plan the change, apply it, and eventually destroy it. Candidates who lose points on this objective usually know the commands but not what each one does to your infrastructure and your state file. That gap is exactly what the scenario questions probe.
This guide walks the core workflow end to end — not as a list of commands, but as a lifecycle. We’ll follow one small configuration from an empty directory to running infrastructure and back to nothing, explaining what Terraform is actually doing at each stage and calling out the exam cues along the way. If you want the quick-reference version of every flag, keep the Terraform commands cheat sheet open in another tab; this article is the why behind it. And if you’re still mapping out your prep, start with the Terraform Associate exam guide for 2026 and the 30-day study plan.
The Workflow at a Glance
HashiCorp describes the core workflow as three steps — Write, Plan, Apply — but in practice you interact with four CLI commands plus destroy:
terraform init # prepare the working directory
terraform plan # preview the changes Terraform would make
terraform apply # create/update/delete real infrastructure
terraform destroy # tear down everything this configuration manages
Here’s the mental model to lock in for the exam:
| Stage | Command | Touches real infra? | Touches state? | Reads config? |
|---|---|---|---|---|
| Write | (your editor) | No | No | — |
| Initialize | terraform init | No | No (reads backend config) | Yes |
| Plan | terraform plan | No (read-only refresh) | Reads, doesn’t write | Yes |
| Apply | terraform apply | Yes | Writes | Yes |
| Destroy | terraform destroy | Yes | Writes | Yes |
The single most tested distinction on this whole topic: plan never changes anything, apply does. plan is a dry run. If a question asks which command is safe to run in a code-review pipeline to show reviewers what would happen, the answer is plan.
Stage 0: Write
The workflow begins with HashiCorp Configuration Language (HCL) in .tf files. Terraform reads every .tf file in the working directory and merges them — file names are for humans, not for Terraform, so main.tf, variables.tf, and outputs.tf is a convention, not a requirement.
Here’s a minimal but complete configuration we’ll use for the whole walkthrough. It provisions a random pet name and writes it to a local file — no cloud account required, so you can run every command below yourself:
# main.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.5"
}
local = {
source = "hashicorp/local"
version = "~> 2.4"
}
}
}
resource "random_pet" "server" {
length = 2
}
resource "local_file" "greeting" {
filename = "${path.module}/server-name.txt"
content = "Hello from ${random_pet.server.id}\n"
}
Notice that local_file references random_pet.server.id. That reference is not just string interpolation — it’s how Terraform learns that the file depends on the pet name. Hold that thought; it becomes important at plan time.
If you want to go deeper on the building blocks used here, the providers and provider block guide covers required_providers, and variables, outputs, and locals covers how to parameterize a config like this.
Stage 1: terraform init — Prepare the Directory
terraform init is the one command you must run before any other, in every new or changed working directory. It is safe, idempotent, and never touches your infrastructure. Run it as many times as you like.
During init, Terraform:
- Reads
required_providersand downloads the matching provider plugins from the registry into a hidden.terraform/directory. - Writes or reads the dependency lock file (
.terraform.lock.hcl), pinning exact provider versions and checksums so every machine gets identical plugins. - Initializes the backend — the place your state file lives. With no
backendblock, that’s the defaultlocalbackend (aterraform.tfstatefile on disk). - Installs modules referenced by any
moduleblocks, downloading remote ones into.terraform/modules/.
Running it on our config looks like this:
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/random versions matching "~> 3.5"...
- Finding hashicorp/local versions matching "~> 2.4"...
- Installing hashicorp/random v3.6.0...
- Installing hashicorp/local v2.4.1...
Terraform has been successfully initialized!
Exam cues around init:
- You must re-run
initafter adding a new provider, adding a module, or changing the backend. Terraform will refuse toplanorapplyand tell you to runinitfirst. terraform init -upgradere-evaluates version constraints and pulls the newest allowed provider versions, rewriting the lock file. Plaininitrespects the already-locked versions.initdoes not read variable values, contact your cloud provider, or create resources. If a question implies init provisions something, it’s wrong.
Stage 2: terraform plan — Preview Before You Touch Anything
terraform plan is where Terraform figures out the difference between three things:
- Your configuration (desired state, from the
.tffiles), - The state file (Terraform’s record of what it last created), and
- Reality (the actual resources, discovered via a refresh).
By default, plan first performs a refresh: it queries the provider APIs for every resource already in state to detect drift (changes made outside Terraform). It then compares desired config against the refreshed state and produces an execution plan — an ordered list of the create, update, and destroy actions needed to make reality match your config.
On a fresh directory with empty state, everything is new:
$ terraform plan
Terraform will perform the following actions:
# local_file.greeting will be created
+ resource "local_file" "greeting" {
+ content = (known after apply)
+ filename = "./server-name.txt"
+ id = (known after apply)
}
# random_pet.server will be created
+ resource "random_pet" "server" {
+ id = (known after apply)
+ length = 2
}
Plan: 2 to add, 0 to change, 0 to destroy.
Read the plan carefully — this is a skill the exam rewards:
+means create,-means destroy,~means update in place, and-/+means destroy and recreate (a “replacement”, forced when an immutable attribute changes).(known after apply)marks values Terraform can’t compute yet — like the pet’s generatedid, which won’t exist until the resource is actually created.- The summary line —
Plan: 2 to add, 0 to change, 0 to destroy— is the number to scan first in real work and in exam screenshots.
The Dependency Graph
Here’s the part many candidates miss. Terraform doesn’t apply resources top-to-bottom in file order. It builds a dependency graph from the references between resources and applies them in the correct order, parallelizing independent resources (up to 10 concurrent operations by default, tunable with -parallelism=n).
In our config, local_file.greeting references random_pet.server.id, so Terraform knows the pet must be created first. That relationship is an implicit dependency — it comes for free from the reference. When two resources have no natural reference but must still be ordered, you declare an explicit dependency with the depends_on meta-argument. Understanding that the graph — not file order — governs execution is a classic exam concept.
Saved Plan Files
plan and apply can be decoupled. You can save a plan to a file and apply exactly that plan later:
terraform plan -out=tfplan # save the plan
terraform apply tfplan # apply the saved plan, no re-prompt
This matters for two reasons the exam cares about:
- Consistency in automation. In a CI pipeline, you generate the plan in one step, have a human approve it, and apply that reviewed plan — guaranteeing what runs is exactly what was reviewed, even if
mainchanged in between. - No second confirmation. Applying a saved plan file skips the interactive “yes” prompt, because the decision was already captured when the plan was saved.
A saved plan file also contains the specific values Terraform intends to set, which is why you should treat it as sensitive — it can include secrets.
Stage 3: terraform apply — Make It Real
terraform apply executes the plan, calling provider APIs to create, update, or delete resources, and then writes the results to the state file. This is the first command in the workflow that changes real infrastructure.
By default, apply runs its own plan first and shows it to you, then pauses for confirmation:
$ terraform apply
# ... same plan output as above ...
Plan: 2 to add, 0 to change, 0 to destroy.
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
random_pet.server: Creating...
random_pet.server: Creation complete after 0s [id=harmless-panda]
local_file.greeting: Creating...
local_file.greeting: Creation complete after 0s [id=a1b2c3...]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Key behaviors to know cold:
applyregenerates the plan by default. Because infrastructure or state may have changed since your lastplan,applycomputes a fresh plan and asks you to confirm it. It is not blindly replaying an old plan — unless you pass a saved plan file.-auto-approveskips the interactive prompt. It’s convenient in automation but dangerous interactively — it’s the option to be suspicious of when a question is about preventing accidental changes.- State is written on success. After each resource is created, its real attributes (IDs, computed values) are recorded in state. If
applyfails partway, the resources it did create are still saved to state, so a re-run continues from where it stopped rather than duplicating them. - Applying twice with no config change is a no-op. Run
applyagain immediately and Terraform refreshes, sees reality already matches your config, and reports0 to add, 0 to change, 0 to destroy. This idempotency — describe the end state, not the steps — is the core promise of declarative IaC.
Changing Something
Now edit the config — bump the pet name to three words:
resource "random_pet" "server" {
length = 3 # was 2
}
Re-plan and you’ll see a replacement, because length can’t be changed on an existing random_pet in place:
$ terraform plan
# random_pet.server must be replaced
-/+ resource "random_pet" "server" {
~ id = "harmless-panda" -> (known after apply) # forces replacement
~ length = 2 -> 3
}
Plan: 1 to add, 0 to change, 1 to destroy.
The -/+ and the # forces replacement comment tell you Terraform will destroy the old pet and create a new one — and because local_file.greeting depends on the pet’s id, it will be updated too. This cascade is the dependency graph in action.
Stage 4: terraform destroy — Tear It Down
terraform destroy deletes every resource in the current state for this configuration. It’s really a specialized apply that plans every managed resource for deletion, in reverse dependency order:
$ terraform destroy
# local_file.greeting will be destroyed
# random_pet.server will be destroyed
Plan: 0 to add, 0 to change, 2 to destroy.
Enter a value: yes
local_file.greeting: Destroying... [id=a1b2c3...]
local_file.greeting: Destruction complete after 0s
random_pet.server: Destroying... [id=harmless-panda]
random_pet.server: Destruction complete after 0s
Destroy complete! Resources: 2 destroyed.
Notice the order: the file is destroyed before the pet it depends on — destroy walks the graph in reverse. Things to remember:
destroyalso prompts for confirmation unless you pass-auto-approve.- To remove a single resource, prefer removing it from your config and running
apply(Terraform plans its deletion), rather than reaching for the deprecated-target. Surgical, whole-stack teardown is whatdestroyis for. - After a successful destroy, the state file still exists but records zero managed resources.
Targeting and Refresh: the Escape Hatches
The exam expects you to know two workflow modifiers exist, even if you rarely use them:
-target=resource.addresslimits a plan or apply to a specific resource and its dependencies. It’s an escape hatch for recovering from mistakes — not a routine workflow tool. Overusing it produces state that doesn’t match your config.-refresh-only(a plan/apply mode) reconciles state with reality without changing any infrastructure. Use it to absorb out-of-band drift into state. This replaced the old standaloneterraform refreshcommand, which is deprecated.
You can also skip the automatic refresh during a normal plan with -refresh=false to speed things up when you’re confident nothing drifted — useful on large states, but it risks planning against a stale picture.
How the Loop Maps to a Real Team
In a real project — and increasingly in exam scenarios — the workflow isn’t one person typing commands. It becomes a collaboration loop:
- A developer writes a change on a feature branch.
- CI runs
terraform initandterraform plan, posting the plan to the pull request so reviewers see exactly what will change. - After approval and merge, an automation step runs
terraform applyagainst a remote backend with state locking, so two applies can’t corrupt state at once.
That remote backend and locking behavior is where the core workflow meets state management. If you haven’t yet, pair this article with the Terraform state management guide — the workflow and state are two halves of the same concept, and the exam frequently tests them together. For refactors inside the loop (renaming or moving resources without destroying them), the count vs. for_each guide and modules guide fill in the moving parts.
Common Workflow Traps on the Exam
| Scenario | The trap | The right call |
|---|---|---|
| Show reviewers a change safely | Running apply | Run plan (or plan -out) — it never modifies infra |
| Added a new provider or module | Jumping straight to plan | Re-run terraform init first |
| Apply what was reviewed, unchanged | Re-running apply from config | plan -out=tfplan then apply tfplan |
| Skipping the confirmation prompt | Assuming apply always prompts | -auto-approve (or a saved plan file) skips it |
| Execution order | Assuming top-to-bottom file order | Terraform follows the dependency graph |
| Absorb drift, change nothing | Running a normal apply | Use -refresh-only mode |
| Remove one resource | Reaching for -target routinely | Delete it from config and apply |
-/+ in a plan | Reading it as an update | It’s destroy and recreate (replacement) |
Practice the Workflow Until It’s Muscle Memory
The core workflow rewards doing over reading. Spin up the tiny random_pet config above, run init, plan, apply, change length, watch the replacement, then destroy — and you’ll internalize the state transitions far faster than by memorizing definitions. Then reinforce the pattern recognition (which command is safe, what -/+ means, when init is required) with realistic questions.
Sailor.sh’s Terraform Associate mock exams include workflow-focused items covering plan reading, saved plans, -auto-approve, refresh modes, and dependency ordering — each with an explanation of why the answer is correct so you learn the reasoning, not just the key. Gauge where you stand first with the free Terraform Associate practice questions, then drill the objectives you’re shakiest on. Treating the workflow as something you rehearse rather than recite is what turns it into the easiest, most reliable points on the exam.
Frequently Asked Questions
What is the core Terraform workflow?
It’s the standard loop of Write → Init → Plan → Apply, plus destroy to tear down. You write HCL configuration, run terraform init to prepare the directory and install providers, terraform plan to preview the changes, and terraform apply to create or modify the real infrastructure and record it in state. terraform destroy deletes everything the configuration manages.
Does terraform plan change my infrastructure?
No. plan is read-only. It refreshes state against reality and computes an execution plan, but it never creates, updates, or deletes resources and never writes to the state file. That’s why it’s safe to run in a code-review pipeline. Only apply and destroy change real infrastructure.
What’s the difference between terraform apply and applying a saved plan file?
Plain terraform apply generates a fresh plan, shows it, and asks you to confirm — because state or infrastructure may have changed since your last plan. Applying a saved plan file (terraform apply tfplan, created with plan -out=tfplan) runs exactly the captured plan with no re-prompt. Saved plans are how CI pipelines guarantee that what gets applied is precisely what was reviewed.
When do I need to run terraform init again?
Any time you add or change a provider, add or update a module, or change the backend configuration. Terraform will refuse to plan or apply and prompt you to re-run init. Running init repeatedly is safe and idempotent — it never touches infrastructure.
What does -/+ mean in a plan?
It means destroy and recreate — a replacement. Terraform shows -/+ when a change forces the resource to be replaced (usually because an immutable attribute changed), and it annotates the offending attribute with # forces replacement. Contrast that with ~, which is an in-place update, + for create, and - for destroy.
In what order does Terraform create resources?
By the dependency graph, not file order. Terraform infers implicit dependencies from references between resources (like one resource using another’s id) and applies them in the correct order, running independent resources in parallel. Use the depends_on meta-argument to declare an explicit dependency when there’s no natural reference. On destroy, it walks the graph in reverse.
How do I skip the “yes” confirmation on apply?
Pass -auto-approve, or apply a saved plan file. Both bypass the interactive prompt. -auto-approve is common in automation but risky when run by hand, since it removes your last chance to catch an unintended change.
Ready to make the workflow a strength? Drill realistic, explained questions with the Sailor.sh Terraform Associate mock exams, then round out the objective with the Terraform commands cheat sheet and the state management guide.