Terraform Import Block Explained: Bring Existing Resources Under Management
Use Terraform import blocks to bring existing AWS resources under management. Code-based import without CLI commands, generate config automatically
DevOps
Import dozens of existing AWS resources into Terraform at once using import blocks, for_each, and generate-config-out.
You have 50 AWS resources created manually in the console. You need them in Terraform. The old terraform import CLI required importing one resource at a time. With import blocks (Terraform 1.5+), you can import everything in a single terraform apply and auto-generate the HCL configuration.
1. Inventory existing resources (AWS CLI / console)
2. Write import blocks (one per resource)
3. Generate config automatically (-generate-config-out)
4. Review and clean up generated code
5. terraform apply → all resources imported
6. Remove import blocksaws ec2 describe-instances \
--query 'Reservations[].Instances[].{ID:InstanceId,Name:Tags[?Key==`Name`].Value|[0],Type:InstanceType,State:State.Name}' \
--filters "Name=instance-state-name,Values=running" \
--output tableaws s3api list-buckets --query 'Buckets[].Name' --output text# VPCs
aws ec2 describe-vpcs --query 'Vpcs[].{ID:VpcId,CIDR:CidrBlock,Name:Tags[?Key==`Name`].Value|[0]}' --output table
# Subnets
aws ec2 describe-subnets --query 'Subnets[].{ID:SubnetId,VPC:VpcId,AZ:AvailabilityZone,CIDR:CidrBlock}' --output table
# Security Groups
aws ec2 describe-security-groups --query 'SecurityGroups[].{ID:GroupId,Name:GroupName,VPC:VpcId}' --output table
# Route Tables
aws ec2 describe-route-tables --query 'RouteTables[].{ID:RouteTableId,VPC:VpcId}' --output tableaws rds describe-db-instances \
--query 'DBInstances[].{ID:DBInstanceIdentifier,Engine:Engine,Class:DBInstanceClass,Status:DBInstanceStatus}' \
--output table# imports.tf — all VPC resources
# VPC
import {
to = aws_vpc.main
id = "vpc-0a1b2c3d4e5f6g7h8"
}
# Subnets
import {
to = aws_subnet.public["us-east-1a"]
id = "subnet-pub1a"
}
import {
to = aws_subnet.public["us-east-1b"]
id = "subnet-pub1b"
}
import {
to = aws_subnet.public["us-east-1c"]
id = "subnet-pub1c"
}
import {
to = aws_subnet.private["us-east-1a"]
id = "subnet-priv1a"
}
import {
to = aws_subnet.private["us-east-1b"]
id = "subnet-priv1b"
}
import {
to = aws_subnet.private["us-east-1c"]
id = "subnet-priv1c"
}
# Internet Gateway
import {
to = aws_internet_gateway.main
id = "igw-abc123"
}
# NAT Gateways
import {
to = aws_nat_gateway.az_a
id = "nat-aaa111"
}
import {
to = aws_nat_gateway.az_b
id = "nat-bbb222"
}
# Route Tables
import {
to = aws_route_table.public
id = "rtb-pub123"
}
import {
to = aws_route_table.private["us-east-1a"]
id = "rtb-priv1a"
}
import {
to = aws_route_table.private["us-east-1b"]
id = "rtb-priv1b"
}
# Security Groups
import {
to = aws_security_group.web
id = "sg-web123"
}
import {
to = aws_security_group.app
id = "sg-app456"
}
import {
to = aws_security_group.db
id = "sg-db789"
}For resources that follow a pattern:
# Import multiple S3 buckets
locals {
existing_buckets = {
logs = "company-logs-prod"
artifacts = "company-artifacts-prod"
backups = "company-backups-prod"
data = "company-data-prod"
static = "company-static-prod"
}
}
import {
for_each = local.existing_buckets
to = aws_s3_bucket.this[each.key]
id = each.value
}
resource "aws_s3_bucket" "this" {
for_each = local.existing_buckets
bucket = each.value
}# Import multiple EC2 instances
locals {
existing_instances = {
web1 = "i-0abc111"
web2 = "i-0abc222"
web3 = "i-0abc333"
api1 = "i-0def444"
api2 = "i-0def555"
}
}
import {
for_each = local.existing_instances
to = aws_instance.servers[each.key]
id = each.value
}Instead of writing all the resource blocks by hand:
terraform plan -generate-config-out=generated.tfTerraform reads each import block, fetches the real resource from AWS, and writes the HCL:
Planning...
aws_vpc.main: Preparing import...
aws_subnet.public["us-east-1a"]: Preparing import...
aws_subnet.public["us-east-1b"]: Preparing import...
...
Generated configuration written to generated.tfThe generated config includes everything — including computed attributes you should remove:
# generated.tf — BEFORE cleanup
resource "aws_vpc" "main" {
# Remove computed attributes
# arn = "arn:aws:ec2:us-east-1:123456789:vpc/vpc-0a1b2c3d"
# id = "vpc-0a1b2c3d"
# default_network_acl_id = "acl-abc"
# default_route_table_id = "rtb-abc"
# default_security_group_id = "sg-abc"
# dhcp_options_id = "dopt-abc"
# main_route_table_id = "rtb-main"
# owner_id = "123456789012"
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "production-vpc"
Environment = "prod"
}
}arn, id, owner_id, etc.)aws_subnet.public.vpc_id = aws_vpc.main.id)vpc.tf, subnets.tf, security_groups.tf)prevent_destroy, ignore_changes)# vpc.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
}
}
# subnets.tf
resource "aws_subnet" "public" {
for_each = var.public_subnets
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr
availability_zone = each.value.az
tags = {
Name = "${var.environment}-public-${each.key}"
}
}# Verify plan shows only imports, no changes
terraform plan
# Plan: 16 to import, 0 to add, 0 to change, 0 to destroy
# Apply the import
terraform apply
# aws_vpc.main: Importing... [id=vpc-0a1b2c3d]
# aws_subnet.public["us-east-1a"]: Importing... [id=subnet-pub1a]
# ...
# Apply complete! Resources: 16 imported, 0 added, 0 changed, 0 destroyed.After successful import, delete imports.tf — the blocks are one-time use:
rm imports.tf
terraform plan
# No changes. Your infrastructure matches the configuration.Automate the tedious part — generating import blocks from existing resources:
#!/bin/bash
# generate-imports.sh — Create import blocks for all EC2 instances
echo '# Auto-generated import blocks' > imports.tf
echo '' >> imports.tf
# EC2 Instances
aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=running" \
--query 'Reservations[].Instances[].{ID:InstanceId,Name:Tags[?Key==`Name`].Value|[0]}' \
--output json | jq -r '.[] | "import {\n to = aws_instance.\(.Name | gsub("[^a-zA-Z0-9_]"; "_"))\n id = \"\(.ID)\"\n}\n"' >> imports.tf
# S3 Buckets
aws s3api list-buckets --query 'Buckets[].Name' --output json | jq -r '.[] | "import {\n to = aws_s3_bucket.\(gsub("[^a-zA-Z0-9_]"; "_"))\n id = \"\(.)\"\n}\n"' >> imports.tf
echo "Generated $(grep -c 'import {' imports.tf) import blocks"Error: Resource already managed by Terraform
aws_instance.web is already in the stateThe resource is already imported. Remove that import block.
Error: Configuration for import target does not existYou need either a resource block or -generate-config-out flag.
Normal — your written config may differ from the actual resource. Either update your HCL to match reality, or accept the planned change.
Bulk importing existing AWS infrastructure into Terraform is now practical: write import blocks (use for_each for patterns), run terraform plan -generate-config-out to auto-generate HCL, clean up the generated code, and apply. For large estates with 50+ resources, combine the AWS CLI inventory script with import blocks to go from manual infrastructure to fully Terraform-managed in an afternoon.
Use Terraform import blocks to bring existing AWS resources under management. Code-based import without CLI commands, generate config automatically
Learn Terraform data sources to read existing AWS resources, look up AMIs, query remote state, and reference external information in your configurations.
Learn when to use Terraform depends_on for explicit resource dependencies. Understand implicit vs explicit dependencies, common use cases
Terraform for_each vs count explained with practical examples. Learn when to use each, how to migrate from count to for_each