Component · 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.
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
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)
// ...
}
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))
}
}
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
}
variable {}locals {} valuesNodePlannableResourceInstance for each resource {}data {} blocksNodeApplyableOutput nodes-target is specifiedinternal/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.
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
}
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.