Component · Apply

terraform apply

Reads the plan, builds an apply-specific graph, calls ApplyResourceChange on each provider in dependency order, and persists the resulting state.

Apply flow diagram
ApplyCommand.Run()  [command/apply.go:27]
  │
  ├─► If plan file provided:
  │     planfile.OpenWrapped(path)  →  *planfile.WrappedPlanFile
  │       Contains: *plans.Plan, config snapshot, prior state
  │
  ├─► PrepareBackend(planFile, args.State, args.ViewType)  [apply.go:195]
  │     Verifies backend config matches plan's embedded backend
  │
  ├─► OperationRequest(be, view, …)  [apply.go:244]
  │     Attaches plan file to operation request
  │
  └─► be.RunOperation(ctx, opReq)
        │
        └─► (local backend) runOperationWithConfig()
              │
              └─► Context.Apply(plan, config, opts)
                    [context_apply.go:81]
                    │
                    ├─► applyGraph(plan, config, opts, validate)
                    │     [context_apply.go:300]
                    │     Apply-specific graph builder:
                    │       NodeApplyableResourceInstance (create/update)
                    │       NodeDestroyResourceInstance   (delete)
                    │       NodeApplyableOutput
                    │       NodeApplyableModuleCallOutput
                    │       ProviderTransformer  (init/close lifecycle)
                    │       ReferenceTransformer
                    │       DestroyEdgeTransformer
                    │
                    ├── Walk — Apply Phase (concurrent, ≤10 goroutines)
                    │     NodeApplyableResourceInstance.Execute():
                    │       1. Look up ResourceInstanceChange in plan
                    │       2. ctx.Provider(providerAddr)  →  providers.Interface
                    │       3. providers.Interface.ApplyResourceChange(
                    │              PlannedState: change.After,
                    │              Config:       evaluated config body,
                    │              PriorState:   change.Before,
                    │              PlannedPrivate: change.Private)
                    │          →  ApplyResourceChangeResponse{NewState, Private}
                    │       4. SyncState.SetResourceInstanceCurrent(
                    │              addr, newObj, providerAddr)
                    │
                    ├── NodeDestroyResourceInstance.Execute():
                    │     providers.Interface.ApplyResourceChange(
                    │         PlannedState: cty.NullVal,   ← signals destroy
                    │         PriorState:   current state)
                    │     SyncState.ForgetResourceInstanceAll(addr)
                    │
                    └─► statemgr.Full.WriteState(finalState)
                          Backend-specific persistence:
                            local:  write terraform.tfstate JSON
                            remote: HTTP PUT to Terraform Cloud / S3 / etc.
ApplyCommand entry
internal/command/apply.go:19–100 GitHub
type ApplyCommand struct {
    Meta
}

func (c *ApplyCommand) Run(rawArgs []string) int {
    // Parse args; detect if a plan file was provided
    args, diags := arguments.ParseApply(rawArgs)
    // ...

    // Try to open a plan file if the last arg looks like one
    planFile, diags := c.LoadPlanFile(args.PlanPath)
    // planFile may be nil if running without a saved plan

    // Set up the backend
    be, beDiags := c.PrepareBackend(planFile, args.State, args.ViewType)
    // ...

    opReq, opDiags := c.OperationRequest(be, view, args.ViewType,
        planFile, args.Operation, args.AutoApprove)
    // ...

    op, err := c.RunOperation(be, opReq)
    // ...
}
Context.Apply — core apply engine
internal/terraform/context_apply.go:81–298 GitHub
// Apply applies the changes described in the plan to the real infrastructure.
// Returns the post-apply state.
func (c *Context) Apply(
    plan *plans.Plan,
    config *configs.Config,
    opts *ApplyOpts,
) (*states.State, tfdiags.Diagnostics) {
    state, _, diags := c.ApplyAndEval(plan, config, opts)
    return state, diags
}

