DeployU
Interviews / Cloud & DevOps / Your company has 200 manually-created EC2 instances. Import them into Terraform without downtime.

Your company has 200 manually-created EC2 instances. Import them into Terraform without downtime.

practical Resource Import Interactive Quiz Code Examples

The Scenario

Your company has 200 EC2 instances that were created manually over 3 years. Management wants everything in Terraform for consistency. Current state:

# Random naming, no tags, various configurations
$ aws ec2 describe-instances --query 'Reservations[].Instances[].{ID:InstanceId,Name:Tags[?Key==`Name`].Value|[0]}' --output table

| InstanceId         | Name              |
|--------------------|-------------------|
| i-0abc123def456789 | web-server-1      |
| i-0def456789abc012 | (none)            |
| i-0789abc012def345 | prod-api          |
| ... 197 more ...   |                   |

Constraints:

  • Zero downtime - these are production servers
  • Must match current configuration exactly
  • Need to establish naming conventions going forward

The Challenge

Design and execute an import strategy that brings all resources under Terraform management without any service disruption.

Wrong Approach

A junior engineer might try to import all 200 instances at once, write Terraform config from scratch without checking actual AWS settings, or skip verification steps. This risks configuration drift that triggers unwanted changes, missed resources, or state corruption.

Right Approach

A senior engineer uses a systematic approach: inventory resources, generate Terraform config that matches current state, import in batches, verify with targeted plans, and iterate. They use tools like terraformer or AWS-to-Terraform generators to bootstrap config, then refine manually.

Phase 1: Inventory and Assessment

# Export all EC2 instances to JSON for analysis
aws ec2 describe-instances \
  --query 'Reservations[].Instances[]' \
  --output json > instances.json

