Component · Plan

terraform plan

Builds a dependency DAG, walks it twice (refresh then plan), and serializes the resulting *plans.Plan to a binary file. No infrastructure is modified.

Plan flow diagram
PlanCommand.Run()  [command/plan.go:22]
  │
  ├─► PrepareBackend(args.State, args.ViewType)  [plan.go:120]
  │     └── Backend.StateMgr(workspace)
  │           └── statemgr.Full.GetState()  →  prior *states.State
  │
  ├─► OperationRequest(be, view, …)  [plan.go:136]
  │     Assembles backendrun.Operation:
  │       ConfigDir, Variables, Targets, Destroy, RefreshOnly, …
  │
  └─► be.RunOperation(ctx, opReq)
        │
        └─► (local backend) runOperationWithConfig()
              │
              └─► Context.Plan(config, prevRunState, opts)
                    [context_plan.go:180]
                    │
                    ├─► planWalk(config, prevRunState, opts)
                    │     [context_plan.go:750]
                    │     │
                    │     ├─► GRAPH BUILD
                    │     │     planGraphBuilder{...}.Build(addrs.RootModuleInstance)
                    │     │     Runs GraphTransformer chain:
                    │     │       RootVariableTransformer
                    │     │       LocalTransformer
                    │     │       ResourceTransformer        ← one node per resource
                    │     │       OrphanResourceTransformer  ← resources in state but not config
                    │     │       DataSourceTransformer
                    │     │       OutputTransformer
                    │     │       ModuleCallTransformer
                    │     │       ProviderTransformer        ← closes provider lifecycle
                    │     │       ReferenceTransformer       ← add edges for references
                    │     │       DestroyEdgeTransformer     ← correct destroy ordering
                    │     │       TargetsTransformer         ← prune for -target
                    │     │       →  *terraform.Graph (DAG, validated for cycles)
                    │     │
                    │     ├─► REFRESH WALK (unless -refresh=false)
                    │     │     c.walk(graph, walkPlanDestroy/walkPlan)
                    │     │     Each NodeAbstractResourceInstance.Execute():
                    │     │       providers.Interface.ReadResource(current state)
                    │     │       →  refreshed *states.ResourceInstanceObjectSrc
                    │     │       Write back to states.SyncState
                    │     │
                    │     └─► PLAN WALK
                    │           c.walk(graph, walkPlan)
                    │           Each NodePlannableResourceInstance.Execute():
                    │             Evaluate config body  →  desired cty.Value
                    │             providers.Interface.PlanResourceChange(
                    │               priorState, config, proposedNewState)
                    │             →  plans.ResourceInstanceChange{Before, After}
                    │             Append to plans.ChangesSync
                    │
                    └─► Serialize: planfile.Writer  →  .tfplan (ZIP archive)
                          Contents: plan proto, config snapshot,
                                    state snapshot, lock file
PlanCommand entry
internal/command/plan.go:18–100 GitHub
type PlanCommand struct {
    Meta
}

func (c *PlanCommand) Run(rawArgs []string) int {
    // Parse raw CLI args into strongly typed arguments struct
    args, diags := arguments.ParsePlan(rawArgs)
    // ...

    // Initialize a backend (local or remote)
    be, beDiags := c.PrepareBackend(args.State, args.ViewType)
    // ...

    // Build operation request with config, vars, targets…
    opReq, opDiags := c.OperationRequest(be, view, args.ViewType,
        args.Operation, args.OutPath, args.GenerateConfigPath)
    // ...

    // Delegate to backend — for local backend this runs Context.Plan()
    op, err := c.RunOperation(be, opReq)
    // ...
}
Context.Plan — core planning engine
internal/terraform/context_plan.go:180 GitHub
// Plan generates an execution plan for the given configuration change.
// prevRunState is the last-known state (after the previous apply or refresh).
func (c *Context) Plan(
    config *configs.Config,
    prevRunState *states.State,
    opts *PlanOpts,
) (*plans.Plan, tfdiags.Diagnostics) {
    plan, _, diags := c.PlanAndEval(config, prevRunState, opts)
    return plan, diags
}

// PlanAndEval additionally returns a lang.Scope for post-plan expression
// evaluation (used by some commands to show computed outputs).
func (c *Context) PlanAndEval(
    config *configs.Config,
    prevRunState *states.State,
    opts *PlanOpts,
) (*plans.Plan, *lang.Scope, tfdiags.Diagnostics) {
    switch opts.Mode {
    case plans.NormalMode:
        return c.plan(config, prevRunState, opts)
    case plans.DestroyMode:
        return c.destroyPlan(config, prevRunState, opts)
    case plans.RefreshOnlyMode:
        return c.refreshOnlyPlan(config, prevRunState, opts)
    default:
        panic(fmt.Sprintf("unsupported plan mode %s", opts.Mode))
    }
}
DAG construction

BasicGraphBuilder applies a sequence of GraphTransformer steps to an initially empty graph. Each transformer adds vertices, edges, or both. After all transformers run, a topological sort validates there are no cycles.

