Skip to main content

Importing Existing Infrastructure into Terraform State

Import AWS resources created manually or outside Terraform into your state file so Terraform can manage them going forward without recreating them.

26 Terms

Overview and What You Will Learn

You joined a company where the entire AWS account was built by clicking in the console over the past two years. There are EC2 instances, VPCs, RDS databases, IAM roles, and S3 buckets — none of it managed by Terraform. The team wants to adopt IaC without destroying and recreating hundreds of production resources.

terraform import is how you bring existing infrastructure under Terraform management without touching the real resources. You write the HCL configuration, run the import command, and Terraform adds the resource to its state file. From that moment on, all future changes go through Terraform.

By the end of this lab you will be able to:

  • Import any AWS resource into Terraform state using the CLI import command
  • Use the new declarative import block (Terraform 1.5+) for cleaner workflows
  • Auto-generate HCL from existing resources using -generate-config-out
  • Fix plan mismatches after import until Terraform shows no changes
  • Handle the most common import errors and gotchas

Why This Matters in Production

At PhonePe, when the platform team decided to adopt Terraform, they had 400+ AWS resources created manually over three years. Destroying and recreating everything was not an option — these were live production systems handling millions of payments per day.

They used terraform import to bring resources under management one service at a time, starting with the least critical. Each import took 15 minutes — write HCL, import, fix the plan diff, commit. In six weeks, the entire infrastructure was Terraform-managed. The next time a new environment was needed, they applied the existing code and had it running in 20 minutes instead of three days.

Core Principles

The Import Flow
Bash
+------------------------------------------+
| Existing AWS resource (not in state) |
| EC2: i-0a1b2c3d4e5f6789 |
| Created manually in AWS console |
+------------------------------------------+
|
Step 1: Write resource block in HCL
|
v
+------------------------------------------+
| resource "aws_instance" "payments_api" { |
| ami = "ami-0f5ee92e..." |
| instance_type = "t3.large" |
| } |
+------------------------------------------+
|
Step 2: terraform import
|
v
+------------------------------------------+
| terraform.tfstate now contains: |
| aws_instance.payments_api: |
| id = "i-0a1b2c3d4e5f6789" |
| all real attributes read from AWS |
+------------------------------------------+
|
Step 3: terraform plan — fix mismatches
|
v
+------------------------------------------+
| No changes. Your infrastructure matches |
| the configuration. |
| Resource is now fully Terraform-managed |
+------------------------------------------+

Detailed Step-by-Step Practical Lab

Step 1 — Write the Resource Configuration

Before running import, write the resource block in your .tf files. You do not need every argument — just enough for Terraform to accept the block syntax. You will fix the rest after import:

HCL
# For an EC2 instance
resource "aws_instance" "payments_api" {
ami = "ami-0f5ee92e2d63afc18" # put the real AMI — find it in the console
instance_type = "t3.large" # put the real type — find it in the console
# More arguments will be added after import when you see the plan diff
}
# For an S3 bucket
resource "aws_s3_bucket" "customer_data" {
bucket = "phonepay-customer-data-prod" # exact real bucket name
}
# For a security group
resource "aws_security_group" "payments_api" {
name = "phonepay-payments-api-sg"
vpc_id = "vpc-0a1b2c3d4e5f" # real VPC ID
}
Step 2 — Find the Resource ID

Every AWS resource has an ID that Terraform uses to locate it. The format varies by resource type:

