IaCInfrastructure as Codeterraformterraform import

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.

Jul 22, 202610 min readUpdated Jul 22, 2026
Import Existing Infrastructure into Terraform

Import Existing Infrastructure into Terraform

Throughout the previous articles in this series, we've always started with a brand-new project.

code
terraform init
terraform plan
terraform apply

Terraform 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:

code
terraform import aws_instance.web i-0123456789abcdef0

Terraform 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:

code
resource "aws_instance" "web" {
 
}

If you immediately run:

code
terraform plan

Terraform 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.

code
resource "aws_s3_bucket" "assets" {
  bucket = "company-production-assets"
}

Then execute:

code
terraform import aws_s3_bucket.assets company-production-assets

The 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:

code
resource "aws_s3_bucket" "assets" {
  bucket = "company-production-assets"
}

Then run:

code
terraform apply

Terraform 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:

code
terraform import

and 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:

code
resource "aws_s3_bucket" "assets" {
  bucket = "company-assets"
}

Or:

code
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:

code
terraform import aws_instance.web i-0123456789abcdef0

For an S3 bucket:

code
terraform import aws_s3_bucket.assets company-assets

Terraform 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.

code
terraform state list

Example output:

aws_instance.web

aws_security_group.web

aws_s3_bucket.assets

To inspect a specific resource:

code
terraform state show aws_instance.web

Terraform 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.

code
resource "aws_instance" "web" {
 
}

After importing, run:

code
terraform plan

Terraform 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.

code
resource "aws_instance" "web" {
  ami           = "ami-123456"
  instance_type = "t3.micro"
 
  tags = {
    Name = "production-web"
  }
}

Run:

code
terraform plan

If 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:

code
terraform plan

should 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.

code
terraform import

It 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:

code
import {
  to = aws_s3_bucket.assets
  id = "company-assets"
}

Then simply run:

code
terraform plan

Terraform 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:

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

Terraform reads the existing infrastructure and generates:

generated.tf

The file may contain something like:

code
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:

code
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:

code
terraform import aws_instance.web i-0123456789abcdef0

Terraform State now contains:

aws_instance.web

But your project doesn't contain:

code
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:

code
resource "aws_instance" "frontend" {
 
}

But you accidentally run:

code
terraform import aws_instance.backend i-0123456789abcdef0

Terraform 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:

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

The generated file is useful,

but it usually contains every attribute returned by the provider.

For example:

code
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 plan reports 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.


Comments

0/2000

Loading comments…