在 GCP 的 Terraform(以下简称 TF)运维中,我们经常会遇到资源生命周期不一致的问题。
最近在做资源清理和重构时,碰到了一个非常经典的 TF 状态机对齐"大坑":如何优雅地下线一个托管实例组(MIG),但保留它所依赖的实例模板(Instance Template)?
1. 场景与痛点
项目里引用了一个封装好的 MIG 模块,它的内部结构很简单:一个 google_compute_instance_template(实例模板),后面紧跟着一个 google_compute_instance_group_manager(MIG 组管理器)引用这个模板。
现在因为架构调整,这个 MIG(托管实例组)不需要了,我们要把它下线。但是,这个实例模板(Instance Template)线上还有其他独立的 VM 在使用,或者我们需要留作备份,绝对不能删。
按常规思路,我们在主配置里直接把模块调用处的 count 设为 0:
hcl
module "my_new_mig" {
count = 0
source = "github.com/nvd11/terraform-mig?ref=1.0.6"
...
}
然而,跑 terraform plan 的时候,终端却给出了毁灭性的提示:
text
# module.mig.module.my_new_mig.google_compute_instance_template.mig_template will be destroyed
- resource "google_compute_instance_template" "mig_template" {
...
}
MIG 确实是不建了,但 TF 居然连带着要把线上好端端的实例模板也一块儿给 Destroy 掉!这显然无法接受。
2. 为什么 TF 会"连带击杀"?
要解决这个问题,首先得明白 TF 的逻辑。
这个 mig_template 是在 module 的"肚子里"声明的。当你把整个模块的 count 设为 0(或者直接在代码中删掉这个 module 块)时,在 TF 看来:这个模块对应的所有代码配置都不复存在了。
但因为这个实例模板之前已经被成功创建过,它的元数据和状态依然保存在当前的 .tfstate(状态文件)里。
TF 在 Plan 时的比对逻辑是:
- 检测到状态文件(State)里有
module.mig.module.my_new_mig.google_compute_instance_template.mig_template的记录。 - 比对当前的配置(Configuration),发现代码里该模块
count = 0。 - 为了让线上真实世界和你的代码配置达成一致,TF 只能执行:"把线上这个处于该 Module 命名空间下的实例模板强制销毁"。
3. 解决方案
针对这种"只要保留线上模板,不要 MIG"的诉求,通常有两种最实用的解法。
方案一:彻底解耦,将模板移出 TF 管理(最推荐、最干净)
如果你以后不需要再用这套 TF 代码去管理、更新这个实例模板,只是想让它在线上安全地躺着,供其他地方手动引用。
直接修改 .tfstate 状态机,让 TF 单向失忆。
第一步:代码层面停用模块
在你的主配置文件(比如 envoy-proxy.tf)中,保持 count = 0(或者直接删掉整个 module "my_new_mig" 块)。
第二步:从状态机中手动移除该资源
在终端执行以下命令,把实例模板从 TF 的"记忆"里强行抹去:
bash
terraform state rm module.mig.module.my_new_mig.google_compute_instance_template.mig_template
(如果旧的 MIG 在 State 里也有残留,可以顺手用 terraform state rm module.mig.module.my_new_mig.google_compute_instance_group_manager.mig 一并清理掉)
原理解析:
terraform state rm 只是修改本地或远端的 State 状态文件,绝对不会 向 GCP 发送任何真实的 Delete 请求。
执行完后,TF 认为这个模板根本不归它管。此时你再跑 terraform plan:
- 代码里:
count = 0,不创建; - 状态里:没有这个模板的任何记忆,无需销毁。
Plan 结果直接变为完美的0 to add, 0 to change, 0 to destroy。线上资源毫发无损。
方案二:状态转移,把模板收归根级资源继续管理
如果你未来还需要在这套 TF 代码里,继续对这个实例模板进行升级、修改,只是单纯地不想要 MIG 这个集群。
思路:把实例模板从模块(Module)的命名空间里"过户"到根级资源下。
第一步:在根级代码中独立声明该实例模板
我们不再通过 Module 管理了,直接写在主配置(如 envoy-proxy.tf)的根级下:
hcl
resource "google_compute_instance_template" "my_envoy_proxy_template" {
name_prefix = "my-envoy-proxy-"
machine_type = "e2-medium"
# ... 抄写之前模块里对应的 disk、network_interface、service_account 等配置 ...
}
同时,保持原有模块 count = 0。
第二步:通过 state mv 完成过户
我们需要把 state 里那个挂在 module 名字下的模板实体,重命名(过户)为你刚刚在根级声明的资源:
bash
terraform state mv \
module.mig.module.my_new_mig.google_compute_instance_template.mig_template \
google_compute_instance_template.my_envoy_proxy_template
原理解析:
通过 state mv,线上同一个实例模板的控制权,从"即将被销毁的 Module 命名空间"顺利转移到了"你刚刚新建 of 根级配置"。
这样在 terraform plan 时,TF 发现代码里有一个根级模板,State 里也刚好对得上(只是改了名字,但指向同一个线上实体),所以它不仅不会去删除它,以后你还能像以前一样继续修改和升级它,同时原模块里的 MIG 也会被安全地清理掉。
4. 总结
Terraform 是声明式的。当我们要下线某个组合模块(Module)时,如果想保留其中的个别"子资源",切忌直接一刀切 count = 0。
- 想放手 :用
state rm解耦,让 TF 遗忘,线上物理保留。 - 想续管 :在根级写好配置,用
state mv过户,平滑承接。