照着官方示例只把 ami 换成自己的镜像 ID,terraform plan 却给出 -/+ must be replaced。
这篇把 Provider、Resource、依赖关系三件事拆开:读完你能读懂 plan 的每一个符号,也知道自己改的那一行会触发什么。
一、场景铺垫:一份配置要同时管两个区域
我接手的配置只部署到单一区域,现在要让它同时覆盖 us-east-1 与 us-west-2,两个区域的实例规格还不一样。第一反应是复制目录改掉 region,一跑 plan,几十台实例被标记为替换 ------ 参数没写错,是没分清三件事各自管什么。
| 角色 | 决定什么 | 写在哪 | 改错后的直接后果 |
|---|---|---|---|
| Provider | Terraform 能操作哪些资源类型 | required_providers 与 provider 块 |
版本约束变化后必须重新 init |
| Resource | 具体创建哪一个基础设施对象 | resource 块 |
命中不可原地更新的属性即触发替换 |
| 依赖关系 | 资源按什么顺序创建与销毁 | 属性引用、depends_on |
顺序缺失会让创建阶段直接失败 |
图
注意:三者里只有 Resource 直接决定「改一行是否重建机器」,Provider 只划定能力边界,依赖关系只决定执行顺序。
这三件事里,哪一个最可能让你在改一行参数之后失去整台机器?
二、踩坑现象:三次 plan 都没按预期走
三个坑发生在同一份配置上,报错与现象原样贴进下面的表里。
| 现象 / 报错 | 根因 | 定位手段 |
|---|---|---|
# aws_instance.web must be replaced,只因给资源加了一个 tag |
该标签所在的字段在 Provider schema 里标记为不可原地更新 | plan 输出中的 -/+ 符号 |
terraform init 报 no available releases match the given constraints |
版本约束与 Registry 现有版本没有交集 | init 输出里 Finding hashicorp/aws versions matching 那一行 |
plan 里属性成片显示 (known after apply),apply 阶段长时间不动 |
depends_on 建立的是粗粒度依赖,必须等整组资源全部操作完成 |
判断这条依赖能否改写成属性引用 |
坑三最像网络故障:apply 卡在等待,我把它当网络问题排查了半天,直到发现卡住的正是被 depends_on 拉进来的 IAM 策略。坑一提醒标签也不是永远无痛,我给实例加的 Name 标签恰好挂在不可原地更新 的字段上;坑二则是把约束写成 = 5.7.1 ------ 以为写死更稳,实际是补丁永远进不来。
注意:同一份配置里,报错出现在 init、plan、apply 三个阶段,说明问题分属 Provider、Resource、依赖关系三层,不能混着修。
init 阶段的报错指向 Provider,plan 阶段的符号指向 Resource,apply 阶段的等待指向依赖关系 ------ 这三层在命令输出里到底是怎么暴露的?
三、底层原理:Provider 插件、Resource 声明、依赖图
Terraform 内核不认识「云主机」「对象存储」这类概念,它只做语言解析、状态管理(state)和依赖图计算。真正与云厂商 API 打交道的是 Provider(提供者)插件,每个 Provider 负责一类服务的认证、资源类型定义与 API 调用。
plain
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.7.0"
}
}
}
source 指定 Provider 在 Terraform Registry 中的地址,version 约束可接受的范围。~> 5.7.0 表示允许 5.7.x 的补丁更新,但不接受 5.8.0 及以上。声明只描述需求,实际下载发生在 terraform init 阶段。
| 版本写法 | 含义 |
|---|---|
~> 5.7.0 |
允许 5.7.x 补丁更新,拒绝 5.8.0 及以上 |
>= 5.0, < 6.0 |
显式划定大版本区间 |
= 5.7.1 |
只接受 5.7.1,连补丁也要手工改 |
Provider 的两层声明
required_providers 回答「需要什么」,provider 块回答「如何连接」。后者至少要提供认证信息与区域;同一 Provider 需要多套配置时,用 alias 区分。
plain
provider "aws" {
region = "us-west-2"
}
provider "aws" {
alias = "use1"
region = "us-east-1"
}
资源内通过 provider = aws.use1 显式选择实例。默认实例没有别名,resource 块不写 provider 参数时自动使用默认配置。
Resource 的两层参数
resource 块内部的参数分两层。一层是 Provider 定义的资源专用参数,例如 aws_instance 的 ami 与 instance_type,其类型、是否必填、能否原地更新都由 Provider 文档规定;另一层是 Terraform 语言内置的元参数(meta-arguments),与具体 Provider 无关,对所有资源通用。
plain
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
count = 2
depends_on = [aws_security_group.web]
lifecycle {
ignore_changes = [tags["LastModified"]]
}
}
| 元参数 | 作用 | 使用约束 |
|---|---|---|
count |
按数量批量创建 | 与 for_each 互斥 |
for_each |
按集合键批量创建 | 与 count 互斥 |
provider |
指定非默认 Provider 实例 | 需配合 alias |
depends_on |
显式声明依赖 | 粒度粗,优先用属性引用 |
lifecycle |
控制创建与销毁行为 | 含 ignore_changes 等子项 |
依赖关系决定执行顺序
plan 与 apply 会先构建一张资源依赖图,再据此决定创建、更新、销毁的顺序。依赖的建立有两条路径。
latex
读取配置与状态
│
▼
构建依赖图
│
┌─────┴─────┐
▼ ▼
隐式依赖 显式依赖
属性引用 depends_on
│ │
└─────┬─────┘
▼
拓扑排序 → 创建 / 更新 / 销毁
| 维度 | 隐式依赖 | 显式依赖 |
|---|---|---|
| 建立方式 | 参数中引用另一资源的属性 | depends_on 列表 |
| 依赖精度 | 精确到具体属性 | 精确到整个资源 |
| 变更影响 | 其他属性变化不牵动下游 | 任一属性变化都牵动下游 |
| 使用建议 | 默认首选 | 仅在属性引用无法表达时使用 |
注意:隐式依赖精确到属性 ,显式依赖精确到整个资源 ------ 这就是 depends_on 会带来无谓等待的原因。
原理层面三件事已经串起来了,落到配置上具体该守哪几条规矩?
四、解决方案:四条改配置的准则
改配置的目标是让 plan 的输出符合预期。以下四条按影响面从大到小排列,每条对应第二节里的一个坑。
| 准则 | 写法 | 规避的坑 |
|---|---|---|
版本用 ~> 约束 |
约束主次版本,放开补丁 | 写死装不上补丁,不写可能跨大版本 |
| 优先属性引用 | 用 aws_vpc.main.id 建立依赖 |
depends_on 粒度粗,产生无谓等待 |
非关键属性加 ignore_changes |
把外部系统写入的字段排除 | 标签类属性变化触发替换 |
| 多区域复用 alias | 同一模块映射到不同 Provider 实例 | 复制配置导致两套代码漂移 |
plain
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = var.instance_type
lifecycle {
ignore_changes = [tags]
}
}
判断一个属性能不能进 ignore_changes,看它是否只由外部系统写入。由 Terraform 自己管理的字段一旦被排除,配置与实际状态就会长期不一致,plan 从此不再提示这类漂移。
多区域部署时,把区域相关的资源全部抽进模块,再用 providers 参数把模块内的 Provider 映射到不同别名的实例,而不是复制一份配置。
plain
module "app_use1" {
source = "./modules/app"
providers = {
aws = aws.use1
}
}
准则落进配置之后,靠什么确认它真的生效?
五、实操验证:init → plan → apply 逐条对账
按上述准则改完配置,你用三个命令依次确认结果。执行环境为本地终端,Terraform 1.9 以上。
5.1 terraform init:先确认版本约束被识别
bash
terraform init
latex
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.7.0"...
- Installing hashicorp/aws v5.7.1...
- Installed hashicorp/aws v5.7.1 (signed by HashiCorp)
Terraform has been successfully initialized!
你要在输出里盯两行:Finding 行确认约束被识别,Installing 行给出实际选中的版本。约束过严导致无匹配版本时,错误在 init 阶段就会暴露,不会拖到 plan 才出现。
5.2 terraform plan:先读符号,再读摘要
bash
terraform plan
latex
# aws_instance.web will be created
+ resource "aws_instance" "web" { ... }
# aws_security_group.old will be destroyed
- resource "aws_security_group" "old" { ... }
# aws_subnet.public will be updated in-place
~ resource "aws_subnet" "public" { ... }
Plan: 1 to add, 1 to change, 1 to destroy.
| 符号 | 含义 | 是否需要警惕 |
|---|---|---|
+ |
新建资源 | 正常 |
- |
删除资源 | 出现非预期删除需回查 |
~ |
原地更新 | 唯一不销毁原资源的动作 |
-/+ |
先销毁再重建 | 高风险,先确认属性是否必要 |
(known after apply) |
值在创建后才确定 | Provider 生成的 ID 属正常 |

