Background

Core Workflow

29 min read

Five commands do almost everything: init, validate, plan, apply, destroy. This page is what each one reads, what it writes, which are safe to run without a human watching, and how they compose into the loop you'll spend the rest of your Terraform career inside. It ends with a real resource in a real account.

Prerequisites: Why IaC Exists


What & Why

The core workflow is the fixed sequence Terraform imposes on every change: initialise a working directory, compute a proposed change, review it, then execute it.

The bad practice it replaces

Deployment scripts that do the thing and tell you afterwards. The deploy.sh in every legacy repository is a single verb — it runs, it changes production, and the only way to find out what it was going to do was to read the source and simulate it in your head. When it half-failed, you read the log to work out where it stopped.

Terraform's contribution isn't apply. Any tool can have an apply. It's that plan is a separate, first-class, reviewable artefact, so the question "what is about to happen to production?" has an answer before it happens rather than after. Everything else on this page follows from taking that seriously.

Where it sits

These commands are the outer loop. Inside plan there's a pipeline — parse, resolve, refresh, build the graph, diff, render — that determines why the proposed change is what it is. That's The Plan/Apply Lifecycle, and it's the part that makes the difference between running Terraform and understanding it. Here we stay outside.

Three things it's confused with

plan and apply are not "test" and "deploy". A plan isn't a test run against a copy — it's a prediction about your real infrastructure, computed by talking to your real cloud account. It's read-only, but it's not a rehearsal in a safe place.

init is not a one-off setup command. It isn't git init. It runs whenever the directory's requirements change — new provider, new module, changed version constraint, changed backend — and you'll run it hundreds of times.

destroy is not "undo". It removes everything the current configuration manages, which is a different set from "the things you changed in the last apply". There is no command that reverses one apply.

When NOT to use it

The workflow itself is non-negotiable — you can't apply without a plan being computed, even if you don't look at it. What's optional is how you drive it, and two habits are worth rejecting outright:

  • terraform apply -auto-approve from a laptop against production. It skips the review that is the entire point. In an automated pipeline where a human already approved a saved plan file, -auto-approve is correct and necessary; typed at a terminal by a person who is in a hurry, it's the single most common cause of a bad day.
  • terraform apply -target=... as a normal way of working. It applies a subset of the graph, and the result is state that's consistent with no version of your configuration. It exists for recovering from a broken state, it says so in its own warning output, and a team that reaches for it weekly has a problem it's routing around rather than fixing.

Core Concepts

Working directorythe folder you run Terraform in. Terraform reads every .tf file in this one directory as a single configuration. It does not recurse into subdirectories; a subdirectory is either a module you explicitly call or invisible.

Root modulethe configuration in your working directory. The entry point. Every other module is a child, called from here. Grammar in Modules.

Initialisationmaking a directory ready to run. terraform init downloads the providers and modules the configuration declares, records exact versions in the lock file, and configures where state lives.

Planthe proposed change, as a reviewable artefact. A computed set of actions — create, update, replace, destroy, read — derived from comparing configuration, state and reality. Rendered for a human by default; saved to a file with -out.

Saved plan filea plan frozen for later execution. A binary file produced by terraform plan -out=tfplan and consumed by terraform apply tfplan. Applying a saved plan does exactly what the plan said or fails, which is why pipelines use it. Not human-readable directly — use terraform show.

Applydoing it. Executes the actions in a plan, in dependency order, writing the results to state as it goes.

Refreshasking the cloud what's really there. Reading the current attributes of every managed resource from the provider's API before computing the diff. Runs as part of plan and apply by default. Historically a standalone command, now deprecated as such — ⚠️ verify current status.

Auto-approveskipping the confirmation prompt. -auto-approve. Correct when a saved plan was already reviewed; dangerous otherwise.

Exit codewhat the command tells a script. 0 success, 1 error. With plan -detailed-exitcode, 0 means no changes and 2 means changes are pending — which is how a pipeline decides whether to bother asking for approval, and how drift detection jobs work.

