Importing Resources
Bring existing infrastructure under Terraform management
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
- Write the resource block
# main.tf
resource "aws_instance" "web" {
# Empty for now
}
- Import the resource
terraform import aws_instance.web i-1234567890abcdef0
- Check what Terraform sees
terraform state show aws_instance.web
- Fill in the configuration
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t2.micro"
subnet_id = "subnet-abc123"
tags = {
Name = "production-web"
}
}
- 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 Resource | Terraform Resource |
|---|---|
| EC2 Instance | aws_instance |
| VPC | aws_vpc |
| Subnet | aws_subnet |
| Security Group | aws_security_group |
| RDS | aws_db_instance |
| S3 Bucket | aws_s3_bucket |
| IAM Role | aws_iam_role |
| Lambda | aws_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!