The Terraform Core Workflow

By Vishvesh Patel · DevOps EngineerUpdated September 13, 2026

The init, plan, apply, destroy loop. What each command does, how to read a plan, and the mistakes that cause outages.

Every Terraform project, from a single S3 bucket to a multi-account platform, runs the same four-command loop. Learning what each command actually does, rather than treating them as magic words, is what makes the rest of Terraform predictable.

The four commands

CommandWhat it doesChanges infrastructure
terraform initDownloads providers and modules, configures the backendNo
terraform planCompares config, state and reality; prints the diffNo
terraform applyExecutes the changesYes
terraform destroyRemoves everything in stateYes

The loop is write → init → plan → apply, repeated for the life of the project. destroy sits outside it and is used deliberately.

terraform init      # once per project, and after provider/backend changes
terraform plan      # every time, before applying
terraform apply     # after reading the plan
terraform destroy   # when tearing an environment down

What actually happens at each step

init

Downloads provider plugins into .terraform/, fetches external modules, configures where state lives, and writes .terraform.lock.hcl pinning exact provider versions.

Nothing else works until it has run, without providers loaded, Terraform does not know what an aws_instance is. Re-run it after adding a provider, changing a version constraint, adding a module, or changing the backend. Terraform tells you when it is needed.

Commit .terraform.lock.hcl; gitignore .terraform/.

plan

This is the step that distinguishes Terraform from clicking in a console. It refreshes state against the real world, compares that to your configuration, and prints exactly what it would do.

Terraform will perform the following actions:

  # aws_instance.web will be updated in-place
  ~ resource "aws_instance" "web" {
        id            = "i-0abc123"
      ~ instance_type = "t3.small" -> "t3.medium"
    }

  # aws_db_instance.main must be replaced
-/+ resource "aws_db_instance" "main" {
      ~ engine_version = "15.4" -> "16.2" # forces replacement
    }

Plan: 1 to add, 1 to change, 1 to destroy.

Four symbols, and one of them matters far more than the others:

SymbolMeaning
+Create
~Update in place (safe)
-Destroy
-/+Replace (destroy then create)

-/+ is the one to read every time. It means the attribute you changed cannot be modified in place, so Terraform will delete the resource and build a new one. On a stateless resource that is fine. On a database, a volume, or anything holding data, it is data loss arriving through what looked like a routine change. Terraform annotates the responsible attribute with # forces replacement, so the cause is always stated.

The summary line, 1 to add, 1 to change, 1 to destroy, is the fastest sanity check that you are pointed at the environment you think you are.

apply

Builds a dependency graph, executes changes in order, runs independent resources in parallel, and writes state as it goes.

If it fails partway, Terraform does not roll back. Resources already created are kept and recorded, because destroying successfully created infrastructure is usually worse. Fix the cause and apply again. Terraform reconciles from where it is.

destroy

Removes every resource in state for that configuration and workspace. Preview it first with terraform plan -destroy, and protect anything important with lifecycle { prevent_destroy = true }.

Two commands that belong in the loop

Neither touches infrastructure, and both belong in CI:

terraform fmt -recursive    # canonical formatting
terraform validate          # syntax and internal consistency

fmt ends formatting debates by having one right answer. validate catches typos, wrong argument names and type errors without contacting a provider, so it fails in seconds rather than at plan time.

Run fmt -check in CI so unformatted code cannot merge.

How the workflow changes in a team

Working alone, the loop above is enough. The moment a second person or a pipeline is involved, three things must change.

State moves to a remote backend with locking. Local state cannot be shared, has no version history, and two simultaneous applies will corrupt it. See Remote Backends.

Plan and apply become separate, reviewed steps. The pipeline plans on a pull request and posts the output for review, then applies on merge. Critically, it applies a saved plan file:

terraform plan -out=tfplan -input=false
# reviewed and approved
terraform apply tfplan

Without -out, the apply recalculates and may not match what was reviewed. With it, the approved change is the applied change.

