Skip to content

Importing, Moving and Refactoring Resources


Terraform configuration inevitably changes. A resource gets renamed. A resource is moved into a module. Infrastructure that was created manually needs to become Terraform-managed. If Terraform interprets every configuration change as a completely new resource, harmless refactoring can turn into destructive infrastructure changes.

The key skill in this chapter is learning to distinguish changing the infrastructure from changing Terraform’s understanding of the infrastructure.

Existing Infrastructure That Terraform Doesn’t Know About

Imagine a server already exists:

Real infrastructure
└── web-server

But your Terraform state contains nothing about it.

You then write:

resource "some_provider_server" "web" {
  name = "web-server"
}

Terraform compares the configuration with its state.

From Terraform’s perspective, some_provider_server.web does not exist in state.

Without importing the existing object, Terraform may propose creating another server.

That is not what you want.

You need to establish the relationship:

Terraform resource address
        │
        ▼
some_provider_server.web
        │
        │ import
        ▼
Existing real object

Importing Resources

Terraform supports importing an existing object into state.

The modern workflow can use an import block:

import {
  to = some_provider_server.web
  id = "server-123"
}

The to argument identifies the Terraform resource instance that should receive the imported object.

The id is the identifier understood by the provider for the existing object. The exact import ID format is provider-specific.

After adding the appropriate resource configuration and import block, run:

terraform plan

Terraform can then plan the import and show how the existing object will be associated with the resource address.

The import itself does not magically make the resource configuration perfect.

Note

Import establishes Terraform’s relationship with an existing object. It does not automatically mean that your .tf configuration exactly describes every attribute of that object.

Import Does Not Equal Configuration Generation

This distinction causes a lot of confusion.

You might import an existing server and then discover that your configuration only contains:

resource "some_provider_server" "web" {
  name = "web-server"
}

The real server might have many additional properties.

Import puts the object into Terraform’s state, but you still need configuration that represents the desired management model.

The workflow is therefore more like:

Existing infrastructure
        │
        ▼
      Import
        │
        ▼
      State
        │
        ▼
Write/reconcile configuration
        │
        ▼
Stable Terraform management

Depending on the provider and Terraform version, configuration generation capabilities may help bootstrap resource configuration, but you should still review generated configuration rather than blindly committing it.

Resource Addresses Are Important

Terraform identifies resources using addresses.

A simple resource has an address such as:

some_provider_server.web

With count:

some_provider_server.web[0]

With for_each:

some_provider_server.web["frontend"]

That address is part of Terraform’s model.

Now imagine you rename:

resource "some_provider_server" "web" {
  name = "web-server"
}

to:

resource "some_provider_server" "frontend" {
  name = "web-server"
}

The real server has not changed.

But Terraform sees two different addresses:

old:
some_provider_server.web

new:
some_provider_server.frontend

If you simply make the change without telling Terraform about the move, Terraform can interpret it as:

remove web
create frontend

That is exactly what you want to avoid.

Moving a Resource

Terraform provides state-aware mechanisms for moving a resource from one address to another.

A modern configuration can use a moved block:

moved {
  from = some_provider_server.web
  to   = some_provider_server.frontend
}

This tells Terraform:

“The object previously known at this address is now represented by this address.”

Important

The important thing is that the real infrastructure does not need to be recreated merely because the Terraform address changed.

Conceptually:

Before

Terraform state
web ─────────────→ real server


After refactoring

Terraform state
frontend ────────→ same real server

The infrastructure stays where it is.

Only Terraform’s address changes.

Moving a Resource Into a Module

This becomes especially valuable when you introduce modules later.

Suppose your resource originally exists at:

some_provider_server.web

Later, you move it into a module:

module.application.some_provider_server.web

Without state-aware refactoring, Terraform may see:

old address → removed
new address → created

But that is not what happened.

You reorganized the configuration; you did not ask for a new server.

A moved block can express the relationship:

moved {
  from = some_provider_server.web
  to   = module.application.some_provider_server.web
}

Terraform can then understand that the same real object has a new address.

Modules themselves are covered in the next section, but the state-address concept is important now.

Inspecting State

When performing refactoring, inspect the current state first.

Useful commands include:

terraform state list

This shows resource addresses known to the current state.

You can inspect a particular instance with:

terraform state show some_provider_server.web

These commands are especially useful before moving or importing resources.

For example:

$ terraform state list

some_provider_network.main
some_provider_server.web

The exact output depends on your configuration.

The important question is:

“What address does Terraform currently use for this object?”

terraform state mv

Terraform also provides the terraform state mv command for moving an object directly in state.

Conceptually:

terraform state mv   some_provider_server.web   some_provider_server.frontend

