Terraform

Understanding the Terraform Lifecycle: Plan, Apply, and Destroy

AGAnurag Gupta05 Apr 2025 Β· Updated 04 Oct 2026 Β· 7 min read
Understanding the Terraform Lifecycle: Plan, Apply, and Destroy

Quick answer: The Terraform lifecycle is the sequence every configuration goes through: terraform init prepares the working directory, terraform plan calculates what will change, terraform apply makes those changes and records them in state, and terraform destroy removes everything when you are done. A separate lifecycle block inside a resource lets you fine-tune how that resource behaves during these phases β€” for example preventing accidental deletion or creating a replacement before destroying the original.

This guide walks through each phase with the commands and flags professionals use, then covers the four lifecycle meta-arguments β€” create_before_destroy, prevent_destroy, ignore_changes and replace_triggered_by β€” with examples, and finishes with the mistakes that most often turn a routine apply into an incident.

The four phases at a glance

Phase Command What happens Touches the cloud?
Initialise terraform init Downloads providers and modules, configures the backend, writes the lock file Only to reach the state backend
Plan terraform plan Refreshes state, compares with code, prints a diff Read-only API calls
Apply terraform apply Creates, updates and deletes resources, writes new state Yes
Destroy terraform destroy Deletes every resource in state in reverse dependency order Yes

Two helpers sit alongside these: terraform fmt standardises formatting and terraform validate checks syntax and types offline. Run both before every commit.

Phase 1: Initialise

terraform init must run before anything else. It downloads each provider listed in required_providers into .terraform/providers/, fetches any modules referenced by source, and configures the backend where state will live. It also writes .terraform.lock.hcl, which pins exact provider versions so every teammate and CI runner uses identical plugins. Re-run it whenever you add a provider or module, change a version constraint (with -upgrade), or switch backends (with -migrate-state).

Phase 2: Plan

terraform plan does three things in order. First it refreshes, asking each provider for the current attributes of every resource in state so drift from console edits is detected. Second it evaluates your configuration. Third it diffs the two and prints each proposed action with +, ~, - or -/+.

# Save the plan so the apply is exactly what was reviewed
terraform plan -out=tfplan

# Plan only a destroy, to see what would be removed
terraform plan -destroy

# Skip the refresh step on huge states to save time (use carefully)
terraform plan -refresh=false

# Force one resource to be replaced even if nothing changed
terraform plan -replace=aws_instance.web

The -replace flag superseded the older terraform taint command in Terraform 0.15.2. In CI pipelines, teams post the plan output into the pull request so reviewers approve infrastructure changes with the same rigour as application code. The standalone terraform refresh command is also deprecated: use terraform apply -refresh-only to update state to match reality without changing infrastructure, for example after someone fixed something by hand during an outage.

Phases 3 and 4: Apply and destroy

terraform apply runs a plan, asks for confirmation, then executes it. If you pass a saved plan file β€” terraform apply tfplan β€” it skips the confirmation and the recalculation, guaranteeing that what was reviewed is what runs. During apply Terraform holds a state lock so a second engineer cannot apply at the same time. When every operation completes it writes the updated state and prints your output values. If an operation fails, the successful ones are still recorded, and a second apply resumes from that point.

terraform destroy is simply an apply whose plan deletes everything in state. It walks the dependency graph backwards β€” instances before subnets, subnets before VPCs β€” so you do not hit “resource in use” errors. Use it for ephemeral environments such as review stacks and training labs. To remove a single resource from a long-lived environment, delete its block and run a normal apply; from Terraform 1.7 a removed block with destroy = false lets you stop managing a resource without deleting it.

The lifecycle block: customising resource behaviour

Inside any resource, a lifecycle block changes how Terraform treats that resource during plan and apply. There are four meta-arguments:

resource "aws_db_instance" "main" {
  identifier        = "tkh-prod-db"
  engine            = "postgres"
  engine_version    = "16.3"
  instance_class    = "db.t4g.medium"
  allocated_storage = 50
  username          = "app"
  password          = var.db_password
  skip_final_snapshot = false

  lifecycle {
    # Refuse any plan that would delete this database
    prevent_destroy = true

    # Storage autoscaling changes this value outside Terraform
    ignore_changes = [allocated_storage]
  }
}

resource "aws_launch_template" "web" {
  name_prefix   = "tkh-web-"
  image_id      = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"

  lifecycle {
    # Build the new template first so the ASG never points at nothing
    create_before_destroy = true
  }
}
  • prevent_destroy – Terraform errors out of any plan that would delete the resource, including a full destroy. Remove the line deliberately when you really mean it. Use on databases, state buckets and KMS keys.
  • ignore_changes – a list of attributes whose drift Terraform should tolerate. Essential when autoscaling, an operator or another tool legitimately modifies something after creation. ignore_changes = all exists but is almost always a mistake.
  • create_before_destroy – reverses replacement order for zero-downtime swaps. Requires that two copies can coexist, so unique names must use name_prefix rather than name.
  • replace_triggered_by (Terraform 1.2+) – forces replacement when a referenced resource or attribute changes, for example replace_triggered_by = [aws_launch_template.web.id] on a worker instance.

Terraform 1.5 added precondition and postcondition blocks inside lifecycle as well, letting you assert facts such as “the AMI must be x86_64” and fail the plan early. One setting often grouped with these lives elsewhere: depends_on is a resource-level meta-argument for ordering between resources that share no attribute reference, covered in Understanding Resource Dependencies and Ordering in Terraform.

Lifecycle mistakes that cause incidents

  1. Applying a fresh plan instead of the reviewed one. Between review and apply, someone else may have changed the code or the cloud. Always apply the saved tfplan file in pipelines.
  2. No prevent_destroy on stateful resources. A renamed block or changed immutable attribute silently plans a -/+ on the production database.
  3. create_before_destroy with a fixed name. The new copy fails with “already exists”. Switch to name_prefix or let the provider generate the name.
  4. ignore_changes = all to silence a noisy plan. You have now disabled drift detection entirely for that resource. Ignore only the specific attributes that are legitimately managed elsewhere.
  5. Running destroy without checking the workspace or backend. terraform workspace show takes five seconds; recreating production takes a weekend.

If you are new to the surrounding workflow of creating and updating individual resources, Creating and Managing Infrastructure Resources in Terraform walks through it step by step.

Frequently asked questions

Is “terraform plan” mandatory before “apply”?

No β€” apply generates its own plan and asks for approval. But running plan separately, saving it and reviewing it is the professional habit, and it is the only way to guarantee the reviewed changes are the applied ones.

What does “refresh” actually change?

Only the state file. A refresh never creates, modifies or deletes infrastructure; it updates Terraform’s record of what already exists so the next plan is accurate.

Can I undo an apply?

There is no built-in rollback. Revert the commit in Git and run apply again. For data-bearing resources, rely on backups and snapshots.

Key takeaways

  • The lifecycle is init, plan, apply, destroy; only the last two change infrastructure.
  • Save plans with -out and apply the file so reviewed and executed changes are identical.
  • Use prevent_destroy on anything irreplaceable and create_before_destroy on anything serving traffic.
  • ignore_changes should list specific attributes; all turns off drift detection.

Want to run the full Terraform lifecycle against real AWS and Azure environments, with CI pipelines and safe production practices built in? Our DevOps course covers Terraform end to end with live projects, mentor support and placement assistance. Prefer video? Follow along on our YouTube channel.

AG
Written byAnurag Gupta

Part of the Techknowledgehub team of industry mentors, writing practical guides to help you build a job-ready tech career.

More articles by Anurag Gupta β†’
Keep reading

Related articles

Leave a Reply