Importing Resources

Bring existing infrastructure under Terraform management

8 min read

Importing Resources

In the previous tutorial, we learned about workspaces and environment management. Now let's tackle a very common real-world scenario.

You've got existing infrastructure created manually — someone clicked around in the AWS console, or another tool built it, or it's been there since before your team adopted Terraform. Now you want Terraform to manage it. Importing brings those resources under Terraform's control without recreating them. No downtime, no drama.

The Challenge

"Terraform doesn't know about my existing VPCs and EC2 instances!"

Exactly. Terraform only knows about resources it created. It doesn't know about your manually-created infrastructure. Import tells Terraform: "hey, this thing already exists — start tracking it."

Import Command (Legacy)

"How has import worked traditionally?"

The old-school way:

terraform import aws_instance.web i-1234567890abcdef0

This adds the resource to state but doesn't create configuration. You have to write the config manually. Yeah, it's tedious. That's why there's a better way now.

Step-by-Step

  1. Write the resource block
# main.tf
resource "aws_instance" "web" {
  # Empty for now
}
  1. Import the resource
terraform import aws_instance.web i-1234567890abcdef0
  1. Check what Terraform sees
terraform state show aws_instance.web
  1. Fill in the configuration
resource "aws_instance" "web" {
  ami           = "ami-12345678"
  instance_type = "t2.micro"
  subnet_id     = "subnet-abc123"

  tags = {
    Name = "production-web"
  }
}
  1. Verify no changes
terraform plan
# Should show "No changes"

If plan shows changes, adjust your config until it matches. It's like tracing a drawing — you keep adjusting until it lines up perfectly.

Import Block (Terraform 1.5+)

"Is there a better way?"

Yes! The modern way — declarative imports:

import {
  to = aws_instance.web
  id = "i-1234567890abcdef0"
}

resource "aws_instance" "web" {
  ami           = "ami-12345678"
  instance_type = "t2.micro"
  # ...
}
terraform plan  # Shows import will happen
terraform apply # Performs import

Benefits Over CLI Import

Why this is way better:

  • Configuration as code — import blocks live in your .tf files (reviewable in PRs!)
  • Plan shows the import — no surprise state changes
  • Works with for_each — import multiple resources at once
  • Generates config — Terraform can write the code for you (!)

Generating Configuration

"Wait, Terraform can write the config FOR me?"

Yep! Terraform 1.5+ can generate resource configuration automatically. This is a game changer:

# main.tf
import {
  to = aws_instance.web
  id = "i-1234567890abcdef0"
}

# No resource block yet!
terraform plan -generate-config-out=generated.tf

This creates generated.tf with the full resource configuration:

# generated.tf
resource "aws_instance" "web" {
  ami                    = "ami-12345678"
  instance_type          = "t2.micro"
  subnet_id              = "subnet-abc123"
  vpc_security_group_ids = ["sg-12345"]
  
  tags = {
    Name = "production-web"
  }
  # ... all other attributes
}

Review it, clean it up, move it to main.tf. It won't be perfectly organized, but it saves you hours of manual work.

Common Import Examples

VPC

# CLI
terraform import aws_vpc.main vpc-1234567890abcdef0
# Import block
import {
  to = aws_vpc.main
  id = "vpc-1234567890abcdef0"
}

Subnet

terraform import aws_subnet.public subnet-1234567890abcdef0

Security Group

terraform import aws_security_group.web sg-1234567890abcdef0

S3 Bucket

terraform import aws_s3_bucket.data my-bucket-name

RDS Instance

terraform import aws_db_instance.main mydb-identifier

IAM Role

terraform import aws_iam_role.app my-role-name

IAM Policy

terraform import aws_iam_policy.custom arn:aws:iam::123456789012:policy/MyPolicy

Route53 Zone

terraform import aws_route53_zone.main Z1234567890ABC

Route53 Record

terraform import aws_route53_record.www Z1234567890ABC_www.example.com_A

EC2 with for_each

import {
  for_each = toset(["i-111", "i-222", "i-333"])
  to       = aws_instance.web[each.key]
  id       = each.key
}

Import Strategy

Importing a whole environment? Here's the battle plan.

1. Discover Resources

# List EC2 instances
aws ec2 describe-instances --query 'Reservations[].Instances[].[InstanceId,Tags[?Key==`Name`].Value|[0]]' --output table

# List VPCs
aws ec2 describe-vpcs --query 'Vpcs[].[VpcId,Tags[?Key==`Name`].Value|[0]]' --output table

# List S3 buckets
aws s3 ls

# List RDS instances
aws rds describe-db-instances --query 'DBInstances[].[DBInstanceIdentifier,Engine]' --output table

2. Map to Terraform Resources

AWS ResourceTerraform Resource
EC2 Instanceaws_instance
VPCaws_vpc
Subnetaws_subnet
Security Groupaws_security_group
RDSaws_db_instance
S3 Bucketaws_s3_bucket
IAM Roleaws_iam_role
Lambdaaws_lambda_function

3. Import in Order

Dependencies matter! Import the foundation first, then build up:

# 1. VPC first
import {
  to = aws_vpc.main
  id = "vpc-123"
}

# 2. Then subnets
import {
  to = aws_subnet.public
  id = "subnet-456"
}

