Terraform moved and removed Blocks with Examples

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
hashicorp/local 2.9.0 (removed demo)
Applies to Any host with Terraform installed
Lab environment Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu
Privilege Normal user
Scope Declarative refactoring — moved block from and to addresses, resource rename, move into a child module, removed block with destroy false, external verification, moved vs state mv and removed vs state rm comparison, brief for_each rename. Does not cover terraform state CLI depth, import, module authoring, count or for_each tutorials, manual state JSON editing, or legacy migration mechanisms.
Related guides terraform state commands
Terraform state explained
Terraform modules
Terraform count and for_each
Terraform Associate certification course

Renaming a resource block or moving it into a module changes its Terraform address. Without migration metadata, Terraform treats the old and new addresses as unrelated objects — the plan proposes destroy-and-create even when the real infrastructure never changed.

hcl
# before
resource "terraform_data" "server" {
  input = "rename lab"
}

# after rename — same input, new label
resource "terraform_data" "application" {
  input = "rename lab"
}

moved and removed blocks record these refactoring decisions in configuration: moved maps an old address to a new address, while removed declares that Terraform should stop managing an existing address. Examples run under ~/terraform-labs/terraform-moved-removed-block/ with terraform_data and local_file — no cloud credentials required.

NOTE
Complete terraform init in each lab directory before plan or apply. For imperative state edits, see terraform state commands — this lesson focuses on declarative blocks.

Rename a resource with a moved block

Create the rename lab directory:

bash
mkdir -p ~/terraform-labs/terraform-moved-removed-block/rename

Work inside it for the rename walkthrough:

bash
cd ~/terraform-labs/terraform-moved-removed-block/rename

Add a minimal resource in main.tf:

hcl
resource "terraform_data" "server" {
  input = "rename lab"
}

Initialize the rename working directory:

bash
terraform init

Apply so terraform_data.server exists in state:

bash
terraform apply -auto-approve

Rename the block label to application — keep the same input value:

hcl
resource "terraform_data" "application" {
  input = "rename lab"
}

Plan before adding moved — Terraform still tracks terraform_data.server:

bash
terraform plan

Sample output:

output
# terraform_data.application will be created
  + resource "terraform_data" "application" { ... }

  # terraform_data.server will be destroyed
  - resource "terraform_data" "server" {
      - id = "224d38c6-d0dd-d24e-6370-97ad386b464d" -> null
    }

Plan: 1 to add, 0 to change, 1 to destroy.

That destroy-and-create pair is unnecessary when only the label changed. Add a moved block above the resource:

hcl
moved {
  from = terraform_data.server
  to   = terraform_data.application
}

resource "terraform_data" "application" {
  input = "rename lab"
}

Plan again — Terraform records the address change instead of replacing the object:

bash
terraform plan

Sample output:

output
# terraform_data.server has moved to terraform_data.application
    resource "terraform_data" "application" {
        id = "224d38c6-d0dd-d24e-6370-97ad386b464d"
    }

Plan: 0 to add, 0 to change, 0 to destroy.

Apply so state permanently uses the new address:

bash
terraform apply -auto-approve

List state to confirm only the new address remains:

bash
terraform state list

Sample output:

output
terraform_data.application

The same underlying terraform_data instance is now tracked under application instead of server.


Move resources into a module

Module refactors change addresses from a root resource to a path inside a child module:

text
terraform_data.example
module.application.terraform_data.example

Create the module-move tree:

bash
mkdir -p ~/terraform-labs/terraform-moved-removed-block/module-move/modules/application

Switch into the module-move root:

bash
cd ~/terraform-labs/terraform-moved-removed-block/module-move

Start with a root-level resource in main.tf:

hcl
resource "terraform_data" "example" {
  input = "module move lab"
}

output "example_id" {
  value = terraform_data.example.id
}

Create the child module in modules/application/main.tf:

hcl
resource "terraform_data" "example" {
  input = "module move lab"
}

Expose the id from modules/application/outputs.tf:

hcl
output "example_id" {
  value = terraform_data.example.id
}

Initialize the root-only layout:

bash
terraform init

Apply so the root resource exists before the module refactor:

bash
terraform apply -auto-approve

Refactor configuration to call the module — remove the root resource block and wire the output through the module:

