The Problem
You have AWS resources created manually through the console or by another tool. Now you want to manage them with Terraform without recreating them. Running terraform apply with new resource definitions would try to create duplicates, which either fails (name conflicts) or creates unwanted resources.
terraform import brings existing resources under Terraform management by adding them to state. But the process has gotchas — the import only updates state, it does not generate configuration. You need both.
Understanding Terraform Import
Import does one thing: it maps a real-world resource to a Terraform resource address in state. After importing, Terraform knows the resource exists and what its current attributes are. But you still need matching HCL configuration, or terraform plan will show a diff (or worse, attempt to destroy the resource).
The workflow:
terraform import to add the resource to stateterraform plan to see attribute differencesterraform plan shows no changesMethod 1: Classic terraform import Command
Import an S3 bucket
# Step 1: Write the resource block
cat >> main.tf << 'EOF'
resource "aws_s3_bucket" "data_lake" {
}
EOF
# Step 2: Import the existing bucket into state
terraform import aws_s3_bucket.data_lake my-company-data-lake-prod
# Step 3: See what attributes the real resource has
terraform state show aws_s3_bucket.data_lake
# Step 4: Copy the output into your resource block and run plan
terraform plan
Import an EC2 instance
# Use the instance ID from AWS console or CLI
terraform import aws_instance.web_server i-0abc123def456789
# View the imported state
terraform state show aws_instance.web_server
Import an IAM role
terraform import aws_iam_role.app_role my-application-role
# IAM policies attached to the role need separate imports
terraform import aws_iam_role_policy_attachment.app_policy \
my-application-role/arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
Import a VPC and its components
VPCs require importing multiple related resources:
# VPC itself
terraform import aws_vpc.main vpc-0abc123
# Subnets
terraform import aws_subnet.public_a subnet-0abc123
terraform import aws_subnet.public_b subnet-0def456
terraform import aws_subnet.private_a subnet-0ghi789
# Internet gateway
terraform import aws_internet_gateway.main igw-0abc123
# Route tables
terraform import aws_route_table.public rtb-0abc123
terraform import aws_route_table_association.public_a rtb-0abc123/subnet-0abc123
# Security groups
terraform import aws_security_group.web sg-0abc123
Method 2: Import Blocks (Terraform 1.5+)
Terraform 1.5 introduced declarative import blocks — no CLI commands needed:
# imports.tf
import {
to = aws_s3_bucket.data_lake
id = "my-company-data-lake-prod"
}
import {
to = aws_instance.web_server
id = "i-0abc123def456789"
}
import {
to = aws_security_group.web
id = "sg-0abc123"
}
Run terraform plan and Terraform will show what it would import. Run terraform apply to execute the imports.
Advantages of import blocks
- Version controlled — the import intent is in code
- Reviewable in PRs — team can see what is being imported
- Batch imports — import many resources in one apply
- Can be combined with
-generate-config-outflag
Generating Configuration Automatically
Terraform 1.5+ can generate HCL configuration for imported resources:
# Generate config for all import blocks into a file
terraform plan -generate-config-out=generated.tf
This creates generated.tf with resource blocks filled in from the real resource attributes. Review and clean up the generated code — it includes every attribute, including computed ones you should remove.
# generated.tf (before cleanup)
resource "aws_s3_bucket" "data_lake" {
bucket = "my-company-data-lake-prod"
object_lock_enabled = false
force_destroy = null
acceleration_status = ""
}
After cleanup:
# main.tf (production-ready)
resource "aws_s3_bucket" "data_lake" {
bucket = "my-company-data-lake-prod"
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
resource "aws_s3_bucket_versioning" "data_lake" {
bucket = aws_s3_bucket.data_lake.id
versioning_configuration {
status = "Enabled"
}
}
Finding Resource Import IDs
The trickiest part of import is knowing what ID format each resource expects. Here are common patterns:
# EC2 instances — use instance ID
terraform import aws_instance.web i-0abc123def456789
# S3 buckets — use bucket name
terraform import aws_s3_bucket.data my-bucket-name
# Security groups — use security group ID
terraform import aws_security_group.web sg-0abc123
# IAM roles — use role name (not ARN)
terraform import aws_iam_role.app my-role-name
# IAM policies — use ARN
terraform import aws_iam_policy.custom arn:aws:iam::123456789:policy/my-policy
# RDS instances — use DB identifier
terraform import aws_db_instance.main my-database-id
# Route53 records — use zone_id_record-name_type
terraform import aws_route53_record.www Z1234567890_www.example.com_A
# Lambda functions — use function name
terraform import aws_lambda_function.api my-function-name
# Load balancers — use ARN
terraform import aws_lb.main arn:aws:elasticloadbalancing:us-east-1:123456789:loadbalancer/app/my-lb/abc123
Check the Terraform registry documentation for each resource — the import section shows the expected ID format.
Bulk Import Strategy
When importing an entire environment with dozens of resources:
Step 1: Inventory existing resources
# List all resources in AWS with tags
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=Environment,Values=production \
--output json > resources.json
# List specific resource types
aws ec2 describe-instances --filters "Name=tag:Environment,Values=production" \
--query "Reservations[].Instances[].{ID:InstanceId,Name:Tags[?Key=='Name'].Value|[0]}" \
--output table
aws s3api list-buckets --query "Buckets[].Name" --output text
Step 2: Generate import blocks programmatically
# Script to generate import blocks for all S3 buckets
aws s3api list-buckets --query "Buckets[].Name" --output text | tr '\t' '\n' | \
while read bucket; do
resource_name=$(echo "$bucket" | tr '-' '_' | tr '.' '_')
echo "import {"
echo " to = aws_s3_bucket.${resource_name}"
echo " id = \"${bucket}\""
echo "}"
echo ""
done > imports_s3.tf
Step 3: Import and generate config
terraform plan -generate-config-out=generated_s3.tf
terraform apply
Step 4: Clean up and organize
Move generated resources into proper module structure and remove the import blocks (they are no longer needed after successful import).
Handling Import Errors
Resource already in state
Error: Resource already managed by Terraform
The resource address already exists in state. Check with terraform state list and either remove the old entry or use a different address.
terraform state list | grep s3_bucket
terraform state rm aws_s3_bucket.data_lake # Remove stale entry
terraform import aws_s3_bucket.data_lake my-bucket # Re-import
Resource not found
Error: Cannot import non-existent remote object
The ID you provided does not match any existing resource. Double-check the resource exists and you are using the correct ID format for that resource type.
Configuration drift after import
# After import, plan shows changes — this is normal
terraform plan
# Review each change:
# ~ means Terraform wants to modify an attribute
# - means Terraform wants to remove an attribute
# + means Terraform wants to add an attribute
# Update your HCL to match the real resource state
# until plan shows "No changes"
Post-Import Verification
After importing, verify everything is clean:
# Plan should show no changes
terraform plan
# If plan shows changes, you have config drift
# Common reasons:
# 1. Default values not specified in HCL
# 2. Computed attributes included in HCL (remove them)
# 3. Resource was modified outside Terraform after import
# Refresh state to pick up any recent changes
terraform refresh
# Validate configuration syntax
terraform validate
Common Mistakes
plan or apply may destroy the resource.arn, id, created_at are computed by AWS. Including them in HCL causes perpetual diffs.i- prefixed ID, IAM roles use the role name.-generate-config-out with Terraform 1.5+ — Manually writing HCL for imported resources is tedious and error-prone. Let Terraform generate the baseline.terraform workspace show confirms where you are.Quick Reference
| Resource Type | Import ID Format |
|---|---|
| EC2 Instance | <code class="inline-code">i-0abc123def456789</code> |
| S3 Bucket | <code class="inline-code">bucket-name</code> |
| Security Group | <code class="inline-code">sg-0abc123</code> |
| IAM Role | <code class="inline-code">role-name</code> |
| IAM Policy | <code class="inline-code">arn:aws:iam::ACCOUNT:policy/name</code> |
| VPC | <code class="inline-code">vpc-0abc123</code> |
| Subnet | <code class="inline-code">subnet-0abc123</code> |
| RDS Instance | <code class="inline-code">db-identifier</code> |
| Lambda Function | <code class="inline-code">function-name</code> |
| Route53 Record | <code class="inline-code">ZONE_ID_name_TYPE</code> |
| ALB | Full ARN |
| ECS Service | <code class="inline-code">cluster-name/service-name</code> |
Summary
Terraform import bridges the gap between manually created infrastructure and infrastructure as code. Use import blocks (Terraform 1.5+) for declarative, reviewable imports with automatic config generation. Always verify with terraform plan showing no changes before considering an import complete. The goal is a clean state where Terraform fully manages the resource without attempting unwanted modifications.
---
Frequently Asked Questions
What does terraform import do?
terraform import brings existing cloud resources under Terraform management by mapping a real resource to a Terraform resource address in your state file. It doesn't generate configuration — you must write the corresponding resource block manually. After import, Terraform will manage the resource's lifecycle including updates and deletion.
How do I import an existing AWS resource into Terraform?
First write the resource block in your .tf file matching the resource type. Then run terraform import aws_instance.example i-1234567890abcdef0 with the resource address and cloud resource ID. After import, run terraform plan to see differences between your config and actual state, then adjust your configuration until the plan shows no changes.
What is the difference between terraform import and terraform state mv?
terraform import adds an existing cloud resource to your state that wasn't previously managed by Terraform. terraform state mv moves a resource already in your state from one address to another (useful when refactoring modules or renaming resources). Import brings new resources in; state mv reorganizes existing ones.
How do I import resources into a Terraform module?
Use the module path in the resource address: terraform import module.vpc.aws_subnet.public subnet-12345. For resources inside nested modules, chain the paths: terraform import module.app.module.db.aws_rds_cluster.main my-cluster-id. Ensure the module's resource block exists and variables are configured before importing.
Can I generate Terraform configuration from existing infrastructure?
Use terraformer (open-source) to auto-generate both configuration and state from existing cloud resources. AWS also offers former2 for CloudFormation/Terraform generation. For smaller imports, use terraform plan after import to see what attributes need to be set in your config. These tools save time but always review and clean up the generated code.
---
Related Resources
- Production Reference Architectures — 6 production reference architectures with IaC patterns
- DevOps Prompt Library — 500 DevOps prompts including Terraform templates
- AWS IAM Least Privilege Guide — IAM policies for Terraform roles
- Terraform State Management — Managing state files in production
- Terraform Modules Best Practices — Structuring reusable Terraform modules