Terraform Workflow

Importing Existing Infrastructure

● Intermediate ⏱ 20 min read terraform

Real infrastructure rarely starts from scratch. Engineers provision resources manually through the console, scripts run before Terraform was adopted, or a team takes over infrastructure they didn't build. Import is Terraform's mechanism for bringing those resources under management — writing them into state so Terraform can plan against them going forward — without destroying and recreating them.

Why Import?

Without import, Terraform has no knowledge of a resource that already exists. If you write a resource block for an existing VPC and run terraform apply, Terraform will try to create a second VPC. Import resolves this by recording the resource in the state file and associating it with a resource block in your configuration.

Common import scenarios:

The terraform import Command

The original import mechanism is a CLI command. It requires you to write the resource block in your configuration first, then tell Terraform which real-world resource it corresponds to:

terraform import <resource_address> <resource_id>

Step 1: Write the resource block

You must have a matching resource block before importing. Start with the minimum required attributes — you'll fill in the rest after inspecting the state:

# main.tf
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

Step 2: Find the resource ID

Each resource type has its own ID format, documented on the provider's registry page under "Import". For AWS resources, IDs are typically the AWS resource ID (e.g., vpc-0a1b2c3d4e5f):

# Find the VPC ID in AWS
aws ec2 describe-vpcs --query 'Vpcs[*].VpcId' --output text

Step 3: Run the import

terraform import aws_vpc.main vpc-0a1b2c3d4e5f
# aws_vpc.main: Importing from ID "vpc-0a1b2c3d4e5f"...
# aws_vpc.main: Import prepared!
# aws_vpc.main: Refreshing state... [id=vpc-0a1b2c3d4e5f]
#
# Import successful!
# The resources that were imported are shown above. These resources are now
# in your Terraform state and will henceforth be managed by Terraform.

Step 4: Sync the configuration

Run terraform plan immediately after importing. The plan will show which attributes in state don't match your resource block. Update the configuration to match until terraform plan shows no changes:

terraform plan
# ~ resource "aws_vpc" "main" {
#     + enable_dns_hostnames = false   # add this to your config
#     + enable_dns_support   = true    # add this to your config
#     + tags = {
#         + "Name" = "production"      # add this to your config
#       }
# }
⚠️
Don't apply until the plan is clean

If you terraform apply before the configuration matches the imported state, Terraform will modify the real resource to match the incomplete config — potentially resetting tags, security group rules, or other settings that are in the cloud but missing from your HCL.

Import Blocks (Terraform 1.5+)

Terraform 1.5 introduced import blocks — a declarative alternative to the CLI command. Import blocks live in your configuration files, are reviewed in plan output, and can be committed to version control as a record of the migration:

# imports.tf
import {
  to = aws_vpc.main
  id = "vpc-0a1b2c3d4e5f"
}

import {
  to = aws_subnet.private[0]
  id = "subnet-0a1b2c3d"
}

import {
  to = aws_subnet.private[1]
  id = "subnet-1e2f3a4b"
}

Run terraform plan — Terraform will show the import alongside any other changes. Run terraform apply to execute. After the apply completes, remove the import blocks (they are one-time operations, not ongoing config).

🧭
Import blocks are reviewable

Because import blocks appear in plan output, a teammate can review exactly what's being imported before it happens. The CLI terraform import command makes an immediate state change with no plan step — import blocks are safer for team environments.

Generating Configuration

Terraform 1.5+ can also generate the resource configuration for you. Add an import block with a to address that doesn't exist in your config yet, then run:

terraform plan -generate-config-out=generated.tf

Terraform reads the resource's current attributes from the cloud provider and writes a complete resource block to generated.tf. Review the file, clean it up (remove read-only computed attributes that can't be set in config), move it to your permanent config file, and run terraform plan again to confirm it's clean.

# Example generated output (trimmed):
resource "aws_vpc" "main" {
  assign_generated_ipv6_cidr_block     = false
  cidr_block                           = "10.0.0.0/16"
  enable_dns_hostnames                 = true
  enable_dns_support                   = true
  enable_network_address_usage_metrics = false
  instance_tenancy                     = "default"
  tags = {
    Name = "production"
  }
}

Config generation dramatically reduces the manual work of matching configuration to live resources — especially for resources with dozens of attributes.

Bulk Import Patterns

When importing many resources (e.g., every EC2 instance in an account), write a script to generate the import blocks:

#!/bin/bash
# Generate import blocks for all EC2 instances
aws ec2 describe-instances \
  --query 'Reservations[*].Instances[*].[InstanceId,Tags[?Key==`Name`].Value|[0]]' \
  --output text | while read id name; do
    echo "import {"
    echo "  to = aws_instance.$(echo $name | tr '-' '_' | tr '[:upper:]' '[:lower:]')"
    echo "  id = \"$id\""
    echo "}"
done > imports.tf

For very large migrations, import in batches — import a handful of resources, verify the plan is clean, apply, then move to the next batch. This limits the blast radius if something goes wrong.

The moved Block

The moved block handles a related but different problem: renaming a resource in your configuration without destroying and recreating it. When you rename aws_vpc.old_name to aws_vpc.new_name, Terraform would normally plan to delete the old and create the new. A moved block tells Terraform these are the same resource:

# Tell Terraform this is the same resource, just renamed
moved {
  from = aws_vpc.old_name
  to   = aws_vpc.new_name
}

Like import blocks, moved blocks are one-time. Remove them after the apply. They also work for moving resources between modules:

moved {
  from = aws_vpc.main
  to   = module.networking.aws_vpc.main
}

Limitations

Not all resources support import. Check the provider documentation — the resource page will include an "Import" section if it's supported. Common limitations:

Key Takeaways