「存量资源纳管与 Import」

讲解如何把手工创建的存量云资源纳入 Terraform 管理:import 块与 CLI import 的差异、状态对齐与 refresh、用 -generate-config-out 生成配置、渐进式接管与模块化,以及避免误删资源的护栏设计。

1. 为什么存量资源需要纳管

一句话总结: 真实世界极少是绿地项目,绝大多数团队面对的是「几百个手工点出来的资源」,纳管它们是把 IaC 从演示推进到生产的第一道门槛。

绿地项目里 terraform apply 从零创建资源,一切干净。但大多数云账号是「棕地」(brownfield):控制台点出来的 EC2、脚本建的 S3、别人用 CLI 开的 RDS。这些资源没有状态,直接写配置再 apply 的后果是「计划创建同名资源」——轻则重名冲突,重则误删重建。

纳管的目标是让 Terraform 相信「这些资源一直是我管的」:

# 纳管前后的对比
terraform plan          # 未纳管:计划创建 12 个资源(危险)
# ...import 之后...
terraform plan          # 已纳管:No changes(理想状态)
阶段目标风险
识别摸清账号里有哪些资源遗漏导致后续冲突
导入把资源写进 state配置与真实属性不一致
对齐plan 收敛到 No changes差异被忽略留下隐患
固化纳入模块与流水线权限过大误删

一句话:纳管的核心不是「import 命令」,而是「让 plan 输出 No changes」——只要还有差异,就说明你对资源的描述还是错的。

2. import 块与 CLI import

一句话总结: CLI 的 terraform import 只写状态不写配置,import 块(1.5+)则把导入声明写进代码、可评审、可批量执行,是当代推荐做法。

CLI 导入是「一次性动作」,配置要自己补:

# 传统方式:先写一个空壳资源块,再 import
terraform import aws_s3_bucket.data my-existing-bucket

# 导入后状态有了,但配置是空的,plan 会显示一堆差异
terraform plan

import 块把「导入」变成声明式代码,可以进 PR、可以被 review、可以一次导入一批:

# imports.tf
import {
  to = aws_s3_bucket.data
  id = "my-existing-bucket"
}

import {
  to = aws_instance.web
  id = "i-0abc123def4567890"
}
# 执行导入(会提示确认)
terraform plan
terraform apply

2.1 两种方式的取舍

维度CLI importimport 块
可评审否,本地动作是,进版本控制
批量脚本循环一个文件写多条
幂等重复执行报错已导入则跳过
配置生成需手工补可配合 generate-config-out
适用临时救火正式纳管流程
# import 块导入后即可删除,避免重复导入报错
# 保留在代码里也是幂等的,Terraform 会识别已导入

一句话:新项目一律用 import 块——它把「我导入了什么」这件事留在了 git 历史里,而不是某个人的终端里。

3. 状态对齐与 refresh

一句话总结: 导入只是把资源 ID 塞进 state,真正的难点是让配置与真实属性完全一致,plan 的差异就是待办清单。

导入后第一件事是跑 plan,逐条看差异。差异通常来自三类:属性没写、属性写错、属性是 Provider 默认值但被显式覆盖。

terraform plan -out=import.tfplan

# 只看会修改/销毁的动作
terraform show -json import.tfplan | \
  jq '[.resource_changes[]
      | select(.change.actions != ["no-op"])
      | {addr: .address, actions: .change.actions}]'

terraform refresh(0.15 后并入 plan)用真实 API 数据刷新 state,能发现控制台里被手工改动的属性:

# 只刷新不计划变更(-refresh-only 是推荐写法)
terraform apply -refresh-only

# 或把刷新结果单独看
terraform plan -refresh-only -out=refresh.tfplan
terraform show refresh.tfplan

3.1 属性不可见的部分怎么处理

有些属性(如 aws_instance 的 user_data)在 API 里不回传明文,导入后 plan 可能永远显示差异。常见处理是用 lifecycle.ignore_changes 明确「这块我不打算管」。

resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.medium"

  lifecycle {
    # 明确声明:存量实例的 user_data 由外部维护,Terraform 不追踪
    ignore_changes = [user_data, ami]
  }
}

一句话:ignore_changes 不是逃避,而是对「这块由谁负责」的显式声明——比留下永远消不掉的 plan 差异要诚实。

4. 生成配置 -generate-config-out

一句话总结: 手写几百行配置去匹配存量资源极其痛苦,-generate-config-out 让 Terraform 根据 state 反向生成 HCL,是纳管效率的关键杠杆。

Terraform 1.5+ 支持在 import 块存在时,用 plan -generate-config-out 让 Provider 依据导入结果生成配置骨架。

# import.tf:只写 import 块,不写 resource 块
import {
  to = aws_s3_bucket.data
  id = "my-existing-bucket"
}

import {
  to = aws_security_group.app
  id = "sg-0abc123def456"
}
terraform plan -generate-config-out=generated.tf

# generated.tf 会包含从真实资源推导出的属性

生成结果形如:

resource "aws_s3_bucket" "data" {
  bucket        = "my-existing-bucket"
  force_destroy = false
  tags = {
    Environment = "prod"
    Team        = "platform"
  }
}

resource "aws_security_group" "app" {
  name        = "app-sg"
  description = "application security group"
  vpc_id      = "vpc-0abc123"
}

4.1 生成后必须人工清理

生成的配置是「能跑」而不是「好读」:它会把所有真实属性写死,包括本该用变量、本该来自 data source 的值。必须人工重构。

