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 --> AddrsRequest 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 codeThe 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:
- A graph builder (
internal/terraform/graph_builder_plan.go,graph_builder_apply.go,graph_builder_init.go,graph_builder_eval.go) constructs aterraform.Graphby running a sequence ofGraphTransformerimplementations. - A graph walker (
internal/terraform/graph_walk_context.go) visits each vertex in topological order, callingExecute(EvalContext, walkOperation)on vertices that implementGraphNodeExecutable. - Vertex implementations (the
node_*.gofiles ininternal/terraform/) hold the per-resource / per-output / per-module / per-provider logic. There are dozens —NodePlannableResourceInstance,NodeApplyableResourceInstance,NodeDestroyResourceInstance,nodeProviderConfig,NodeLocal,NodeOutput, etc. - 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 frominternal/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.protoandtfplugin6.proto. The host side lives ininternal/plugin/(protocol v5) andinternal/plugin6/. Provider installation, including signature verification and the lock file, is ininternal/getproviders/andinternal/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; thelocal,remote, andcloudbackends additionally implementbackendrun.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.
Where to read next
- CLI / command package — how flags and subcommands are wired together.
- Configuration loading — the path from
.tffiles to aconfigs.Configtree. - Terraform Core engine — graph builders, transforms, and vertex evaluation.
- State and Plans — the in-memory and on-disk representations of the two main artifacts.
- Plugin protocol — how Terraform talks to providers.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.