Init, Validate, Plan, Apply, and Destroy
Init, validate, plan, apply, and destroy are the core commands that turn Terraform configuration into controlled infrastructure changes. The outcome is a repeatable workflow: prepare the working directory, check that configuration is internally consistent, preview the proposed changes against state, make only the reviewed changes, and intentionally remove managed objects when the environment is no longer needed.
In the Terraform Foundations section, this lesson is the operational bridge between writing configuration and managing real resources. The commands are often shown as a simple sequence, but each one answers a different question. init asks whether Terraform can assemble the working directory. validate asks whether the configuration is well formed. plan asks what Terraform would change. apply asks Terraform to execute a chosen change graph. destroy asks Terraform to plan and apply removal actions for objects it still tracks in state.
What Terraform Does Between Commands
Terraform reads files ending in .tf in the current module and builds an internal model from blocks, expressions, variables, provider requirements, resources, data sources, outputs, and lifecycle rules. The configuration is declarative: a resource block describes the desired object, not the individual API calls needed to create it. Terraform then uses provider plugins to translate that desired object into remote API operations.
terraform init prepares the working directory. It creates or updates .terraform/, installs the provider plugins required by the root module and child modules, reads backend configuration, and writes dependency selections to .terraform.lock.hcl. The lock file is important because it records the provider versions and checksums that were selected during initialization. Sharing it with the team reduces accidental provider drift between laptops and automation.
terraform validate performs static configuration checks without contacting most remote APIs. It verifies block shapes, argument names, references, types that can be known without planning, and provider schema compatibility after initialization has installed providers. Validation can catch an unknown argument or a missing variable, but it cannot prove that a cloud account has enough quota or that a generated name is globally available.
terraform plan is where Terraform combines configuration, input variables, provider schemas, existing state, and usually refreshed remote objects. State maps Terraform resource addresses, such as local_file.note, to real object identities. The plan calculates actions: create, update, replace, delete, or no-op. Replacement means Terraform cannot update an attribute in place, so it must destroy one object and create another. The plan output is a proposal, not a guarantee that every later API call will succeed.
terraform apply walks the planned dependency graph and asks providers to perform the required operations. If you apply without a saved plan file, Terraform creates a fresh plan and asks for approval. If you apply a saved plan file, Terraform applies exactly that plan, which is the safer pattern for reviewed automation. terraform destroy is not a separate engine. It is a destroy-mode plan followed by apply, causing Terraform to remove tracked resources in dependency order.
Command Anatomy
The usual local workflow starts with a directory containing Terraform configuration. Run terraform fmt when editing, then terraform init before validation or planning. Use terraform validate for fast feedback, terraform plan -out=tfplan to save a reviewed plan, terraform apply tfplan to execute that exact plan, and terraform destroy only when teardown is intended. In shared environments, add a remote backend with locking so that two operators cannot safely write the same state at once.
terraform init
terraform validate
terraform plan -out=tfplan
terraform apply tfplan
terraform destroy
The command sequence is simple, but the boundary is stateful. Deleting configuration from a file does not delete the remote object until a plan and apply remove it from state. Deleting the state file does not delete the remote object at all; it only makes Terraform forget the binding. That distinction is one of the most important safety concepts in Terraform.
Example 1: Validate a Minimal Module
This first example uses the local provider so you can see the Terraform workflow without provisioning cloud infrastructure. The configuration asks Terraform to manage one file named hello.txt. The provider requirement tells init which plugin to install. The resource address is local_file.hello, and that address is what Terraform records in state after apply.
terraform {
required_providers {
local = {
source = "hashicorp/local"
}
}
}
resource "local_file" "hello" {
filename = "${path.module}/hello.txt"
content = "hello from terraform\n"
}
After saving this as main.tf, terraform init installs the provider and terraform validate should report that the configuration is valid. If you misspell filename as file_name, validation fails before any file is created because the provider schema has no argument with that name.
terraform validate
Expected successful output is deterministic enough to use as a check: Success! The configuration is valid. This example proves only syntax and schema correctness. It does not yet prove that Terraform can write to the directory, because that write happens during apply.
Example 2: Plan a Change Before Applying
Now change the file content to make the desired state different from any previous state. Planning shows Terraform’s proposed action. If no state exists yet, Terraform proposes one create. If the file was already created by the previous configuration, Terraform proposes an in-place update because the provider can rewrite the content without replacing the resource address.
resource "local_file" "hello" {
filename = "${path.module}/hello.txt"
content = "updated by a reviewed plan\n"
}
terraform plan -out=tfplan
For a first run, the important line is Plan: 1 to add, 0 to change, 0 to destroy. For a content-only edit after the file is already managed, expect Plan: 0 to add, 1 to change, 0 to destroy. The plan also marks changed attributes with symbols such as + for additions and ~ for updates. Review those symbols carefully; a replacement or destroy action deserves more scrutiny than a tag or text update.
Saving the plan with -out=tfplan creates a binary plan file tied to the configuration, state, variables, and provider selections used at planning time. Do not edit configuration and then apply an older saved plan expecting it to include the new edits. A saved plan is evidence of what was reviewed, not a live pointer to the latest files.
Example 3: Apply and Destroy Deliberately
Applying the saved plan executes the reviewed graph. With the local file example, Terraform creates or rewrites hello.txt and records the managed object in terraform.tfstate when using the default local backend. You can verify the managed output with ordinary shell commands and with Terraform state inspection.
terraform apply tfplan
cat hello.txt
terraform state list
Expected verification after the updated example is that cat hello.txt prints updated by a reviewed plan and terraform state list includes local_file.hello. If the file is edited manually outside Terraform, a later plan may show drift and propose to restore the configured content. That is Terraform reconciliation: configuration plus state determines what Terraform believes should exist.
When the resource is no longer needed, terraform destroy asks for confirmation and then removes tracked objects. For this example, the file is deleted and the state entry is removed. If you want noninteractive teardown in automation, use a saved destroy plan: terraform plan -destroy -out=destroy.tfplan, review it, then run terraform apply destroy.tfplan.
Design Choices and Trade-offs
Interactive terraform apply is convenient for learning because the plan and approval happen in one command. The trade-off is that review and execution are coupled. Saved plans are better for team workflows because one step produces reviewable evidence and another step executes exactly that artifact. The trade-off is operational discipline: configuration, variables, provider lock files, and state must remain consistent between plan and apply.
Local state is transparent and easy to inspect, but it is risky for shared infrastructure because it can be lost, copied, or edited without coordination. Remote state with locking improves collaboration by serializing writes and centralizing the current resource mapping. The trade-off is that your backend becomes part of the reliability path. If the backend is unavailable, planning and applying may be blocked even though the target provider is healthy.
Automatic approval with -auto-approve is appropriate only when another control has already reviewed the exact plan or when the environment is disposable. In long-lived environments, requiring human or policy review for creates, replacements, and destroys catches many expensive mistakes. Terraform makes change easy; the workflow must make unintended change visible.
Failure Modes and Troubleshooting
Provider installation fails during init. Symptoms include errors about unavailable provider packages, checksum mismatch, or inability to reach the registry. The cause is usually network access, an incorrect provider source, a platform without a matching provider build, or a lock file that conflicts with the requested constraints. Diagnose with terraform init -upgrade only when you intend to select newer versions, inspect required_providers, and check whether the lock file was committed from a compatible platform. Correct by fixing the source address, restoring network access, or intentionally updating and reviewing the lock file.
Validate succeeds but plan fails. Symptoms include permission errors, invalid remote names, missing credentials, quota limits, or data source lookup failures. The cause is that validation is mostly static while planning may refresh state and ask providers to read remote APIs. Diagnose by confirming credentials, running the plan with the same variables used by automation, and reading the first provider error rather than the final summary. Correct by granting the minimum required permission, changing the invalid argument, or removing the dependency on a remote lookup that is not available in that workspace.
Apply fails halfway. Symptoms include some resources created and others missing, followed by an error from a provider API. Terraform records successful operations it knows about, but the final desired graph is incomplete. Diagnose with a fresh terraform plan, provider console checks, and terraform state list. Correct the underlying error and apply again; Terraform is designed to converge from partial progress when state accurately reflects completed operations. Avoid manual deletion unless you also understand whether the state needs terraform import, terraform state rm, or no state change.
Destroy removes more than expected. The symptom is a destroy plan with resources unrelated to the intended cleanup. The cause is usually running in the wrong directory, workspace, backend, or variable set. Diagnose before approving by checking terraform workspace show, backend configuration, resource addresses in the plan, and environment-specific variables. Correct by switching to the intended workspace or backend and generating a new destroy plan. Never use destroy as a way to test whether you are pointed at the right environment.
Security, Performance, and Reliability
Terraform state can contain sensitive values, provider identifiers, and enough metadata to change infrastructure. Protect it as operationally sensitive data. Use backend access controls, encryption where supported, state locking, and separate workspaces or backends for environments that should not share lifecycle. Keep provider credentials outside configuration files and prefer short-lived credentials in automation.
Plan performance depends on provider refresh behavior, number of resources, remote API latency, and dependency graph size. A large plan is not just slower; it is harder to review accurately. Split configuration into modules and states along ownership and lifecycle boundaries, not merely by file length. A database and a short-lived test file should not usually share a state if they are approved, restored, and destroyed by different people.
Reliability comes from repeatable inputs. Commit configuration and lock files, make variable values explicit, save plans for approval, and keep state writes locked. When a command fails, rerunning blindly can hide the first useful error. Capture the command, workspace, variables, selected providers, and first provider diagnostic in the incident notes.
Hands-on Lab
Prerequisites: a shell, Terraform installed, internet access for provider installation, and a disposable empty directory. The lab manages a local file only, so no cloud account is required.
- Create a new directory and save the first example as
main.tf. - Run
terraform init. Verify that.terraform.lock.hclexists and namesregistry.terraform.io/hashicorp/local. - Run
terraform validate. Verification passes when it reports the configuration is valid. - Run
terraform plan -out=tfplan. Verify the summary says there is one object to add. - Run
terraform apply tfplan. Verifyhello.txtexists andterraform state listshowslocal_file.hello. - Edit
content, create a new saved plan, and confirm the summary changes to one in-place update rather than a destroy. - For cleanup, run
terraform plan -destroy -out=destroy.tfplan, review that onlylocal_file.hellois selected, then runterraform apply destroy.tfplan.
Rollback for this lab is simple: if apply created the file and you do not want it, run the destroy plan. If state is accidentally deleted but the file remains, remove the file manually because Terraform no longer has the state binding needed to manage it.
Assessment Exercises
- A teammate says
terraform validatepassed, so production apply is safe. Explain two important risks that validation does not cover. - You see a plan summary of
0 to add, 1 to change, 1 to destroy. What specific parts of the plan would you inspect before approval, and why? - Why is
terraform apply tfplansafer in a review workflow than running a fresh interactiveterraform applyafter approval? - A destroy plan shows resources from the wrong environment. List the checks you would run before changing any infrastructure.
- After a failed apply, why is a fresh plan usually a better next step than manually editing the state file?
Summary
The Terraform lifecycle commands form a state-aware control loop. init assembles providers and backend settings, validate checks configuration structure, plan calculates a proposed change graph, apply executes a reviewed graph, and destroy removes tracked objects through the same planning engine. Use saved plans, protected state, explicit variables, careful review of replacements and deletes, and verified cleanup to keep infrastructure changes understandable and reversible.
