1. State 是什么与为什么需要
一句话总结: Terraform 是声明式的,但它必须借助 state 这份「现实记录」来算出期望状态与现实状态之间的 diff,state 是声明式 IaC 得以成立的账本。
写 HCL 只描述了「期望状态」,真实云环境里到底创建了哪些资源、ID 是多少、属性如何,都记录在 state 中。terraform plan 的本质就是「期望 state 与现实 state 的差异计算」。
期望状态(.tf 配置) ──┐
├──► 对比 ──► 生成 plan(增/删/改)
现实状态(terraform.tfstate)──┘
1.1 State 承担的三类职责
| 职责 | 说明 |
|---|---|
| 资源映射 | 记录资源地址(aws_instance.web)与真实云资源 ID 的对应 |
| 元数据 | 记录属性值、依赖关系、Provider 版本 |
| 性能优化 | 无需调用云 API 即可得知已管理哪些资源 |
# 配置中引用另一资源的输出
resource "aws_instance" "web" {
subnet_id = aws_subnet.main.id
}
如果没有 state,Terraform 就不知道 aws_subnet.main 到底对应哪个真实子网,也就无法构建依赖与 diff。
2. State 文件结构与格式
一句话总结: state 是一个带
serial与backend元信息的 JSON 文件,核心是resources数组;读懂它的字段结构是排障与迁移的基础。
默认本地 state 存放在 terraform.tfstate。其顶层结构如下:
{
"version": 4,
"terraform_version": "1.7.0",
"serial": 7,
"lineage": "8a1b...",
"outputs": {},
"resources": [
{
"module": "module.vpc",
"mode": "managed",
"type": "aws_subnet",
"name": "main",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"schema_version": 1,
"attributes": {
"id": "subnet-0a1b2c3d",
"cidr_block": "10.0.1.0/24"
},
"sensitive_attributes": [],
"private": "bm9wZQ=="
}
]
}
]
}
2.1 字段含义
| 字段 | 含义 | 排障要点 |
|---|---|---|
version | state 格式版本 | 4 为当前主流 |
serial | 变更序号 | 每次写操作递增,用于乐观锁 |
lineage | state 身份标识 | 同名 state 若 lineage 不同说明被重建过 |
resources[].type | 资源类型 | 与配置中 block 名对应 |
resources[].instances[] | 具体实例 | 每个元素对应一个 count/for_each 实例 |
sensitive_attributes | 敏感字段路径 | 标记为 sensitive 的字段在此列出 |
2.2 本地 state 的目录形态
terraform.tfstate # 主状态文件
terraform.tfstate.backup # 上次成功写入前的备份
terraform.tfstate.d/ # 使用 -state 参数时的目录
.terraform/ # 缓存 Provider 与后端配置
避坑:本地 state 默认不加密,且会记录
sensitive字段的明文值(只是渲染时打码)。单人演示可以用,团队协作必须切换到远程 backend。
3. Backend 本地与远程
一句话总结: 团队协作的底线是把 state 放到带锁的远程 backend(S3、GCS、Azure Storage、Terraform Cloud),本地 state 只适用于单人演示。
backend 定义了 state 存储在哪里。配置写在 terraform block 的 backend 内:
terraform {
backend "s3" {
bucket = "my-infra-tfstate"
key = "prod/network/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
dynamodb_table = "tf-state-lock"
}
}
3.1 常用 backend 对比
| Backend | 锁支持 | 加密 | 适用 |
|---|---|---|---|
| local | 无 | 无 | 单人、演示 |
| s3 + DynamoDB | 有 | SSE-S3/KMS | AWS 生态 |
| gcs | 有 | 默认加密 | GCP 生态 |
| azurerm | 有 | 默认加密 | Azure 生态 |
| terraform cloud | 有 | 有 | HashiCorp 云平台 |
| http | 视实现 | 视实现 | 自研/网关 |
3.2 避免在 HCL 中硬编码凭据
# 推荐:partial configuration,凭据与敏感信息由环境注入
terraform {
backend "s3" {} # 参数从命令行 / CI 环境读取
}
terraform init -backend-config="bucket=my-infra-tfstate" \
-backend-config="key=prod/network/terraform.tfstate" \
-backend-config="region=ap-northeast-1"
把 bucket 名、key、区域从 .tf 中剥离,留给 -backend-config 或环境变量注入,避免敏感信息进入版本库。
4. 状态锁与一致性
一句话总结: 状态锁防止两个人同时 plan/apply 导致 state 被并发写坏;锁被卡住时要先确认没有正在运行的 apply,再谨慎
force-unlock。
远程 backend(S3+DynamoDB)在 apply 前会自动获取锁,apply 结束后释放。锁记录存在 DynamoDB 表中:
# 正常流程:plan/apply 自动加锁
terraform apply
# 锁被占用时的报错
# Error: Error acquiring the state lock
# LockInfo: { "ID": "...", "Operation": "OperationTypeApply", "Info": "...", "Who": "...", "Created": "..." }
# 排查后强制解锁(谨慎!)
terraform force-unlock <LOCK_ID>
4.1 锁的生命周期
plan ──► 尝试加锁 ──► 读取 state ──► 计算 diff ──► 释放锁
apply ──► 加锁 ──► 写新 state ──► 释放锁
| 环节 | 锁状态 | 说明 |
|---|---|---|
| 读取 | 短暂锁 | 防止读到写一半的 state |
| apply | 长锁 | 贯穿整个资源变更 |
| 写回 | 短暂锁 | 配合 serial 乐观锁防覆盖 |
4.2 锁被卡住的常见原因
| 现象 | 原因 | 对策 |
|---|---|---|
| 上次 apply 崩溃 | 进程被 kill 未释放锁 | 确认无运行中进程后 force-unlock |
| CI 超时残留 | 流水线中断 | 增加锁超时与清理任务 |
| 锁表被误删 | DynamoDB 表重建 | 先重建表,再检查是否有在跑 apply |
| 多人同时触发 | 门禁缺失 | 串行化流水线 + 锁等待策略 |
一句话:
force-unlock是「原子弹」,用之前必须确认没有别的进程正在写 state,否则会造成状态损坏。
5. Import 采纳存量资源
一句话总结:
terraform import把「手工创建的存量资源」纳入 state 管理,让 IaC 覆盖范围可以从零开始接管历史资产。
云上已经存在大量手工创建的资源(旧控制台点的、历史脚本建的),用 import 把它们纳入 state,之后即可用 Terraform 统一管理。
# 先写好对应的资源块(属性可先留空,import 后补全)
resource "aws_instance" "legacy" {
# 只写必填字段,其余属性 import 后会自动填充
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
# 导入:把真实实例 i-0a1b2c3d 映射到 aws_instance.legacy
terraform import aws_instance.legacy i-0a1b2c3d
# 验证
terraform state show aws_instance.legacy
5.1 Import 的完整流程
| 步骤 | 命令 | 目的 |
|---|---|---|
| 1 编写资源块 | 手写 | 声明纳入范围 |
| 2 导入 | terraform import <addr> <id> | 绑定 state |
| 3 检查差异 | terraform plan | 找出属性漂移 |
| 4 修正配置 | 手改 | 让配置匹配现实 |
| 5 收敛 | terraform plan 无 diff | 完成采纳 |
# 用 import block 声明式导入(Terraform 1.5+ 推荐)
import {
to = aws_instance.legacy
id = "i-0a1b2c3d"
}
resource "aws_instance" "legacy" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
避坑:import 只把资源纳入 state,不会自动把配置写成「零 diff」。之后必须
terraform plan逐项对齐属性,否则可能误判为资源会被重建。
6. Refactor 与 Move 资源重构
一句话总结: 资源改名、挪进模块、count 改 for_each 都靠
terraform state mv或movedblock 完成「地址迁移」,让重构不产生「删旧建新」的破坏性 diff。
6.1 命令行迁移
# 资源改名
terraform state mv aws_instance.web aws_instance.web_server
# 挪进模块
terraform state mv aws_subnet.main module.vpc.aws_subnet.main
# count 索引迁移
terraform state mv 'aws_instance.web[0]' 'aws_instance.web["a"]'
6.2 用 moved block 记录历史迁移
# 把地址迁移写进配置,团队成员 plan 时自动重映射,无需逐个跑命令
moved {
from = aws_instance.web
to = aws_instance.web_server
}
moved {
from = aws_subnet.main
to = module.vpc.aws_subnet.main
}
| 重构场景 | 命令 / moved | 注意事项 |
|---|---|---|
| 资源改名 | terraform state mv | 同时更新配置 |
| 挪入模块 | moved { from/to } | 模块 source 版本保持一致 |
| count 改 for_each | moved 按 key 迁移 | 原索引地址需逐一列出 |
| 拆分大资源 | 手写 import + state mv | 先拆 state 再拆配置 |
6.3 拆 state 与挪 state
# 从当前 state 中导出子集到独立文件
terraform state pull > full.tfstate
terraform state rm aws_instance.worker # 从原 state 移除
# 在新目录中导入
terraform state push full.tfstate # 覆盖写入(谨慎)
一句话:任何「地址变了但资源不能变」的重构,优先用
movedblock 表达,它是可提交、可追溯、可 Review 的迁移记录。
7. 状态管理最佳实践与避坑
一句话总结: 远程存储 + 环境隔离 + 定期备份 + 禁止手改 state 文件,这四条是状态管理的黄金法则。
| 实践 | 做法 | 收益 |
|---|---|---|
| 远程存储 | S3/GCS/Azure Storage + 锁 | 团队协作、防丢失 |
| 环境隔离 | 每个环境独立 bucket/key | 互不干扰、权限隔离 |
| 定期备份 | 版本化 + 跨区域复制 | 防误删、防损坏 |
| 禁止手改 | 全部走 CLI | 避免损坏 state |
| 最小权限 | state 桶只给流水线角色读写 | 安全 |
7.1 环境隔离的 key 设计
terraform {
backend "s3" {
bucket = "my-infra-tfstate"
key = "${var.env}/network/terraform.tfstate" # 通过 -backend-config 注入
region = "ap-northeast-1"
}
}
prod/network/terraform.tfstate
prod/app/terraform.tfstate
staging/network/terraform.tfstate
staging/app/terraform.tfstate
7.2 常见避坑清单
| 坑 | 现象 | 对策 |
|---|---|---|
| state 入库版本库 | 明文泄露密钥 | 立即迁远程 + 轮换密钥 |
| 多人共用本地 state | 互相覆盖 | 统一远程 backend |
| 手改 tfstate | 格式损坏、重建风暴 | 只用 terraform state 系列命令 |
| 忘配 DynamoDB 锁 | 并发写坏 state | 配锁表 |
| state 与配置严重漂移 | plan 显示重建 | 先 import / state mv 收敛 |
| 生产与测试同 key | 环境互相污染 | 按环境拆分 bucket/key |
# 安全查看 state(字段会被打码)
terraform state show aws_instance.web
# 导出与恢复备份
aws s3 cp s3://my-infra-tfstate/prod/network/terraform.tfstate ./backup.tfstate
8. 总结
State 是 Terraform 声明式引擎的「现实账本」,管理好它等于管理好整套 IaC 的可靠性:
| 环节 | 要点 |
|---|---|
| 本质 | 记录期望与现实之间 diff 所需的资源映射 |
| 结构 | JSON 文件,serial 与 resources 数组是核心 |
| 存储 | 团队必须用远程 backend + 锁 |
| 锁 | 防并发写坏,force-unlock 须先确认无运行中 apply |
| import | 采纳存量资源,完成后 plan 收敛 |
| refactor | 地址迁移用 state mv / moved block |
| 黄金法则 | 远程存储、环境隔离、定期备份、禁止手改 |
一句话收尾:先让 state 落在一个安全、可锁、可备份的远程位置,再谈模块与流水线。下一篇「模块化设计模式」将讲解如何用模块封装复用逻辑,把散落的资源组织成可组合的组件。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。