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 fulldestroy. 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 = allexists 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 usename_prefixrather thanname.replace_triggered_by(Terraform 1.2+) β forces replacement when a referenced resource or attribute changes, for examplereplace_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
- 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
tfplanfile in pipelines. - No
prevent_destroyon stateful resources. A renamed block or changed immutable attribute silently plans a-/+on the production database. create_before_destroywith a fixedname. The new copy fails with “already exists”. Switch toname_prefixor let the provider generate the name.ignore_changes = allto silence a noisy plan. You have now disabled drift detection entirely for that resource. Ignore only the specific attributes that are legitimately managed elsewhere.- Running
destroywithout checking the workspace or backend.terraform workspace showtakes 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
-outand apply the file so reviewed and executed changes are identical. - Use
prevent_destroyon anything irreplaceable andcreate_before_destroyon anything serving traffic. ignore_changesshould list specific attributes;allturns 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.