hcl
module "application" {
  source = "./modules/application"
}

output "example_id" {
  value = module.application.example_id
}

Re-run terraform init so Terraform initializes the newly referenced local child module:

bash
terraform init

Plan before moved — Terraform sees a root destroy and a module create:

bash
terraform plan

Sample output:

output
# terraform_data.example will be destroyed
  - resource "terraform_data" "example" { id = "383e586c-..." ... }

  # module.application.terraform_data.example will be created
  + resource "terraform_data" "example" { ... }

Plan: 1 to add, 0 to change, 1 to destroy.

Changes to Outputs:
  ~ example_id = "383e586c-..." -> (known after apply)

Add the module move to main.tf:

hcl
moved {
  from = terraform_data.example
  to   = module.application.terraform_data.example
}

module "application" {
  source = "./modules/application"
}

output "example_id" {
  value = module.application.example_id
}

Plan after moved:

bash
terraform plan

Sample output:

output
# terraform_data.example has moved to module.application.terraform_data.example
    resource "terraform_data" "example" {
        id = "383e586c-2d5e-6080-1032-c3b3bdfaa77a"
    }

Plan: 0 to add, 0 to change, 0 to destroy.

Apply the module move:

bash
terraform apply -auto-approve

Confirm the output id is unchanged after the address change:

bash
terraform output example_id

State now lists module.application.terraform_data.example instead of the root address.


Stop managing a resource with a removed block

Sometimes you want Terraform to stop managing an object without deleting it — handoff to another team, retention after a tool migration, or leaving a file on disk. The removed block declares that transition in configuration.

Create the removed demo directory:

bash
mkdir -p ~/terraform-labs/terraform-moved-removed-block/removed

Move into it for the stop-managing walkthrough:

bash
cd ~/terraform-labs/terraform-moved-removed-block/removed

Declare hashicorp/local in versions.tf:

hcl
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}

Create the file in main.tf:

hcl
resource "local_file" "keep" {
  content  = "survives removed"
  filename = "${path.module}/keep.txt"
}

Initialize the removed demo:

bash
terraform init

Create keep.txt through Terraform before you stop managing it:

bash
terraform apply -auto-approve

Replace the resource block with a removed block — configuration keeps only the removal declaration:

hcl
removed {
  from = local_file.keep

  lifecycle {
    destroy = false
  }
}

Plan — Terraform drops management without calling destroy:

bash
terraform plan

Sample output:

output
# local_file.keep will no longer be managed by Terraform, but will not be destroyed
 # (destroy = false is set in the configuration)
 . resource "local_file" "keep" { ... }

Plan: 0 to add, 0 to change, 0 to destroy.

Warning: Some objects will no longer be managed by Terraform

If you apply this plan, Terraform will discard its tracking information for
the following objects, but it will not remove them from your infrastructure:
 - local_file.keep

Apply the removal from management:

bash
terraform apply -auto-approve

Confirm the file still exists on disk — destroy = false means the provider delete API was not called:

bash
ls -la keep.txt

Sample output:

output
-rwxr-xr-x 1 user user 16 Aug 12 10:20 keep.txt

The metadata line confirms the file is still on disk. Read the content to verify nothing was truncated:

bash
cat keep.txt

Sample output:

output
survives removed

State no longer tracks the resource:

bash
terraform state list

The command prints nothing — local_file.keep is gone from state while keep.txt remains on disk.

IMPORTANT
removed with destroy = false is not the same as deleting the resource block and running apply without a removed declaration — that path can plan destruction. Declare the removal explicitly when infrastructure should survive.

moved vs state mv and removed vs state rm

Declarative blocks and imperative CLI commands solve similar problems at different layers:

text
moved
→ declarative refactor recorded in configuration

terraform state mv
→ imperative one-time state operation

removed
→ declarative removal from management

terraform state rm
→ imperative one-time state removal
Mechanism Lives in Best for
moved .tf configuration Renames and address changes every teammate and CI environment should apply consistently
terraform state mv CLI session Emergency repairs, labs, or one-off fixes when you cannot wait for a configuration rollout
removed .tf configuration Stopping management with an explicit destroy choice recorded in Git
terraform state rm CLI session Immediate state-only forget when configuration already changed or for surgical CLI work

