Skip to main content
Terraform·11 min read

Terraform Import — Bringing Existing AWS Resources Under Control Without Destroying Them

Learn how to import existing AWS infrastructure into Terraform state. Covers terraform import, import blocks, generating configuration, and handling complex resources like VPCs and IAM roles.

DT

DevOps Engineer & Technical Writer

The Problem

Terraform Import Workflow BEFORE (unmanaged) AFTER (managed) Cloud Resource Exists but not in state No HCL Config terraform plan = create new terraform import Maps to state file terraform plan Shows drift / diff Write HCL Config Match real attributes terraform apply Now fully managed

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:

  • Write a resource block in HCL (can be empty initially)
  • Run terraform import to add the resource to state
  • Run terraform plan to see attribute differences
  • Update HCL until terraform plan shows no changes
  • Method 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-out flag

    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

  • Importing without writing HCL first — Import only updates state. Without matching configuration, the next plan or apply may destroy the resource.
  • Including computed attributes in HCL — Attributes like arn, id, created_at are computed by AWS. Including them in HCL causes perpetual diffs.
  • Forgetting dependent resources — Importing a VPC without its subnets, route tables, and gateways leaves gaps. Map the full dependency tree.
  • Wrong ID format — Each resource type expects a different ID format. An S3 bucket uses its name, EC2 uses i- prefixed ID, IAM roles use the role name.
  • Not using -generate-config-out with Terraform 1.5+ — Manually writing HCL for imported resources is tedious and error-prone. Let Terraform generate the baseline.
  • Importing into the wrong workspace or state — Always verify your backend configuration and workspace before importing. terraform workspace show confirms where you are.
  • Quick Reference

    Resource TypeImport 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>
    ALBFull 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.

    ---