Importing Existing Infrastructure
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:
- Migrating a manually-managed environment to Terraform
- Adopting infrastructure from another team or tool
- Recovering from a state corruption where resources still exist in the cloud
- Moving a resource between Terraform configurations
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
# }
# }
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).
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:
- Read-only resources (data sources) cannot be imported — they always refresh from the API.
- Some attributes are write-only — passwords, private keys, and other secrets are never stored in state and can't be read back after creation. You must supply them in the config or Terraform will try to update the resource.
- Nested resources (e.g., individual security group rules created with
aws_security_group_rule) often require importing each rule separately. - The ID format varies per resource — always check the provider docs before importing. A wrong ID produces a cryptic error.
Key Takeaways
terraform importrecords an existing resource in state without touching the real infrastructure — no destroy/create.- After importing, always run
terraform planand make the configuration match before applying anything. - Import blocks (Terraform 1.5+) are the preferred approach: they're declarative, reviewable in plan output, and can be version-controlled.
- Config generation (
-generate-config-out) writes the resource block for you from live attributes — dramatically reduces manual work. - The
movedblock handles renaming resources in config without recreating them in the cloud. - Not every resource supports import — check the provider docs. Secrets and write-only attributes require special handling.