Declarative blocks travel through version control and terraform plan in every workspace. That repeatability is why teams prefer moved and removed for refactors that must land the same way in dev, staging, and production. The terraform state commands lesson covers imperative mv and rm in depth — including when re-applying after state rm may plan a create instead of reconnecting to existing infrastructure.

Keep moved blocks in configuration until every environment that might still reference the old address has applied the migration at least once. Removing the block too early can strand stale state on addresses your configuration no longer declares.


Refactor for_each resource addresses

moved also works when a for_each resource changes its block label. In foreach-move/, start with:

hcl
resource "terraform_data" "nodes" {
  for_each = toset(["web", "api"])

  input = "foreach ${each.key}"
}

Apply, then rename the block to instances and add:

hcl
moved {
  from = terraform_data.nodes
  to   = terraform_data.instances
}

resource "terraform_data" "instances" {
  for_each = toset(["web", "api"])

  input = "foreach ${each.key}"
}

Plan reports one move per instance key — terraform_data.nodes["api"] to terraform_data.instances["api"], and the same for web — then settles on no changes. Individual key renames inside the map need explicit from and to addresses for each affected instance; see Terraform count and for_each for address syntax, not repetition here.


Common refactoring mistakes

Symptom Likely cause Fix
Destroy + create on a label rename Renamed block without moved Add moved { from = ... to = ... } and re-plan
Wrong resource replaced or move ignored Incorrect from or to address Match exact addresses from terraform state list
Real infrastructure deleted on refactor Deleted resource block without removed Use removed with destroy = false when objects should survive
removed still plans destroy lifecycle { destroy = true } or missing removed Set destroy = false explicitly when retention is required
Teammate still sees old address errors moved block removed before all envs migrated Keep moved until every workspace applied once
One machine fixed, others still broken Used state mv instead of moved Add declarative moved so CI and other environments get the same migration

The before-and-after plan evidence matters more than memorizing block syntax. For moved, confirm the plan reports the old address moving to the new address without replacement. For removed with destroy = false, confirm Terraform explicitly says the object will no longer be managed but will not be destroyed. The 0 to add, 0 to change, 0 to destroy summary counts infrastructure actions; applying a removed plan still changes Terraform state.


References


Summary

moved and removed blocks declare address changes in configuration so Terraform refactors without unnecessary destroy-and-create cycles. You renamed terraform_data.server to terraform_data.application and saw the plan flip from one add and one destroy to a recorded move with no infrastructure change. The module exercise moved terraform_data.example into module.application.terraform_data.example the same way — professional refactors often cross module boundaries, and moved carries the instance identity with them.

removed with lifecycle { destroy = false } stops management while leaving keep.txt on disk — the distinction from destroy and from bare configuration deletion is worth internalizing before you touch production state. Declarative blocks beat one-off CLI edits when every environment must apply the same migration; imperative state mv and state rm remain valid for surgical repairs, as the state commands lesson covers.

Keep moved blocks until all workspaces have applied the migration, quote indexed addresses carefully when debugging, and plan before apply whenever you change resource paths. Import and deeper module authoring live in sibling lessons on this course track.


Frequently Asked Questions

1. What does a Terraform moved block do?

A moved block tells Terraform that a resource instance changed address in configuration, such as a rename or a move into a module. On the next plan, Terraform updates state addressing instead of planning unnecessary destroy-and-create actions when the real object is unchanged.

2. What is the difference between moved and terraform state mv?

moved is declarative refactor metadata in your configuration that every environment applies on the next plan. terraform state mv is an imperative one-time CLI change to state. Prefer moved when the rename should live in version control history across teammates and environments.

3. What does a removed block with destroy false do?

A removed block stops Terraform from managing a resource address. With lifecycle destroy set to false, Terraform removes the object from state without calling the provider to delete the real infrastructure.

4. When should I delete a moved block from configuration?

Keep moved blocks until every environment that might still have the old address in state has applied the migration at least once. Removing the block too early can strand teammates or stale workspaces on addresses that no longer match configuration.

5. Does removed destroy the real infrastructure?

Only when lifecycle destroy is true or when you delete the resource block without a removed block and apply a destroy plan. With destroy false, Terraform forgets the binding and leaves the real object in place.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)