14.2 Terraform 与声明式基础设施
一句话
Terraform 把 “基础设施” 变成可 diff、可评审、可复现的代码:你写期望状态,它算出 “现状 → 期望” 的差异(plan)并执行(apply);真正的难点不是语法,而是 state 管理、模块边界与漂移治理。
三件套:代码、状态、计划
| 组件 | 作用 | 出问题时的表现 |
|---|---|---|
代码(.tf) | 声明资源与依赖关系 | 改错一行,plan 里出现 “销毁重建” |
| state | 记录 “代码 ↔ 真实资源” 的映射 | 丢失/冲突 → 重复创建或误删 |
| plan | 差异预览与评审依据 | 不看 plan 直接 apply = 盲改生产 |
resource "aws_db_instance" "orders" {
engine = "postgres"
instance_class = "db.r6g.large"
storage_gb = 200
multi_az = true # 改这一行会触发重建,plan 里必须看清
}
state:必须远端 + 加锁
| 做法 | 后果 |
|---|---|
| state 提交进 git | 多人并行时互相覆盖,且含敏感信息 |
| state 放本地 | 换机器即 “资源丢失”,下次 apply 会重建 |
| 远端后端 + 锁(S3+DynamoDB / Terraform Cloud / OSS) | 并发安全,可审计、可回滚 |
敏感数据(密码、密钥)会进 state,所以 state 存储必须加密 + 严格权限,必要时用 sensitive = true 并配合外部密钥系统(见 13.4 密钥与凭证管理)。
模块与目录:按 “变更半径” 切
| 切法 | 好处 | 坏处 |
|---|---|---|
| 按环境(dev/staging/prod 各一套) | 变更互相隔离 | 代码重复 |
| 按资源层(网络/数据/应用) | 影响面清晰 | 跨层依赖要显式传参 |
| 单一大 state | 一次 plan 看全貌 | 任一处变更都要锁全量,风险大 |
推荐:环境隔离 + 按层拆分 + 模块复用。一个 state 管一个 “可独立重建的单元”,别把整个公司塞进一个 state。
漂移:代码与现实的裂缝
| 漂移来源 | 检测 | 处理 |
|---|---|---|
| 控制台手改 | 定期 terraform plan(应为空) | 改回代码,或把变更写进代码 |
| 其他工具创建 | 同上 | 用 import 纳入管理 |
| 云厂商侧变更(如默认值) | plan 出现 “无操作但有 diff” | 显式声明,别依赖默认值 |
“plan 不为空” 应该是告警,而不是常态。
落到数字:plan 里的三种动作
| plan 标记 | 含义 | 风险 |
|---|---|---|
~ update | 原地更新 | 低(但要确认是否触发重启) |
+ create | 新建 | 低(注意配额) |
-/+ replace | 销毁重建 | 高:数据库/存储可能丢数据 |
- destroy | 删除 | 最高(retain 保护是否生效) |
terraform plan -out=tf.plan && terraform show -json tf.plan | jq '.resource_changes[] | select(.change.actions!=["no-op"]) | {addr:.address, act:.change.actions}'
# 判据:replace 与 destroy 必须逐条人工确认
反模式
| 反模式 | 后果 |
|---|---|
| 手动改控制台,不回写代码 | 下次 apply 把手工修复抹掉 |
不看 plan 直接 apply -auto-approve | 销毁重建生产数据库 |
| 一个 state 管全部环境 | 改测试要锁生产,且误操作影响全公司 |
用 latest 镜像/版本 | 计划不可复现 |
把密钥写在 .tf 里 | state 与 git 双重泄露 |
用 -target 频繁“精确修复” | 状态与代码逐渐偏离,漂移难查 |
三条铁律
🔑 state 远端化 + 加锁;本地 state 等于没有基础设施代码。
🔑 plan 必须人工评审;
-auto-approve只用于非生产。
🔑 漂移即告警:定期 plan 为空,是 IaC 健康的唯一标志。
自检
Q1 为什么 “改了 multi_az 一行” 可能在 plan 里显示 “销毁并重建”?
▸ 答案:因为 Terraform 依据云 API 的能力判断 “该属性是否支持原地更新”:不支持时只能删除旧资源、创建新资源(-/+)。这类操作对数据库意味着停机与数据迁移,必须在 plan 阶段识别,改用 “先建新实例再切换” 的迁移方案。
Q2 state 文件为什么不能进 git?
▸ 答案:两个原因:① 并发冲突:多人同时 apply 会互相覆盖,导致 “资源在云上但 state 不知道”(下次重复创建);② 敏感信息:state 里保存了资源属性(含密码、密钥、连接串)。正确做法是远端后端 + 锁 + 加密 + 访问审计。
Q3 控制台手改了一个安全组,下次 apply 会怎样?
▸ 答案:会被改回代码里的状态(这就是漂移被纠正)。如果手改是应急修复,正确流程是 “先改代码 → 再 apply”,否则应急变更会在下一次发布时被静默抹掉,形成 “修复消失” 的诡异故障。
Q4 什么时候该拆 state?
▸ 答案:当满足任一条件:① 不同团队/环境需要独立发布节奏;② 单次 plan 超过几分钟(资源太多);③ 变更的影响面差异巨大(网络层 vs 应用层);④ 需要独立的重建/销毁生命周期。拆分的代价是跨 state 的依赖要显式传参(data/remote_state)。
Q5 Terraform 与 K8s 的声明式有什么共同点与区别?
▸ 答案:共同点是 期望状态 + 控制器收敛 + 可评审的 diff(见 7.1 声明式 API 与控制器模式)。区别在于:Terraform 是客户端批量收敛(apply 一次),K8s 是常驻控制器持续收敛;前者适合 “低频、强顺序” 的基础设施,后者适合 “高频、自愈” 的应用运行时。
Q6 为什么 “先 terraform import 再改代码” 比 “让它重建” 更安全?
▸ 答案:因为 import 把已存在的资源纳入 state 管理,之后 apply 只做真正的差异;直接重建意味着删除线上资源(数据、IP、证书),尤其对数据库与对象存储是灾难。import 的成本是手工写资源块,远低于重建的代价。