Skip to main content

What is an Execution Plan?

An execution plan is Terraform’s proposal for what changes need to be made to bring your infrastructure from its current state to match your configuration. The plan is created during terraform plan and executed during terraform apply.
A key design tenet of Terraform is that any action with externally-visible side-effects must go through the plan-and-apply workflow. This ensures predictability and enables review before changes are made.Source: docs/planning-behaviors.md

The Plan Data Structure

A plan is represented by the plans.Plan struct, which contains:
type Plan struct {
    // The set of changes to be applied
    Changes *ChangesSrc
    
    // Resources that drifted from expected state
    DriftedResources []*ResourceInstanceChangeSrc
    
    // Previous state before refresh
    PrevRunState *states.State
    
    // Current state after refresh
    PriorState *states.State
    
    // Variable values used during planning
    VariableValues map[string]DynamicValue
    
    // Targeting and filtering
    TargetAddrs []addrs.Targetable
    ForceReplaceAddrs []addrs.AbsResourceInstance
    
    // Plan metadata
    Timestamp time.Time
    Complete bool
    Applyable bool
}
The plan structure includes both the changes to make (Changes) and the context in which those changes were planned (PriorState, VariableValues, etc.).

Planning Process

The planning process follows these steps, orchestrated by context_plan.go:
1

Load Configuration

Parse configuration files and build the configuration tree.
2

Refresh State

Read current state of resources from the remote system to detect drift.This produces PriorState by calling ReadResource on providers.
3

Build Plan Graph

Construct a dependency graph for the plan operation using PlanGraphBuilder.
4

Walk the Graph

Execute each vertex in dependency order, calling provider PlanResourceChange for each resource.
5

Collect Changes

Aggregate all planned changes into the Changes structure.
6

Return Plan

Package everything into a Plan object.

Action Types

Terraform supports several action types for resource instances:
ActionSymbolDescriptionWhen It Occurs
NoOpNo changes neededResource matches configuration
Create+Create new resourceResource doesn’t exist in state
ReadRefresh data sourceData source needs reading
Update~Update in-placeResource exists but attributes differ
Delete-Destroy resourceResource in state but not in config
Replace-/+ or +/-Delete then create, or create then deleteResource requires replacement
Implementation: plans/action.go

Replace Actions

Replace is a “meta-action” that decomposes into separate Create and Delete operations:
1. Delete existing resource
2. Create new resource at same address
Use when: Resource can tolerate brief downtimeRisk: If creation fails, resource is gone
Create-before-delete requires that two instances of the resource can exist simultaneously. This may not be possible if there are uniqueness constraints (like names or IP addresses).

Default Planning Behavior

When given no special instructions, Terraform’s planning logic (from docs/planning-behaviors.md) automatically proposes:

Create

When:
  • A resource block in configuration has no corresponding resource in state
  • A resource block’s count/for_each includes a new instance key

Delete

When:
  • A resource in state has no corresponding resource block in configuration
  • A resource block’s count/for_each no longer includes an instance key from state

Update

When:
  • Resource exists in both configuration and state
  • Provider detects differences that aren’t just normalization
  • Resource is not marked as “tainted”

Replace

When:
  • Resource exists in both configuration and state
  • Resource is marked as “tainted” (previous apply failed)
  • Or provider indicates attributes require replacement

Special Planning Behaviors

Terraform supports three categories of special planning behaviors:

1. Configuration-Driven Behaviors

Specified in the Terraform configuration by module authors:
resource "aws_instance" "web" {
  # ... configuration ...
  
  lifecycle {
    # Ignore external changes to tags
    ignore_changes = [tags]
    
    # Force replacement when security group changes
    replace_triggered_by = [aws_security_group.web]
    
    # Use create-before-destroy
    create_before_destroy = true
    
    # Prevent accidental deletion
    prevent_destroy = true
  }
}

# Document state refactoring
moved {
  from = aws_instance.old_name
  to   = aws_instance.web
}
Configuration-driven behaviors are ideal when the behavior relates to a specific module and should apply for anyone using that module.

2. Provider-Driven Behaviors

Activated by providers during PlanResourceChange:
  • Forced replacement - Provider indicates certain attribute changes require replace
  • Normalization - Provider returns prior state value instead of config value for equivalent serializations
  • Computed values - Provider sets unknown values that will be determined during apply
// Provider response during planning
PlanResourceChange {
    // Indicate these attributes require replacement
    RequiresReplace: ["availability_zone", "instance_type"]
    
    // Return proposed new state with unknowns
    PlannedState: {
        id: Unknown,  // Will be known after create
        public_ip: Unknown,
        // ...
    }
}
Source: docs/planning-behaviors.md

