The lifecycle block is a nested meta-argument available on any resource that overrides Terraform's default behavior for how it handles replacement, deletion, and drift on specific attributes.
create_before_destroy
resource "aws_launch_configuration" "app" {
name_prefix = "app-"
image_id = var.ami_id
instance_type = "t3.micro"
lifecycle {
create_before_destroy = true
}
}
By default, when a resource must be replaced (a ForceNew attribute changed), Terraform destroys the old one first, then creates the new one. For resources where a gap in availability matters — a launch configuration referenced by an auto-scaling group, a DNS record, a load balancer target — that ordering can cause an outage. create_before_destroy = true flips the order: create the replacement first, then destroy the old one, so there's overlap instead of a gap. (Note: this often requires the resource's name to not collide, hence name_prefix instead of a fixed name above.)
prevent_destroy
resource "aws_db_instance" "prod" {
# ...
lifecycle {
prevent_destroy = true
}
}
Makes any plan that would destroy this resource — including via terraform destroy, removing the resource block, or a count/for_each change that would eliminate this instance — fail outright with an error, rather than actually destroying it. A guardrail for irreplaceable resources like production databases, where an accidental destroy would be catastrophic.
ignore_changes
resource "aws_instance" "web" {
# ...
lifecycle {
ignore_changes = [tags, ami]
}
}
Tells Terraform to stop reporting (and stop trying to reconcile) drift on the listed attributes. Useful when something else legitimately manages that field outside of Terraform — e.g., an autoscaling process that updates desired_capacity, or a tagging-automation Lambda that adds cost-allocation tags Terraform doesn't know about. Without ignore_changes, every plan would show a spurious diff trying to revert that external change.
Interview-ready summary
All three are ways of telling Terraform "don't apply your default replace/destroy/drift-correction behavior here" — used for availability (create_before_destroy), safety (prevent_destroy), and coexistence with external processes (ignore_changes).
Related Resources
Provisioners let a resource run an action (typically a script) as part of its creation or destruction, but they sit awkwardly outside Terraform's core declarative model — which is exactly why HashiCorp's own documentation recommends using them only when nothing else will do.
The three main provisioner types
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t3.micro"
provisioner "file" {
source = "app.conf"
destination = "/etc/app/app.conf"
}
provisioner "remote-exec" {
inline = ["sudo systemctl restart app"]
}
provisioner "local-exec" {
command = "echo ${self.private_ip} >> inventory.txt"
}
}
filecopies a file to the new resource over SSH/WinRM.remote-execruns commands on the newly created resource.local-execruns a command on the machine running Terraform itself.
Why they're a "last resort"
- Outside the state model. A provisioner's success/failure isn't tracked as a first-class part of resource state the way create/update/destroy is — if a provisioner fails partway through, the resource may be left in a state Terraform considers "created" but that's actually broken, and there's no clean automatic retry/rollback.
- Not part of the plan.
terraform plancan't preview what a provisioner will do — it's opaque imperative script execution bolted onto an otherwise declarative diff. - Coupled to reachability at apply time.
remote-exec/filerequire SSH/WinRM connectivity to the new resource at the exact momentapplyruns, which introduces network/credential dependencies that have nothing to do with whether the infrastructure itself was created correctly. - Not idempotent by default — re-running
applydoesn't naturally re-run provisioners unless the resource itself is replaced, unlike a proper configuration-management tool designed around repeated convergence.
What to use instead
- Cloud-init / user-data for initial instance bootstrapping — declarative, provider-native, and doesn't require live SSH access during
apply. - A custom machine image (built with Packer) that already has everything baked in, so there's no post-create configuration step at all.
- A dedicated configuration-management tool (Ansible, Chef) run as a separate, purpose-built step after provisioning.
Provisioners remain a legitimate escape hatch for genuine gaps (odd one-off bootstrapping with no provider-native equivalent), but reaching for them by default is a common anti-pattern interviewers will probe for.
Related Resources
Both mechanisms exist to force Terraform to replace a resource that its normal plan/diff logic wouldn't otherwise flag for replacement — but they differ significantly in visibility and modern support.
terraform taint (deprecated)
terraform taint aws_instance.web
This marked a resource in state as tainted, meaning "the next plan should destroy and recreate this" — but the marking itself happened immediately, as a direct state mutation, completely outside of any reviewable plan. You'd only see the consequence (a forced replace) on the next terraform plan, and there was no way to preview the replace in the same step you requested it. It's deprecated in modern Terraform versions in favor of the option below.
terraform apply -replace (modern approach)
terraform apply -replace="aws_instance.web"
or, to preview first:
terraform plan -replace="aws_instance.web"
This forces the replacement of a specific resource as part of a normal, reviewable plan — the plan output explicitly shows the destroy+create for that resource (marked -/+) alongside anything else that would change, and you approve it the same way as any other apply. Nothing happens silently; the forced replacement is visible in the same plan output you'd review for any other change.
When you'd use this
- A resource is misconfigured or corrupted in a way that isn't reflected in any attribute Terraform's diff would catch (e.g., a VM with a stuck/corrupted disk that provider APIs still report as "running fine").
- You want to force a rolling replacement (e.g., to pick up a new AMI baked with the same ID, or to "reset" a resource) without changing any configuration value that would otherwise trigger it.
Interview-ready summary
Both force replacement of a resource outside Terraform's normal diff-driven decision-making, but -replace does it as an explicit, visible part of the standard plan/apply flow, while the older taint command mutated state directly and only showed its effect on a subsequent, separate plan — which is exactly why it was deprecated in favor of the more transparent -replace flag.
Related Resources
Terraform doesn't apply resources in the order they're written in a file — it builds a directed acyclic graph (DAG) from every resource, data source, and module, and uses that graph to determine both correctness (what must happen before what) and performance (what can happen in parallel).
Implicit dependencies (the common case)
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id # <- this reference creates the dependency
cidr_block = "10.0.1.0/24"
}
Simply referencing aws_vpc.main.id inside the subnet's configuration is enough — Terraform parses the expression, sees the reference, and automatically adds an edge in the graph: "subnet depends on vpc." No extra syntax needed, and this is how the vast majority of dependencies in real configurations are expressed. It's also self-documenting — anyone reading the subnet resource can see exactly why it depends on the VPC.
Explicit dependencies (depends_on)
Sometimes a real dependency exists that no resource attribute reflects. Classic example: an application needs an IAM policy attached before it starts, but the compute resource's configuration doesn't reference the policy resource at all:
resource "aws_iam_role_policy" "app_permissions" {
# ...
}
resource "aws_instance" "app" {
# no attribute here references aws_iam_role_policy.app_permissions,
# yet the app must not start until that policy exists.
depends_on = [aws_iam_role_policy.app_permissions]
}
depends_on forces the ordering edge explicitly, even without any data flowing between the two resources.
Why prefer implicit dependencies
- They're derived directly from real data flow, so they can never silently go stale the way a
depends_onlist can (nothing stops someone from refactoring away the reason for adepends_onand forgetting to remove it). - The dependency graph is exactly what enables Terraform to parallelize unrelated resource creation — resources with no path between them in the graph can be created concurrently, which is a major reason large applies aren't purely sequential.
depends_on is a valid, sometimes-necessary escape hatch, but it should be reached for only when there's genuinely no attribute reference that could express the same relationship implicitly.
Related Resources
Understanding exactly what terraform plan does under the hood — and specifically how it chooses between an in-place update and a full destroy/recreate — is a strong signal of real Terraform experience versus surface familiarity.
Step by step
- Refresh (unless
-refresh=false): Terraform queries each managed resource's real current attributes from the provider and updates its in-memory view of state accordingly (this doesn't persist untilapply, in recent Terraform versions this happens as part of the plan itself rather than a separate step). - Graph walk: Terraform walks the dependency graph, and for each resource, diffs the refreshed prior state against the desired configuration.
- Action classification: for every resource, the diff resolves to one of: no-op (no change), create (new resource block, nothing in state yet), update-in-place, or destroy-and-recreate ("replace").
- Output: the plan renders each resource's action with a symbol —
+create,-destroy,~update in place,-/+(or+/-) destroy and recreate.
How update-in-place vs. replace is decided
This comes from the provider's schema, not from Terraform Core's own logic. Every attribute a resource type exposes is annotated by the provider as either:
- Updatable in place — the cloud API supports a "modify" operation for this attribute (e.g., changing an EC2 instance's
tags, or a security group's description). Terraform issues an update call, and the resource keeps its identity (same ID). ForceNew— the underlying API has no way to modify this attribute on an existing object (e.g., an EC2 instance'savailability_zone, or an RDS instance'sengine). Any change to aForceNewattribute forces Terraform to destroy the existing resource and create a brand-new one to satisfy the new configuration.
# aws_instance.web must be replaced
-/+ resource "aws_instance" "web" {
~ availability_zone = "us-east-1a" -> "us-east-1b" # forces replacement
instance_type = "t3.micro"
}
Why this matters practically
Recognizing which attributes are ForceNew for a given resource type (documented in each provider's resource reference) helps you predict — before ever running plan — whether a given configuration change will cause a disruptive replace or a safe in-place update, which is critical for reasoning about downtime risk before touching production infrastructure.