Chapter 09

迁移:import 现有资源

将手工创建的云资源纳入 Terraform 管理——无损迁移的完整流程

为什么需要 import?

现实场景

很多团队在引入 Terraform 前,已经有大量通过控制台或 CLI 手工创建的云资源:几年前搭建的 VPC、手工配置的 RDS 实例、运行中的 EC2 集群……这些资源不能随便删除重建,但又希望纳入 Terraform 管理,享受版本控制、变更审计和代码化的好处。

terraform import 解决的正是这个问题:把已存在的云资源的状态信息写入 Terraform State 文件,让 Terraform「认领」这个资源,此后的所有变更通过 Terraform 进行管理。

import 不会自动生成 .tf 代码

很多初学者误解 terraform import 的作用。传统的 terraform import 命令只更新 state 文件,不会自动生成对应的 .tf 配置代码。你还需要手动编写(或用 -generate-config-out 生成)对应的资源块。如果 state 中有记录但 .tf 文件中没有对应资源块,下次 terraform apply 会尝试删除该资源。

terraform import 命令(传统方式)

基本用法

传统的 terraform import 是命令式操作:指定资源类型+名称,以及云资源的唯一 ID。

# 命令格式:terraform import <资源地址> <云资源ID>
# 资源地址格式:资源类型.资源名称

# 导入 EC2 实例
terraform import aws_instance.web i-0123456789abcdef0

# 导入 S3 Bucket(Bucket 名称即为 ID)
terraform import aws_s3_bucket.assets my-existing-bucket-name

# 导入安全组
terraform import aws_security_group.app sg-0123456789

# 导入 RDS 实例
terraform import aws_db_instance.production mydb-instance-identifier

# 导入 VPC
terraform import aws_vpc.main vpc-0123456789abcdef0

# 导入子网
terraform import aws_subnet.private subnet-0123456789abcdef0

# 导入 IAM 角色
terraform import aws_iam_role.app MyExistingRoleName

# 导入模块内的资源(用点分隔路径)
terraform import module.vpc.aws_vpc.main vpc-0123456789

如何找到云资源 ID?

不同资源类型使用不同的 ID 格式。查找方法:

Terraform 文档
每种资源的文档页底部都有 Import 章节,说明使用什么 ID 格式。例如 aws_instance 的 Import 章节说明使用 Instance ID(i-xxxxx)。
AWS 控制台
在 AWS 控制台找到对应资源,查看详情页的资源 ID 字段。
AWS CLI
aws ec2 describe-instances --filters "Name=tag:Name,Values=my-server" --query 'Reservations[0].Instances[0].InstanceId'
复合 ID
有些资源 ID 是复合的,多个字段用特殊字符分隔。例如 aws_route_table_association 的 ID 格式是 subnet_id/route_table_id
# 导入后,用 terraform show 查看 state 中的属性
(这是了解资源有哪些属性的最快方法,然后据此编写 .tf 文件)
terraform state show aws_instance.web

# 输出示例:
# # aws_instance.web:
# resource "aws_instance" "web" {
#     ami                          = "ami-0c55b159cbfafe1f0"
#     arn                          = "arn:aws:ec2:us-east-1:..."
#     instance_type                = "t3.medium"
#     tags = {
#         "Name" = "production-web-server"
#     }
#     ...
# }

# 导入后执行 plan,检查是否有 drift(state 和实际配置的差异)
terraform plan -no-color

传统 import 的工作流

传统 terraform import 的完整工作流 步骤 1:先在 .tf 文件中写一个最小化的资源块 (只需要 resource 类型和名称,不需要所有属性) resource "aws_instance" "web" {} 步骤 2:执行 terraform import terraform import aws_instance.web i-0123456789 步骤 3:用 terraform state show 查看导入后的 state 内容 terraform state show aws_instance.web 步骤 4:根据 state 内容,补全 .tf 文件中的资源配置 步骤 5:执行 terraform plan,目标是「No changes」 反复调整 .tf 文件直到 plan 显示 No changes 步骤 6:提交 .tf 文件到 Git(state 文件不提交)

import 块(Terraform 1.5+,推荐方式)

声明式 import

Terraform 1.5 引入了 import 块,让 import 操作变成声明式的——写在 .tf 文件中,受版本控制管理,而不是一条一条地执行命令。

# import.tf — 声明式导入(Terraform 1.5+)

# 导入 VPC
import {
  to = aws_vpc.main
  id = "vpc-0123456789abcdef0"
}

# 导入子网
import {
  to = aws_subnet.private_a
  id = "subnet-0a1b2c3d4e5f"
}