Idempotencerunning it twice changes nothing the second time. Defined in Why IaC Exists; this is the property that makes apply safe to re-run after a failure.


How It Works

Each command has a precise contract about what it reads and what it touches. Memorising this table is worth more than memorising any set of flags, because it answers "is this safe to run right now?" for every situation you'll meet.

Command Reads Writes Needs cloud credentials Changes infrastructure
init .tf files, lock file .terraform/, .terraform.lock.hcl Only to reach the backend No
fmt .tf files .tf files (rewrites formatting) No No
validate .tf files, .terraform/ Nothing No No
plan .tf files, state, cloud API State (refresh results), optionally a plan file Yes No
apply .tf files or plan file, state, cloud API State, cloud infrastructure Yes Yes
destroy .tf files, state, cloud API State, cloud infrastructure Yes Yes
show State or a plan file Nothing No No
output State Nothing No No

Diagram of the Terraform core workflow loop from write through init, validate, plan, review and apply, with destroy branching off

Three things in that table surprise people.

plan writes. It's called read-only, and it is — with respect to your infrastructure. But by default it refreshes, and the refreshed attributes are persisted to state. So a plan can modify the state file without modifying a single cloud resource. This matters the first time two people run plan simultaneously against shared state and one of them gets a locking error.

validate doesn't need credentials but does need init. It checks syntax, types and internal consistency — that a referenced resource exists, that an argument is the right type. It cannot check that a value is acceptable to the provider, because it never calls the provider's API. An instance type that doesn't exist passes validate and fails at apply. Catching that class of error is what tflint is for; see Testing & Validation.

apply re-plans unless you give it a plan file. terraform apply with no arguments computes a fresh plan and shows it to you — so the plan you approved in your terminal is the plan that runs. But terraform plan followed later by terraform apply are two different plans, computed at different times against possibly different realities. When the gap matters — and in a pipeline it always does — save the plan with -out and apply that file.

What happens when it fails

Terraform applies resources in dependency order, with parallelism. A failure partway through does not roll back — there's nothing to roll back to. What you get is:

  • Resources created before the failure: created, and recorded in state.
  • The resource that failed: possibly created but not fully configured. Terraform records what it can.
  • Resources that depended on the failure: not attempted.

The state file is therefore accurate about a partially-built world, and the correct response is almost always to fix the configuration and run apply again — it will create only what's missing. That's idempotence earning its keep. The cases where this isn't enough — a resource created in the cloud but not recorded in state, a lock left behind by a killed process — are Failure & Recovery.


Getting Started

One bucket, one directory, local state, five minutes. Azure needs a resource group to contain the storage account; AWS and GCP have no equivalent, so its tab has one extra resource.

Create an empty directory and one file. Change the suffix — bucket names are globally unique.

```hcl
# main.tf
terraform {
  required_version = ">= 1.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "eu-west-1"
}

resource "aws_s3_bucket" "demo" {
  bucket = "tf-workflow-demo-0725"
}

output "bucket_name" {
  value = aws_s3_bucket.demo.bucket
}
```
```hcl
# main.tf
terraform {
  required_version = ">= 1.5"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "demo" {
  name     = "tf-workflow-demo"
  location = "westeurope"
}

resource "azurerm_storage_account" "demo" {
  name                     = "tfworkflowdemo0725" # 3–24 chars, lowercase alphanumeric
  resource_group_name      = azurerm_resource_group.demo.name
  location                 = azurerm_resource_group.demo.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

output "bucket_name" {
  value = azurerm_storage_account.demo.name
}
```
```hcl
# main.tf
terraform {
  required_version = ">= 1.5"
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 5.0"
    }
  }
}

provider "google" {
  project = "your-project-id"
  region  = "europe-west1"
}

resource "google_storage_bucket" "demo" {
  name     = "tf-workflow-demo-0725"
  location = "EUROPE-WEST1"
}

output "bucket_name" {
  value = google_storage_bucket.demo.name
}
```

