Back to Blog

The Terraform Core Workflow Explained: init, plan, apply & destroy for the Terraform Associate Exam

A hands-on walkthrough of the core Terraform workflow — write, init, plan, apply, destroy — for the HashiCorp Terraform Associate exam. Learn what actually happens at each stage, the dependency graph, saved plan files, refresh, targeting, and how the loop maps to real team CI, with a full worked example.

By Sailor Team , July 14, 2026

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:

StageCommandTouches real infra?Touches state?Reads config?
Write(your editor)NoNo
Initializeterraform initNoNo (reads backend config)Yes
Planterraform planNo (read-only refresh)Reads, doesn’t writeYes
Applyterraform applyYesWritesYes
Destroyterraform destroyYesWritesYes

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:

  1. Reads required_providers and downloads the matching provider plugins from the registry into a hidden .terraform/ directory.
  2. Writes or reads the dependency lock file (.terraform.lock.hcl), pinning exact provider versions and checksums so every machine gets identical plugins.
  3. Initializes the backend — the place your state file lives. With no backend block, that’s the default local backend (a terraform.tfstate file on disk).
  4. Installs modules referenced by any module blocks, 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 init after adding a new provider, adding a module, or changing the backend. Terraform will refuse to plan or apply and tell you to run init first.
  • terraform init -upgrade re-evaluates version constraints and pulls the newest allowed provider versions, rewriting the lock file. Plain init respects the already-locked versions.
  • init does 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 .tf files),
  • 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 generated id, 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:

  1. 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 main changed in between.
  2. 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:

  • apply regenerates the plan by default. Because infrastructure or state may have changed since your last plan, apply computes 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-approve skips 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 apply fails 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 apply again immediately and Terraform refreshes, sees reality already matches your config, and reports 0 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:

  • destroy also 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 what destroy is 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.address limits 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 standalone terraform refresh command, 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:

  1. A developer writes a change on a feature branch.
  2. CI runs terraform init and terraform plan, posting the plan to the pull request so reviewers see exactly what will change.
  3. After approval and merge, an automation step runs terraform apply against 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

ScenarioThe trapThe right call
Show reviewers a change safelyRunning applyRun plan (or plan -out) — it never modifies infra
Added a new provider or moduleJumping straight to planRe-run terraform init first
Apply what was reviewed, unchangedRe-running apply from configplan -out=tfplan then apply tfplan
Skipping the confirmation promptAssuming apply always prompts-auto-approve (or a saved plan file) skips it
Execution orderAssuming top-to-bottom file orderTerraform follows the dependency graph
Absorb drift, change nothingRunning a normal applyUse -refresh-only mode
Remove one resourceReaching for -target routinelyDelete it from config and apply
-/+ in a planReading it as an updateIt’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.

Limited Time Offer: Get 80% off all Mock Exam Bundles | Sale ends in 7 days. Start learning today.

Claim Now