7AA7 博客

IaC 入门:Terraform 的状态管理与模块化

Talen UID:73
2026-10-03 22:15
79 2 3

基础设施即代码的核心,是把资源期望状态放进可评审的声明文件,再由工具比较现状并生成变更。命令式脚本描述“创建网络、再创建实例”,重复执行前必须自行判断对象是否存在;声明式配置描述“网络和实例应当具备这些属性”,调谐过程依据差异决定创建、更新或删除。幂等能力来自这种状态比较,但仍受资源提供方接口、副作用和配置完整性影响。

声明式配置与执行图

Terraform 解析资源引用,建立依赖图,并在无依赖冲突时并行操作。显式引用通常比手写 depends_on 更准确:

resource "example_network" "main" {
  cidr = var.network_cidr
}

resource "example_instance" "api" {
  network_id = example_network.main.id
  image_id   = var.image_id
}

network_id 引用已经形成依赖。过度添加 depends_on 会扩大未知值范围,让计划更保守,也降低并行度。若依赖只存在于外部副作用、无法从属性引用表达,才需要显式声明。

执行 apply 也不是持续运行的控制器。配置外的变更不会立刻被改回,只有刷新、计划或应用时才会重新比较。因此,声明式不等于环境自动免疫人工修改,仍需要定期检测漂移。

state 文件为何存在

云端或虚拟化平台只认识资源 ID,不知道代码中的 example_instance.api 对应哪台实例。state 保存资源地址与远端对象 ID 的绑定、部分属性快照、依赖信息和输出,使工具能计算差异、追踪重命名并安全删除受管对象。丢失 state 后,即使资源仍在运行,工具也可能把它们视为未创建。

团队不能把本地 state 当作个人文件使用。两人同时 apply,后写入者可能覆盖前一人的状态,随后计划基于错误快照做出危险判断。生产 state 应放在支持版本、加密和并发锁的远端后端,并限制读写权限。加锁覆盖的是“读取—变更—写回”整个事务,而不是只锁写入瞬间。

terraform {
  backend "remote_backend" {
    bucket         = "iac-state-prod"
    key            = "network/core.tfstate"
    locking        = true
    encryption     = true
  }
}

后端参数因实现而异,示例只表达能力要求。锁超时后应调查是否仍有任务运行,不要直接强制解锁;只有确认原任务终止且不会回写时才能解除。state 不应提交到 Git,也不应由 CI 作为普通构建产物公开保存。

读懂 plan 的动作语义

计划不是“有几项变化”的统计,而是变更审核材料。常见动作可这样理解:

动作 含义 审核重点
create 新建资源 配额、命名、网络暴露、费用边界
update 原地修改 是否重启、连接是否中断、属性副作用
destroy 删除资源 数据、依赖方、恢复路径
replace 删除旧对象并新建 地址变化、停机窗口、数据迁移

replace 常由不可原地修改的属性触发,计划中可能显示 -/+ 或 +/-,顺序取决于生命周期策略。它是高危信号,不能因为计划整体“只有一项变化”就快速放行。数据库、固定地址、身份对象等有状态资源尤其要确认备份、切换与回退。

terraform plan -out=change.tfplan
terraform show change.tfplan
terraform apply change.tfplan

审批与执行应使用同一份已保存计划,并保证期间代码、变量与 state 未改变。若执行环境重新生成计划,审批看到的内容可能不是实际应用内容。对关键资源可设置防删除生命周期约束,但它只是额外护栏,不能替代变更评审。

漂移的成因与发现

漂移是远端实际状态与代码、state 记录不一致。它可能来自控制台手工修改、自动伸缩系统、事故处置脚本、资源提供方默认值变化,也可能是外部控制器合法管理某个属性。定期在只读凭据或受限执行环境中运行 plan -detailed-exitcode,可以把“无变化”“有差异”“计划失败”区分开。

发现漂移后不要机械 apply。若手工修改是合理的新基线,应把配置更新为实际值;若对象已存在但未纳管,可评审后 import;若代码地址重构,使用 moved 块或受控 state move 保留绑定。直接编辑 state 文本容易破坏序列号和对象结构,应使用受支持命令,并提前保留后端版本。

某些属性确实由外部系统维护,可以精确忽略该属性的差异。大范围 ignore_changes 会把真实漂移一并隐藏,尤其不应为了“让计划变干净”而忽略整个资源。

模块化的合适粒度

模块应封装具有清晰生命周期和稳定接口的一组能力,例如“一个带路由和日志的网络”或“一组同策略的应用实例”。只把单个资源包一层,调用者仍要理解全部细节,增加了跳转却没有形成约束;把网络、数据库、应用、监控全塞进一个模块,又会让每次变更都扩大计划范围。

模块输入应表达业务意图,输出只暴露下游确实需要的标识。避免传入一个包含几十个任意字段的对象,也不要输出完整资源对象。模块版本要固定,升级单独评审:

module "service_network" {
  source  = "./modules/service-network"
  cidr    = "10.24.0.0/20"
  zone_count = 3
}

模块内部资源重命名时提供迁移声明,否则调用方升级后可能看到无意义的销毁重建。模块测试至少覆盖格式、校验、计划快照和一个临时环境的创建销毁。

多环境的组织方式

工作区允许同一配置维护多份 state,适合结构几乎一致、仅变量不同的环境。它的风险是当前工作区不够醒目,操作者可能在错误环境执行;后端权限也常难按目录细分。差异较大或权限边界严格的生产环境,更适合独立根模块、独立后端 key 和独立执行身份,共享经过版本化的子模块。

iac/
  modules/
    service-network/
  live/
    dev/
    staging/
    prod/

环境目录只包含组合关系、变量与后端设置,不复制模块实现。CI 应根据变更路径选择目标环境,并在日志和审批页面突出显示 state 地址与执行身份。

敏感值会进入 state

把变量标记为 sensitive 只会减少终端展示,不会把值从 state 中删除。资源参数、数据源返回值和输出都可能以明文结构存在 state。远端后端必须启用静态加密和传输加密,读取权限应比普通代码仓库更严格,并开启访问审计与版本保留。

更稳妥的模式是让 Terraform 只管理密钥容器、访问策略或密钥引用,真实凭据由专用系统在运行时生成和交付。若提供方要求把秘密值写入资源,需接受它会进入 state 的事实,缩小 state 粒度与可读主体,并建立轮换。CI 日志也应禁止输出完整计划 JSON 或调试环境变量。

执行前确认当前目录、工作区、后端地址和计划中的 replace/destroy,执行后保存运行记录并核对关键输出。状态管理做对了,代码评审才真正对应环境变化;状态边界混乱时,再整洁的 HCL 也无法提供可靠的变更控制。

1.4k浏览量
79赞
2评论
2参与用户
9 分钟阅读时间

评论(2)

  • Moon 槐 · 2026-10-04 00:31

    这个思路我用在日志清洗上了,效果意外地好。

    回复
  • 孟知微 · 2026-10-04 09:46

    看完去翻了下我们的日志,果然全是噪音。

    回复