terraform apply, destroy, and fmt: executing and maintaining changes
terraform apply executes the changes proposed in an execution plan — creating, updating, or destroying real infrastructure and recording the results in state — while terraform destroy tears down everything tracked in the current state, and terraform fmt rewrites configuration into HashiCorp's canonical style. Apply is the only core-workflow command that modifies infrastructure, which is why it demands explicit approval: it shows you a plan and waits for you to type yes, unless you pass -auto-approve or hand it a saved plan file that was already reviewed. This lesson covers the approval prompt and running apply unattended (-auto-approve versus -input=false, TF_INPUT and TF_IN_AUTOMATION), applying saved plans, what happens when an apply fails partway, how apply walks the dependency graph with up to ten concurrent operations, destroying with terraform destroy and its equivalent terraform apply -destroy, what destroy does and does not delete, prevent_destroy, targeted destroys, and the fmt flags (-check, -diff, -write=false, -list, -recursive) that keep formatting enforceable in CI. It closes with a validate recap and the pipeline order: fmt, init, validate, plan, apply.
On this page10 sections
- terraform apply: executing the plan
- The approval prompt and running apply unattended
- Applying a saved plan file
- When an apply fails partway: no rollback
- Parallelism: how apply orders and batches work
- terraform destroy: tearing down managed infrastructure
- Protecting resources with prevent_destroy
- Targeted operations with -target
- terraform fmt: canonical formatting
- Validate recap and the full pipeline order
- Execute infrastructure changes with terraform apply and explain when the approval prompt appears and when it is skipped
- Run apply unattended correctly, distinguishing -auto-approve and saved plans from -input=false, TF_INPUT and TF_IN_AUTOMATION
- Apply a saved plan file and explain what it fixes at plan time: actions, configuration, variable values and the state it was computed from
- Describe what a failed or partially failed apply leaves behind, including tainted resources after a failed creation-time provisioner
- Control concurrency with -parallelism and describe how apply and destroy walk the resource dependency graph
- Destroy managed infrastructure with terraform destroy, terraform apply -destroy and -target, and predict exactly what is and is not deleted
- Protect a resource with prevent_destroy and explain its limits
- Enforce canonical formatting with terraform fmt and choose between -check, -diff, -write=false, -list and -recursive
terraform apply: executing the plan
terraform apply executes the actions in an execution plan against real infrastructure and records the results in the state file. Run without arguments, it first generates a fresh plan — exactly as terraform plan would — prints it, and then pauses for confirmation. Only after you approve does it start creating, updating, and destroying resources. As each operation completes, apply writes the resulting resource attributes (server-assigned IDs, IP addresses, ARNs) into state, which is how later plans know what already exists.
$ terraform apply
Terraform will perform the following actions:
# google_compute_instance.app will be created
+ resource "google_compute_instance" "app" {
+ name = "app-server"
+ machine_type = "e2-medium"
+ zone = "europe-west1-b"
}
Plan: 1 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
google_compute_instance.app: Creating...
google_compute_instance.app: Creation complete after 14s [id=projects/demo/zones/europe-west1-b/instances/app-server]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.If the refreshed state already matches the configuration, the plan is empty: apply reports No changes. Your infrastructure matches the configuration. and finishes without asking for approval, because there is nothing to approve.
While it runs, apply holds the state lock (on backends that support locking) so concurrent runs cannot corrupt state. If another run holds the lock, -lock-timeout=DURATION (for example -lock-timeout=60s) makes Terraform keep retrying the lock for that long before giving up. It only affects lock acquisition — it does nothing about provider API rate limits, which are what -parallelism is for.
The approval prompt and running apply unattended
The interactive prompt exists because apply is the one command that changes real infrastructure: Terraform shows the plan and accepts only the literal word yes before proceeding. Anything else aborts with no changes made. This is the default whenever apply generates its own plan.
$ terraform apply -auto-approve $ terraform destroy -auto-approve
-auto-approve skips the prompt entirely — apply plans and immediately executes. It exists for automation: a scripted pipeline or an ephemeral test environment has no human to type yes. There are exactly two ways apply proceeds without an interactive approval: you passed -auto-approve, or you supplied a saved plan file (covered next), where the review already happened at plan time. In shared or production environments, prefer the saved-plan pattern over -auto-approve — auto-approving a fresh plan means executing changes nobody looked at.
-input=false, TF_INPUT and TF_IN_AUTOMATION are not approval
-input=false disables all interactive prompts. That includes the approval prompt, so a bare terraform apply -input=false (no saved plan, no -auto-approve) cannot ask for approval and Terraform conservatively assumes you do not want to apply: the run fails without changing anything. It also turns the prompt for an unset required variable (one with no default and no value from -var, a tfvars file or TF_VAR_name) into an error. That is why pipelines combine the two: -input=false makes missing inputs fail fast, while -auto-approve (or a saved plan) supplies the approval — -auto-approve approves the plan only; it never fills in a missing variable.
| Setting | What it does | Approves the apply? |
|---|---|---|
-auto-approve | Skips the approval prompt | Yes |
| Saved plan file | Executes an already-reviewed plan without prompting | Yes (approval happened at plan time) |
-input=false | Disables all prompts; missing input becomes an error | No — apply without a saved plan fails |
TF_INPUT=0 (or false) | Environment-variable form of -input=false | No |
TF_IN_AUTOMATION (any non-empty value) | Only adjusts output, e.g. stops suggesting the next command to type | No |
Applying a saved plan file
Passing a saved plan file makes apply execute exactly the actions that were reviewed: terraform plan -out=tfplan followed by terraform apply tfplan. Because the plan was already generated and reviewed, apply does not re-plan and does not prompt — it goes straight to execution. This is the standard CI/CD shape: the plan stage produces the artifact and a human or policy check approves it; the apply stage consumes the identical artifact.
$ terraform plan -out=tfplan $ terraform show tfplan # review what will happen $ terraform apply tfplan Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
A plan file is a snapshot of every planning decision. It contains the proposed actions, a copy of the configuration and the input variable values in effect at plan time. Consequences:
- Editing a
.tfor.tfvarsfile after the plan was saved — for example changing a variable's default — has no effect onterraform apply tfplan; it runs what was planned. To pick up the edit, plan again. - You do not pass
-varor-var-fileagain, and apply rejects extra planning options with a saved plan (such as-target,-replaceor-destroy), because those decisions are already baked in. The exception is ephemeral input variables (Terraform 1.10 and later), whose values are deliberately not stored and must be supplied again at apply. - The plan is tied to the state it was computed from. If state has changed since (another apply ran in between), Terraform rejects the plan as stale and you must generate a new one. Staleness is judged against state, not against the configuration files.
- The file is binary and may contain sensitive values — inspect it with
terraform show(orterraform show -jsonfor tooling) and store it securely.
The same works for teardowns: terraform plan -destroy -out=teardown.tfplan saves a destroy plan, and terraform apply teardown.tfplan executes it without a prompt — the approval was the review of the saved plan.
When an apply fails partway: no rollback
Terraform apply is not transactional: if an operation fails partway through, nothing that already succeeded is undone. Resources created before the failure stay created and are recorded in state as each one completes; the failed and not-yet-attempted operations are reported as errors. The fix is to correct the cause (a missing permission, a bad argument) and run terraform apply again — the new plan compares configuration against the updated state and proposes only the remaining work. Do not destroy and start over, and do not delete the state.
One special case: a creation-time provisioner (such as remote-exec) that fails with the default on_failure = fail leaves the newly created object in state marked tainted. The object exists, but Terraform no longer trusts it, so the next plan proposes to replace it (destroy, then create). Setting on_failure = continue instead ignores the provisioner error and keeps the resource untainted.
| Failure | What state records | What the next plan proposes |
|---|---|---|
| A resource's create/update call fails | Everything that completed before it | Only the remaining changes |
Creation-time provisioner fails (default on_failure = fail) | The object, marked tainted | Replace the tainted object |
Parallelism: how apply orders and batches work
Apply walks Terraform's resource dependency graph, not your file order: resources that depend on each other run strictly in dependency order, and resources with no relationship run concurrently. By default Terraform performs at most 10 concurrent operations; -parallelism=n changes that ceiling for a single run of plan, apply or destroy.
$ terraform apply -parallelism=5 # gentler on rate-limited APIs $ terraform apply -parallelism=30 # more concurrency for large graphs
Lowering parallelism helps when a provider's API throttles you; raising it can shorten applies with many independent resources. Parallelism is a runtime flag, not something you set in configuration, and it never overrides dependencies. A resource is only created once the values it references are known, so a chain where each resource references the previous one always runs one link at a time — a subnet that references a VPC waits for the VPC no matter how high the limit is set. Only the independent resources benefit from a higher ceiling.
terraform destroy: tearing down managed infrastructure
terraform destroy destroys every remote object tracked in the current state of the working directory. Like apply, it first shows you a plan (all lines marked -) and requires you to type yes; -auto-approve skips the prompt. Destruction runs in reverse dependency order: dependents are destroyed before the things they depend on, so an instance goes before its security group or subnet, and the subnet before its VPC. Independent deletions run concurrently, up to the same -parallelism limit (default 10) as apply.
$ terraform destroy
Terraform will perform the following actions:
# aws_instance.web will be destroyed
- resource "aws_instance" "web" {
- ami = "ami-0c101f26f147fa7fd"
- instance_type = "t3.micro"
}
Plan: 0 to add, 0 to change, 1 to destroy.
Do you really want to destroy all resources?
Enter a value: yes
Destroy complete! Resources: 1 destroyed.terraform destroy is an alias for terraform apply -destroy — the same destroy planning mode expressed as an apply flag. To preview a teardown without executing it, run terraform plan -destroy, or save it with terraform plan -destroy -out=tfplan and apply that file later.
What destroy deletes — and what it leaves
Destroy plans from state, not from the resource blocks you can see, so commenting out or deleting a block does not shield it from a destroy (and editing configuration is not a way to narrow one — use -target). Its scope:
| Destroyed | Not touched |
|---|---|
| Every managed resource in the current state, including resources created inside child modules | Objects only read through data blocks — Terraform never manages them |
| Only the selected workspace's state (with dev selected, prod's resources are untouched) | Resources created outside Terraform or already removed from state; your configuration files; and the state file itself (it remains, now tracking no resources) |
Destroy is routine for ephemeral environments — spin up a test stack, run the tests, destroy it — and rare, deliberate, and heavily reviewed for production.
Protecting resources with prevent_destroy
prevent_destroy is a lifecycle meta-argument that makes Terraform reject, with an error, any plan that would destroy the resource's object. It defaults to false; set it to true on objects that are costly to recreate, such as a production database.
resource "aws_db_instance" "main" {
# ...
lifecycle {
prevent_destroy = true
}
}Two behaviours matter:
- It fails the plan rather than skipping the resource. A full
terraform destroyin a configuration containing a protected resource errors at plan time and destroys nothing — not even the unprotected resources beside it. The same applies to an apply whose plan would replace the protected resource. - It only protects while it is in the configuration. If someone deletes the whole resource block, the
prevent_destroysetting goes with it, and the next normalterraform applyplans to destroy the object. Code review on removed blocks is the real safeguard.
The common ways a single managed object ends up destroyed outside a full teardown are therefore: removing its block and applying (the normal, permanent way), a targeted destroy, a plan that replaces it, or a count/for_each change that drops that instance. Each of these is still blocked while prevent_destroy = true remains on the block.
Targeted operations with -target
-target restricts a plan, apply, or destroy to specific resource addresses. To remove a single resource while leaving the rest of the stack running, target it in a destroy:
$ terraform destroy -target=aws_instance.web $ terraform apply -target=module.network # apply just one module subtree
The flag accepts resource addresses (aws_instance.web), module paths (module.network), and can be repeated to name several targets. Terraform automatically widens the selection where the graph requires it: a targeted apply also includes the target's dependencies, and a targeted destroy also includes everything that depends on the target. Targeting a VPC that four subnets reference therefore plans to destroy the VPC and the four subnets — you cannot destroy a network and leave its subnets orphaned in state. Read the destroy plan before confirming.
-target is for exceptional situations — recovering from errors, working around bugs, surgically removing one resource — not for routine use. Routinely applying subsets leaves infrastructure drifting from what a full plan would produce, and Terraform prints a warning on every targeted run for that reason. Related tools do different jobs:
| Tool | Effect on the real object | Effect on state |
|---|---|---|
terraform destroy -target=ADDR | Destroyed (with its dependents) | Removed |
terraform apply -replace=ADDR | Destroyed and recreated | Updated with the new object |
terraform state rm ADDR | Left running, unmanaged | Removed |
Delete the block, then terraform apply | Destroyed | Removed — the permanent, recommended way |
terraform fmt: canonical formatting
terraform fmt rewrites Terraform configuration files (.tf and .tfvars) in place into HashiCorp's canonical style and prints the names of the files it changed. It needs no init, no providers and no credentials, and it performs no validity checking beyond parsing the files: fmt does not validate, and validate does not format.
What it changes: indentation, spacing, and alignment of = signs within a block. It keeps blocks and arguments in the order you wrote them — it aligns, it never sorts. It also makes a few minor readability adjustments to the expressions themselves, such as unwrapping an interpolation-only string like "${var.region}" to var.region and unquoting a legacy quoted type constraint like type = "string". So "cosmetic" means the configuration's behaviour never changes, not that only whitespace is touched. Because the canonical style can shift slightly between Terraform versions, run fmt after upgrading.
Scope. The usage is terraform fmt [options] [target...]. With no target it processes only the current directory; -recursive descends into subdirectories. A target can be a directory or file — terraform fmt modules/network formats just that directory (add -recursive to include its subdirectories).
$ terraform fmt main.tf $ terraform fmt -recursive # format the whole tree $ terraform fmt -check -recursive # CI gate: list unformatted files, exit non-zero $ terraform fmt -check modules/network # check one directory $ terraform fmt -check -diff # show what would change, modify nothing
| Flag | Rewrites files? | Output | Exit status when files need formatting |
|---|---|---|---|
| (none) | Yes | Names of changed files | 0 |
-check | No (implies -write=false) | Names of unformatted files | Non-zero — fails a CI build |
-diff | Yes, unless combined with -check or -write=false | Line-level diff of the changes | 0 on its own |
-write=false | No | Names of unformatted files | 0 |
-list=false | Yes | Does not list file names | 0 |
-recursive | Yes (subdirectories too) | Names of reformatted files, as by default | 0 |
The near-twins to keep apart: -check and -write=false both leave files unchanged and list the offenders, but only -check exits non-zero — use it to fail a build, and -write=false when a script must continue. -diff alone still rewrites the files; to only see the changes, pair it with -check or -write=false.
Validate recap and the full pipeline order
terraform validate is the correctness counterpart to fmt's style: it verifies that the configuration is syntactically valid and internally consistent — legal argument names, resolvable references, matching types — without contacting any cloud API or reading state. It requires an initialized directory (provider schemas come from init), so CI jobs without backend credentials run terraform init -backend=false first. Use -json for machine-readable diagnostics.
Put together, the canonical automated pipeline the exam expects is:
| Order | Command | Purpose | Key flags |
|---|---|---|---|
| 1 | terraform fmt | Enforce canonical style | -check, -recursive, -diff, -write=false |
| 2 | terraform init | Install providers/modules, configure backend | -backend=false, -upgrade |
| 3 | terraform validate | Syntax and consistency check, offline | -json |
| 4 | terraform plan | Produce and review the execution plan | -out, -detailed-exitcode |
| 5 | terraform apply | Execute the reviewed plan | saved plan file, -auto-approve, -parallelism |
| — | terraform destroy | Tear down managed infrastructure | -auto-approve, -target, -parallelism |
Memorize the flag-to-command pairings in that table — a classic question format offers -check or -auto-approve against the wrong command and asks which invocation is valid.
Tip. Expect questions on when the apply approval prompt appears and the two ways to bypass it (-auto-approve or a saved plan file), contrasted with -input=false, TF_INPUT and TF_IN_AUTOMATION, which do not approve anything. Saved plans: what they fix at plan time (configuration and variable values) and when they are stale. Partial failures (no rollback, tainted resources after a failed provisioner), the default parallelism of 10 and what -parallelism cannot change. For destroy: equivalence with apply -destroy and plan -destroy, exactly what is and is not deleted (workspace, child modules, data sources), prevent_destroy behaviour and its caveat, and -target as an exceptional-use flag that pulls in dependents. For fmt: -check versus -write=false versus -diff, -recursive or a path for scope, and that fmt formats but never validates.
- terraform apply is the only core-workflow command that changes infrastructure: it plans, waits for the literal word yes, executes in dependency order, and records results in state while holding the state lock. An empty plan finishes without a prompt.
- Apply skips the approval prompt in exactly two cases: -auto-approve, or applying a saved plan file. -input=false (or TF_INPUT=0) disables prompts, so a bare apply fails; TF_IN_AUTOMATION only adjusts output.
- A saved plan embeds the actions, a configuration snapshot and variable values: later .tf/.tfvars edits do not affect it, extra planning options are rejected, and it is refused as stale if state changed since it was created.
- Apply is not transactional: completed operations stay in state and you re-run apply after fixing the cause. A failed creation-time provisioner leaves the resource tainted, so the next plan replaces it.
- Up to 10 operations run concurrently by default; -parallelism=n changes the ceiling for plan, apply or destroy but never overrides dependencies. -lock-timeout only governs retrying the state lock.
- terraform destroy (= terraform apply -destroy) deletes everything in the selected workspace's state, including child-module resources, in reverse dependency order; data-source objects, unmanaged resources, the configuration and the state file remain.
- prevent_destroy = true makes any destroying plan error (a full destroy deletes nothing), but it protects only while the block is in configuration.
- -target is exceptional-use; a targeted destroy also removes the target's dependents. terraform fmt rewrites in place (current directory unless -recursive or a path is given); -check exits non-zero, -write=false exits 0, -diff alone still writes, and fmt never validates or reorders.
- Pipeline order: fmt -check, init, validate, plan -out, then apply the saved plan.
Frequently asked questions
When does terraform apply skip the approval prompt?
In two cases only: when you pass -auto-approve, or when you apply a saved plan file (terraform apply tfplan), where the review already happened when the plan was generated. -input=false does not count — it disables all prompts, so an apply without a saved plan or -auto-approve fails instead of proceeding. If the plan has no changes, apply simply finishes with nothing to approve.
What is the difference between -auto-approve and -input=false?
-auto-approve approves the plan, so apply proceeds without a prompt. -input=false disables every interactive prompt, including approval, so Terraform assumes you do not want to apply and the run fails; it also turns a prompt for an unset required variable into an error. Pipelines often use both: -input=false so missing inputs fail fast, and -auto-approve (or a saved plan) for approval. TF_INPUT=0 is the environment-variable form of -input=false, and TF_IN_AUTOMATION only adjusts output.
Is using -target a normal way to work with Terraform?
No. -target is meant for exceptional situations, such as recovering from an error or surgically removing one resource. Routinely applying subsets of the configuration leaves infrastructure out of step with what a full plan would produce, which is why Terraform prints a warning on every targeted run. Terraform also widens a target where the dependency graph requires it: a targeted apply includes the target's dependencies, and a targeted destroy includes everything that depends on the target.
What does terraform destroy leave behind?
terraform destroy (an alias for terraform apply -destroy) deletes every managed resource in the selected workspace's state, including resources created by child modules. It leaves objects that are only read through data blocks, resources created outside Terraform, other workspaces' resources, your configuration files and the (now empty) state file. Because it plans from state, commenting out a resource block does not exclude it from a destroy.
What does prevent_destroy do during terraform destroy?
prevent_destroy = true in a resource's lifecycle block makes Terraform reject any plan that would destroy that object with an error. A full terraform destroy therefore fails at plan time and deletes nothing, rather than skipping the protected resource. The protection lasts only while the setting is in configuration: if the whole resource block is deleted, a normal apply will destroy the object.
Does terraform apply roll back if something fails midway?
No. Terraform has no rollback: resources created before the failure remain and are recorded in state, and the failed and unattempted operations are reported. You fix the cause and run apply again, and the new plan proposes only the remaining work. If a creation-time provisioner failed with the default on_failure = fail, the created resource is marked tainted and the next apply replaces it.
How many resources does Terraform change at once, and can I control it?
By default, at most 10 resource operations run concurrently. Pass -parallelism=n to terraform plan, apply or destroy to change the ceiling for that run — lower it when a provider API rate-limits you, raise it to speed up large graphs of independent resources. It is a CLI flag, not a configuration setting, and dependencies always take precedence: a chain of resources that reference each other still runs one at a time.
How do terraform fmt -check, -diff and -write=false differ?
-check leaves files unchanged, lists files that are not canonically formatted, and exits non-zero if any exist, which fails a CI build. -write=false also leaves files unchanged and lists them, but exits 0. -diff shows line-level changes but, on its own, still rewrites the files; combine it with -check or -write=false to only see them. By default fmt covers only the current directory; add -recursive or pass a directory path.
Source
This lesson covers the "Core Terraform workflow" domain of the official Terraform 004 exam guide. Vendors revise their guides — check the source for the current version.
- HashiCorp Terraform Associate certification page — HashiCorp
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.