想象一下,你是这座城市的绿化管理员。玫瑰园在 A 苗圃,樟树在 B 苗圃,滴灌管道要找 C 厂商——每个苗圃的下单流程、验收标准都不同,而你手上只有一张“理想花园设计图”。如果每换一个苗圃都要重新学一套语言,花园永远建不起来。 Terraform 就是这位“智能园林总管家”。它把“我要一座花园”翻译成一份份清清楚楚的订单:找谁订货(Provider)、定什么货(Resource)、按什么顺序种(依赖图)、记在哪本台账(State),并严格按“先出图纸、再动土、定期巡检、按需清场”的流程执行。 本文用这座花园作比喻,带你看懂 Provider 与 Resource 的生命周期:从定义、配置、创建、读取、更新到删除,再到保护规则、状态台账与依赖顺序,最后用可运行的 Python 演示把整个流程跑一遍。读完你就能自信地解释“为什么 apply 前还要跑 plan”“台账丢了会怎样”“古树为什么拔不掉”。
掌握了 Provider 与 Resource 生命周期,你就能精准控制云端资源从生到死的每一步,写出可预测、可回滚、可审计的基础设施代码——这是云原生时代 SRE、DevOps 与平台工程师的核心基本功。
目录
可视化演示
展示Resource的CRUD四阶段生命周期、Core经Provider桥接外部API的数据流,以及配置与实际状态间的漂移检测。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Resource CRUD生命周期 创建/读取/更新/删除四阶段 CRUD→State 每次操作后资源属性写入State文件。
供应商基础:Provider 是什么
市政绿化处准备在滨江路种一百棵樱花树。电话打到苗圃,接单员张口就问:你们用哪家的苗木编码?土球规格怎么报?验收要不要带检疫证?你若不懂这套“苗圃语言”,一株都订不回来。于是绿化处聘请了一位专职的“苗圃对接员”——他熟记这家苗圃的所有下单规则,你把需求写给他,他去下单、跟踪、验收。
在 Terraform 的世界里,这位对接员就是 Provider。AWS、Azure、阿里云……每个云平台都有自己的“苗圃语言”:API、鉴权方式、资源类型各不相同。Provider 是一个插件,它清楚某个云平台的全部规则,把 Terraform 的通用指令翻译成该平台能执行的具体请求。你在配置里写 resource "alicloud_instance",Provider 就负责把它变成阿里云真实的一次创建调用。
Provider 通常在 .tf 文件中声明,并配上版本约束,避免团队“用的对接员版本不同、下单口径不同”。执行 terraform init 时,Terraform 会根据约束从 Registry(插件仓库)下载对应版本的插件并缓存在本地,像开工前把供货协议签好:
所以,当你说“我要一百棵樱花树”,真正去和苗圃打交道的是对接员(Provider)。后续创建、浇水、换盆、移除,全靠这位对接员用苗圃听得懂的语言一一落地。选对接员,是花园工程的第一步。
- 资源集合 \(\mathcal{R}\)所有你想在云上创建的“植物”,比如虚拟机、网络、磁盘,它们构成资源集合。
- 存在性 \(\exists\, p \in \mathcal{P}\)每种资源必须能找到归属的 Provider。
alicloud_instance由阿里云 Provider 负责,从不越界。 - Provider 动作 \(\alpha(p)\)Provider 内部把资源操作翻译成真实 API 请求,也就是“苗圃下单”那一步。
terraform { required_providers { alicloud = { source = "aliyun/alicloud" version = "~> 1.0" } }}provider "alicloud" { region = "cn-hangzhou"}
资源定义:Resource 块
台账上要给每株植物登记品种、位置、浇水频率。品种决定了它能长多大,位置决定它种在哪块地,属性则是它的“习性清单”。Resource 就是这张登记卡——告诉你“种什么、叫什么、怎么养护”。
Terraform 用 resource 块声明“我们希望云端存在某个对象”,它有三个核心要素:type(品种)、name(本地名字)、attrs(属性清单)。把这些写进 .tf 文件,terraform plan 就能据此计算出期望状态。
以阿里云 VPC 为例:
alicloud_vpc 是 type,web 是 name,cidr_block 和 vpc_name 是属性。type 决定能力——它告诉 Terraform 该去找哪个 Provider;name 只在当前配置内有效,类似台账上的编号;属性描述期望状态。其他资源通过 alicloud_vpc.web.id 这样的表达式引用它,从而形成依赖链条。
Resource 就是台账上那一行完整记录。品种对,名字清,属性全,花园里的每一件东西才说得清、管得住。
- 三元组结构资源的身份由 type 和 name 共同决定,全局唯一地址写作
type.name。 - 属性集合 \(\mathcal{A}\)每个属性都是一条“期望状态”,比如网段、名称、实例规格。
- 资源引用通过
type.name.attr把属性暴露给其他资源,比如alicloud_vpc.web.id。
resource "alicloud_vpc" "web" { cidr_block = "10.0.0.0/16" vpc_name = "web-env"}
生命周期四阶段
一株云杉从种子到老树,要经历播种、发芽、长大、移走。Terraform 把这压缩成四个官方操作:Create、Read、Update、Delete。任何资源都逃不出这个循环,就像植物的一生被归纳成四个动作。
这四个操作由 Provider 内部的资源实现提供,对应真实云 API 的若干次调用。Terraform 内核不关心“怎么创建一台 ECS”,它只负责在合适的时机调用 Provider 暴露的四个接口。
- Create:检测到资源不在 state 中,调用创建接口,成功后记录全新 ID;
- Read:查询真实资源的状态,用于漂移检测,常在
plan和refresh时触发;
- Update:配置属性与 state 有 diff 时,调用修改接口完成变更;
- Delete:执行 destroy 时调用删除接口,并把资源从 state 中移除。
一株植物从种子到移除,四个动作循环往复。Terraform 的全部魔法,就是精确掌控这四步的触发时机与执行顺序。
- 空状态 \(S_{\emptyset}\)起点和终点都是“空”——资源还不存在或已被彻底移除。
- 创建 \(S_{\mathrm{running}}\)Create 调用云端接口,资源进入运行态,state 记下新身份。
- 读取 \(S_{\mathrm{verified}}\)Read 核对真实环境与记录是否一致,状态被“验证”。
- 更新与删除Update 把资源改到期望状态,Delete 释放它,环回到空集。
生命周期规则:Lifecycle 块
园丁会在树旁挂保护牌:“古树严禁移走”“先栽新区再挖旧树”“这个字段不必每次更新”。牌子不影响植物生长,只约束园丁的操作方式。Terraform 的 lifecycle 块就是这些牌子。
把规则写进资源定义里,可精确干预替换、删除、更新行为:
- create_before_destroy:需要替换时先建新实例、成功后再删旧实例,减少停机窗口;
- prevent_destroy:禁止任何删除或替换动作,一旦 plan 中出现删除,apply 会直接报错;
- ignore_changes:忽略指定属性的变更,允许控制台手动修改而不被 Terraform “纠偏”。
保护牌约束的是园丁,不是台风。生命周期规则只影响 Terraform 自己的行为,明白这一点,才不会把“禁止删除”误当成万能保险。
- 偏序符号 \(\prec\)表示“先执行”。CBD 规则规定新资源创建必须先于旧资源销毁。
- 空集 \(\varnothing\)prevent_destroy 使删除操作集合为空,任何计划中的销毁都被禁止。
- 忽略集合
ignore_changes = [ami, tags]等价于从 diff 集合中剔除这些属性。
resource "aws_instance" "db" { lifecycle { create_before_destroy = true prevent_destroy = true ignore_changes = [ami, tags] }}
状态管理:那本不能丢的绿化台账
老张管花园十三年,靠的是一本厚台账:哪儿种了什么、什么状态、上次修剪是什么时候。一天刮台风,园子面目全非,老张拿着台账一棵棵核对,发现七棵桂花东倒西歪——这就是漂移。台账要是丢了,哪些是自家种的、哪些是野生的,根本分不清。
Terraform 的 terraform.tfstate 就是这本台账。它保存每个资源从 Provider 返回的唯一 ID 与全部属性,是 plan 和 apply 的计算基础:判断资源该增、该改还是该删,全看“台账 vs 期望 vs 真实环境”三者的关系。多人协作时,可把 state 放到远端后端(如 S3 + DynamoDB 锁)防止并发写坏;一旦发现漂移,terraform apply 会把真实环境修回台账记录。
台账让园丁认得每一株自家的植物。state 让 Terraform 认得每一个自己创建的资源,也让它一进门就能发现哪儿不对劲。
- 属性函数 \(\mathrm{Attr}\)
Attr_real是云端的真实属性,Attr_state是台账里记的属性。 - 不等号 \(\neq\)只要两边任何属性不一致,就判定该资源发生漂移,需要巡检或修复。
核心工作流:四道施工工序
花园改造不是抡起铁锹就挖。懂行的施工队先备料(init),再出图纸给你确认(plan),签字后动工(apply),完工后撤场并复垦(destroy)。四道工序,一道都不能乱。
Terraform 的核心工作流正是这四步:
- init:下载 Provider 插件,初始化状态后端,相当于“备料+签协议”;
- plan:对比 state 与真实环境,输出变更计划(
+新增、~修改、-删除),相当于“出图纸”;
- apply:按依赖图依次执行变更,逐步更新 state,相当于“动工”;
- destroy:删除 state 中记录的全部资源,释放云端占用,相当于“清场”。
辅助命令 fmt、validate、refresh 则像施工前的检查工具,保证图纸整洁、文法正确、台账够新。
工序井然,花园才不会越修越乱。Terraform 用四道固定工序,把“改基础设施”从即兴发挥变成可评审、可回滚的工程流程。
- 时点 \(T_0 \sim T_3\)每个时点代表环境的一类状态:未初始化、已计划、已应用、已清理。
- 命令箭头每个箭头是一次命令执行,箭头上的命令把状态推进到下一时点。
- 闭环返回 \(T_0\)destroy 把环境还原成起点,便于下次从零开始。
依赖关系图:树荫下的先后顺序
玉簪花喜阴,必须先种大树再种花;喷灌要把水管接好才能浇花。花园里到处是这种“先有谁、后有谁”的关系。园丁把关系画成一张网,按顺序施工,互不相关的角落则同时开工。
Terraform 通过解析资源间的引用(比如某个安全组引用了 VPC 的 ID)自动建立隐式依赖;也可以在资源配置中写 depends_on 做显式声明。所有依赖关系汇成一张有向无环图(DAG),通过拓扑排序决定执行顺序:无依赖的节点并行执行,有依赖的严格按序。如果出现循环依赖,plan 会直接报错,提示“你的花园设计里有个死结”。
先种大树,再种喜阴的花;先修水管,再浇灌花园。依赖图把这份园丁的经验固化成可计算的顺序,也让 Terraform 在复杂的基建中永远知道谁先谁后。
- 顶点集 \(\mathcal{V}\) 与边集 \(\mathcal{E}\)每个资源是一个顶点,每条依赖关系是一条有向边。
- 有向边 \((u, v)\)表示“u 先于 v”,比如 VPC 必须先于交换机创建。
- 拓扑序 \(\text{order}\)拓扑排序给每个顶点一个序号,保证所有边都从小编号指向大编号。
AI 应用场景
多云统一交付:一张设计图管两座苗圃
阶段:基础设施即代码(IaC)定义与编排
解决的问题:AI 训练集群需要 GPU 实例、VPC 网络、对象存储。传统方式要在多个云控制台手工点击几十次,容易配错安全组、记漏资源。用 Terraform 的多 Provider,一份代码同时对接阿里云与火山引擎,把不同云的差异封装在资源类型后面。
具体案例:某算法团队用 aliyun/alicloud 与 volcengine/volcengine 两个 Provider 定义同一套训练环境模板。terraform apply 一次拉起两朵云的资源,plan 先预览所有差异,误配在动工前就被拦下。
漂移巡检与自动恢复:给台账装上警报器
阶段:持续运维与状态一致性保障
解决的问题:生产环境偶尔被人绕过 Terraform 手动改动(例如临时开端口),导致台账失真。SRE 定时执行 terraform plan -detailed-exitcode,把非零退出码接入告警系统,再用受控的 apply 自动恢复,像园丁每天巡园、把被风吹歪的树扶正。
具体案例:某金融公司每晚自动巡检 200 余个资源,发现过 3 次未登记的安全组变更,均在 2 分钟内自动收敛回期望状态。
临时测试环境生命周期管理:用完即拆的临时花园
阶段:CI/CD 中的环境供给与回收
解决的问题:每次代码合并都要一套完整测试环境,人工搭建慢、长期占用又费钱。Terraform 的标准工作流正好解决:init + plan + apply 自动拉环境,合并后 destroy 一键回收。
具体案例:某互联网公司在 GitLab CI 中加入 Terraform 流水线,每个 MR 启动一套包含 RDS、Redis 的隔离环境,合并后自动销毁,云成本下降约 40%。
Python 演示
# -*- coding: utf-8 -*-"""迷你 Terraform: 演示 Provider 的资源生命周期与依赖排序""" from collections import deque class Provider: """迷你 Provider: 像苗圃对接员, 负责把资源操作翻译成云端动作""" def __init__(self, name): self.name = name self.backend = {} self.log = [] def create(self, res): self.backend[res.node_id] = {"state": "available"} self.log.append("[{}] Create {}".format(self.name, res.node_id)) def read(self, res): if res.node_id not in self.backend: self.log.append("[{}] Read {} 漂移: 真实环境不存在".format(self.name, res.node_id)) else: self.log.append("[{}] Read {} 状态正常".format(self.name, res.node_id)) def update(self, res): if res.node_id in self.backend: self.backend[res.node_id]["state"] = "updated" self.log.append("[{}] Update {}".format(self.name, res.node_id)) else: self.log.append("[{}] Update {} 被跳过".format(self.name, res.node_id)) def delete(self, res): existed = self.backend.pop(res.node_id, None) is not None self.log.append("[{}] Delete {} {}".format(self.name, res.node_id, "成功" if existed else "跳过")) class Resource: """一个资源: 只有名字和它依赖的其他资源""" def __init__(self, node_id, deps=None): self.node_id = node_id self.deps = deps or [] def topo_sort(resources): """拓扑排序: 先建被依赖方(VPC), 再建依赖方(ECS)""" visited = set() order = deque() def dfs(node): if node.node_id in visited: return visited.add(node.node_id) for dep in node.deps: dfs(dep) order.append(node) for res in resources: dfs(res) return list(order) if __name__ == "__main__": provider = Provider("aliyun") vpc = Resource("vpc-web") vswitch = Resource("vswitch-web", deps=[vpc]) ecs = Resource("ecs-web", deps=[vswitch]) resources = [vpc, vswitch, ecs] seq = topo_sort(resources) print("依赖顺序:", " -> ".join(res.node_id for res in seq)) for res in seq: provider.create(res) for res in seq: provider.read(res) provider.backend["vswitch-web"]["state"] = "port_changed" for res in seq: provider.update(res) for res in reversed(seq): provider.delete(res) print("\n".join(provider.log))
class Provider:定义一个迷你 Provider,相当于苗圃对接员,所有 CRUD 操作都由它执行。self.backend = {}用字典模拟云端后端:key 是资源 ID,value 是资源状态。def create(self, res):对应生命周期的 Create 阶段,把资源登记进“云端”。if res.node_id not in self.backend:Read 时先查表,查不到就说明发生了漂移。def topo_sort(resources):实现依赖图的拓扑排序,保证 VPC 先于 VSwitch 创建。visited.add(node.node_id)避免重复访问同一个节点,防止依赖图中出现环时死循环。order.append(node)深度优先搜索的后序写入,天然保证“被依赖方排在前面”。for res in seq:apply 阶段按依赖顺序逐个执行 Create。provider.backend["vswitch-web"]["state"] = "port_changed"模拟有人绕过 Terraform 手动改资源,制造漂移。for res in reversed(seq):destroy 必须逆拓扑序,先删 ECS 再删 VPC,否则会破坏依赖。print("\n".join(provider.log))输出完整操作日志,让整个生命周期一目了然。
常见误区
误区看到 `provider "aws" {}` 就以为“我在配置里写了一个 Provider,就相当于创建了一台 AWS 服务器”。
正解Provider 是翻译官、是插件,它自己不创建任何东西;真正被创建的是 Resource。Provider 负责把资源操作翻译成云平台 API 调用。你雇了对接员不等于苗圃里已经种好了树,还得下订单(定义 resource)才行。
为何常见:HCL 里 provider 和 resource 块长得像,加上 init 会“下载东西”,容易让人误以为 Provider 就是云端对象。
误区`terraform apply` 输出 "Apply complete!" 就万事大吉,资源会一直听话。
正解Terraform 只保证 apply 那一刻的状态收敛,不提供“持续守护”。真实环境可能被手动改动、被故障破坏,这就是漂移。要像园丁定期巡园一样,靠 plan/refresh 发现漂移,再靠 apply 修复。
为何常见:“Apply complete!“ 太有误导性,让人以为基础设施是”一次性交付”,忽略了声明式系统的常态是持续校准。
误区给资源加上 `prevent_destroy = true`,就再也不怕资源丢掉了。
正解它只在 Terraform 自己的 destroy/替换流程中生效,拦不住有人在控制台手动删除、账号被清理,更防不住 state 文件被删。而且加了它之后,想删这个资源必须先改代码,反而增加维护负担。挂牌子约束的是园丁,不是台风。
为何常见:规则名字太像“安全保险”,让人高估了它对真实世界的约束力。
误区反正有 `.tf` 配置,把 `terraform.tfstate` 删了,重新 apply 就能再建出来。
正解配置是“目标”,state 是“现状”。丢了 state,Terraform 就不知道真实资源的 ID,plan 拿配置和“空台账”比较,会把已有资源当成全新资源,apply 后可能创建出重复资源,甚至先 destroy 再 create,造成业务中断。台账丢了,园丁根本不认识谁是自己种的。
为何常见:混淆了“期望状态”与“实际状态”,以为配置里写清楚就万事俱备。
误区`terraform destroy` 很安全,它只删除“这次新建”的资源。
正解destroy 会按照依赖图删除 state 中记录的全部资源,等于把整座花园连根拔起。对共享或生产环境执行,后果是灾难性的。应通过 terraform destroy -target=... 精确清理,或用目录/workspace 隔离环境。
为何常见:没有意识到 state 记录的是“全部家当”,而不是“最近的操作记录”。
面试题
Q1. Terraform 中 Provider 的主要职责是?
- A.编写资源属性的配置
- B.对接云平台的 API 插件← 正确答案
- C.创建云端对象实例
- D.存储资源状态文件
答案:B。Provider 是翻译官插件,负责把资源操作翻译成云平台 API 调用,本身不创建对象(那是 Resource 的事),也不负责存储状态(那是 state 的事)。就像苗圃对接员只负责下单沟通,不负责种树。
Q2. terraform init 命令的核心作用是?
- A.下载插件并初始化后端← 正确答案
- B.创建云端全部资源
- C.生成资源变更计划
- D.删除已创建的资源
答案:A。init 对应施工前的“备料+签协议”:下载 Provider 插件并初始化状态后端。创建资源是 apply,生成计划是 plan,删除资源是 destroy。四道工序各司其职,init 永远在最前面。
Q3. 以下哪个生命周期规则表示“先建新资源再删旧资源”?
- A.prevent_destroy 表示禁止删除
- B.create_before_destroy 先建后删← 正确答案
- C.ignore_changes 忽略字段变更
- D.depends_on 声明资源依赖
答案:B。create_before_destroy 的语义是 Create(新) 先于 Destroy(旧),用于减少替换资源时的停机窗口。prevent_destroy 是禁止删除,ignore_changes 是忽略属性 diff,depends_on 则属于依赖声明,不属于生命周期规则。
Q4. 资源引用表达式 alicloud_vpc.web.id 的三段结构是?
- A.资源类型 资源名 属性名← 正确答案
- B.资源名 资源类型 属性名
- C.Provider 名 资源类型 属性名
- D.属性名 资源类型 资源名
答案:A。Terraform 的资源引用固定为 type.name.attr:alicloud_vpc 是类型,web 是本地名字,id 是属性。这与台账上“品种-编号-字段”的登记逻辑一致。
Q5. 基础设施漂移 (drift) 指的是什么?
- A.配置代码修改但没重新执行 apply
- B.Provider 插件版本互相冲突
- C.真实环境与云端台账记录不一致← 正确答案
- D.依赖关系图中出现环路错误
答案:C。漂移数学定义是 Attr_real(r) ≠ Attr_state(r),即真实环境与 state 记录不一致。选项 A 只是“没执行 apply”,B 是版本冲突,D 是依赖图环,都不属于漂移。
Q6. 关于依赖图与执行顺序,正确的是?
- A.隐式依赖的优先级低于显式依赖
- B.互不依赖的资源可以并行执行← 正确答案
- C.Terraform 始终按照声明顺序执行
- D.依赖图中有环时自动跳过一条边
答案:B。DAG 的拓扑排序允许无依赖节点并行执行,有依赖的严格按序。隐式依赖与显式依赖没有优先级之分;Terraform 不按声明顺序执行;遇到循环依赖会在 plan 阶段直接报错,而不是跳边。
Q7. Read 读取操作通常在什么阶段触发?
- A.init 阶段在下载插件时触发
- B.destroy 删除资源前触发
- C.apply 创建完资源后触发一次
- D.plan 或 refresh 巡检真实环境时触发← 正确答案
答案:D。Read 主要用于漂移检测,在 terraform plan 和 terraform refresh 时读取真实环境与 state 对比,就像园丁巡园核对台账。init 只下载插件,create 时才写 state,destroy 前不强制 read。
Q8. 哪些做法可能导致 state 与实际环境不一致?
- A.在控制台上手动删除资源实例← 正确答案
- B.将 Provider 版本约束改得更严格
- C.绕过 Terraform 直接修改资源属性← 正确答案
- D.每天定时执行 terraform plan 检查
答案:AC。手动删除资源或直接改属性都会让真实环境偏离台账,产生漂移。修改版本约束只是改变插件版本选型;定期执行 plan 反而是发现漂移的正确做法,不会造成不一致。
Q9. 关于 prevent_destroy 规则,下列说法正确的有?
- A.它能阻止替换该资源时先删旧资源← 正确答案
- B.需要删除资源时必须先移除该规则← 正确答案
- C.它能防止后端 state 文件被误删
- D.对数据源 data 资源也有保护效果
答案:AB。prevent_destroy 会让一切 destroy 计划(包括替换)直接报错,因此想删必须先删掉这个规则——A、B 正确。它约束不了手动删 state 或控制台操作,且只作用于 resource,对 data 源无效。
Q10. 关于状态管理与工作流的说法,正确的有?
- A.远端后端配合锁机制防范并发写入← 正确答案
- B.plan 会综合比较配置、真实环境与 state← 正确答案
- C.apply 执行失败后 state 一定不会更新
- D.destroy 仅删除本次新创建的资源
答案:AB。远端后端通常用锁(如 DynamoDB)防止多人同时写 state,A 正确;plan 的输出正是三向对比的结果,B 正确。apply 部分失败时 state 只更新成功部分,C 错误;destroy 删除的是 state 中记录的全部资源,D 错误。