# 导入安全组
import {
  to = aws_security_group.app
  id = "sg-0123456789"
}

# 导入模块内的资源
import {
  to = module.vpc.aws_vpc.this
  id = "vpc-0123456789abcdef0"
}

-generate-config-out:自动生成配置文件

Terraform 1.5 配合 import 块提供了 -generate-config-out 参数,可以自动生成资源的 .tf 配置代码,大大减少手工编写的工作量。

# 第一步:写 import.tf(声明要导入哪些资源)
cat > import.tf <<'EOF'
import {
  to = aws_instance.web
  id = "i-0123456789abcdef0"
}
EOF

# 第二步:自动生成 .tf 配置文件
# -generate-config-out 会根据 state 内容生成完整的资源块
terraform plan -generate-config-out=generated_resources.tf

# 第三步:查看自动生成的配置
cat generated_resources.tf

# 第四步:检查并精简(去除只读属性,保留必要字段)
# 自动生成的代码包含所有属性(含只读属性),需要手动清理

# 第五步:执行 apply 完成导入
terraform apply
精简自动生成的配置

自动生成的 .tf 文件包含资源的所有属性,包括计算属性(如 arnid)。这些只读属性不应该出现在配置中(Terraform 会自动填充)。导入后需要:1) 删除只读计算属性;2) 用变量或 local 替换硬编码值;3) 提取共用配置到模块。

批量导入大量资源

# 使用 for_each 批量导入(Terraform 1.7+)

# 假设要导入 3 个 S3 Bucket
locals {
  existing_buckets = {
    "logs"   = "company-logs-bucket"
    "assets" = "company-assets-bucket"
    "backup" = "company-backup-bucket"
  }
}

# 声明式批量导入
import {
  for_each = local.existing_buckets
  to = aws_s3_bucket.buckets[each.key]
  id = each.value
}

# 对应的资源声明
resource "aws_s3_bucket" "buckets" {
  for_each = local.existing_buckets
  bucket   = each.value
}

Terraformer:逆向工程整个账号

Terraformer 是什么?

Terraformer 是 Google 开源的工具,可以扫描整个 AWS/GCP/Azure 账号,自动生成对应的 Terraform 代码和 state 文件。适合需要一次性将大量资源纳入 Terraform 管理的场景。

# 安装 Terraformer(macOS)
brew install terraformer

# 或者下载二进制
curl -LO "https://github.com/GoogleCloudPlatform/terraformer/releases/latest/download/terraformer-aws-darwin-amd64"
chmod +x terraformer-aws-darwin-amd64

# 从 AWS 账号生成 Terraform 代码
# --resources:要导入的资源类型
# --regions:AWS 区域
# --profile:AWS CLI profile
terraformer import aws \
  --resources=vpc,subnet,sg,igw,nat,ec2_instance,s3,rds \
  --regions=us-east-1 \
  --profile=my-aws-profile

# 生成的文件结构:
# generated/aws/
# ├── vpc/
# │   ├── main.tf        ← 资源定义
# │   ├── outputs.tf     ← 输出值
# │   └── terraform.tfstate
# ├── ec2_instance/
# └── ...

# 也可以只导入特定标签的资源
terraformer import aws \
  --resources=ec2_instance \
  --filter="aws_instance=tags.Environment=production" \
  --regions=us-east-1
Terraformer 生成代码质量有限

Terraformer 自动生成的代码质量参差不齐:大量硬编码 ID、没有变量参数化、不遵循模块化最佳实践。通常需要大量人工重构。建议把它的输出作为参考,而不是直接使用。对于大规模迁移,推荐先用 Terraformer 理解资源间的依赖关系,再手写干净的 Terraform 代码。

moved 块:安全地重构资源地址

重命名的痛点

在重构 Terraform 代码时,经常需要重命名资源(如把 aws_instance.server 改为 aws_instance.web_server),或者把根模块中的资源移入子模块。

如果直接改名而不做任何处理,Terraform 会认为旧资源被删除、新资源需要创建,触发销毁并重建——这对生产资源来说是灾难性的。moved 块解决了这个问题。

# moved.tf — 声明资源移动(Terraform 1.1+)

# 场景 1:重命名资源(不触发重建)
moved {
  from = aws_instance.server          # 旧地址
  to   = aws_instance.web_server      # 新地址
}

# 场景 2:将根模块资源移入子模块
moved {
  from = aws_s3_bucket.logs                          # 以前在根模块
  to   = module.storage.aws_s3_bucket.logs           # 现在在 storage 模块
}

