跳到主要内容

资源与数据源

上一篇把 HCL 语言本身过了一遍,这篇回到 Terraform 的主业:用 resource 和 data 管东西。资源怎么寻址、依赖怎么排、删错了怎么挡、别人手动改过了怎么发现——这些是日常运维里天天打交道的部分。示例基于 Terraform 1.9+,依然只用 local_file、null_resource、random_pet 这类本地 provider,macOS / Linux 下不需要云账号即可照抄执行。

resource 与 data source​

这两兄弟的分工一句话说清:

  • resource 管「创建与维护」:声明一个应该存在的对象,Terraform 负责创建它、配置变了更新它、从配置里删掉就销毁它。它的完整生命周期归 Terraform 管。
  • data source 管「查询已有的」:只读,用来拿一个不由当前配置管理的对象的信息。plan 阶段执行查询,结果可以当属性引用。

用本地文件打个对照,一看就明白:

# data:读取磁盘上已经存在的文件,Terraform 不会创建或修改它
data "local_file" "existing" {
filename = "${path.module}/existing.txt"
}

# resource:这个文件由 Terraform 负责创建,内容变了会覆盖重写
resource "local_file" "managed" {
filename = "${path.module}/managed.txt"
content = data.local_file.existing.content
}

执行 terraform apply 之前,先手动创建 existing.txt 随便写点内容,apply 之后 managed.txt 的内容就和它一致了。哪天手动把 managed.txt 改掉,下一次 plan 会报差异并计划改回配置声明的内容——这就是「管理」的含义。

data source 的读取时机

data source 默认在 plan 阶段查询。如果它依赖一个本次 apply 才会创建的资源,查询会被推迟到 apply 阶段执行(plan 输出里标 read during apply)。查询结果不存在的对象会直接报错,data source 不能用来「探测对象是否存在」。

资源寻址​

每个资源实例在 state 里都有唯一地址,格式是 资源类型.资源名:

local_file.managed                  # 普通资源
local_file.by_count[0] # count 创建的实例,按下标
local_file.configs["app"] # for_each 创建的实例,按键
module.network.local_file.demo # 子模块里的资源,加 module 前缀

terraform state list 列出来的就是这些地址,后面 import、taint、-replace 等所有命令的参数都是它:

# 列出 state 里所有资源地址
terraform state list

# 查看某个资源的完整属性
terraform state show local_file.managed
地址就是资源的身份证

Terraform 靠地址判断「这个资源是新建、更新还是删除」。给资源改名(改块标签)在 Terraform 眼里是「删旧的建新的」,想保留对象就配 terraform state mv 迁移地址,或者直接用 1.1+ 的 moved 块声明地址变迁。

依赖管理​

Terraform 会根据属性引用自动排出资源的创建顺序:引用即依赖。上面例子里 managed 的 content 引用了 data.local_file.existing 的结果,依赖关系就建立了,不需要任何额外声明。

但有一类场景引用表达不了:两个资源之间没有数据传递,只有执行顺序要求,典型的是带副作用的操作。这时用 depends_on 显式声明:

resource "null_resource" "init_dirs" {
provisioner "local-exec" {
command = "mkdir -p ${path.module}/output/logs"
}
}

resource "null_resource" "write_log" {
# 和 init_dirs 没有数据引用关系,但必须等目录建好再写日志
depends_on = [null_resource.init_dirs]

provisioner "local-exec" {
command = "echo started >> ${path.module}/output/logs/app.log"
}
}
depends_on 是最后手段

能靠引用表达的就别写 depends_on。显式依赖会让 Terraform 失去对依赖原因的感知,plan 输出里出现「依赖变化但原因未知」的级联更新提示,大多源自多余的 depends_on。

lifecycle 生命周期​

lifecycle 嵌套块用来改写资源的默认生命周行为,日常用得到的就三个参数。

create_before_destroy:先建后删​

默认的替换顺序是「先删旧、再建新」,中间有服务空窗。加上 create_before_destroy 后反过来,新实例就绪了再销毁旧的:

variable "app_version" {
type = string
default = "1.0"
}

resource "random_pet" "server_name" {
# keepers 变化会触发资源替换重建
keepers = {
version = var.app_version
}

lifecycle {
create_before_destroy = true
}
}

terraform apply -var="app_version=2.0" 时,先生成新名字再销毁旧资源。适合负载均衡后端、服务实例这类「不能同时没有」的对象。

prevent_destroy:防误删​

给删不得的资源上一道锁:

resource "local_file" "critical_data" {
filename = "${path.module}/important.txt"
content = "删了要背锅的数据"

lifecycle {
prevent_destroy = true
}
}

之后任何包含销毁该资源的 plan 会直接报错中止,terraform destroy 也被挡在门外。真要删的时候,先从配置里去掉这个 lifecycle 块,apply 一次,再删资源。

ignore_changes:忽略指定字段​

某些字段会被外部系统或人工修改,不希望 Terraform 每次都「纠正」回来,就把字段列入忽略名单:

resource "local_file" "app_conf" {
filename = "${path.module}/app.conf"
content = "initial config"

lifecycle {
ignore_changes = [content]
}
}

之后手动改 app.conf 的内容,plan 不会再计划把它改回来。ignore_changes = all 可以忽略全部字段,相当于「只管创建、不管维护」,慎用,理由见文末常见坑。