摘要行 Plan: 1 to add, 1 to change, 1 to destroy 是判断变更规模的入口。改完配置后这里一旦出现意料之外的 destroy 或替换计数,先停下来查属性,不要直接 apply。
5.3 terraform apply:执行与输出值
plain
output "instance_id" {
value = aws_instance.web.id
description = "Web 实例的 ID"
}
bash
terraform apply
latex
Apply complete! Resources: 1 added, 1 changed, 1 destroyed.
Outputs:
instance_id = "i-0a1b2c3d4e5f67890"
标记为 sensitive = true 的输出在终端只显示 (sensitive value),真实值仍完整写进状态文件。遮蔽的只是屏幕显示,状态文件的访问权限要单独控制。
注意:plan 的摘要行只告诉你规模,符号才告诉你性质,先读符号再读摘要。
三个命令都跑通之后,这三层知识怎么变成下次改配置的速度?
六、总结延伸:从改参数到会调试
Provider 决定 Terraform 能做什么,Resource 决定做什么,依赖关系决定按什么顺序做。命令输出是这三件事的行为痕迹:init 暴露版本约束,plan 暴露变更类型,apply 暴露最终结果。
| 你看到的现象 | 对应哪一层 | 处置 |
|---|---|---|
| init 报无满足条件的版本 | Provider 版本约束 | 放宽约束或指定可用版本 |
plan 出现 -/+ |
Resource 属性不可原地更新 | 评估重建成本,或改用 ignore_changes |
| apply 长时间等待 | 依赖粒度 | 把 depends_on 改为属性引用 |
输出值显示 (sensitive value) |
输出标记 | 保护状态文件,而非改输出配置 |
我把这四行贴进了配置仓库的 README,改配置前先对照一遍,比事后翻 plan 便宜得多。

注意:真正需要背下来的只有一句 ------ plan 里的符号比参数本身更早暴露风险。
下次在 plan 里撞上一个没预料到的 -/+,你打算先去翻 Provider 的 schema,还是先改参数试一次?
术语速查表
| 术语 | 一句话解释 |
|---|---|
| Provider(提供者) | Terraform 与云厂商 API 之间的插件适配层 |
| Resource(资源) | 一个待创建或被管理的基础设施对象 |
| 元参数(meta-arguments) | Terraform 语言内置、对所有资源通用的参数 |
| 隐式依赖 | 由属性引用自动推断出的依赖关系 |
| 显式依赖 | 由 depends_on 手动声明的依赖关系 |
| 状态文件(state) | 记录真实资源与配置映射关系的文件 |
参考链接
- Terraform 语言 · Providers:https://developer.hashicorp.com/terraform/language/providers
- Terraform 语言 · Resources:https://developer.hashicorp.com/terraform/language/resources
- 元参数 depends_on:https://developer.hashicorp.com/terraform/language/meta-arguments/depends_on
- 命令参考 · terraform plan:https://developer.hashicorp.com/terraform/cli/commands/plan
- Terraform Registry:https://registry.terraform.io/