Import Existing Infrastructure into Terraform
Learn how to bring existing infrastructure under Terraform management using terraform import and Import Blocks without recreating resources or disrupting production systems.

Import Existing Infrastructure into Terraform
Throughout the previous articles in this series, we've always started with a brand-new project.
terraform init
terraform plan
terraform applyTerraform creates everything from scratch.
However, real-world production environments are very different.
It's extremely rare to start with an empty AWS account or a clean Azure subscription.
More often, you'll inherit an environment that already contains:
- Hundreds of EC2 instances
- Existing VPCs created years ago
- Production databases
- Cloudflare DNS records
- Vercel projects serving real users
- S3 buckets containing terabytes of data
These resources are already running.
You cannot simply delete and recreate them just to adopt Terraform.
So how can Terraform start managing infrastructure that already exists?
That's exactly what Terraform Import is for.
A Common Misconception
One of the most common assumptions beginners make is:
Terraform can only manage resources that were originally created by Terraform.
This is not true.
Terraform doesn't care who created a resource.
It could have been created through:
- The AWS Console
- Azure Portal
- Cloudflare Dashboard
- Vercel Dashboard
- AWS CLI
- Azure CLI
- A shell script
- Another Infrastructure as Code tool
Terraform only cares about one thing:
"Does Terraform State already know about this resource?"
If the resource exists in State, Terraform can manage it.
If it doesn't, Terraform assumes the resource doesn't exist.
That is why Import exists.
What is Terraform Import?
At a high level, Terraform Import does one thing.
It tells Terraform:
"This resource already exists. Record it in Terraform State."
Imagine your production environment already has an EC2 instance.
EC2 Instance
ID
i-0123456789abcdef0
Terraform knows nothing about it.
State:
(empty)
Now run:
terraform import aws_instance.web i-0123456789abcdef0Terraform reads the EC2 instance from AWS.
Then it stores the resource inside Terraform State.
Terraform State
↓
aws_instance.web
↓
ID = i-0123456789abcdef0
Notice what did not happen.
Terraform did not create a new EC2 instance.
Terraform did not modify the existing EC2 instance.
It simply updated Terraform State.
Import Does Not Create Configuration
This is one of the biggest surprises for newcomers.
Suppose you've successfully imported an EC2 instance.
Terraform State now contains:
aws_instance.web
But your project still doesn't have:
resource "aws_instance" "web" {
}If you immediately run:
terraform planTerraform won't have enough information to understand how that resource should be managed.
Terraform always needs both:
Configuration
+
State
Import only updates the second part.
You are still responsible for writing the Terraform configuration.
Three Pieces Must Always Match
Terraform continuously compares three things.
Configuration
↓
State
↓
Infrastructure
All three must stay synchronized.
If Configuration exists without State,
Terraform thinks the resource hasn't been created yet.
If State exists without Configuration,
Terraform doesn't know how the resource should be managed.
Only when both exist can Terraform accurately calculate an execution plan.
A Simple Example
Suppose AWS already contains an S3 bucket.
company-production-assets
You write the following configuration.
resource "aws_s3_bucket" "assets" {
bucket = "company-production-assets"
}Then execute:
terraform import aws_s3_bucket.assets company-production-assetsThe result becomes:
Configuration
✓
State
✓
Infrastructure
✓
From this point onward,
Terraform officially manages that bucket.
What Happens Without Import?
Imagine the bucket already exists.
But instead of importing it, you simply write:
resource "aws_s3_bucket" "assets" {
bucket = "company-production-assets"
}Then run:
terraform applyTerraform checks its State.
Bucket not found.
Terraform concludes:
This bucket needs to be created.
It sends a Create Bucket request to AWS.
AWS responds with an error such as:
BucketAlreadyExists
or
BucketAlreadyOwnedByYou
This is one of the most common mistakes teams make when migrating existing infrastructure to Terraform.
Import Is Not the Entire Migration
Many engineers think the migration process looks like this.
terraform import
↓
Done
In reality, Import is only the beginning.
After importing a resource, you still need to:
- Write the Terraform configuration
- Match the configuration with the existing infrastructure
- Check for drift
- Run Terraform Plan
- Make sure Terraform reports No changes
Only then can you confidently say that the resource has been successfully migrated into Terraform.
The Import Workflow in Real Projects
When people first learn Terraform, they often imagine the workflow looks like this.
Write Configuration
↓
Import
↓
Done
In production, the workflow is almost the opposite.
Identify Existing Resource
↓
Write Configuration
↓
Import
↓
Terraform Plan
↓
Adjust Configuration
↓
Plan = No Changes
↓
Commit
The ultimate goal is always the same:
Terraform should not create, modify, or destroy anything unexpectedly.
Step 1 — Identify the Resource
The first step is to identify exactly which resource Terraform should manage.
For example, AWS might already contain:
EC2
ID = i-0123456789abcdef0
Or:
S3 Bucket
company-assets
Or perhaps:
Cloudflare Zone
example.com
Every provider identifies resources differently.
Some common examples include:
AWS EC2
Instance ID
AWS S3
Bucket Name
Cloudflare
Zone ID
Vercel
Project ID
Azure
Full Resource ID
Before importing anything, always verify which identifier the provider expects.
Step 2 — Write the Terraform Configuration
This is where many people make their first mistake.
They immediately run:
terraform importand only write the Terraform configuration afterward.
While this technically works, it usually results in confusing projects.
A better approach is to create the resource block first.
For example:
resource "aws_s3_bucket" "assets" {
bucket = "company-assets"
}Or:
resource "aws_instance" "web" {
}The configuration doesn't need to be complete yet.
It simply needs to define the correct resource address.
Once the resource block exists, you're ready to import.
Step 3 — Import the Resource
For an EC2 instance:
terraform import aws_instance.web i-0123456789abcdef0For an S3 bucket:
terraform import aws_s3_bucket.assets company-assetsTerraform performs the following process.
Provider API
↓
Read Existing Resource
↓
Terraform State
Notice something important.
Terraform never changes the infrastructure during an import.
It only records information inside State.
Inspecting Terraform State
After importing, you can verify the results.
List every resource currently stored in State.
terraform state listExample output:
aws_instance.web
aws_security_group.web
aws_s3_bucket.assets
To inspect a specific resource:
terraform state show aws_instance.webTerraform will display all known attributes.
For example:
instance_type = t3.micro
ami = ami-123456
availability_zone = ap-southeast-1a
private_ip = ...
...
This is Terraform's current understanding of the resource.
Running Terraform Plan
Suppose your configuration still looks like this.
resource "aws_instance" "web" {
}After importing, run:
terraform planTerraform might report:
~ ami
~ instance_type
~ monitoring
~ tags
~ ebs_optimized
...
Many engineers assume something went wrong.
Actually, this behavior is expected.
Terraform compares:
Configuration
↓
State
↓
Infrastructure
Your configuration is incomplete.
Terraform therefore assumes these values should change.
Updating the Configuration
Now begin adding the missing attributes.
resource "aws_instance" "web" {
ami = "ami-123456"
instance_type = "t3.micro"
tags = {
Name = "production-web"
}
}Run:
terraform planIf Terraform still reports changes,
continue updating the configuration.
Repeat this process until Terraform reports:
No changes.
This is the desired outcome.
The End Goal
A successful migration always looks like this.
Configuration
✓
State
✓
Infrastructure
✓
All three layers are synchronized.
Running:
terraform planshould produce:
No changes.
Your infrastructure matches the configuration.
Only then should you commit the Terraform project.
Terraform 1.5 — Import Blocks
For many years, Terraform only supported the traditional import command.
terraform importIt works well,
but it has one major drawback.
The import process is never recorded in source control.
If another engineer clones the repository,
there is no indication of how the resources were originally imported.
Terraform 1.5 introduced Import Blocks to solve this problem.
Example:
import {
to = aws_s3_bucket.assets
id = "company-assets"
}Then simply run:
terraform planTerraform performs the import automatically.
No manual command is required.
This makes importing part of the Infrastructure as Code workflow itself.
Automatically Generating Configuration
Terraform can also generate configuration for imported resources.
For example:
terraform plan \
-generate-config-out=generated.tfTerraform reads the existing infrastructure and generates:
generated.tf
The file may contain something like:
resource "aws_s3_bucket" "assets" {
...
}This is an excellent way to bootstrap a Terraform project.
However, the generated configuration usually contains many provider defaults.
For example:
monitoring = false
force_destroy = false
acceleration_status = "Suspended"
...Many of these attributes don't need to be explicitly managed.
Instead of committing the generated file directly,
treat it as a reference.
Then simplify it according to your team's coding standards.
Traditional Import vs Import Blocks
Today, both approaches are fully supported.
If you're importing a single resource,
the traditional command is usually faster.
terraform import
However, when migrating an entire production environment,
Import Blocks are generally the better choice.
Because they:
- Live inside the Terraform configuration
- Can be reviewed in Pull Requests
- Are version-controlled in Git
- Can be executed in CI/CD
- Eliminate manual import steps
- Make migrations reproducible
This is also the direction HashiCorp recommends for modern Terraform projects.
Common Mistakes When Importing Resources
The terraform import command itself is fairly straightforward.
The real challenge is keeping Configuration, State, and Infrastructure synchronized.
This is where most migration problems occur.
Mistake 1 — Importing Before Writing Configuration
This is probably the most common mistake.
Imagine running:
terraform import aws_instance.web i-0123456789abcdef0Terraform State now contains:
aws_instance.web
But your project doesn't contain:
resource "aws_instance" "web" {
}A few weeks later, another engineer joins the project.
They open the repository and search for the EC2 definition.
Nothing found.
Yet the resource exists in Terraform State.
This creates confusion and makes the project difficult to maintain.
A better workflow is:
Write Configuration
↓
Import
↓
Commit
The configuration should always exist before importing the resource.
Mistake 2 — Importing into the Wrong Resource Address
Suppose your configuration contains:
resource "aws_instance" "frontend" {
}But you accidentally run:
terraform import aws_instance.backend i-0123456789abcdef0Terraform may fail immediately.
Or worse,
the resource may be imported into the wrong address.
Later, nobody understands which Terraform resource represents the real infrastructure.
Always verify that the resource address exactly matches the configuration.
Mistake 3 — Importing Only Part of the Infrastructure
An EC2 instance rarely exists alone.
It is usually connected to several other resources.
For example:
- Security Groups
- IAM Roles
- Elastic IPs
- EBS Volumes
- Network Interfaces
- Load Balancers
Imagine importing only the EC2 instance.
EC2
✓ Imported
Security Group
✗
IAM Role
✗
EBS Volume
✗
Terraform may later attempt to create missing resources or fail to resolve dependencies correctly.
When migrating an existing system, think about the entire infrastructure rather than a single resource.
Mistake 4 — Applying Changes Immediately After Import
Another dangerous mistake looks like this.
terraform import
↓
terraform plan
↓
Many changes detected
↓
terraform apply
This is risky.
After importing, Terraform often reports changes because the configuration is incomplete.
Applying those changes immediately could modify production resources.
Instead, your goal should always be:
terraform plan
↓
No changes
If Terraform still wants to modify resources,
understand why before applying anything.
Mistake 5 — Trusting Generated Configuration Completely
Terraform can automatically generate configuration.
For example:
terraform plan \
-generate-config-out=generated.tfThe generated file is useful,
but it usually contains every attribute returned by the provider.
For example:
monitoring = false
ebs_optimized = false
disable_api_stop = false
...
Many of these are simply provider defaults.
Keeping everything makes the configuration unnecessarily verbose.
Instead,
treat the generated file as a starting point.
Keep only the attributes your team intentionally wants to manage.
Mistake 6 — Importing Directly in Production
Some teams follow this workflow.
Production
↓
terraform import
↓
terraform apply
Any mistake immediately affects production.
A much safer workflow looks like this.
Clone the Repository
↓
Write Configuration
↓
Import
↓
Terraform Plan
↓
Review
↓
Commit
↓
Pull Request
↓
CI/CD
↓
Production
Import should be treated like any other infrastructure change.
It deserves code review.
It deserves testing.
And it deserves careful planning.
A Typical Production Migration Workflow
Large production environments are rarely migrated all at once.
Instead, most teams follow an incremental approach.
Inventory Existing Resources
↓
Write Terraform Configuration
↓
Import Resources
↓
Terraform Plan
↓
Fix Configuration
↓
Plan = No Changes
↓
Pull Request
↓
Code Review
↓
Merge
↓
CI/CD
The key milestone is always:
Terraform Plan
↓
No changes
Only after reaching this point should Terraform begin managing the resource in production.
When Should You Use Import?
Import is the right choice when:
- Adopting Terraform for an existing infrastructure
- Migrating from manual infrastructure management
- Taking ownership of an existing cloud environment
- Managing resources originally created through cloud consoles
- Bringing third-party services such as Cloudflare, Vercel, or GitHub under Terraform management
If your infrastructure has been managed by Terraform from day one,
you may rarely need to use Import.
When Shouldn't You Import?
Not every resource belongs in Terraform.
Examples include:
- Temporary test resources
- Sandbox environments
- Short-lived infrastructure
- Automatically generated resources owned by another service
- Disposable development environments
Importing these resources often adds unnecessary complexity to your Terraform State.
Terraform should focus on long-lived infrastructure that your team intentionally manages.
A Different Way to Think About Import
Many people describe Import like this:
Terraform is learning about the infrastructure.
That's close,
but not entirely accurate.
The infrastructure never changes.
Terraform simply updates its own memory.
Earlier in this series, we compared Terraform to this model.
Configuration represents the desired state.
State represents Terraform's memory.
Following that analogy,
Import is simply the process of synchronizing Terraform's memory with reality.
Once Terraform understands reality,
it can produce an accurate execution plan.
Summary
Terraform Import is one of the most valuable features when adopting Infrastructure as Code for existing systems.
The most important points to remember are:
- Import never creates a new resource.
- Import does not modify infrastructure.
- Import only updates Terraform State.
- Configuration and State must always stay synchronized.
- Never apply changes until
terraform planreports No changes. - For Terraform 1.5 and later, prefer Import Blocks because they make the import process part of your Infrastructure as Code.
Understanding Import allows you to gradually migrate real production environments into Terraform without rebuilding everything from scratch.
What's Next?
At this point in the series, we've learned how to write Terraform configurations, organize repositories, build reusable modules, and import existing infrastructure.
However, one major question remains.
How can multiple engineers work on the same Terraform project without overwriting each other's State?
Where should Terraform State be stored so that it's secure, supports locking, and integrates cleanly with CI/CD pipelines?
In the next article, we'll explore Remote State Backends, one of the most essential building blocks of any production-grade Terraform project.
Continue the Series
Comments
Loading comments…