1. Initialise

terraform init
# UNVERIFIED — confirm against a real run
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.x.x...
- Installed hashicorp/aws v5.x.x (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above.

Terraform has been successfully initialized!

A .terraform/ directory and a .terraform.lock.hcl file now exist. Both are dissected in Anatomy of a Project.

2. Validate

terraform fmt       # rewrites files to canonical formatting
terraform validate
# UNVERIFIED — confirm against a real run
Success! The configuration is valid.

Neither command talked to your cloud account. Both are fast enough to run on every save.

3. Plan

terraform plan
# UNVERIFIED — confirm against a real run
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_s3_bucket.demo will be created
  + resource "aws_s3_bucket" "demo" {
      + arn                         = (known after apply)
      + bucket                      = "tf-workflow-demo-0725"
      + bucket_domain_name          = (known after apply)
      + id                          = (known after apply)
      + region                      = (known after apply)
      + tags_all                    = (known after apply)
      ...
    }

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

Changes to Outputs:
  + bucket_name = "tf-workflow-demo-0725"

Read the last line first, always. 1 to add, 0 to change, 0 to destroy is the summary that should match your intention before you look at anything else. If the destroy count isn't what you expected, stop.

The (known after apply) values are the second thing to notice: Terraform doesn't know the bucket's ARN because the bucket doesn't exist yet. That propagation of unknown values is a large part of why plans sometimes show more churn than you expect, and it's covered in The Plan/Apply Lifecycle.

4. Apply

terraform apply

It shows the same plan again and asks:

# UNVERIFIED — confirm against a real run
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

aws_s3_bucket.demo: Creating...
aws_s3_bucket.demo: Creation complete after 2s [id=tf-workflow-demo-0725]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

bucket_name = "tf-workflow-demo-0725"

The bucket exists. A terraform.tfstate file now exists too.

5. Run it again

terraform plan
# UNVERIFIED — confirm against a real run
No changes. Your infrastructure matches the configuration.

6. Destroy

terraform destroy

It shows the plan in reverse — 1 to destroy — and asks for the same confirmation.

# UNVERIFIED — confirm against a real run
Destroy complete! Resources: 1 destroyed.

Do this now. An orphaned bucket is exactly the problem this article opened with.


In Practice

The saved plan file is the difference between a workflow and a habit. In a pipeline the sequence is plan -out=tfplan, publish the rendered plan for a human to read, gate on approval, then apply tfplan. What this buys you is that the approved artefact and the executed artefact are the same object. Without it, the plan a colleague approved at 14:00 and the plan that runs at 14:40 after someone else merged are different plans, and nobody will notice until one of them destroys something.

Pipeline showing a saved plan file passing from the plan step through a human approval gate to apply, with the rejected alternative of re-planning at apply time shown as a dashed branch

Plan files are also not secret-free — they contain resource attributes including sensitive values, in plaintext. Treat a tfplan artefact with the same care as a state file: short retention, restricted access, never a public CI artefact. See Secrets & Sensitive Data.

Render a saved plan for humans and for machines:

terraform plan -out=tfplan            # produce
terraform show tfplan                 # human-readable, for the PR comment
terraform show -json tfplan > plan.json   # machine-readable, for policy checks and cost estimation

That JSON output is the integration point for everything in Governance & Policy as Code — Conftest, OPA, Infracost and Sentinel all consume it rather than parsing terminal output.

Pin the Terraform version, not just the providers. required_version = ">= 1.5" prevents someone on 1.2 from failing confusingly, but a newer Terraform will upgrade the state file format, and once upgraded, colleagues on older versions cannot read it. That's a genuinely disruptive afternoon. Teams that care use a version manager (tenv, or tfenv) with a .terraform-version file committed, and pin a range with an upper bound in CI.

What a reviewer should look for in a pull request touching Terraform:

  • The plan output is in the PR. No plan, no approval. This is the whole discipline in one rule.
  • The summary line matches the PR description. "Adds a bucket" and 3 to destroy is a conversation.
  • Every destroy and every replace (-/+) is explained. Replacement is where data dies.
  • No -target in the proposed command, and no -auto-approve outside a saved-plan pipeline.
  • terraform fmt has been run — otherwise the diff is polluted with whitespace and nobody reads the real change.

Blast radius and rollback. The workflow's protection is entirely in the review step; once apply starts, Terraform will do what the plan said as fast as it can. Reverting the commit and re-applying restores the described state, but not data — a destroyed bucket comes back empty. The commands that limit damage are prevent_destroy on things that must not die (Meta-Arguments) and splitting state so no single apply can reach everything (Repo & Environment Structure).


Ecosystem

Version managers — tenv (or the older tfenv). Install and switch Terraform versions per directory from a .terraform-version file. The glue: commit that file, and everyone including CI runs the same binary. ⚠️ verify current maintenance status of tfenv; tenv is the more actively developed successor.

pre-commit with the terraform hooks. Runs fmt, validate, tflint and terraform-docs on commit, so the formatting argument never reaches review. The glue: a .pre-commit-config.yaml in the repo root and one pre-commit install per clone.

TF_LOG and terraform console. TF_LOG=DEBUG terraform plan prints provider API calls, which is how you find out which call is failing rather than guessing; TF_LOG_PATH sends it to a file because it's enormous. terraform console is an interactive expression evaluator against your current state — invaluable for working out what a for expression actually returns before putting it in a resource.

Atlantis. Runs the workflow from pull request comments: atlantis plan posts the plan as a comment, atlantis apply runs it after approval. The glue is a webhook and a atlantis.yaml. It's the self-hosted alternative to HCP Terraform, and the model matters more than the tool — see CI/CD & Automation.

HCP Terraform (formerly Terraform Cloud). Hosted state, remote runs, approval gates, policy enforcement. The glue is a cloud {} block replacing your backend configuration. Worth knowing as the default answer to "where does state live and who approves applies" for teams who don't want to build it. ⚠️ verify current naming and free-tier limits.

Provider behaviour across the three clouds is identical here. init, plan and apply do the same things regardless of provider; what varies is authentication — which credentials the provider finds and in what order — and that's Providers & the Registry.


Production

Security

The workflow's credentials are the most privileged in your organisation: whatever can apply can delete production. Two rules follow. First, plan and apply should not use the same permissions where you can manage it — a plan needs read access plus the ability to write state, which is a much smaller grant than create-and-destroy. Second, long-lived access keys in CI are the thing to eliminate first; OIDC federation gives the pipeline a short-lived credential scoped to a specific repository and branch. Locally, the risk is a developer with production credentials in ~/.aws/credentials and muscle memory for -auto-approve.

Blast radius

destroy is the obvious hazard, and it's not the dangerous one, because it announces itself. The dangerous one is replacement — a plan that shows -/+ on a database because an immutable attribute changed. It reads as a change; it is a delete and a create. Watch for -/+ and must be replaced in every plan you approve. The other under-appreciated hazard is an apply run against the wrong directory or the wrong credentials, which is an argument for the environment separation in Repo & Environment Structure.

Scale

The workflow's cost is dominated by refresh: plan makes at least one API call per managed resource, so plan time grows linearly with state size and is bounded by provider rate limits. At a few thousand resources this becomes minutes, and the temptations are -refresh=false (fast, and lies to you about drift) and -parallelism=N (helps until the provider starts throttling). Both are tuning; the real answer is usually to split the state, which is Scale & Performance.

Team workflow

Shared state means concurrent runs contend for a lock, and the correct experience of that is a clear "state is locked by " message rather than corruption. The organisational failure to guard against is applies from laptops: once one person has applied a branch that wasn't merged, the repository no longer describes production and nobody knows it. Applies come from one place.

Reliability

apply is safely re-runnable after failure — that's the load-bearing property of this whole design. What isn't automatic: a killed process can leave a lock behind (force-unlock, carefully), and a resource created just before a crash can exist in the cloud without being recorded in state. Both are Failure & Recovery. The habit to build now is to always let an apply finish rather than pressing Ctrl-C — Terraform handles one interrupt gracefully and finishes in-flight work, but a second interrupt kills it mid-operation.


Interview Questions

Conceptual

Walk me through the Terraform workflow from a blank directory to a running resource.

Write the configuration; terraform init to install providers and set up the backend; terraform fmt and terraform validate for formatting and internal consistency; terraform plan to compute and review the proposed change; terraform apply to execute it after confirmation; terraform destroy when it's no longer needed. A strong answer adds why the review step is separate — the plan is a reviewable artefact, which is the thing deployment scripts never had.

What does `terraform init` actually do, and when do you need to re-run it?

It prepares a working directory: downloads the providers named in required_providers, downloads any modules, records the exact provider versions and checksums in .terraform.lock.hcl, and configures the backend where state lives. Re-run it whenever those inputs change — a new provider or module, a changed version constraint, a changed backend configuration, or a fresh clone of the repository. It's safe to re-run at any time; it's not a one-off.

Which commands are safe to run without a human watching?

fmt, validate, show and output are safe unconditionally — no infrastructure changes, and validate doesn't even need credentials. init is safe. plan is safe with respect to infrastructure, but it does hit the cloud API, acquire a state lock and persist refresh results, so "read-only" needs that qualification. apply and destroy change infrastructure and should only run unattended against a plan file that a human already approved.

Is `terraform plan` read-only?

With respect to your infrastructure, yes — it never creates, modifies or deletes cloud resources. But it isn't side-effect-free: it acquires a lock on state, refreshes by calling the provider's API, and persists the refreshed attributes to state. So a plan can change the state file. The distinction matters the first time two people plan simultaneously and one gets a lock error.

What's the difference between `terraform apply` and `terraform apply tfplan`?

Bare apply computes a fresh plan, shows it and asks for confirmation. apply tfplan executes a previously saved plan without prompting and without recomputing — if reality has moved on such that the plan is no longer valid, it fails rather than adapting. The second form is what pipelines use, because it guarantees the thing that was approved is the thing that runs.

Technical depth

What's the difference between `validate` and a linter like `tflint`?

validate checks syntax, type correctness and internal consistency — references resolve, argument types match the provider's schema, required arguments are present. It never calls the provider's API, so it cannot know whether a value is acceptable: a non-existent instance type or an invalid region passes validate and fails at apply. tflint has provider-specific rule sets that catch exactly that class, plus stylistic and deprecation rules. They're complementary, and validate requires init first because it needs the provider schemas.

What happens if `apply` fails halfway through?

Nothing rolls back — there's no transaction. Resources created before the failure exist and are recorded in state; the failing resource may or may not have been created, and Terraform records what it can determine; resources downstream of the failure aren't attempted. The state file therefore describes a real, partially-built world, and the normal fix is to correct the configuration and run apply again, which creates only what's missing. The nasty case is a resource created in the cloud but not recorded — the API call succeeded and the response was lost — which needs import.

What is `-detailed-exitcode` and what would you use it for?

terraform plan -detailed-exitcode returns 0 for no changes, 1 for error and 2 for changes present. Two uses: a pipeline can skip the approval stage entirely when a plan is empty, and a scheduled job can run plan against production and alert when the exit code is 2 — which is drift detection built out of one flag. Without it, a plan that finds changes and a plan that finds none both exit 0, so a script can't tell them apart without parsing text.

Why would you use `-out` rather than just running `plan` then `apply`?

Because they're two different plans computed at two different moments. Between them, someone can merge a change or modify a resource in the console, and the apply will silently do something other than what was reviewed. -out freezes the decision into a file, and apply tfplan either executes exactly that or fails. It also gives you a machine-readable artefact via terraform show -json for policy and cost tooling. The catch: plan files contain sensitive values in plaintext, so they need the same handling as state.

When is `-target` legitimate, and why is it discouraged otherwise?

It restricts the operation to a resource and its dependencies. Legitimate for recovering from a broken state — unblocking a resource that's failing and preventing everything else from applying — and Terraform prints a warning saying exactly that. It's discouraged as a routine practice because the resulting state is consistent with no complete version of your configuration: you've applied part of a graph, so subsequent plans show changes that look inexplicable. Regular use of -target usually means the state is too large and should be split.

How does this differ across AWS, Azure and GCP?

The commands and their semantics don't differ at all — that's the point of the tool, and saying so confidently is part of a good answer. Three practical differences sit around the workflow.

Authentication, which is what init, plan and apply need before doing anything: AWS resolves a credential chain (environment variables, shared config, instance profile, or an assumed role via OIDC); Azure uses the CLI login, a service principal, managed identity or workload identity federation, and the provider additionally requires a features {} block and, in recent major versions, an explicit subscription; GCP uses application default credentials, a service account key, or workload identity federation, with project set on the provider.

Where state naturally lives, which init configures: S3 for AWS, an Azure Storage container for Azure, a GCS bucket for GCP.

Apply duration, which isn't a Terraform property but shapes the experience — some resources take tens of minutes on any cloud, and the workflow doesn't have a "background" mode.

The canonical comparison table is in Providers & the Registry.

Scenario and design

A plan you're reviewing shows `-/+ must be replaced` on your production database. What do you do?

Don't apply it. Find which attribute forced it: the plan marks the offending line with # forces replacement. Then decide whether replacement is genuinely required — some attributes are immutable in the cloud API and any change means a new resource — or whether it's an accident, such as a renamed resource address, an unintended change to an identifier, or a provider upgrade changing a default. If it's a rename, moved is the fix and it produces no infrastructure change at all (Import & Refactoring). If replacement is genuinely needed, it's a data migration with a runbook, not a Terraform change — and prevent_destroy should have stopped you getting this far.

Your CI pipeline runs `terraform apply -auto-approve` on every merge to main. Critique it.

The -auto-approve isn't the problem in itself — a pipeline can't answer a prompt. The problem is that it's applying a freshly computed plan that no human has seen, so the review happened on a diff of .tf files rather than on the actual proposed change, and those aren't the same thing: a provider version bump can produce replacements that the code diff doesn't hint at.

The fix: plan -out=tfplan on the pull request, post terraform show tfplan as a comment, and on merge run apply tfplan against the saved artefact. Add an environment protection rule for production, and a policy check on show -json that fails the build on unexpected destroys. Also worth raising: what credentials it uses, and whether the plan artefact is stored somewhere that exposes secrets.

Two engineers run `terraform apply` at the same time against the same state. What happens?

With a backend that supports locking, the second acquires nothing and fails fast with a message naming who holds the lock and since when — that's the intended behaviour, and it's an argument on its own for remote state. Without locking — local state on a shared drive, or a backend with locking disabled — both proceed, both write state, and the last writer wins, silently discarding the other's record. The resources the loser created are now real and unmanaged.

The mitigation isn't just enabling locking: it's applying from one place, so this contention is architecturally rare rather than merely detected. See State & Backends.

Someone pressed Ctrl-C during an apply against production. Walk me through what you check.

First, what state the interrupt left things in — one interrupt makes Terraform stop starting new work and finish what's in flight, so a single Ctrl-C is comparatively graceful; a second one kills it, and that's where a resource can exist without being recorded. So: was it one or two?

Then, is the state lock still held? A killed process doesn't release it, and force-unlock with the lock ID is the fix — after confirming no apply is genuinely still running, because unlocking a live run is how state gets corrupted.

Then run plan and read it very carefully. Creates for things you believe were made are the signal for an orphaned resource that needs importing. Once it's clean, re-run apply — it's idempotent and will complete what's missing. Full treatment in Failure & Recovery.


Commands & Gotchas

terraform init                          # install providers/modules, write lock file, configure backend
terraform init -upgrade                 # re-resolve version constraints and update the lock file
terraform fmt -recursive                # canonical formatting, including subdirectories
terraform validate                      # syntax, types, internal consistency — no credentials needed
terraform plan                          # compute and render the proposed change
terraform plan -out=tfplan              # ...and freeze it into a file
terraform plan -detailed-exitcode       # exit 0 = no changes, 1 = error, 2 = changes pending
terraform apply tfplan                  # execute exactly that plan, no prompt, no recompute
terraform show -json tfplan             # machine-readable plan, for policy and cost tooling
terraform output -raw bucket_name       # a single output value, unquoted, for shell scripts
terraform destroy                       # remove everything this configuration manages
Behaviour Why it matters
plan needs credentials; validate doesn't validate runs in CI with no cloud access at all — put it in the fast pre-check job
plan writes refresh results to state and takes a lock "Read-only" is about infrastructure, not about state
apply with no plan file re-plans from scratch The plan you looked at ten minutes ago is not the plan that runs
apply tfplan fails rather than adapting if reality moved That's the feature — it's what makes approval meaningful
-/+ means destroy-then-create Read every replacement. Look for # forces replacement in the plan body
Failed applies don't roll back State is accurate about a half-built world; re-running apply finishes the job
One Ctrl-C is graceful, two is not The second interrupt is how resources end up existing but unmanaged
-target and -auto-approve are recovery tools, not workflow Regular use of either is a signal about state size or review discipline
Plan files contain secrets in plaintext Treat a tfplan artefact exactly as carefully as a state file

← Back to Orientation · Next: Anatomy of a Project →


⚠️ Verification checklist (delete before publishing)

Command output — every block marked UNVERIFIED needs a real capture

  • terraform init output wording and the exact lock-file sentence.
  • terraform validate success message.
  • Full plan output for the AWS bucket — in particular which attributes appear as (known after apply). The list shown is plausible but reconstructed, and region may not appear as a bucket attribute at all in aws v5+.
  • The apply confirmation prompt wording and the Creation complete after Ns [id=...] format.
  • The No changes. wording on the second plan.
  • destroy summary line.
  • Capture Azure and GCP equivalents of at least the plan summary line — currently only AWS output is shown, which slightly undercuts the multi-cloud promise. Decide whether to add a tab set for plan output or state explicitly that it's materially identical.

Behavioural claims

  • Ctrl-C behaviour: that one interrupt lets in-flight work finish and a second kills it. ⚠️ verify — this is asserted twice on the page and used in an interview answer.
  • That plan persists refresh results to state by default in current Terraform versions. This changed historically and is central to the "plan isn't read-only" claim. ⚠️ verify.
  • Status of the standalone terraform refresh command — deprecated, hidden, or removed? ⚠️ verify before describing it as deprecated.
  • -detailed-exitcode exit code meanings (0/1/2).
  • That terraform validate requires init to have run.
  • That apply tfplan fails rather than re-planning when state has moved on — and what the error says.
  • That -target prints a warning describing itself as for exceptional recovery use, and the current wording.
  • That plan files contain sensitive values in plaintext. ⚠️ verify how they're encoded and whether show -json exposes them differently from the binary file.
  • The claim that a newer Terraform upgrades the state format and older versions then can't read it. ⚠️ verify which minor versions actually do this.

Versions and syntax

  • All three provider version constraints (~> 5.0 aws, ~> 3.0 azurerm, ~> 5.0 google) are probably stale — same issue as the previous page. azurerm 4.x requires subscription_id.
  • required_version = ">= 1.5" — confirm nothing on this page needs a higher floor.
  • terraform output -raw flag name and behaviour.
  • terraform fmt -recursive flag exists.
  • HCP Terraform naming, the cloud {} block syntax, and current free-tier limits. ⚠️ verify.
  • tfenv maintenance status and whether tenv is the correct successor to recommend.

Rendering

  • The single <Tabs> block renders; the many <details> blocks render.
  • All relative links resolve once targets exist.