Open-Source Wikis

/

Terraform

/

Terraform

/

Architecture

hashicorp/terraform

Architecture

Terraform Core's job, on every terraform plan or terraform apply, is to take three inputs — a configuration, a prior state, and a set of variable values — and produce a plan (a description of changes) which it can then execute against provider plugins. The architecture of the codebase is shaped around that pipeline.

Top-level component map

graph TD
    subgraph CLI["CLI layer"]
        Main["main.go / commands.go<br/>cli.CLI dispatch"]
        CmdPkg["internal/command<br/>per-subcommand handlers"]
    end

    subgraph Config["Configuration"]
        Configs["internal/configs<br/>HCL → typed model"]
        ConfigLoad["internal/configs/configload<br/>module installer"]
    end

    subgraph Backends["Backends"]
        BInit["internal/backend/init<br/>name → constructor"]
        Local["internal/backend/local"]
        Remote["internal/backend/remote-state/*<br/>S3, GCS, Azure, K8s, HTTP, …"]
        Cloud["internal/backend/remote<br/>+ internal/cloud (HCP Terraform)"]
    end

    subgraph Core["Terraform Core engine"]
        Ctx["terraform.Context"]
        GB["graph builders<br/>(plan, apply, eval, init)"]
        Walk["graph walk<br/>(internal/dag)"]
        Eval["EvalContext / nodes<br/>per-vertex Execute()"]
    end

    subgraph Plugins["Plugin host"]
        Plugin["internal/plugin, internal/plugin6"]
        GetProv["internal/getproviders<br/>installer + registry client"]
    end

    subgraph DataModel["Domain model"]
        Addrs["internal/addrs"]
        States["internal/states (+ statemgr, statefile)"]
        Plans["internal/plans (+ planfile)"]
        Lang["internal/lang (HCL eval, funcs)"]
    end

    Main --> CmdPkg
    CmdPkg --> Configs
    Configs --> ConfigLoad
    CmdPkg --> BInit
    BInit --> Local
    BInit --> Remote
    BInit --> Cloud
    Local --> Ctx
    Cloud --> Ctx
    Ctx --> GB
    GB --> Walk
    Walk --> Eval
    Eval --> Lang
    Eval --> Plugin
    Plugin --> GetProv
    Eval --> States
    Eval --> Plans
    GB --> Addrs
    Eval --> Addrs

Request flow: terraform plan

The journey of a single terraform plan invocation:

sequenceDiagram
    autonumber
    actor User
    participant Main as main.go
    participant Plan as command.PlanCommand
    participant Backend as backend.Backend
    participant Local as local.Local
    participant Ctx as terraform.Context
    participant GB as graph builder
    participant Walk as dag.AcyclicGraph.Walk
    participant Provider as provider plugin (gRPC)

    User->>Main: terraform plan
    Main->>Main: load CLI config, build provider source
    Main->>Plan: Run(args)
    Plan->>Plan: parse flags → backendrun.Operation
    Plan->>Backend: select via internal/backend/init
    Backend-->>Local: wrap if not an OperationsBackend
    Local->>Ctx: build with config + prior state
    Ctx->>GB: Plan() → PlanGraphBuilder.Build()
    GB->>GB: apply graph transforms
    GB-->>Ctx: terraform.Graph
    Ctx->>Walk: AcyclicGraph.Walk(visit)
    loop for each vertex in DAG order
        Walk->>Ctx: Execute()
        Ctx->>Provider: PlanResourceChange (gRPC)
        Provider-->>Ctx: planned change
    end
    Ctx-->>Local: plans.Plan
    Local-->>Plan: render diff via views
    Plan-->>User: textual diff, exit code

The apply flow is the same shape but the graph is built from the plan's changes (not the configuration), and PlanResourceChange is replaced with ApplyResourceChange.

Three layers, three vocabularies

Terraform Core has three distinct internal layers, and contributors confuse them often.

Layer Lives in Talks about Lifetime
CLI / command commands.go, internal/command/ flags, working directory, UI streams, command.Meta one process invocation
Backend / operation internal/backend/, internal/cloud/ workspaces, state storage, where work runs one operation (plan, apply, refresh, …)
Core engine internal/terraform/, internal/configs/, internal/states/, internal/plans/ graphs, vertices, resource instance addresses, change sets one graph walk

When debugging a problem, the first question is always: which layer is this? A bug in flag parsing belongs in command/. A bug in how state migrates between backends belongs in internal/command/meta_backend_migrate.go. A bug in how a for_each expands belongs in internal/terraform/eval_for_each.go.

The graph engine

Most of the interesting behavior of Terraform — dependency ordering, parallelism, dynamic resource expansion via count/for_each, planning vs applying vs destroying — is implemented as variations on one pattern:

  1. A graph builder (internal/terraform/graph_builder_plan.go, graph_builder_apply.go, graph_builder_init.go, graph_builder_eval.go) constructs a terraform.Graph by running a sequence of GraphTransformer implementations.
  2. A graph walker (internal/terraform/graph_walk_context.go) visits each vertex in topological order, calling Execute(EvalContext, walkOperation) on vertices that implement GraphNodeExecutable.
  3. Vertex implementations (the node_*.go files in internal/terraform/) hold the per-resource / per-output / per-module / per-provider logic. There are dozens — NodePlannableResourceInstance, NodeApplyableResourceInstance, NodeDestroyResourceInstance, nodeProviderConfig, NodeLocal, NodeOutput, etc.
  4. The EvalContext (internal/terraform/eval_context_builtin.go) gives each vertex coordinated access to providers, the in-progress state (states.SyncState), the in-progress plan (plans.ChangesSync), and the expression evaluator from internal/lang.

Sub-graphs are how Terraform handles things that can't be known statically. A resource with count = var.size becomes one vertex in the main graph; when that vertex executes, it dynamically expands into a sub-graph with one instance vertex per element. This is the GraphNodeDynamicExpandable pattern.

Pluggability boundaries

Terraform Core is designed around three plug-in surfaces:

  • Providers. Resource and data-source implementations, packaged as separate binaries. Communication is over gRPC using the schemas in docs/plugin-protocol/tfplugin5.proto and tfplugin6.proto. The host side lives in internal/plugin/ (protocol v5) and internal/plugin6/. Provider installation, including signature verification and the lock file, is in internal/getproviders/ and internal/depsfile/.
  • Backends. Where state lives and (sometimes) where operations execute. New backends register a constructor in internal/backend/init/init.go. Most backends only implement state storage; the local, remote, and cloud backends additionally implement backendrun.OperationsBackend.
  • Provisioners. A small, deprecated extension point for running scripts on remote machines. The built-ins live in internal/builtin/provisioners/. The contribution policy explicitly discourages new provisioner work.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Architecture – Terraform wiki | Factory