Overview
The terraform import command brings existing infrastructure under Terraform management by creating state entries for resources that were created outside of Terraform. This is essential for adopting Terraform in environments with pre-existing infrastructure.
How Import Works
Import reads the current state of a resource from the provider and writes it to Terraform state, without modifying the actual infrastructure.
Import Process
From the implementation in internal/command/import.go:
Parse Target Address : Convert resource address string to structured address
Validate Configuration : Ensure target resource exists in configuration
Acquire State Lock : Lock state to prevent concurrent modifications
Call Provider : Read resource from provider API
Write State : Add resource to state file
Persist State : Save updated state to backend
Release Lock : Unlock state
Reference : internal/command/import.go:30-271
Import Command Structure
terraform import [options] ADDRESS ID
ADDRESS : Terraform resource address where the resource will be imported
ID : Provider-specific resource identifier
Example Imports
# AWS EC2 instance
terraform import aws_instance.web i-0123456789abcdef0
# AWS VPC
terraform import aws_vpc.main vpc-0a1b2c3d4e5f6g7h8
# Module resource
terraform import module.network.aws_subnet.private subnet-0a1b2c3d
# Resource with count
terraform import 'aws_instance.app[0]' i-0123456789abcdef0
# Resource with for_each
terraform import 'aws_instance.app["web"]' i-0123456789abcdef0
Address Validation
Import validates that the target address exists in configuration:
// From internal/command/import.go:51
traversal , travDiags := hclsyntax . ParseTraversalAbs ( traversalSrc , "<import-address>" , hcl . Pos { Line : 1 , Column : 1 })
addr , addrDiags := addrs . ParseAbsResourceInstance ( traversal )
Reference : internal/command/import.go:51-68
Resource Mode Validation
Only managed resources can be imported:
// From internal/command/import.go:70
if addr . Resource . Resource . Mode != addrs . ManagedResourceMode {
diags = diags . Append ( errors . New (
"A managed resource address is required. Importing into a data resource is not allowed."
))
}
You cannot import data sources.
Reference : internal/command/import.go:70-74
Configuration Requirements
Resource Must Exist in Configuration
Before importing, the resource block must exist:
// From internal/command/import.go:98
config , configDiags := c . loadConfig ( parsedArgs . ConfigPath )
// Verify that the given address points to something that exists in config
targetConfig := config . DescendentForInstance ( addr . Module )
if targetConfig == nil {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Import to non-existent module" ,
// ...
})
}
Reference : internal/command/import.go:98-115
Resource Block Example
Create the resource block before importing:
# Define empty resource block
resource "aws_instance" "web" {
# Configuration will be filled in after import
# For now, just the block definition is required
}
Then import:
terraform import aws_instance.web i-0123456789abcdef0
Missing Resource Error
If the resource block doesn’t exist:
// From internal/command/import.go:136
if rc == nil {
c . Ui . Error ( fmt . Sprintf (
importCommandMissingResourceFmt ,
addr , modulePath , resourceRelAddr . Type , resourceRelAddr . Name ,
))
return 1
}
Error message suggests creating the resource block:
Error: resource address "aws_instance.web" does not exist in the configuration.
Before importing this resource, please add configuration for this resource.
resource "aws_instance" "web" {
# (resource arguments)
}
Reference : internal/command/import.go:126-144
Import Execution
Import Targets
The import operation accepts a list of targets:
// From internal/command/import.go:225
newState , importDiags := lr . Core . Import ( lr . Config , lr . InputState , & terraform . ImportOpts {
Targets : [] * terraform . ImportTarget {
{
LegacyAddr : addr ,
LegacyID : parsedArgs . ID ,
},
},
SetVariables : lr . PlanOpts . SetVariables ,
})
Currently limited to one target per command invocation.
Reference : internal/command/import.go:225-237
State Persistence
After successful import, state is written:
// From internal/command/import.go:253
log . Printf ( "[INFO] Writing state output to: %s " , c . Meta . StateOutPath ())
if err := state . WriteState ( newState ); err != nil {
c . Ui . Error ( fmt . Sprintf ( "Error writing state file: %s " , err ))
return 1
}
if err := state . PersistState ( schemas ); err != nil {
c . Ui . Error ( fmt . Sprintf ( "Error writing state file: %s " , err ))
return 1
}
Reference : internal/command/import.go:253-261
Import Command Options
Configuration Path
terraform import -config= "path/to/config" aws_instance.web i-abc123
Specifies where to find Terraform configuration files.
Variable Values
terraform import -var= "region=us-west-2" aws_instance.web i-abc123
terraform import -var-file= "prod.tfvars" aws_instance.web i-abc123
Provides variables needed by provider configuration.
State Locking
terraform import -lock=false aws_instance.web i-abc123
terraform import -lock-timeout=5m aws_instance.web i-abc123
Disabling state locking during import is dangerous if others might be modifying the state.
Legacy Local Backend Options
terraform import -state= "custom.tfstate" aws_instance.web i-abc123
terraform import -state-out= "new.tfstate" aws_instance.web i-abc123
terraform import -backup= "backup.tfstate" aws_instance.web i-abc123
These only work with the local backend.
Post-Import Workflow
1. Verify Import
After importing, verify the resource is in state:
terraform state show aws_instance.web
2. Generate Configuration
Use the state to generate configuration:
terraform show -no-color | grep -A 50 'resource "aws_instance" "web"'
Or use terraform state show output as a reference:
terraform state show aws_instance.web
3. Update Configuration
Add the actual configuration to match the imported resource:
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "web-server"
}
# Add other attributes to match current state
}
4. Plan to Verify
Run plan to ensure configuration matches state:
Expect “No changes” if configuration matches imported state.
5. Refine Configuration
Adjust configuration to be more maintainable:
resource "aws_instance" "web" {
ami = data . aws_ami . ubuntu . id # Use data source
instance_type = var . instance_type # Use variable
tags = merge (
var . common_tags ,
{
Name = "web-server"
}
)
}
Importing Multiple Resources
Sequential Import
terraform import aws_vpc.main vpc-12345
terraform import aws_subnet.public subnet-12345
terraform import aws_subnet.private subnet-67890
Import Script
#!/bin/bash
# Import VPC and related resources
terraform import aws_vpc.main vpc-0a1b2c3d
terraform import aws_internet_gateway.main igw-0a1b2c3d
# Import subnets
for subnet_id in subnet-111 subnet-222 subnet-333 ; do
terraform import "aws_subnet.public[ \" ${ subnet_id } \" ]" "${ subnet_id }"
done
Import Limitations
One Resource at a Time
Import command handles one resource per invocation. For bulk imports, use scripts or tools like Terraformer .
No Configuration Generation
Import only creates state entries. You must manually write or generate configuration.
Provider-Specific IDs
Each provider defines its own ID format:
# AWS uses resource IDs
terraform import aws_instance.web i-0123456789abcdef0
# Google Cloud uses self_link
terraform import google_compute_instance.web projects/my-project/zones/us-central1-a/instances/my-instance
# Azure uses resource IDs
terraform import azurerm_virtual_machine.web /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup/providers/Microsoft.Compute/virtualMachines/myvm
Consult provider documentation for ID formats.
Module Resources
When importing into modules, include the module path:
terraform import module.vpc.aws_vpc.main vpc-0a1b2c3d
terraform import 'module.app["web"].aws_instance.server' i-0123456789
Import and Resource Dependencies
Imported resources have empty dependency lists initially:
// From internal/states/instance_object.go:70
func NewResourceInstanceObjectFromIR ( ir providers . ImportedResource ) * ResourceInstanceObject {
return & ResourceInstanceObject {
Status : ObjectReady ,
Value : ir . State ,
Private : ir . Private ,
Identity : ir . Identity ,
// Dependencies is empty
}
}
Dependencies are rebuilt during the next apply.
Reference : internal/states/instance_object.go:70-77
Import Error Handling
Error: Invalid address
The address "aws_instance" is not valid. Must be a resource instance address.
For information on valid syntax, see:
https://developer.hashicorp.com/terraform/cli/state/resource-addressing
Resource Not Found by Provider
Error: Cannot import non-existent remote object
While attempting to import an existing object to "aws_instance.web",
the provider detected that no object exists with the given id.
Configuration Mismatch
If imported state doesn’t match configuration:
Shows differences:
Terraform will perform the following actions:
# aws_instance.web will be updated in-place
~ resource "aws_instance" "web" {
~ instance_type = "t2.micro" -> "t3.micro"
# ...
}
Update configuration to match or accept the change.
Import Best Practices
Import Before Terraform Manages
Import existing resources before making Terraform changes to avoid accidental destruction.
Test Import in Non-Production First
Practice import workflow in development environments before production.
Maintain documentation mapping resource addresses to provider IDs for reference.
Use State Show to Generate Config
Leverage terraform state show output as a starting point for configuration.
Always run terraform plan after import to verify configuration matches state.
Import Dependencies Together
Import related resources (VPC + subnets, instance + security groups) in logical groups.
Commit state after successful imports to track import history.
For importing many resources, consider these tools:
terraformer import aws --resources=vpc,subnet,instance --regions=us-east-1
Generates both state and configuration files.
tf-import-github-org --org my-org
Imports entire GitHub organization resources.
Some providers offer import tools:
AWS: former2 for generating CloudFormation and Terraform
Azure: aztfexport for exporting resources
Terraform 1.5 introduced import blocks for configuration-driven import:
import {
to = aws_instance . web
id = "i-0123456789abcdef0"
}
resource "aws_instance" "web" {
# Configuration
}
Run terraform plan to preview import, then terraform apply to execute.
Source Code References
Import Command : internal/command/import.go
Import Arguments : internal/command/arguments/import.go
Import from Provider : internal/states/instance_object.go:70-77
Address Parsing : internal/command/import.go:51-68
Configuration Validation : internal/command/import.go:98-144