Environments get separate state. Production and staging in one state file means one mistake reaches both.

The mistakes that cause real problems

Applying without reading the plan. Particularly missing a -/+ on something stateful.

Editing infrastructure in the console. Terraform will plan to undo it on the next run. If a change must be made by hand, bring it back into configuration promptly or Terraform becomes untrustworthy, and a tool people distrust gets bypassed, which compounds.

Committing state or .terraform/. State holds secrets in plain text; .terraform/ holds large binaries.

Using -target routinely. It is a recovery tool. Habitual use means part of your configuration is never validated against reality.

Not pinning provider versions. An unpinned provider can change behaviour between runs. Use required_providers with constraints and commit the lock file.

Explore each command

CommandWhat you will learn
terraform initProvider downloads, backend setup, the lock file
terraform planReading plans, saved plan files, drift, targeting
terraform applyParallelism, auto-approve, handling failures
terraform destroySafe teardown, partial destroys, fmt and validate

Quick reference

TaskCommand
Initialiseterraform init
Upgrade providersterraform init -upgrade
Preview changesterraform plan
Save a planterraform plan -out=tfplan
Apply a saved planterraform apply tfplan
Preview a teardownterraform plan -destroy
Formatterraform fmt -recursive
Validateterraform validate
Read outputsterraform output
Evaluate an expressionterraform console

Final Thoughts

The loop is four commands, but the discipline is one habit: read the plan before every apply. Almost every Terraform incident is a plan somebody scrolled past: a -/+ on a database, a destroy count larger than expected, or an apply against the wrong workspace. The plan tells you all three before anything happens.


Frequently Asked Questions

What is the correct order of Terraform commands?

terraform init first, once per project and again whenever providers, modules or the backend change. Then terraform plan to see what would happen, and terraform apply to execute it. terraform destroy tears everything down when an environment is no longer needed. In practice you also run terraform fmt and terraform validate before planning, and both belong in CI so unformatted or invalid configuration cannot merge.

Do I need to run terraform plan before apply?

terraform apply produces a plan and asks for confirmation, so technically no. You should anyway, and in a team you must. The plan is the only place Terraform tells you that a change will replace a resource rather than update it, which on a database means data loss. In CI, run plan -out=tfplan and apply that file, which guarantees the change reviewed is the change applied.

What does the -/+ symbol mean in a Terraform plan?

Replace: Terraform will destroy the existing resource and create a new one, because the attribute you changed cannot be modified in place. The plan annotates the responsible attribute with # forces replacement. This is the single most important thing to look for, since on stateless resources it is harmless and on anything storing data it is destructive. Protect critical resources with lifecycle { prevent_destroy = true } so a replacement cannot be applied by accident.

What happens if terraform apply fails halfway through?

Terraform stops and keeps whatever it successfully created, recording it in state. There is no rollback, because destroying working infrastructure is usually worse than leaving it. Fix the underlying problem and run apply again; Terraform reconciles from the current state and creates only what is still missing. If the process was killed rather than erroring, a state lock may remain and need clearing with terraform force-unlock.

How often do I need to run terraform init?

Once when you start, and again after anything it configures changes. Adding or upgrading a provider, adding or changing a module source, or modifying the backend. You also need it after cloning a repository, since .terraform/ is not committed. Terraform detects when initialisation is stale and refuses to proceed with a clear message, so you rarely have to remember.

What is the difference between terraform fmt and terraform validate?

fmt rewrites files into canonical formatting, indentation and alignment, and changes nothing about behaviour. validate checks that the configuration is syntactically valid and internally consistent: correct argument names, sensible types, references that resolve. Neither contacts a provider or touches infrastructure, both run in under a second, and both belong in CI, with fmt -check failing the build on unformatted code.

Continue Learning

Explore Related Topics

Try the Tool

Practise this in the browser

Terraform Playground

Run init, plan, apply and destroy against simulated infrastructure.

When it breaks

Related Resources