1. 重构心智模型
一句话总结: Terraform 重构的核心是「让代码意图与 state 地址对齐」,任何改动分三步——改代码、对 state、验证 plan,且每一步都不应产生意外删除。
Terraform 把「代码里的资源地址」与「state 里的记录」一一对应。重构的本质是:代码变了,但云端资源没变,所以要同步改地址映射,否则 plan 会把资源当成「删除再重建」。
代码地址 state 记录 云端资源
aws_instance.web[0] ─────── 同名记录 ──────────────── i-0abc123
(改名后) (地址不匹配)
aws_instance.web_server 无记录 → 计划创建 (仍是旧实例)
aws_instance.web 记录 → 计划删除!
1.1 重构三原则
| 原则 | 含义 |
|---|---|
| 先对状态,后看计划 | 改地址后先 plan,确认无「删除」类意外 |
| 一次只动一处 | 地址迁移与属性变更分开提交 |
| 以 plan 为镜子 | 任何重构都以「plan 无意外删除/重建」为通过标准 |
2. moved block 地址迁移
一句话总结:
movedblock 声明「资源从旧地址搬到新地址」,Terraform 据此把 state 记录迁移过去,是代码级重构的官方首选。
moved block 写在配置里,声明地址迁移关系。apply 时 Terraform 自动把旧地址的 state 记录搬去新地址,随后计划收敛。
# 原来叫 aws_instance.web
resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
# 声明迁移:从旧地址到新地址
moved {
from = aws_instance.web
to = aws_instance.web_server
}
2.1 批量迁移与模块场景
重构进模块、重命名 module 实例、拆分资源时,moved 同样适用,且可多个 block 同时声明。
# 资源被收进模块
moved {
from = aws_instance.web
to = module.webserver.aws_instance.web
}
# 模块实例改名
moved {
from = module.legacy
to = module.webserver
}
2.2 moved 与 count/for_each 迁移
for_each 键名变化或 count 变 for_each 时,用 moved 逐条声明键映射。
resource "aws_iam_user" "users" {
for_each = toset(["alice", "bob", "carol"])
name = each.value
}
moved {
from = aws_iam_user.alice
to = aws_iam_user.users["alice"]
}
一句话:moved block 是「可提交进仓库的迁移声明」,评审友好、可保留历史,适合随代码一起演进;完成后再择机删除。
3. state mv 手工搬移
一句话总结:
terraform state mv在命令行直接搬移 state 记录,适合临时性、脚本化的地址调整;它不是代码的一部分,适合一次性或批量操作。
当地址变化是一次性操作(或者不方便在代码里加 moved block)时,用 state mv 直接搬。
# 单地址搬移
terraform state mv aws_instance.web aws_instance.web_server
# 批量前缀搬移(模块化时常用)
terraform state mv 'aws_instance.web' 'module.webserver.aws_instance.web'
# 加 dry-run 确认
terraform state mv -dry-run aws_instance.web aws_instance.web_server
3.1 state mv 的语义细节
- mv 只改 state 地址,不触发云上任何变更。
- 目标地址若已存在记录会报错,除非用
-replace(谨慎)。 - 搬移后立即
terraform plan,确认与代码意图一致。
3.2 moved 与 state mv 的选择
| 方式 | 是否入库 | 适用场景 |
|---|---|---|
moved block | 是,随代码提交 | 长期重构、模块化演进 |
terraform state mv | 否,命令操作 | 一次性调整、批量迁移 |
两者可以混用:大规模调整先用 state mv 脚本批量处理,后续演进用 moved block 固化在代码里。
4. import 采纳存量资源
一句话总结:
terraform import把「云上已存在、Terraform 未管理」的资源纳入 state,是「从零到 IaC」和「接纳历史资产」的标准入口。
历史遗留的手工资源,或从其他工具接管过来的资源,先用 import 让 Terraform 认识它,再逐步收敛配置。
# 语法:terraform import <地址> <云资源ID>
terraform import aws_instance.web_server i-0abc123
4.1 import 后必须对齐代码
import 只是「登记」:state 里有了记录,但代码里若没有对应 resource 块,下次 plan 仍会计划删除。正确流程是三步走。
# 1. 先写好 resource 块(属性先留占位)
resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
# 2. import 登记
# terraform import aws_instance.web_server i-0abc123
# 3. plan 收敛:补齐必填属性,消除 diff
4.2 import 常见场景
| 场景 | 示例 |
|---|---|
| 接管手工资源 | 手工建的 S3 桶、安全组 |
| 从 CloudFormation 迁移 | 已存在 EC2/RDS 转 Terraform |
| 从其他 IaC 迁移 | Pulumi/Ansible 管理的资源 |
| 子资源导入 | 单独纳管某个对象 |
4.3 import 块(Terraform 1.5+)
Terraform 1.5 起支持声明式 import block,把导入写成代码的一部分。
import {
to = aws_instance.web_server
id = "i-0abc123"
}
terraform plan 时自动执行导入并校验差异,比命令行 import 更可评审、可回放。
5. 循环引用消除
一句话总结: 循环引用是重构时最常见的编译期障碍,解法是先打破依赖环,把「互相引用」改成「数据源读取」或「先算后传」。
5.1 循环引用的成因
两个资源互相引用对方属性就会成环。例如安全组互相放行彼此,或 EC2 与弹性 IP 双向引用。
# ❌ 成环:A 引用 B,B 又引用 A
resource "aws_instance" "a" {
vpc_security_group_ids = [aws_security_group.b.id]
}
resource "aws_security_group" "b" {
ingress {
security_groups = [aws_instance.a.security_groups]
}
}
5.2 打破循环的三种方式
| 方式 | 做法 |
|---|---|
| 拆环 | 把环上的一个引用改成已知值(CIDR、变量) |
| 数据源 | 用 data 读取对方已存在的 ID,解除引用关系 |
| 分步 | 一个资源先建,另一个后引用其输出 |
# ✅ 用 data 读取既有安全组,打破循环
data "aws_security_group" "existing" {
name = "shared-sg"
}
resource "aws_instance" "a" {
vpc_security_group_ids = [data.aws_security_group.existing.id]
}
一句话:循环引用是「依赖图打结」,数据源与变量是剪线的剪刀,先剪断环再谈其他重构。
6. 大变更拆分
一句话总结: 大规模迁移要拆成「可独立验证的小步」,每步一次 apply、一次 plan 审计,先迁后删,避免一把梭触发大面积删除。
6.1 安全拆分的节奏
第 1 步:加新代码,用 import/moved 让 state 认识
第 2 步:plan 确认只有新增、没有删除
第 3 步:apply,验证新资源
第 4 步:移除旧代码,plan 确认只删除旧资源
第 5 步:apply,清理旧资源与旧 state
每步之间都要 terraform plan 审计「变更集合是否可控」。
6.2 批量迁移脚本示例
#!/bin/bash
# 批量 import 一批 EC2
for id in i-0abc1 i-0abc2 i-0abc3; do
terraform import "aws_instance.web[\"$id\"]" "$id"
done
# 逐条确认迁移结果
terraform state list | grep aws_instance
terraform plan -detailed-exitcode
6.3 拆分的判断信号
| 信号 | 处理 |
|---|---|
| plan 显示超过 N 个删除 | 立即停止,拆小步 |
| 迁移与属性变更混在一起 | 属性变更单独提交 |
| 涉及不可重建资源(数据库) | 单独演练,先备份 |
7. 迁移演练与回滚
一句话总结: 迁移前先做演练(dry-run、临时分支),回滚靠「备份 state + 代码回退」,生产迁移选择低峰窗口并保留快照。
7.1 演练
# 状态备份
terraform state pull > backup.$(date +%s).tfstate
# dry-run 迁移
terraform state mv -dry-run aws_instance.web aws_instance.web_server
# 计划审计
terraform plan -detailed-exitcode
7.2 回滚路径
| 阶段 | 回滚手段 |
|---|---|
| 代码未 apply | 直接回退 PR |
| state 已 mv 未 apply | state mv 反方向搬回 |
| 已 apply | 代码回退 + 按需 terraform apply 恢复 |
| 云资源已变 | 依赖备份与快照恢复 |
迁移期间全程禁用 terraform apply -auto-approve,任何自动审批都会让回滚窗口消失。
8. 总结
资源重构与迁移,是把「老代码」平滑带到「新结构」而不触发云上破坏的系统工程:
| 环节 | 要点 |
|---|---|
| 心智 | 代码地址与 state 地址对齐,plan 为准 |
| moved | 入库的地址迁移声明,评审友好 |
| state mv | 命令级搬移,适合一次性批量 |
| import | 接纳存量资源,先登记再收敛 |
| 循环引用 | 用数据源/变量剪断依赖环 |
| 拆分 | 先迁后删,每步 plan 审计 |
| 回滚 | 备份 state + 代码回退 + 演练 |
一句话收尾:重构不是「改代码」,而是「改地址映射」,每一步都以 plan 无意外删除为绿灯。把 moved/import/拆分与回滚用熟,再大的架构演进都能小步安全落地。下一篇「漂移检测与收敛」将讲如何发现并修复「云端现状与代码不一致」的问题。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。