# 生成版:硬编码 vpc_id
vpc_id = "vpc-0abc123"

# 重构版:改为引用
vpc_id = data.aws_vpc.main.id

一句话:生成配置是「脚手架」不是「成品」——它把从零手写的三小时压缩到三十分钟,剩下的重构仍需人工判断。

5. 渐进式接管与模块化

一句话总结: 一次性纳管整个账号风险极高,正确做法是按业务域分批、按依赖顺序推进,每批纳管后立刻让 plan 收敛并纳入流水线。

推荐的推进顺序:先纳管「无状态、易重建」的资源(安全组、IAM、S3),再纳管「有状态」的资源(数据库、存储卷),最后纳管「网络骨干」(VPC、子网、路由表)。

# 按批次推进,每批一个分支、一次 PR
# 批次 1:IAM 与安全组
# 批次 2:S3 与 SQS
# 批次 3:RDS(有状态,需快照兜底)
# 批次 4:VPC 网络(影响面最大)

5.1 用模块收敛重复结构

纳管过程中会发现大量结构相同的资源(每个环境一套子网),此时抽取模块,把「导入」与「抽象」同步完成。

module "app_subnets" {
  source = "./modules/subnets"

  for_each = var.environments

  environment = each.key
  vpc_id      = data.aws_vpc.main.id
  cidr_blocks = each.value.subnet_cidrs
}

import {
  to = module.app_subnets["prod"].aws_subnet.private[0]
  id = "subnet-0aaa111"
}

5.2 用 moved 块整理地址

纳管时常需要把资源从 aws_x.a 改到 module.m.aws_x.a,用 moved 块可以避免 destroy/create。

moved {
  from = aws_subnet.private
  to   = module.app_subnets["prod"].aws_subnet.private[0]
}

一句话:渐进式接管的关键是「每批都可回退」——state 有备份、配置有分支、plan 已收敛,任何一批出问题都能停下而不影响其余。

6. 避免误删的护栏

一句话总结: 纳管最大的恐惧是「一次 apply 删掉生产数据库」,护栏由 prevent_destroy、删除保护、状态备份与权限收敛四层构成。

resource "aws_db_instance" "main" {
  identifier     = "prod-mysql"
  engine         = "mysql"
  instance_class = "db.r6g.large"

  lifecycle {
    # 任何试图销毁该资源的计划都会直接报错
    prevent_destroy = true
  }
}
# 存储桶也建议加删除保护
resource "aws_s3_bucket" "data" {
  bucket = "prod-data-lake"

  lifecycle {
    prevent_destroy = true
  }
}

四层护栏:

护栏实现拦截什么
配置层prevent_destroy = true配置被误删导致的销毁
云平台层RDS deletion_protection、S3 MFA delete任何来源的删除
状态层state 版本化 + 定期备份状态损坏后的恢复
权限层apply 角色不含 Delete*越权的销毁操作
# 纳管期间每次操作前先备份 state
terraform state pull > backup/state-$(date +%Y%m%d-%H%M%S).tfstate

# 查看当前状态里有哪些资源
terraform state list

一句话:护栏的成本是一次性配置,收益是「永远不会因为一个手滑而丢数据」——这笔买卖永远划算。

7. 大规模纳管的自动化

一句话总结: 当资源数以百计,手工写 import 块不现实,需要用云 API 枚举资源、脚本批量生成 import 块,再分批评审落地。

# 枚举账号内所有未纳管的 EC2 实例
aws ec2 describe-instances \
  --query 'Reservations[].Instances[].{Id:InstanceId,Name:Tags[?Key==`Name`]|[0].Value}' \
  --output json > instances.json

# 与 state 对比,找出未纳管的
terraform state list | grep '^aws_instance' | sed 's/.*\.//' > managed.txt
jq -r '.[].Id' instances.json | sort > all.txt
comm -23 all.txt managed.txt > to-import.txt
# 批量生成 import 块
while read -r id; do
  echo 'import {'
  echo "  to = aws_instance.legacy_${id//-/}"
  echo "  id = \"$id\""
  echo '}'
done < to-import.txt > imports-generated.tf

7.1 分批与命名约定

自动生成的 to 地址需要符合团队的命名约定,建议统一用 legacy_ 前缀标记「待重构」的资源,后续用 moved 块重命名。

# 生成后人工审阅,按业务域拆成多个文件
# imports-prod-network.tf / imports-prod-app.tf
terraform plan -generate-config-out=generated-prod-app.tf

一句话:自动化的边界是「生成」——生成 import 块与配置骨架,但「这一批该不该纳管」的判断永远由人来做。

8. 总结

存量资源纳管是一条从「不可见」到「可收敛」的路径:

环节手段关键点
识别云 API 枚举 + state 对比找出未纳管清单
导入import 块 / CLI import优先 import 块,可评审
生成-generate-config-out脚手架,需人工重构
对齐plan 收敛到 No changes差异即待办清单
重构模块化 + moved 块抽象与改名同步完成
护栏prevent_destroy + 删除保护四层防线避免误删
固化纳入流水线纳管完成才算真正接管

一句话收尾:纳管不是一次 import,而是一次「把基础设施的所有权从控制台转移到代码」的交接。当 plan 稳定输出 No changes、护栏就位、配置进入流水线,那些曾经只存在于某个人记忆里的资源,才真正成为团队可维护、可审计的资产。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「terraform」更多文章

  1. Helm Provider 与应用发布:值注入与回滚
  2. 模块注册表与分发:版本、文档与测试
  3. DNS 与证书编排:托管区域与自动验证