func (c *Context) ApplyAndEval(
    plan *plans.Plan,
    config *configs.Config,
    opts *ApplyOpts,
) (*states.State, *lang.Scope, tfdiags.Diagnostics) {
    // Build the apply graph
    graph, operation, diags := c.applyGraph(plan, config, opts, true)
    // ...

    // Walk the graph — this is where provider calls happen
    walker, walkDiags := c.walk(graph, operation, &graphWalkOpts{
        Config: config,
        InputState: plan.PriorState,
        Changes: plan.Changes,
    })
    diags = diags.Append(walkDiags)

    return walker.State.Close(), walker.EvalScope(nil, nil, nil), diags
}
Apply graph — key node types
internal/terraform/context_apply.go:300 GitHub
func (c *Context) applyGraph(
    plan *plans.Plan,
    config *configs.Config,
    opts *ApplyOpts,
    validate bool,
) (*Graph, walkOperation, tfdiags.Diagnostics) {
    // Selects ApplyGraphBuilder or DestroyPlanGraphBuilder
    // based on plan.UIMode
}

Node types in the apply graph

  • NodeApplyableResourceInstance — handles create and update. Calls ApplyResourceChange then writes the response into SyncState.
  • NodeDestroyResourceInstance — handles delete. Calls ApplyResourceChange with a null proposed state, then removes the instance from SyncState.
  • NodeApplyableOutput — evaluates output expressions and records them in the state.
  • NodeApplyableModuleCallOutput — passes module output values back to the calling module's scope.
  • nodeModuleExpand — handles count / for_each expansion by registering instance keys with the instances.Expander.

Create-before-destroy: When a resource requires replacement (the plan contains Action: Replace), the apply graph inserts both a create node and a destroy node with the create running first. The old instance is temporarily stored in state as a "deposed" instance until the destroy completes.

State persistence after apply

After the graph walk completes, the final *states.State is flushed from SyncState and written through the statemgr.Full interface. The backend implementation decides how state is stored.

internal/states/statemgr/full.go — statemgr.Full interface GitHub
// Full combines all state management operations.
type Full interface {
    Reader
    Writer
    Refresher
    Persister
    Locker   // optional — backends that support locking
}

type Writer interface {
    WriteState(*states.State) error
}

type Persister interface {
    PersistState(*schemas.Schemas) error
}

// Locker provides advisory locking to prevent concurrent applies.
type Locker interface {
    Lock(info *LockInfo) (string, error)
    Unlock(id string) error
}

Backend implementations

  • Local — writes terraform.tfstate (or the path from -state) as JSON
  • Terraform Cloud / Enterprise — PUT to the workspace state API
  • S3PutObject to the configured bucket/key; DynamoDB for locking
  • GCS, AzureRM, Consul, … — each implements statemgr.Full

Partial failure safety: State is written after each successful resource apply, not only at the end. If Terraform crashes mid-apply, the last-written state still reflects all resources that were successfully applied, so a subsequent terraform apply can pick up where it left off.

Backend interface
internal/backend/backend.go:44 GitHub
// Backend defines the interface for Terraform backends.
// Backends handle state storage and optionally remote operations.
type Backend interface {
    // ConfigSchema returns the schema for the backend's configuration.
    ConfigSchema() *configschema.Block

    // PrepareConfig validates configuration before Configure is called.
    PrepareConfig(cty.Value) (cty.Value, tfdiags.Diagnostics)

    // Configure initializes the backend with the given configuration.
    Configure(cty.Value) tfdiags.Diagnostics

    // StateMgr returns a state manager for the given workspace.
    StateMgr(workspace string) (statemgr.Full, tfdiags.Diagnostics)

    // DeleteWorkspace removes the given workspace and its state.
    DeleteWorkspace(name string, force bool) tfdiags.Diagnostics

    // Workspaces returns the list of existing workspaces.
    Workspaces() ([]string, tfdiags.Diagnostics)
}

// OperationsBackend extends Backend with support for remote plan/apply.
type OperationsBackend interface {
    Backend
    // RunOperation executes a plan or apply in the backend's context.
    RunOperation(context.Context, *backendrun.Operation) (*backendrun.RunningOperation, error)
}