This changes the state address without changing the real infrastructure.

The command is useful for controlled state migrations, but it changes state directly, so you should understand exactly what you’re moving before running it.

For configuration-managed refactoring, a moved block is often preferable because the move becomes part of the Terraform configuration and can be reviewed and applied as part of the normal workflow.

terraform state rm

There are situations where Terraform should stop managing an object without destroying the real infrastructure.

That is what terraform state rm is for:

terraform state rm some_provider_server.web

This removes the resource from Terraform’s state.

It does not mean:

Destroy the real server

Instead:

Terraform state
      │
      │ remove association
      ▼
No longer tracked
      │
      ▼
Real server still exists

This is a powerful command and therefore easy to misuse.

If you remove an object from state and then leave its resource configuration in place, a future plan may attempt to create a new object because Terraform no longer knows about the existing one.

Warning

terraform state rm changes Terraform’s tracking information. It does not delete the remote object. Understand what will happen on the next plan before using it.

Importing, Moving, and Removing Are Different

Keep these operations separate in your head:

OperationReal infrastructureTerraform state
ImportStaysAdd existing object
MoveStaysChange resource address
State removeStaysRemove tracking
DestroyDeletedRemove tracking

The distinction is critical.

Import:
existing object → Terraform starts tracking it

Move:
same object → different Terraform address

State remove:
object remains → Terraform stops tracking it

Destroy:
object is deleted → Terraform stops tracking it

These operations affect Terraform’s relationship with infrastructure in different ways.

A Practical Rename Refactoring

Suppose you begin with:

refactor-example/
└── main.tf

and:

resource "some_provider_server" "web" {
  name = "web-server"

  # Other arguments required by your provider...
}

After applying it, you decide that web is a poor Terraform name and want frontend.

Change the configuration to:

resource "some_provider_server" "frontend" {
  name = "web-server"

  # Other arguments required by your provider...
}

moved {
  from = some_provider_server.web
  to   = some_provider_server.frontend
}

Then run:

terraform plan

The desired result is that Terraform recognizes the move rather than proposing destruction and creation.

You should not see a plan that treats the real server as a brand-new object simply because its Terraform address changed.

After confirming the plan:

terraform apply

The actual infrastructure remains the same while Terraform now uses:

some_provider_server.frontend

instead of:

some_provider_server.web

The exact resource arguments depend on your provider, so use the same valid resource configuration you already use in your environment rather than inventing provider-specific arguments.

Refactoring Is Not Infrastructure Change

This distinction is worth making explicit.

Suppose you change:

resource "..." "web"

to:

resource "..." "frontend"

You changed the Terraform configuration structure.

You did not necessarily change the infrastructure.

Similarly, moving:

resource

into:

module
└── resource

is a configuration refactoring.

Terraform needs to be told that the real object has the same identity even though its Terraform address changed.

That is why state-aware refactoring matters.

A Safe Refactoring Workflow

When reorganizing existing Terraform configuration, use a deliberate process:

1. Inspect current state
          │
          ▼
2. Identify old address
          │
          ▼
3. Decide new address
          │
          ▼
4. Tell Terraform about the move
          │
          ▼
5. Run terraform plan
          │
          ▼
6. Confirm no unintended replacement
          │
          ▼
7. Apply

Useful inspection commands include:

terraform state list

and:

terraform state show <resource-address>

Do not skip the plan.

A refactoring that looks harmless in .tf files can have very different consequences if Terraform does not understand that two addresses represent the same object.

The Big Picture

By now, the pieces of Terraform’s engine should fit together:

    flowchart TD
    A[Terraform Configuration] --> B[Terraform Engine]
    B --> C[State]
    B --> D[Dependency Graph]
    C --> E[Resource Identity]
    D --> F[Execution Order]
    E --> G[Real Infrastructure]
    F --> G
    H[Data Sources] --> B
    I[Lifecycle Rules] --> B
  

Terraform is doing much more than reading HCL and executing commands.

It maintains resource identity through state, constructs a graph of dependencies, reads existing information through data sources, creates resource instances through meta-arguments, applies lifecycle rules when changes occur, and supports state-aware refactoring when your configuration evolves.

The most useful mental model to carry forward is:

              Terraform Configuration
                       │
                       ▼
                Terraform Engine
                  /           \
                 /             \
                ▼               ▼
             State        Dependency Graph
                │               │
                └───────┬───────┘
                        ▼
                Real Infrastructure

You declare what you want, Terraform maintains what it knows, and the provider represents what actually exists.

That relationship is the foundation for the reusable infrastructure, team workflows, and production Terraform practices covered in the sections ahead.

Last updated on