internal/terraform/graph_builder.go:26 GitHub
// BasicGraphBuilder applies each Step in order to the graph,
// then validates for cycles.
type BasicGraphBuilder struct {
    Steps               []GraphTransformer
    Name                string
    SkipGraphValidation bool
}

func (b *BasicGraphBuilder) Build(path addrs.ModuleInstance) (
    *Graph, tfdiags.Diagnostics) {
    g := &Graph{Path: path}
    for _, step := range b.Steps {
        if err := step.Transform(g); err != nil {
            // ...
        }
    }
    // Validate: topological sort to detect cycles
    if !b.SkipGraphValidation {
        if err := g.Validate(); err != nil { ... }
    }
    return g, diags
}

// GraphTransformer is the single-method interface for graph mutations.
type GraphTransformer interface {
    Transform(*Graph) error
}

Key graph transformers (plan)

  • RootVariableTransformer — adds a node for each variable {}
  • LocalTransformer — adds nodes for locals {} values
  • ResourceTransformer — adds NodePlannableResourceInstance for each resource {}
  • OrphanResourceTransformer — adds destroy nodes for resources in state but absent from config
  • DataSourceTransformer — adds nodes for data {} blocks
  • OutputTransformer — adds NodeApplyableOutput nodes
  • ProviderTransformer — connects resources to their provider node; adds init/close lifecycle
  • ReferenceTransformer — adds dependency edges for every HCL expression reference
  • DestroyEdgeTransformer — flips edges so destroy nodes run after dependents
  • TargetsTransformer — prunes the graph when -target is specified
Graph walk — parallel execution
internal/terraform/graph.go:21–37 GitHub
// Graph extends dag.AcyclicGraph with a module-instance path label.
type Graph struct {
    dag.AcyclicGraph
    Path addrs.ModuleInstance
}

// Walk executes all vertices in topological order, running up to
// -parallelism goroutines concurrently. Returns combined diagnostics.
func (g *Graph) Walk(walker GraphWalker) tfdiags.Diagnostics

// GraphWalker drives the walk; ContextGraphWalker is the production impl.
type GraphWalker interface {
    EvalContext() EvalContext
    enterScope(evalContextScope) EvalContext
    exitScope(evalContextScope)
    Execute(EvalContext, GraphNodeExecutable) tfdiags.Diagnostics
}

// Each node that wants to run logic implements GraphNodeExecutable.
type GraphNodeExecutable interface {
    Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics
}

Parallelism: The walk uses a semaphore (default 10) so at most 10 nodes execute simultaneously. A node is only dispatched after all its dependencies complete successfully.

plans.Plan — the output artifact
internal/plans/plan.go:34 GitHub
type Plan struct {
    UIMode             Mode              // NormalMode, DestroyMode, RefreshOnlyMode
    VariableValues     map[string]DynamicValue
    VariableMarks      map[string][]cty.PathValueMarks
    Changes            *ChangesSrc       // resource and output changes
    DriftedResources   []*ResourceInstanceChangeSrc
    DeferredResources  []*DeferredResourceInstanceChangeSrc
    Backend            *Backend          // persisted backend config
    StateStore         *StateStore
    Complete           bool              // true if nothing was deferred
    Applyable          bool              // true if there are meaningful changes
    // ...
}
internal/plans/changes.go:22 — Changes and ResourceInstanceChange GitHub
// Changes is the top-level collection of all planned changes.
type Changes struct {
    Resources         []*ResourceInstanceChange
    Queries           []*QueryInstance
    ActionInvocations ActionInvocationInstances
    Outputs           []*OutputChange
}

// ResourceInstanceChange records what will happen to one resource instance.
type ResourceInstanceChange struct {  // [changes.go:327]
    Addr         addrs.AbsResourceInstance
    PrevRunAddr  addrs.AbsResourceInstance
    ProviderAddr addrs.AbsProviderConfig
    Change       Change               // Before, After cty.Value + Action
    ActionReason ResourceInstanceChangeActionReason
    Private      []byte               // opaque provider metadata
}

// Change carries the Action and the before/after values.
type Change struct {
    Action          Action   // Create, Read, Update, Delete, Replace, NoOp
    Before          cty.Value
    After           cty.Value
    BeforeValMarks  []cty.PathValueMarks
    AfterValMarks   []cty.PathValueMarks
}
Plan file format

The *.tfplan file is a ZIP archive containing several named entries. planfile.Writer produces it; planfile.Reader reads it back for terraform apply.

internal/plans/planfile/ (ZIP contents) GitHub
tfplan              ← protobuf-encoded plans.Plan
tfstate             ← JSON-encoded prior state snapshot
tfconfig/           ← snapshot of all *.tf files
  root.json
  modules/
    vpc.json
    ...
.terraform.lock.hcl ← provider lock file at time of planning

Why embed config and state? Apply reads the plan file in isolation. Embedding the config snapshot guarantees apply uses exactly the same configuration that was planned, even if files changed on disk between plan and apply.