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-serverBut 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 objectImporting 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 planTerraform 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 managementDepending 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.webWith 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.frontendIf you simply make the change without telling Terraform about the move, Terraform can interpret it as:
remove web
create frontendThat 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 serverThe 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.webLater, you move it into a module:
module.application.some_provider_server.webWithout state-aware refactoring, Terraform may see:
old address → removed
new address → createdBut 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 listThis shows resource addresses known to the current state.
You can inspect a particular instance with:
terraform state show some_provider_server.webThese commands are especially useful before moving or importing resources.
For example:
$ terraform state list
some_provider_network.main
some_provider_server.webThe 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.frontendThis 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.webThis removes the resource from Terraform’s state.
It does not mean:
Destroy the real serverInstead:
Terraform state
│
│ remove association
▼
No longer tracked
│
▼
Real server still existsThis 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:
| Operation | Real infrastructure | Terraform state |
|---|---|---|
| Import | Stays | Add existing object |
| Move | Stays | Change resource address |
| State remove | Stays | Remove tracking |
| Destroy | Deleted | Remove 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 itThese operations affect Terraform’s relationship with infrastructure in different ways.
A Practical Rename Refactoring
Suppose you begin with:
refactor-example/
└── main.tfand:
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 planThe 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 applyThe actual infrastructure remains the same while Terraform now uses:
some_provider_server.frontendinstead of:
some_provider_server.webThe 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:
resourceinto:
module
└── resourceis 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. ApplyUseful inspection commands include:
terraform state listand:
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 InfrastructureYou 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.