Bash
# EC2 Instance — ID starts with i-
aws ec2 describe-instances \
--filters "Name=tag:Name,Values=phonepay-payments-api-prod" \
--query "Reservations[0].Instances[0].InstanceId" \
--output text
# i-0a1b2c3d4e5f6789
# S3 Bucket — ID is the bucket name
# Just use the bucket name directly
# Security Group — ID starts with sg-
aws ec2 describe-security-groups \
--filters "Name=group-name,Values=phonepay-payments-api-sg" \
--query "SecurityGroups[0].GroupId" \
--output text
# sg-0a1b2c3d4e5f
# RDS Instance — ID is the DB identifier
aws rds describe-db-instances \
--query "DBInstances[0].DBInstanceIdentifier" \
--output text
# phonepay-payments-postgres-prod
# IAM Role — ID is the role name
aws iam list-roles \
--query "Roles[?RoleName=='phonepay-payments-api-role'].RoleName" \
--output text
# phonepay-payments-api-role
# VPC — ID starts with vpc-
aws ec2 describe-vpcs \
--filters "Name=tag:Name,Values=phonepay-prod-vpc" \
--query "Vpcs[0].VpcId" \
--output text
# vpc-0a1b2c3d4e5f
Step 3 — Run terraform import
Bash
# Syntax: terraform import <resource_type>.<resource_name> <resource_id>
# Import EC2 instance
terraform import aws_instance.payments_api i-0a1b2c3d4e5f6789
# aws_instance.payments_api: Importing from ID "i-0a1b2c3d4e5f6789"...
# aws_instance.payments_api: Import prepared!
# Prepared aws_instance for import
# aws_instance.payments_api: Refreshing state... [id=i-0a1b2c3d4e5f6789]
#
# Import successful!
# The resources that were imported are shown above. These resources are now in
# your Terraform state and will henceforth be managed by Terraform.
# Import S3 bucket
terraform import aws_s3_bucket.customer_data phonepay-customer-data-prod
# Import security group
terraform import aws_security_group.payments_api sg-0a1b2c3d4e5f
# Import RDS instance
terraform import aws_db_instance.main phonepay-payments-postgres-prod
# Import IAM role
terraform import aws_iam_role.payments_api phonepay-payments-api-role
# Import VPC
terraform import aws_vpc.main vpc-0a1b2c3d4e5f
# Import a subnet — needs both the subnet ID as the resource ID
terraform import aws_subnet.private_1 subnet-0a1b2c3d4e5f
# Import an S3 bucket policy — needs bucket name as ID
terraform import aws_s3_bucket_policy.customer_data phonepay-customer-data-prod
Step 4 — Fix Plan Mismatches

After import, run terraform plan. It will almost always show differences between your skeleton HCL and the real resource. Your job is to update the HCL until the plan shows no changes:

Bash
terraform plan
# The plan will show changes like:
#
# ~ aws_instance.payments_api will be updated in-place
# ~ ebs_optimized = false -> true <- real instance has this on
# ~ monitoring = false -> true <- real instance has monitoring
# ~ iam_instance_profile = null -> "phonepay-payments-api-profile"
# ~ metadata_options {
# ~ http_tokens = "optional" -> "required" <- IMDSv2 is required
# }
#
# Fix: update your resource block to match every ~ item

Update your HCL to match:

HCL
resource "aws_instance" "payments_api" {
ami = "ami-0f5ee92e2d63afc18"
instance_type = "t3.large"
ebs_optimized = true # added
monitoring = true # added
iam_instance_profile = "phonepay-payments-api-profile" # added
metadata_options {
http_endpoint = "enabled"
http_tokens = "required" # added
http_put_response_hop_limit = 1
}
tags = {
Name = "phonepay-payments-api-prod"
Environment = "prod"
ManagedBy = "terraform"
}
}

Repeat terraform plan → fix differences → terraform plan until:

Bash
terraform plan
# No changes. Your infrastructure matches the configuration.

This is the goal. When the plan is clean, the resource is fully managed by Terraform.

Step 5 — Import S3 Sub-Resources

S3 buckets have many sub-resources in the AWS provider. After importing the bucket itself, import each sub-resource separately:

Bash
# Import the bucket
terraform import aws_s3_bucket.customer_data phonepay-customer-data-prod
# Import public access block — ID is the bucket name
terraform import aws_s3_bucket_public_access_block.customer_data phonepay-customer-data-prod
# Import versioning — ID is the bucket name
terraform import aws_s3_bucket_versioning.customer_data phonepay-customer-data-prod
# Import encryption configuration — ID is the bucket name
terraform import aws_s3_bucket_server_side_encryption_configuration.customer_data \
phonepay-customer-data-prod
Step 6 — The Declarative Import Block (Terraform 1.5+)

Terraform 1.5 introduced import blocks that live in your .tf files. Instead of running a CLI command, import happens as part of terraform apply:

HCL
# imports.tf — declarative import configuration
import {
to = aws_instance.payments_api
id = "i-0a1b2c3d4e5f6789"
}
import {
to = aws_s3_bucket.customer_data
id = "phonepay-customer-data-prod"
}
import {
to = aws_security_group.payments_api
id = "sg-0a1b2c3d4e5f"
}
Bash
# With import blocks, terraform plan shows what will be imported
terraform plan
# aws_instance.payments_api will be imported
# aws_s3_bucket.customer_data will be imported
# aws_security_group.payments_api will be imported
# Apply performs the imports
terraform apply
# After apply, remove the import blocks — they are only needed once
# The resources are now in state and import blocks are no longer required
Step 7 — Auto-Generate HCL with -generate-config-out (Terraform 1.5+)

If you have many resources to import and do not want to write skeleton HCL manually, Terraform 1.5 can generate it for you:

HCL
# Step 1: Write import blocks WITHOUT any resource blocks
# imports.tf
import {
to = aws_instance.payments_api
id = "i-0a1b2c3d4e5f6789"
}
Bash
# Step 2: Run plan with -generate-config-out
terraform plan -generate-config-out=generated.tf
# Terraform reads the real EC2 instance and writes HCL to generated.tf
# The generated file will look like:
# resource "aws_instance" "payments_api" {
# ami = "ami-0f5ee92e2d63afc18"
# instance_type = "t3.large"
# ebs_optimized = true
# monitoring = true
# ... every single attribute ...
# }
Bash
# Step 3: Review and clean generated.tf
# Remove computed-only attributes like id, arn, private_dns
# Remove attributes you want Terraform to manage dynamically (like tags from variables)
# Keep only the attributes that define the desired configuration
# Step 4: Run plan until clean
terraform plan
# Fix remaining mismatches, then apply
terraform apply

Production Best Practices and Common Pitfalls

  • Import one service at a time, not all at once. Importing 200 resources in a single session is overwhelming. Start with one service — say, the payments API EC2 instances. Get the plan clean. Commit and apply. Then move to the next service. Each increment is manageable and reviewable.

  • Run terraform state show to understand what was imported. After each import, run terraform state show <resource> to see every attribute Terraform read from the real resource. Use this as the reference for updating your HCL to match.

  • Expect the plan to show changes after every import. Your skeleton HCL will never perfectly match reality on the first try. Multiple rounds of plan → fix → plan is normal. The goal is getting to "No changes" — not getting it right on the first attempt.

  • Remove import blocks after apply. Import blocks in .tf files are only needed once. Leave them in and Terraform will try to import on every apply (it will fail on subsequent runs since the resource is already in state). Remove them after the first successful apply.

  • Never import a resource that is already in state. Running import on a resource that Terraform already tracks causes an error. Always check terraform state list before importing.

  • Some resource attributes cannot be imported. Certain attributes are write-only in the AWS provider — like initial database passwords. After import, Terraform may show them as unknown. These need to be set with ignore_changes lifecycle rules or managed separately.

Quick Reference and Troubleshooting Commands

Command What It Does
terraform import <addr> <id> Import a resource into state by its real-world ID
terraform state show <addr> Show all attributes of an imported resource — use to update HCL
terraform state list Verify which resources are already in state
terraform plan -generate-config-out=file.tf Auto-generate HCL from import blocks (Terraform 1.5+)
terraform state rm <addr> Remove from state if you imported the wrong resource
Error Root Cause Fix
Resource already managed by Terraform Already in state Check terraform state list — already imported
Cannot import non-existent remote object Wrong resource ID Verify the ID in AWS console or CLI
Error: Resource type does not support import Provider limitation Check provider docs — not all resources support import
InvalidInstanceID.NotFound Wrong EC2 ID format Must be i- followed by hex characters
Plan shows -/+ after import HCL triggers a replacement Read (forces replacement) note — some attributes cannot be changed in-place
Generated HCL causes errors Auto-generated code has computed-only attributes Remove id, arn, private_dns and other computed fields

Resources

Explore More in Terraform State and Backend Management

All 3 Topics

Frequently Asked Questions

Is Importing Existing Infrastructure into Terraform State free to learn on DevOps Network?

Yes - this topic, like everything on DevOps Network, is 100% free with no paywall or sign-up gate.

What does the Importing Existing Infrastructure into Terraform State topic cover?

Import AWS resources created manually or outside Terraform into your state file so Terraform can manage them going forward without recreating them.