状态管理与后端:持久化、锁定与迁移
前置知识
- ›第 1 篇:架构与代码库导航
- ›对 Terraform 状态概念的基本了解(terraform.tfstate、远程后端)
状态管理与后端:持久化、锁定与迁移
Terraform 状态是配置的声明式意图与真实基础设施之间的桥梁。没有状态,Terraform 就无法知道哪些真实资源对应哪些配置块,无法检测漂移,也无法规划最小化变更。状态子系统的重要性不言而喻,其架构也通过精心的分层设计体现了这一点。
本文将深入探讨三层状态模型、决定状态存储位置的后端抽象,以及 terraform init 执行期间在后端之间进行迁移的完整流程。
内存状态模型
internal/states/state.go#L27-L52 中的 State 结构体是顶层的内存表示:
type State struct {
Modules map[string]*Module
RootOutputValues map[string]*OutputValue
CheckResults *CheckResults
}
Modules 映射的键是模块实例路径(如 module.network 或 module.network[0]),根模块始终存在。每个 Module 包含若干资源映射,每个资源又包含实例映射(用于处理 count 和 for_each)。
classDiagram
class State {
+Modules map[string]*Module
+RootOutputValues map[string]*OutputValue
+CheckResults *CheckResults
+Empty() bool
+Module(addr) *Module
}
class Module {
+Addr ModuleInstance
+Resources map[string]*Resource
+OutputValues map[string]*OutputValue
}
class Resource {
+Addr AbsResource
+Instances map[InstanceKey]*ResourceInstance
+ProviderConfig AbsProviderConfig
}
class ResourceInstance {
+Current *ResourceInstanceObjectSrc
+Deposed map[DeposedKey]*ResourceInstanceObjectSrc
}
State --> Module : contains
Module --> Resource : contains
Resource --> ResourceInstance : contains
ResourceInstance 上的 Deposed 映射值得特别关注。当 Terraform 需要使用 create_before_destroy 替换某个资源时,它会先创建新实例,将旧实例移入"deposed"槽,然后在新实例成功创建后再销毁旧实例。如果 apply 在创建和销毁之间失败,deposed 实例会以随机 DeposedKey 的形式保留在状态中,直到下一次成功的 apply 将其清理。
提示: 如果你在状态中看到"deposed"对象并感到困惑——它们是被中断的
create_before_destroy留下的残留。再次运行terraform apply即可规划并销毁它们。
SyncState:图遍历期间的线程安全访问
正如第 3 篇所介绍的,图遍历会并行执行各顶点。原始的 State 类型明确不是并发安全的。internal/states/sync.go#L36-L40 中的 SyncState 包装器解决了这一问题:
type SyncState struct {
state *State
writable bool
lock sync.RWMutex
}
classDiagram
class SyncState {
-state *State
-writable bool
-lock sync.RWMutex
+Module(addr) *Module
+SetResourceInstanceCurrent(addr, obj, provider)
+RemoveResourceInstanceDeposed(addr, key)
+OutputValue(addr) *OutputValue
+Lock() / Unlock()
}
class State {
+Modules map
+RootOutputValues map
}
SyncState --> State : wraps
每个读取方法在获取 RLock() 后,返回所请求数据的深拷贝。这是一项关键的安全措施——如果图节点直接持有状态数据的引用并在未加锁的情况下进行修改,数据竞争将不可避免。通过返回副本,SyncState 确保各节点可以自由操作本地视图,而不会影响其他并发节点。
写入方法会获取完整的 Lock() 并直接修改底层状态。writable 字段提供了额外的安全检查——在 plan 遍历期间,"上次运行状态"和"刷新状态"视图是只读的,而"计划状态"视图是可写的。
State Manager:持久化与锁定
内存中的 State 需要持久化到某处——本地文件、S3 存储桶、Consul 键值等。statemgr 包定义了一套分层的接口体系:
internal/states/statemgr/filesystem.go#L29-L64 中的 Filesystem 状态管理器是本地实现:
type Filesystem struct {
mu sync.Mutex
path string
readPath string
backupPath string
stateFileOut *os.File
lockID string
created bool
file *statefile.File
readFile *statefile.File
backupFile *statefile.File
writtenBackup bool
}
它实现了 Full 接口——该接口组合了 Reader、Writer、Refresher、Persister 和 Locker。这种分离很有意义:Reader/Writer 负责处理内存中的临时副本,Refresher 从磁盘读取,Persister 写入磁盘。Locker 接口则提供 Lock()/Unlock(),用于防止多个 Terraform 进程并发访问。
classDiagram
class Reader {
<<interface>>
+State() *State
}
class Writer {
<<interface>>
+WriteState(*State) error
}
class Refresher {
<<interface>>
+RefreshState() error
}
class Persister {
<<interface>>
+PersistState(schemas) error
}
class Locker {
<<interface>>
+Lock(LockInfo) (string, error)
+Unlock(string) error
}
class Full {
<<interface>>
}
Reader <|-- Full
Writer <|-- Full
Refresher <|-- Full
Persister <|-- Full
Locker <|-- Full
Full <|.. Filesystem
对于本地后端,锁定使用操作系统级别的文件锁;对于 S3 等远程后端,锁定可能借助 DynamoDB 表来实现。这层抽象确保 Terraform Core 无需关心具体的锁定机制。
后端接口层次结构
后端抽象定义于 internal/backend/backend.go#L44-L106:
type Backend interface {
ConfigSchema() *configschema.Block
PrepareConfig(cty.Value) (cty.Value, tfdiags.Diagnostics)
Configure(cty.Value) tfdiags.Diagnostics
StateMgr(workspace string) (statemgr.Full, tfdiags.Diagnostics)
DeleteWorkspace(name string, force bool) tfdiags.Diagnostics
Workspaces() ([]string, tfdiags.Diagnostics)
}
这是基础接口——每个后端都必须知道如何存储和检索状态。但在 internal/backend/backendrun/operation.go#L38-L53 中还定义了一个功能更强大的接口:
type OperationsBackend interface {
backend.Backend
Operation(context.Context, *Operation) (*RunningOperation, error)
ServiceDiscoveryAliases() ([]HostAlias, error)
}
这一区分在架构上意义重大。只有两个后端实现了 OperationsBackend:local 和 cloud(HCP Terraform)。其他所有后端——S3、GCS、AzureRM、Consul 等——只实现 Backend。当你使用这些后端时,Terraform 会将其包装在 local.Local 中,由后者提供 Operation() 方法,从而在本地执行 plan/apply,同时将状态远程存储。
flowchart TD
subgraph "OperationsBackend"
Local["local backend<br/>(runs operations locally)"]
Cloud["cloud backend<br/>(runs operations remotely)"]
end
subgraph "Backend only (state storage)"
S3["s3"]
GCS["gcs"]
Azure["azurerm"]
Consul["consul"]
Others["pg, http, cos, oss,<br/>kubernetes, oci, inmem"]
end
S3 -->|"wrapped in"| Local
GCS -->|"wrapped in"| Local
Azure -->|"wrapped in"| Local
Consul -->|"wrapped in"| Local
Others -->|"wrapped in"| Local
内置后端与注册机制
所有后端都以硬编码方式定义在 internal/backend/init/init.go#L52-L76 中:
backends = map[string]backend.InitFn{
"local": func() backend.Backend { return backendLocal.New() },
"remote": func() backend.Backend { return backendRemote.New(services) },
"azurerm": func() backend.Backend { return backendAzure.New() },
"consul": func() backend.Backend { return backendConsul.New() },
"cos": func() backend.Backend { return backendCos.New() },
"gcs": func() backend.Backend { return backendGCS.New() },
"http": func() backend.Backend { return backendHTTP.New() },
"inmem": func() backend.Backend { return backendInmem.New() },
"kubernetes": func() backend.Backend { return backendKubernetes.New() },
"oss": func() backend.Backend { return backendOSS.New() },
"pg": func() backend.Backend { return backendPg.New() },
"s3": func() backend.Backend { return backendS3.New() },
"oci": func() backend.Backend { return backendOCI.New() },
"cloud": func() backend.Backend { return backendCloud.New(services) },
}
该映射上方的注释(第 33–43 行)解释了为何后端不支持插件化:
后端被硬编码进 Terraform,是因为后端 API 使用了复杂的内部结构,通过插件系统来支持这些结构在目前实现起来极为困难。如果需要自定义后端,可以通过重新编译源码来实现。
这是一个务实的选择。后端需要访问 statemgr.Full、configschema.Block、tfdiags.Diagnostics 等深层内部类型。要在插件边界上暴露这些类型,需要一套与 provider 协议同样复杂的通信协议,而考虑到后端列表变动并不频繁,这样的投入性价比极低。
Init() 函数在启动时调用一次(位于 main.go#L212),用于填充全局 backends 映射。此外还有一个 RemovedBackends 映射,记录已废弃的后端(如 artifactory、etcd、swift),在 init 时会给出友好的错误提示。
后端初始化与状态迁移
运行 terraform init 时,后端配置是其中最复杂的任务之一。整个流程从 InitCommand 开始,委托给 command/meta_backend.go 中的后端处理逻辑。
初始化过程需要处理以下几种场景:
- 首次配置 — 之前未配置任何后端
- 相同后端,配置变更 — 例如切换 S3 存储桶
- 切换后端 — 例如从本地迁移到 S3
- 移除后端 — 回退到本地存储
sequenceDiagram
participant User
participant Init as InitCommand
participant Meta as meta_backend
participant OldBE as Old Backend
participant NewBE as New Backend
User->>Init: terraform init
Init->>Meta: BackendForPlan / configureBackend
Meta->>Meta: Detect backend change
alt Backend changed
Meta->>OldBE: StateMgr("default")
OldBE-->>Meta: old state manager
Meta->>OldBE: RefreshState()
Meta->>NewBE: Configure(newConfig)
Meta->>NewBE: StateMgr("default")
NewBE-->>Meta: new state manager
Meta->>User: "Do you want to migrate state?"
User-->>Meta: "yes"
Meta->>NewBE: WriteState(oldState)
Meta->>NewBE: PersistState()
end
Meta-->>Init: configured backend
状态迁移默认是交互式的——在后端之间移动状态之前,Terraform 会提示用户确认。在自动化环境(TF_IN_AUTOMATION=1)中,可以通过 -input=false 配合预设变量来跳过提示。
提示: 迁移后端时,Terraform 会同时锁定新旧两个状态,以防止迁移过程中的并发访问。如果无法获取锁,迁移会安全失败,不会造成数据丢失。
下一步
状态与后端是持久化层,CLI 则是展示层。第 6 篇将介绍命令如何围绕共享的 Meta 进行组织、views 层如何将渲染逻辑与业务逻辑分离,以及诊断系统如何用带有源码归因的富信息替代 Go 的常规错误处理。