# 3. Then security groups
import {
  to = aws_security_group.web
  id = "sg-789"
}

# 4. Finally instances
import {
  to = aws_instance.web
  id = "i-abc"
}

Handling Import Issues

"My import worked but terraform plan shows changes!"

Welcome to the most common import experience. Here's how to deal.

Resource Has Attributes Not in Config

terraform plan
# aws_instance.web will be updated in-place
#   ~ cpu_options { ... }

Either add the missing attribute to config or use lifecycle:

resource "aws_instance" "web" {
  # ...

  lifecycle {
    ignore_changes = [cpu_options]
  }
}

Conflicting Values

# Plan shows change
instance_type = "t2.micro" -> "t2.small"

Your config says one thing, the actual resource is different. Decide which is correct and adjust accordingly.

Resource Doesn't Support Import

"Terraform says this resource can't be imported!"

Some resources (rare) don't support import. Check the provider documentation.

Workaround: Delete from state, let Terraform recreate:

terraform state rm aws_some_resource.thing
terraform apply

Bulk Import Workflow

Large Infrastructure

Step 1: Export inventory
Step 2: Generate import blocks
Step 3: Generate configurations
Step 4: Clean up and organize
Step 5: Verify with plan

Script to Generate Import Blocks

#!/bin/bash
# generate-imports.sh

# Get all EC2 instances
aws ec2 describe-instances \
  --query 'Reservations[].Instances[].InstanceId' \
  --output text | while read id; do
    name=$(aws ec2 describe-tags \
      --filters "Name=resource-id,Values=$id" "Name=key,Values=Name" \
      --query 'Tags[0].Value' --output text)
    
    # Convert name to terraform resource name
    tf_name=$(echo "$name" | tr '[:upper:]' '[:lower:]' | tr '-' '_')
    
    echo "import {"
    echo "  to = aws_instance.$tf_name"
    echo "  id = \"$id\""
    echo "}"
    echo ""
done

Terraformer

"Is there a tool that just imports EVERYTHING?"

Terraformer is a third-party tool that auto-imports entire AWS accounts:

# Install
brew install terraformer

# Import all EC2 resources
terraformer import aws --resources=ec2_instance --regions=us-west-2

# Import specific resources
terraformer import aws \
  --resources=vpc,subnet,ec2_instance,s3 \
  --regions=us-west-2 \
  --filter=aws_instance=id=i-123456

Creates complete .tf files and imports state.

Caution with Terraformer

Don't expect perfection:

  • Generates verbose code (includes everything, even defaults)
  • May not match your coding style
  • Good starting point, needs cleanup

Think of it as a rough draft, not a finished product.

Import and Modules

Import Into Module

import {
  to = module.networking.aws_vpc.main
  id = "vpc-123"
}

Import With for_each Module

import {
  to = module.web_servers["server-1"].aws_instance.this
  id = "i-123"
}

State Surgery Alternative

"Can I just manipulate the state directly?"

Sometimes that's easier. Here's your toolkit:

Move Resource

# Rename resource in state
terraform state mv aws_instance.old aws_instance.new

Remove from State

# Stop managing resource (doesn't delete the actual resource in AWS!)
terraform state rm aws_instance.web

List Resources in State

terraform state list
# aws_vpc.main
# aws_subnet.public
# aws_instance.web

Show Resource Details

terraform state show aws_instance.web

Complete Import Example

Import an existing VPC setup:

1. Discover

aws ec2 describe-vpcs
# VPC: vpc-12345, CIDR: 10.0.0.0/16

aws ec2 describe-subnets --filters "Name=vpc-id,Values=vpc-12345"
# Subnet: subnet-abc (10.0.1.0/24)
# Subnet: subnet-def (10.0.2.0/24)

aws ec2 describe-internet-gateways --filters "Name=attachment.vpc-id,Values=vpc-12345"
# IGW: igw-xyz

2. Create Import Blocks

# imports.tf
import {
  to = aws_vpc.main
  id = "vpc-12345"
}

import {
  to = aws_subnet.public["a"]
  id = "subnet-abc"
}

import {
  to = aws_subnet.public["b"]
  id = "subnet-def"
}

import {
  to = aws_internet_gateway.main
  id = "igw-xyz"
}

3. Generate Config

terraform plan -generate-config-out=generated.tf

4. Review and Organize

Move generated code to proper files, clean up, add variables:

# vpc.tf
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "production"
  }
}

resource "aws_subnet" "public" {
  for_each = {
    "a" = { cidr = "10.0.1.0/24", az = "us-west-2a" }
    "b" = { cidr = "10.0.2.0/24", az = "us-west-2b" }
  }

  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value.cidr
  availability_zone = each.value.az

  tags = {
    Name = "public-${each.key}"
  }
}

5. Apply and Verify

terraform apply
# Import complete

terraform plan
# No changes. Infrastructure is up-to-date.

Boom! When you see "No changes," that means the import is perfect. You've matched reality.

What's Next?

You just learned one of the most practical Terraform skills. You now know:

  • Legacy CLI import vs modern import blocks
  • Auto-generating config (seriously, use this)
  • Import order and dependency management
  • Handling the inevitable plan discrepancies
  • Bulk import strategies and Terraformer

But how do you make sure your configs are correct before you apply them? Let's learn about testing and validation. Let's go!