照着官方示例只把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 调用。
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区分。
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 无关,对所有资源通用。
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会先构建一张资源依赖图,再据此决定创建、更新、销毁的顺序。依赖的建立有两条路径。
读取配置与状态 │ ▼ 构建依赖图 │ ┌─────┴─────┐ ▼ ▼ 隐式依赖 显式依赖 属性引用 depends_on │ │ └─────┬─────┘ ▼ 拓扑排序 → 创建 / 更新 / 销毁| 维度 | 隐式依赖 | 显式依赖 |
|---|---|---|
| 建立方式 | 参数中引用另一资源的属性 | depends_on列表 |
| 依赖精度 | 精确到具体属性 | 精确到整个资源 |
| 变更影响 | 其他属性变化不牵动下游 | 任一属性变化都牵动下游 |
| 使用建议 | 默认首选 | 仅在属性引用无法表达时使用 |
注意:隐式依赖精确到属性,显式依赖精确到整个资源—— 这就是depends_on会带来无谓等待的原因。
原理层面三件事已经串起来了,落到配置上具体该守哪几条规矩?
四、解决方案:四条改配置的准则
改配置的目标是让 plan 的输出符合预期。以下四条按影响面从大到小排列,每条对应第二节里的一个坑。
| 准则 | 写法 | 规避的坑 |
|---|---|---|
版本用~>约束 | 约束主次版本,放开补丁 | 写死装不上补丁,不写可能跨大版本 |
| 优先属性引用 | 用aws_vpc.main.id建立依赖 | depends_on粒度粗,产生无谓等待 |
非关键属性加ignore_changes | 把外部系统写入的字段排除 | 标签类属性变化触发替换 |
| 多区域复用 alias | 同一模块映射到不同 Provider 实例 | 复制配置导致两套代码漂移 |
resource "aws_instance" "web" { ami = "ami-0abcdef1234567890" instance_type = var.instance_type lifecycle { ignore_changes = [tags] } }判断一个属性能不能进ignore_changes,看它是否只由外部系统写入。由 Terraform 自己管理的字段一旦被排除,配置与实际状态就会长期不一致,plan从此不再提示这类漂移。
多区域部署时,把区域相关的资源全部抽进模块,再用providers参数把模块内的 Provider 映射到不同别名的实例,而不是复制一份配置。
module "app_use1" { source = "./modules/app" providers = { aws = aws.use1 } }准则落进配置之后,靠什么确认它真的生效?
五、实操验证:init → plan → apply 逐条对账
按上述准则改完配置,你用三个命令依次确认结果。执行环境为本地终端,Terraform 1.9 以上。
5.1 terraform init:先确认版本约束被识别
terraform initInitializing 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:先读符号,再读摘要
terraform plan# 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:执行与输出值
output "instance_id" { value = aws_instance.web.id description = "Web 实例的 ID" }terraform applyApply 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/