What happens during `terraform plan`, and how does it decide between update-in-place vs. destroy-and-recreate?

5 minadvancedterraformplanresource-lifecycle

Quick Answer

`plan` refreshes in-memory state against real infrastructure (unless `-refresh=false`), walks the dependency graph, and for each resource diffs its configuration against state to classify the action as no-op, create, update-in-place, or destroy-and-recreate. The choice between update-in-place and replace comes from each provider's schema: every argument is marked either updatable in place or `ForceNew` — changing a `ForceNew` attribute (e.g., an EC2 instance's `availability_zone`) forces destroy+create, while changing a mutable attribute (e.g., `tags`) triggers an in-place API update. This is why the plan output explicitly marks each resource with `~` (update), `-/+` (replace), `+` (create), or `-` (destroy).

Detailed Answer

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

  1. 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 until apply, in recent Terraform versions this happens as part of the plan itself rather than a separate step).
  2. Graph walk: Terraform walks the dependency graph, and for each resource, diffs the refreshed prior state against the desired configuration.
  3. 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").
  4. 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's availability_zone, or an RDS instance's engine). Any change to a ForceNew attribute 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.

Related Resources