故障排查与最佳实践
系列最后一篇,把「出问题怎么办」讲透:调试手段、五个最常见报错的原因和处理、状态漂移怎么修、大项目变慢怎么治,最后用一份最佳实践清单收束全系列。这篇建议收藏,出事的时候按目录翻。示例基于 Terraform 1.9+,操作系统 macOS / Linux,云厂商示例以 AWS 为主,其他云同理。
调试手段
第一招:TF_LOG 开日志。Terraform 的详细日志默认不输出,用环境变量打开:
# DEBUG 一般够用,能看到每次 API 调用
TF_LOG=DEBUG terraform plan
# TRACE 最啰嗦,连内部调度细节都打出来,排查 provider 疑难杂症才用
TF_LOG=TRACE terraform plan
# 输出太长就写进文件,终端保持干净
TF_LOG=DEBUG TF_LOG_PATH=./terraform-debug.log terraform plan
日志级别从细到粗:TRACE > DEBUG > INFO > WARN > ERROR,设了 DEBUG 会连更粗的级别一起输出。排完记得关掉,尤其是写进 shell 配置文件的情况——忘了关会让日常输出没法看:
unset TF_LOG TF_LOG_PATH
第二招:-detailed-exitcode 让脚本读懂 plan。默认 plan 不管有没有变更,成功退出码都是 0,脚本里没法区分。加上 -detailed-exitcode 后:
| 退出码 | 含义 |
|---|---|
| 0 | 成功且无变更 |
| 1 | 出错 |
| 2 | 成功且有变更 |
terraform plan -detailed-exitcode -out=plan.tfplan
code=$?
case $code in
0) echo "无变更,跳过 apply" ;;
1) echo "plan 出错" >&2; exit 1 ;;
2) echo "有变更,进入确认流程" ;;
esac
CI 里「无变更不打扰、有变更才贴 plan 结果」这类逻辑就靠它实现。
第三招:缩小范围做最小复现。报错信息看不懂时,先把范围缩到最小:用 -target 只 plan 报错的那个资源,或者新建一个空目录放最小复现配置。九成时候问题在缩小的过程中自己就暴露了——要么是变量没传对,要么是资源间的隐式依赖想错了。
常见报错清单
Error acquiring the state lock
报错长这样:
╷
│ Error: Error acquiring the state lock
│
│ Error message: ConditionalCheckFailedException: ...
│ Lock Info:
│ ID: 4b1a5c2e-....-............(一串 UUID)
│ Path: my-terraform-state/prod/terraform.tfstate
│ Operation: OperationTypeApply
│ Who: zhangsan@macbook-pro
│ Created: 2026-10-24 09:12:33 ...
╵
原因:支持锁的远程后端(S3 + DynamoDB、Consul 等)在 plan / apply 期间会加锁,防止并发写 state。报这个错说明锁被占着——要么真有人在跑,要么上次运行挂了(Ctrl+C、CI 超时被杀)锁没释放。
处理:
- 先看输出里的
Who和Created,确认是不是同事正在跑。是的话排队等,别抢。 - 确认没人跑了(CI job 已死、锁的创建时间对得上那次卡死的运行),用锁 ID 强制解锁:
terraform force-unlock 4b1a5c2e-....-............
强制解锁时如果其实还有人正在 apply,两个进程并发写 state,结果就是 state 损坏。解锁前确认:CI 里没有在跑的 job、同事都问过了、锁信息确实对得上那次卡死的运行。
Provider 认证失败
典型报错:
╷
│ Error: configuring Terraform AWS Provider: no valid credential sources found
╵
Azure 是 building account: getting authenticated object ID: ...,阿里云是 InvalidAccessKeyId.NotFound,都归这一类。
原因:环境变量没设或设错了、profile 名字对不上、临时凭据过期。CI 里最常见的是变量名打错,或者在非保护分支上跑导致 Protected 变量不可见。
处理,按顺序查:
# 1. 环境变量在不在
env | grep -i aws
# 2. 凭据本身对不对(绕过 Terraform 直接问云)
aws sts get-caller-identity
# 3. 用了 profile 的话,名字拼对没有
cat ~/.aws/credentials
aws sts get-caller-identity 能通而 Terraform 不通,九成是 Terraform 配置里的 profile / region 和环境对不上。CI 场景则检查变量是否勾了 Masked / Protected,以及当前分支是否满足可见条件。
403 / rate limit 限流
典型报错:
╷
│ Error: reading EC2 Instances: operation error EC2: DescribeInstances,
│ api error ThrottlingException: Rate exceeded
╵
GitHub provider 则是 403 API rate limit exceeded 之类。
原因:云厂商 API 都有调用频率限制。state 里资源几百上千个时,一次 plan 要发几百个 Describe / Get 请求,容易撞限流;多人共用一套凭据时更明显。
处理:
- 先直接重试,限流大多是瞬时的,退避几秒再来。
- 降并发:
terraform plan -parallelism=3(默认 10),让请求发得慢一点。 - 治本:大 state 拆分(见下文「性能与规模」),以及别滥用 data source——每个 data source 每次 plan 都真实调一次 API。
Provider produced inconsistent result after apply
报错长这样:
╷
│ Error: Provider produced inconsistent result after apply
│
│ When applying changes to aws_security_group_rule.ssh, provider
│ ... produced an unexpected new value: .cidr_blocks: was
│ cty.ListVal(...), but now cty.ListVal(...)
╵
原因:apply 成功后 provider 回头读资源,读到的值和它自己算出的预期不一致。常见诱因三个:云 API 是最终一致的,刚写完立刻读读到了旧值;API 会把值规范化(CIDR 写法、标签大小写、字段顺序);provider 本身的 bug。
处理:
- 先再跑一次
terraform plan——多数时候它会告诉你还差哪一点没对上,再apply补一刀就收敛了。 - 把 provider 升到最新 patch 版本,这类问题很多是已修 bug。
- 升级后仍能复现,用
TF_LOG=DEBUG抓日志,对比「发给 API 的值」和「API 返回的值」,去 provider 的 GitHub 仓库搜 issue 或提 issue,附上日志。
Cycle 依赖环
报错长这样:
╷
│ Error: Cycle: aws_instance.a, aws_instance.b
╵
原因:资源间的依赖成了环——a 依赖 b,b 又依赖 a。八成是 depends_on 手写多了:属性引用产生的隐式依赖 Terraform 自己会算,手写的 depends_on 容易和隐式依赖叠出环。
处理:把环上多余的 depends_on 删掉,能用属性引用表达的就不要手写:
# 错误示范:两边互相 depends_on
# resource "aws_instance" "a" { depends_on = [aws_instance.b] ... }
# resource "aws_instance" "b" { depends_on = [aws_instance.a] ... }
# 正确:真的需要顺序保证时,只保留单向依赖
resource "aws_instance" "b" {
depends_on = [aws_instance.a]
# ...
}
如果是模块间的环(模块 A 的输出喂给模块 B,B 的输出又喂回 A),说明模块边界划错了,把互相引用的资源收进同一个模块。
状态漂移与修复
漂移 = 真实环境和 state 对不上。成因基本都是有人绕过 Terraform 在控制台手改了资源,或者别的工具(Ansible、脚本)动了 Terraform 管的东西。
发现漂移:日常 terraform plan 就能撞见——你明明没改代码,plan 却冒出一堆变更。只想看漂移、不想看变更建议,用:
terraform plan -refresh-only
它只对比 state 和现实,输出「哪些资源和 state 里记的不一样」,不给出修改建议。修复方向两条,按「以谁为准」选:
- 以代码为准(推荐):
terraform apply把现实改回代码描述的样子。前提是确认手改的内容没有保留价值。 - 以现实为准:改
.tf代码去匹配现状,直到 plan 干净为止。适合手改确实有道理的情况——比如救火时临时调大了规格,复盘后决定保留。
导入遗漏的资源:历史上在控制台建的资源要纳入管理,Terraform 1.5+ 推荐 import 块写法,导入动作跟着代码走 review:
import {
to = aws_s3_bucket.logs
id = "my-company-logs-2024"
}
# plan 会显示这条 import 计划,确认后 apply 完成导入
terraform plan
terraform apply
导完记得用 -generate-config-out 生成配置草稿并核对精简,否则下次 plan 发现「state 里有、代码里没有」会提议把资源删掉。完整流程第 4 篇讲透了,这里不再展开。
state 要动手术时,先备份:
# 动 state 之前,永远先拉一份完整的下来存档
terraform state pull > state-backup-$(date +%Y%m%d-%H%M%S).json
state mv、state rm、手改 state 这些操作,做完一步就 terraform plan 看一眼,输出符合预期再动下一步。state push 覆盖远程 state 属于最后手段,推之前确认本地这份确实比远程的新、比远程的对。
state 文件是明文,可能含数据库密码等敏感字段。备份文件别提交 git、别留在共享目录,用完加密保存或妥善删除。
性能与规模
项目长大后的典型症状:plan 从 10 秒变成 5 分钟,还动不动撞 API 限流。治法按见效程度排序。
拆分 state(治本)。一个 state 里资源越多,每次 plan 要 refresh 的对象就越多,速度线性变慢,爆炸半径还大——一次误 apply 影响所有东西。按「环境 × 组件」拆成多个目录、各自独立 state:network、database、app 各管各的,跨目录引用用 terraform_remote_state 或参数传递。目录怎么组织,回看 模块化与项目组织。
-parallelism 调并发(治标)。默认 10 个并发操作,资源多且云 API 配额宽裕时可以拉高:
terraform plan -parallelism=30
反过来,撞限流时调低到 3 甚至 1 能救命。这个参数只影响本次运行、不写进任何文件,按需使用。
减少 data source 滥用。每个 data source 在每次 plan 都真实调一次云 API,哪怕它查的东西一年不变。几十上百个叠起来,plan 慢、限流也近。替代方案:基本不变的值直接写成 variable 或 local;确需动态查询的,结果用 local 中转复用,别让同一个查询在多个文件里重复声明。
最佳实践清单
收束全系列,新项目立项或老项目体检时对着过一遍:
- state 远程化且带锁:本地 state 只用于学习,团队项目第一天就该上远程后端,做法见 State 状态管理。
.terraform.lock.hcl提交 git:团队与 CI 用同一份 provider 版本,细节见第 7 篇版本约束一节。- 版本用
~>锁死:required_version和required_providers都写约束,模块来源也带版本号(registry version 或 git ref),不锁版本的依赖等于定时炸弹。 - 小步 apply:一次 PR 干一件事,plan 输出能在一屏内读完。变更攒多了,plan 里混进一个 destroy 根本看不出来。
- PR 必须过 plan 评审:plan 进 PR 评论,评审对着 plan 评,合并后才允许 apply。
- fmt / validate / tflint 进 CI:本地 pre-commit 一道,CI 一道,不合规的代码到不了 main。
- 控制台零手改:所有变更走代码,漂移出现了立刻处理,不攒着。
- 敏感值不落明文:凭据走环境变量,secret 类输出标
sensitive = true,state 文件按敏感资产保管。 - 动 state 先备份:
terraform state pull存档再手术,做完一步 plan 验证一步。
小结
Terraform 的坑九成都长一个样:锁占住了、凭据没配上、限流了、provider 状态没对齐、依赖成环——对照清单处理,剩下的交给 TF_LOG=DEBUG。回看全系列八篇的脉络:从安装上手和 核心概念与架构 起步,把 provider、resource、state 的关系讲清;HCL 语法和资源写法打底;State 状态管理 把团队协作的地基打牢;模块化与项目组织 让代码可复用可维护;第 7 篇把变更流程接进 CI 流水线;本篇收尾——会写、会管、会协作,出了问题也会修。剩下的就是拿自己的基础设施练手:先把一个非核心服务迁进 Terraform,踩过一轮坑,这个系列才算真正读完。