3. Single-Run Behaviors

Activated via command-line flags:
# Force replacement of specific resources
terraform plan -replace=aws_instance.web

# Target specific resources
terraform plan -target=aws_vpc.main

# Refresh-only mode (no changes, just update state)
terraform plan -refresh-only

# Destroy everything
terraform plan -destroy
Single-run behaviors require custom support in any wrapper around Terraform Core. Use sparingly and document when you do.

Resource Instance Change Lifecycle

The full lifecycle of planning and applying a resource change involves multiple provider calls:
PlanResourceChange is called twice: once during planning (with potentially unknown config values) and once during apply (with fully known values).Source: docs/resource-instance-change-lifecycle.md

Provider Planning Contracts

When providers implement PlanResourceChange, they must follow strict contracts:
Any attribute that was non-null in configuration must either:
  • Preserve the exact configuration value, OR
  • Return the prior state value (for normalization only)
Never change user-provided values to something else.
Attributes marked as computed in the schema and null in configuration may be set to any appropriate value, or left unknown.
If a planned attribute has a known value, that exact value must appear in the state returned by ApplyResourceChange.Use unknown values when the final value will be determined during apply.
When PlanResourceChange is called the second time during apply:
  • Known values from initial plan must remain unchanged
  • Unknown values may become known or stay unknown
Source: docs/resource-instance-change-lifecycle.md

Plan Files

Plans can be saved to disk for later execution:
# Create and save plan
terraform plan -out=tfplan

# Apply the saved plan
terraform apply tfplan

Plan File Contents

A plan file (in internal/plans/planfile) contains:
  • The complete set of proposed changes
  • Prior state snapshots (PrevRunState and PriorState)
  • Variable values used during planning
  • Provider configurations
  • Terraform version information
Plan files may contain sensitive data (passwords, tokens, etc.). Store them securely and never commit to version control.

Drift Detection

Terraform detects drift during the refresh phase:
# Show drift without planning changes
terraform plan -refresh-only
Drifted resources appear in the plan output:
Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform
since the last "terraform apply":

  # aws_instance.web has changed
  ~ resource "aws_instance" "web" {
        id            = "i-1234567890abcdef0"
      ~ tags          = {
          ~ "Environment" = "dev" -> "production"
        }
    }
Drift is tracked separately in Plan.DriftedResources.

Plan Quality

Plans have a quality indicator (from plans/quality.go):
  • Complete - Plan includes all resources
  • Partial - Some resources couldn’t be planned (but plan is still applyable)
  • Errored - Planning failed, plan is not applyable

Plan Metadata

Every plan includes metadata:
type Plan struct {
    // When the plan was created
    Timestamp time.Time
    
    // Is this a complete plan?
    Complete bool
    
    // Can this plan be applied?
    Applyable bool
    
    // Did planning fail?
    Errored bool
    
    // What mode was used? (normal, destroy, refresh-only)
    UIMode Mode
}

Working with Plans in Code

Creating a plan in Terraform Core:
// Create context
ctx, diags := terraform.NewContext(&terraform.ContextOpts{
    Providers: providers,
    // ... other options
})

// Run plan operation
plan, diags := ctx.Plan(config, state, &terraform.PlanOpts{
    Mode: plans.NormalMode,
    SetVariables: variables,
})

// Check if plan is applyable
if plan.Applyable {
    // Save to file
    planfile.Create("tfplan", plan, config, state)
}
Source: internal/terraform/context_plan.go

Why Plans Matter

Safety

Review changes before applying prevents accidental destruction or misconfiguration.

Predictability

Know exactly what will change, enabling informed decisions.

Audit Trail

Plans can be saved and reviewed later to understand what changed and why.

Automation

Enable automated approval workflows based on plan analysis.

Best Practices

Never run terraform apply without first reviewing the plan. Use terraform plan to preview changes.
For production changes, save plans to files and review them before applying:
terraform plan -out=prod.tfplan
# Review the plan
terraform apply prod.tfplan
If Terraform plans to replace resources unexpectedly, investigate why before applying. It may indicate:
  • Provider bug
  • Incorrect attribute value
  • Missing lifecycle rules
When drift is detected, decide whether to:
  • Import the changes into configuration
  • Restore the original state
  • Accept and ignore the drift (ignore_changes)

Next Steps

Resource Graph

Learn how Terraform determines the order of operations in a plan

State Management

Understand how state is used during planning

References

Build docs developers (and LLMs) love