基础设施即代码的核心,是把资源期望状态放进可评审的声明文件,再由工具比较现状并生成变更。命令式脚本描述“创建网络、再创建实例”,重复执行前必须自行判断对象是否存在;声明式配置描述“网络和实例应当具备这些属性”,调谐过程依据差异决定创建、更新或删除。幂等能力来自这种状态比较,但仍受资源提供方接口、副作用和配置完整性影响。
声明式配置与执行图
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 也无法提供可靠的变更控制。
评论(2)
这个思路我用在日志清洗上了,效果意外地好。
看完去翻了下我们的日志,果然全是噪音。
请 登录 后参与评论。