# 场景 3:重命名 for_each 中的 key
moved {
  from = aws_s3_bucket.buckets["old-key"]
  to   = aws_s3_bucket.buckets["new-key"]
}

# 场景 4:将 count 资源转为 for_each
moved {
  from = aws_subnet.private[0]
  to   = aws_subnet.private["us-east-1a"]
}
moved 块使用建议

moved 块在 apply 后其历史记录可以删除,但建议保留一段时间(至少保留到下一个 release),以便其他团队成员了解变更历史。也可以把 moved 块保留在代码中作为重构文档。

配置漂移(Drift)的检测与处理

什么是配置漂移?

配置漂移(Configuration Drift)是指云资源的实际状态与 Terraform state 文件中记录的状态出现了不一致。通常的原因是有人在 Terraform 之外直接修改了云资源(例如在控制台手工修改了安全组规则、修改了实例类型等)。

配置漂移的产生与检测 正常状态: .tf 代码 ──apply──→ 云资源 ↑ state 记录真实状态 漂移产生(有人手工修改了云资源): .tf 代码 state 实际云资源 port=80 ←→ port=80 ≠ port=8080(被手工改了) ↑________↑ ↑ 一致 已漂移 检测漂移(-refresh-only): terraform plan -refresh-only → 只刷新 state,不修改云资源 → 报告「实际状态 vs state 的差异」
# 检测漂移(只刷新 state,不修改资源)
terraform plan -refresh-only

# 输出示例(当发现安全组被手工修改时):
# ~ update in-place
# aws_security_group.app
#   ~ ingress = [
#       - {from_port=80, to_port=80, cidr=[0.0.0.0/0]},
#       + {from_port=8080, to_port=8080, cidr=[0.0.0.0/0]},
#     ]

# 选项 1:将漂移同步到 state(接受手工修改)
terraform apply -refresh-only

# 选项 2:下次 plan/apply 时 Terraform 会将资源改回 .tf 定义的状态
(这是默认行为:代码定义的状态优先)

# 手动强制刷新 state(更新单个资源的 state)
terraform refresh   # 已废弃,等同于 apply -refresh-only

定期漂移检测(调度任务)

# .github/workflows/drift-detection.yml
name: Terraform Drift Detection

on:
  schedule:
    # 每天 UTC 00:00 检测一次漂移
    - cron: '0 0 * * *'

jobs:
  drift-check:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      issues: write    # 创建 GitHub Issue 报告漂移

    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/TerraformReadRole
          aws-region: us-east-1

      - uses: hashicorp/setup-terraform@v3

      - name: Terraform Init
        run: terraform init
        working-directory: terraform/environments/prod

      - name: Check for Drift
        id: drift
        run: |
          terraform plan -refresh-only -detailed-exitcode -out=drift.tfplan 2>&1 | tee plan_output.txt
          echo "exit_code=$?" >> $GITHUB_OUTPUT
        working-directory: terraform/environments/prod
        continue-on-error: true

      # exit code 2 = changes detected(发现漂移)
      - name: Create Issue on Drift
        if: steps.drift.outputs.exit_code == '2'
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: '⚠️ Terraform 配置漂移检测:生产环境',
              body: '检测到生产环境配置与 Terraform State 不一致,请立即处理。\n\n详见 Actions 运行日志。',
              labels: ['drift', 'infrastructure']
            })

State 文件管理

常用 state 子命令

# 查看 state 中的所有资源
terraform state list

# 查看单个资源的详细属性
terraform state show aws_instance.web

# 从 state 中移除资源(不销毁!只是让 Terraform 不再管理它)
# 用于:不需要 Terraform 管理的资源,或需要手动移除再重新导入
terraform state rm aws_instance.old_server

# 在 state 中重命名资源地址(等同于 moved 块,但是命令式操作)
terraform state mv aws_instance.server aws_instance.web_server

# 将资源从一个 state 文件迁移到另一个(多 state 拆分时使用)
terraform state mv \
  -state=source.tfstate \
  -state-out=target.tfstate \
  aws_vpc.main aws_vpc.main

# 拉取远程 state 的内容到本地(调试用)
terraform state pull > current.tfstate

# 强制推送本地 state 到远程(危险!谨慎使用)
terraform state push emergency-fix.tfstate
state 操作是高危操作

terraform state rm 会让 Terraform 忘记某个资源,但不会删除云上的实际资源。terraform state push 会直接覆盖远程 state,如果推送了错误的文件可能导致灾难性后果。在执行任何 state 操作前,务必先用 terraform state pull > backup.tfstate 备份当前 state。

本章小结

本章核心要点