导入存量资源 terraform import​

Terraform 只能管 state 里有记录的对象。已经存在的基础设施(人肉在控制台建的、别的工具建的)要纳入管理,先 import 进来。

命令式用法:terraform import 地址 ID,ID 的格式由 provider 定义(查 provider 文档的 import 一节):

# 把磁盘上已有的 existing.txt 导入为 local_file.existing
# local_file 的 import ID 就是文件路径
terraform import local_file.existing "${PWD}/existing.txt"

命令式 import 只写 state,不会自动生成配置——导入之后必须手工把对应的 resource 块补进 .tf 文件,否则下一次 plan 会认为这个资源该删掉。

Terraform 1.5+ 推荐声明式的 import 块写法,把导入意图写进配置,plan 阶段就能预览:

import {
to = local_file.existing
id = "${path.module}/existing.txt"
}

配合配置生成,连资源草稿都能让 Terraform 代写:

# 预览导入计划,并把资源的现有属性生成为配置草稿
terraform plan -generate-config-out=generated.tf

# 确认无误后执行导入
terraform apply
生成的配置要人工核对

generated.tf 里是资源所有属性的快照,包含很多可省略的默认值。核对后精简一遍挪进正式配置文件,顺手把 import 块删掉(导入成功后它就完成使命了)。

标记重建 taint/untaint​

资源本身坏了(文件被手动改坏、初始化脚本跑了一半),但配置没问题,想强制重建它——这是 taint 的用途:

# 标记为「已污染」,下一次 apply 会销毁并重建
terraform taint local_file.managed

# 反悔了可以取消标记
terraform untaint local_file.managed

不过 terraform taint 从 0.15.2 起就被标记为废弃,因为它先改 state 再跑 apply,两步之间状态说不清。1.x 里的推荐姿势是 -replace 选项,一步完成且可以先预览:

# 先预览替换计划,确认影响范围
terraform plan -replace=local_file.managed

# 确认后执行替换
terraform apply -replace=local_file.managed

效果与 taint + apply 完全等价,但语义更明确:变更直接体现在这一次的 plan 里,不用先污染 state。

漂移 drift​

漂移(drift)指真实世界的状态偏离了 state 的记录——典型场景是有人绕过 Terraform 在控制台或命令行手动改了东西。漂移不会让 apply 报错,但会让「配置即真相」的前提失效。state 与 plan 的基础概念可参考核心概念与架构一篇。

用本地文件亲手制造一次漂移:terraform apply 建好 managed.txt 后,手动改它的内容,再跑 plan:

echo "someone changed me" > managed.txt
terraform plan

plan 输出开头会先刷新现实状态(refresh),出现 Objects have changed outside of Terraform 段落,逐项列出检测到的差异,然后给出把现实改回配置的计划。

只想「看看漂了没有、不打算动任何东西」时,用只刷新模式:

# 只做 refresh 对比并展示差异,不生成变更计划
terraform plan -refresh-only

# 接受现实:把真实状态写回 state(不改配置、不改资源)
terraform apply -refresh-only

注意 -refresh-only 的 apply 改的是 state 而不是资源本身,适用于「确认手动改动是合法的,让 Terraform 认账」的场景。

老命令 refresh 已废弃

老资料里的 terraform refresh 会不经确认直接改写 state,0.15.4 起废弃,统一用 -refresh-only 这一套先预览再执行的组合。

常见坑​

按踩坑频率排序:

  1. prevent_destroy 挡住 destroy 时的表现:报错信息是 Instance cannot be destroyed,在 plan 阶段就中止,整条流水线停住。这不是 bug,是锁在工作。确认要删之后:去掉 lifecycle 块 → apply 一次让锁解除 → 再删资源或跑 destroy。紧急情况下也有人用 terraform state rm 把资源移出管理再手动处理,但那等于放弃 Terraform 的善后,清理残留要自己负责。

  2. ignore_changes 滥用导致配置失真:图省事写 ignore_changes = all,结果就是 Terraform 只负责创建,之后资源被改成什么样都不管,配置文件的描述和现实彻底脱节,新人看配置以为懂了系统,其实完全是两回事。正确做法是只列具体被外部接管的字段,并在旁边写注释说明谁在改它。

  3. import 后 state 与配置不一致:import 只管写 state,不核对配置。资源的实际属性和 .tf 里声明的对不上时,导入后的第一次 plan 会甩出一大串变更计划,吓人且危险。标准流程是:import 块 + -generate-config-out 生成草稿 → 人工核对精简 → apply 导入 → 再跑一遍 plan 确认 No changes 才收工。

小结​

  • resource 管创建维护,data source 管只读查询;data 在 plan 期执行,依赖未建资源时会推迟到 apply。
  • 资源地址是 type.name,count 实例带下标、for_each 实例带键、模块里的带前缀,terraform state list 里看到的就是它。
  • 引用即隐式依赖,depends_on 只留给无数据引用的副作用排序。
  • lifecycle 三件套:create_before_destroy 先建后删不停机、prevent_destroy 防误删、ignore_changes 只列具体字段。
  • 存量资源用 1.5+ 的 import 块声明式导入,plan 先预览,-generate-config-out 代写配置草稿。
  • 强制重建用 apply -replace,别再用废弃的 taint;漂移用 plan 暴露,只看不改用 -refresh-only。