# Categorize by purpose/environment
cat instances.json | jq -r '
  .[] |
  {
    id: .InstanceId,
    type: .InstanceType,
    vpc: .VpcId,
    subnet: .SubnetId,
    ami: .ImageId,
    name: (.Tags // [] | map(select(.Key == "Name")) | .[0].Value // "unnamed"),
    env: (.Tags // [] | map(select(.Key == "Environment")) | .[0].Value // "unknown")
  }
' > inventory.json

# Count by instance type
cat instances.json | jq -r '.[].InstanceType' | sort | uniq -c | sort -rn

Phase 2: Generate Terraform Configuration

Option A: Use Terraformer (recommended for large imports)

# Install terraformer
brew install terraformer

# Generate Terraform code from existing AWS resources
terraformer import aws \
  --resources=ec2_instance,security_group,vpc,subnet \
  --regions=us-east-1 \
  --profile=production

# Output structure:
# generated/
#   └── aws/
#       └── ec2_instance/
#           ├── instance.tf
#           └── provider.tf

Option B: Manual approach with import blocks (Terraform 1.5+)

# imports.tf - New in Terraform 1.5+
import {
  to = aws_instance.web_server_1
  id = "i-0abc123def456789"
}

import {
  to = aws_instance.prod_api
  id = "i-0789abc012def345"
}

# Generate config from imports
# terraform plan -generate-config-out=generated.tf

Option C: Script-based config generation

#!/bin/bash
# generate-tf-config.sh

aws ec2 describe-instances --output json | jq -r '
  .Reservations[].Instances[] |
  "resource \"aws_instance\" \"\(.Tags | map(select(.Key==\"Name\"))[0].Value // .InstanceId | gsub(\"-\"; \"_\"))\" {
    # Instance ID: \(.InstanceId)
    ami           = \"\(.ImageId)\"
    instance_type = \"\(.InstanceType)\"
    subnet_id     = \"\(.SubnetId)\"

    vpc_security_group_ids = [\(.SecurityGroups | map(\"\\\"\(.GroupId)\\\"\") | join(\", \"))]

    tags = {
      Name = \"\(.Tags | map(select(.Key==\"Name\"))[0].Value // \"unnamed\")\"
    }
  }
  "
' > generated_instances.tf

Phase 3: Import in Batches

#!/bin/bash
# import-batch.sh - Import instances in manageable batches

BATCH_SIZE=10
INSTANCES=$(aws ec2 describe-instances --query 'Reservations[].Instances[].InstanceId' --output text)

count=0
for instance_id in $INSTANCES; do
  # Get instance name for resource naming
  name=$(aws ec2 describe-instances --instance-ids $instance_id \
    --query 'Reservations[].Instances[].Tags[?Key==`Name`].Value' --output text)

  # Sanitize name for Terraform resource name
  tf_name=$(echo "${name:-$instance_id}" | tr '-' '_' | tr '[:upper:]' '[:lower:]')

  echo "Importing $instance_id as aws_instance.$tf_name"
  terraform import "aws_instance.$tf_name" "$instance_id"

  ((count++))

  # Verify after each batch
  if [ $((count % BATCH_SIZE)) -eq 0 ]; then
    echo "=== Verifying batch (${count} instances imported) ==="
    terraform plan -detailed-exitcode
    if [ $? -eq 2 ]; then
      echo "WARNING: Plan shows changes. Review before continuing."
      read -p "Continue? (y/n) " -n 1 -r
      echo
      if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        exit 1
      fi
    fi
  fi
done

Phase 4: Verify Configuration Matches Reality

# Run plan - should show NO changes if config matches
terraform plan

# If plan shows changes, your config doesn't match actual state
# Common mismatches:

# 1. Root volume settings
resource "aws_instance" "web" {
  # Add this block to match existing root volume
  root_block_device {
    volume_size           = 100  # Match actual size
    volume_type           = "gp3"
    delete_on_termination = true
    encrypted             = true
  }
}

# 2. Network interfaces
resource "aws_instance" "web" {
  # If instance has multiple ENIs, might need:
  network_interface {
    network_interface_id = "eni-0abc123"
    device_index         = 0
  }
}

# 3. IAM instance profile
resource "aws_instance" "web" {
  iam_instance_profile = "existing-profile-name"
}

Phase 5: Handle Lifecycle Configurations

# Prevent accidental changes to critical attributes
resource "aws_instance" "production_db" {
  # ... config ...

  lifecycle {
    # Prevent destruction without explicit approval
    prevent_destroy = true

    # Ignore changes made outside Terraform
    ignore_changes = [
      user_data,  # Often changed for bootstrapping
      tags["LastUpdated"],  # Auto-updated tags
    ]
  }
}

Phase 6: Establish Naming Conventions

# locals.tf - Standardize naming going forward
locals {
  name_prefix = "${var.environment}-${var.application}"

  common_tags = {
    Environment  = var.environment
    Application  = var.application
    ManagedBy    = "terraform"
    ImportedFrom = "manual"
    ImportDate   = "2024-01-15"
  }
}

# Use for new resources and gradually update imported ones
resource "aws_instance" "web" {
  # ... config ...

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-web-${count.index + 1}"
    Role = "web-server"
  })
}
# Don't forget dependent resources!
# Security Groups
import {
  to = aws_security_group.web
  id = "sg-0abc123def"
}

# EBS Volumes (if not root volumes)
import {
  to = aws_ebs_volume.data
  id = "vol-0abc123def"
}

# Elastic IPs
import {
  to = aws_eip.web
  id = "eipalloc-0abc123def"
}

# EIP Association
import {
  to = aws_eip_association.web
  id = "eipassoc-0abc123def"
}

Import Checklist

StepActionVerification
1Inventory all resourcesCount matches AWS console
2Generate Terraform configSyntax valid (terraform validate)
3Import resourcesState shows resources
4Run terraform planNo changes shown
5Fix config mismatchesRe-run plan until clean
6Test with -targetIndividual resources stable
7Document imported resourcesREADME updated

Common Import Gotchas

# 1. Security group rules - import the group, not individual rules
terraform import aws_security_group.web sg-0abc123
# Rules are imported with the group

# 2. Auto-scaling groups - instances are separate
terraform import aws_autoscaling_group.web my-asg
# Don't import the instances - ASG manages them

# 3. RDS with read replicas - import separately
terraform import aws_db_instance.primary my-db
terraform import aws_db_instance.replica my-db-replica

# 4. Resources with count/for_each
terraform import 'aws_instance.web[0]' i-0abc123
terraform import 'aws_instance.web["server-1"]' i-0def456

Practice Question

After importing 200 EC2 instances, terraform plan shows changes to all instances. What is the most likely cause?