Terraform 实战技巧:5 个提效方法
从一个惨痛的下午说起
去年我们团队接手了一个 60 多个模块的基础设施项目,光是 terraform apply 一次就要跑 25 分钟。有一次同事在本地跑了个 plan 没注意 state 文件是旧的,直接把一个生产 RDS 实例标记为要替换——幸好我们在 apply 前多看了一眼。那天下午我痛定思痛,花了两周时间把团队的工作流重新梳理了一遍。下面这 5 个技巧,是我实际用下来提效最明显的,每一条都有真实的坑做背书。
技巧一:用 targeted apply 处理大项目,但要小心
我们的项目一次 apply 要 25 分钟,但大部分时候我们只改了一两个模块。Terraform 提供了 -target 参数:
terraform apply -target=module.ecs_web.aws_launch_pattern.web
配合 terraform graph 或者直接看 plan 输出,可以只重建依赖链上的一小部分资源。我们 25 分钟的 apply,用 target 之后经常 3 分钟搞定。
但这里有个我踩过的坑:-target 会让 Terraform 跳过依赖检查。有一次我用 target 更新了一个安全组,忘了它上游还有个引用了它 CIDR 的 NACL,结果流量直接被打断了。所以我的规矩是:
-target只用于日常迭代和调试- 任何合并到 main 分支之前的最终验证,必须跑一次不带 target 的完整 plan
- CI 里加一条注释提醒:如果最近的 commit 里有
-target的痕迹,强制完整 plan
技巧二:用 validate + fmt + lint 组合拳,在 CI 里挡住 90% 的问题
本地手滑太正常了。我们现在的流程是三个工具串起来:
terraform fmt -recursive -check
terraform validate
tflint --recursive
tflint 是真正值得装的。它能查出 validate 查不出来的东西,比如实例类型写错了:
# tflint 会报错:us-west-2 不存在 db.r5.xlarge 这个配置(举例)
resource "aws_db_instance" "main" {
instance_class = "db.r5.xlagre" # 手滑
}
terraform validate 对这个完全没意见,直到你 apply 才炸。我们把这三条命令放进 pre-commit hook 和 CI 里,第一天就拦下了一个把 ap-northeast-1 写成 ap-northeast-la 的提交。
技巧三:plan 文件 + 自动化审批,告别“plan 看完 apply 变了”
这是我们踩过最阴的坑。你跑了 terraform plan,看了输出没问题,然后跑 terraform apply——但在这两步之间,有人改了配置或者 state 被更新了,apply 时重新计算的 plan 和你看的那个不是同一个。
解法是把 plan 固化成文件:
terraform plan -out=tfplan
terraform apply tfplan
第二个命令会直接应用你刚看的那个 plan,不再重新计算。配合 CI 的话更香:MR 流水线里跑 plan 并把输出贴到 merge request 评论里,审批人看的就是将要执行的 plan,批准后 apply 用同一个文件。
一个小提示:plan 文件里可能包含敏感信息(密码、密钥),不要提交到 git,用完即删或者放在加密的 artifact 存储里。
技巧四:用 moved 块替代手改 state
重构的时候经常需要把资源改名或者挪模块。老办法是 terraform state mv,但这是纯手工操作,不进版本控制,换个环境就得再来一遍。
现在直接写代码:
moved {
from = aws_instance.web
to = module.web.aws_instance.this
}
跑 plan 时你会看到 terraform 报告这些资源将被 move 而不是 destroy/create——这一点非常关键,意味着数据库不会重建。我们上个月把一个单体配置拆成三个模块,靠的就是几十个 moved 块,全程零停机,而且迁移历史清清楚楚地留在 git 里。
同理,import 块(Terraform 1.5+)也很好用:
import {
to = aws_s3_bucket.legacy_logs
id = "company-legacy-logs-2021"
}
terraform plan 会生成导入配置,不用再手敲 terraform import 命令。
技巧五:用 workspace 还是多目录?想清楚再选
新手最容易纠结这个。我两个都试过:
workspaces 适合环境差异小的场景:
terraform workspace new staging
terraform workspace select prod
问题是代码里会散布 terraform.workspace == "prod" ? ... : ... 这种三元表达式,环境多了之后可读性雪崩。我们曾经有 5 个环境,每个环境还有细微差异,最后这个方案维护不动了。
多目录 + 共享模块是我们现在的做法:
environments/
├── staging/
│ └── main.tf # 引用 modules/
└── prod/
└── main.tf
modules/
└── vpc/
每个环境一个独立的 state,爆炸半径小,prod 永远不会被 staging 的误操作波及。代价是有些配置要复制,但这点重复换来隔离性,值。
最后的实话实说
几个诚实的提醒:
-target是拐杖不是路,长期依赖它说明你的模块拆分有问题- plan 文件方案在 monorepo 很大时会让 CI 变慢,我们目前用缓存 .terraform 目录 + provider 缓存缓解,但 plan 时间还是超过了 10 分钟
moved块在 1.1 之前的版本不可用,如果你们还在用老版本(我见过不少公司卡在 0.12),只能继续state mv- tflint 规则要按团队口味裁剪,全开的话告警太多,大家会开始忽略它
这些技巧没有一个是黑科技,全是笨办法,但基础设施这东西,笨办法往往就是最可靠的办法。如果你有 5 分钟,先把 -out=tfplan 这个习惯养起来——它是这五条里投入产出比最高的。