Remove a Terraform Resource Without Destroying It

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
hashicorp/local 2.9.0
kreuzwerker/docker 3.9.0
Applies to Any host with Terraform installed
Lab environment Single Ubuntu VM with Terraform and Docker — Terraform lab environment on Ubuntu
Privilege Normal user
Scope Removing Terraform management without destroying real infrastructure — default destroy when a resource block disappears, removed block with lifecycle destroy false, terraform state rm, independent verification outside Terraform, behavior when configuration still declares the resource, removed versus state rm comparison, Docker import handoff to another root module, and common safety mistakes. Does not cover manual tfstate JSON editing, full state disaster recovery, or deleting state files as a fix.
Related guides Terraform moved and removed blocks
terraform state commands
Terraform import
Terraform state explained
Terraform troubleshooting

Deleting a resource block and running terraform apply tells Terraform to destroy whatever that address tracked. That is correct when you want the provider to delete a file, volume, or cloud object. It is wrong when the real infrastructure should survive and only Terraform should stop managing it.

text
Delete block + apply     → provider destroy (default)
removed { destroy=false } → forget in state, object stays
terraform state rm       → forget in state immediately, object stays

Each scenario uses its own directory under ~/terraform-labs/terraform-remove-resource-without-destroy/. The lab verifies files with cat and sha256sum, and a Docker volume with docker volume inspect — not only Terraform output.

IMPORTANT
Never delete terraform.tfstate or remote state objects to “forget” one resource. That removes tracking for the entire workspace and does not call provider APIs safely. Use removed, terraform state rm on a single address, or a documented import and migration path instead.
NOTE
Run terraform init in each lab directory. For declarative refactor context, see Terraform moved and removed blocks. For imperative state edits, see terraform state commands.

Why removing the resource block plans a destroy

Terraform reconciles configuration with state. When an address exists in state but no longer appears in configuration, the default action is destroy — Terraform asks the provider to delete the real object.

bash
mkdir -p ~/terraform-labs/terraform-remove-resource-without-destroy/demos/why-destroy/generated
cd ~/terraform-labs/terraform-remove-resource-without-destroy/demos/why-destroy
bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"

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

resource "local_file" "keep" {
  filename = "${path.module}/generated/keep-me.txt"
  content  = "why-destroy-lab-distinctive-content-v1"
}
EOF

Initialize and apply so the file exists on disk and in state:

bash
terraform init -input=false
bash
terraform apply -auto-approve -input=false -no-color

Confirm the bytes landed outside Terraform with cat and a checksum:

bash
cat ~/terraform-labs/terraform-remove-resource-without-destroy/demos/why-destroy/generated/keep-me.txt
output
why-destroy-lab-distinctive-content-v1
bash
sha256sum ~/terraform-labs/terraform-remove-resource-without-destroy/demos/why-destroy/generated/keep-me.txt
output
97e1a131892d18b73b8f3771dc680715c89b3a49e6a76c06d8b190c693fc6900  .../keep-me.txt

With the resource block still present, plan reports no changes. Remove the resource "local_file" "keep" block — keep only the terraform block:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"

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

Plan again:

bash
terraform plan -no-color -input=false
output
# local_file.keep will be destroyed
  # (because local_file.keep is not in configuration)
  - resource "local_file" "keep" {
      - filename = "./generated/keep-me.txt" -> null
      ...
    }

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

Terraform plans to delete the file through the local provider because the address left configuration without a removed block or prior state rm. Do not apply this plan if you want the file to remain — use one of the methods below instead.


Method 1: removed block with destroy = false

Terraform 1.7+ supports a declarative removed block that records “stop managing this address” in configuration. Set lifecycle { destroy = false } so apply forgets the binding without calling the provider delete API.

bash
mkdir -p ~/terraform-labs/terraform-remove-resource-without-destroy/demos/removed-block/generated
cd ~/terraform-labs/terraform-remove-resource-without-destroy/demos/removed-block

Create the resource first:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"

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

resource "local_file" "persist" {
  filename = "${path.module}/generated/persist-removed.txt"
  content  = "removed-block-lab"
}
EOF

Initialize and apply:

bash
terraform init -input=false
bash
terraform apply -auto-approve -input=false -no-color

Replace the resource block with a removed block:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"

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

removed {
  from = local_file.persist

  lifecycle {
    destroy = false
  }
}
EOF

Plan shows forget-without-destroy — not a provider delete:

bash
terraform plan -no-color -input=false
output
# local_file.persist will no longer be managed by Terraform, but will not be destroyed
 # (destroy = false is set in the configuration)

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

Warning: Some objects will no longer be managed by Terraform
 - local_file.persist

Apply drops the address from state:

bash
terraform apply -auto-approve -input=false -no-color

After apply, terraform state list is empty for this demo — Terraform no longer tracks local_file.persist.


Method 2: terraform state rm

terraform state rm ADDRESS removes one resource instance from state immediately. It does not run a plan and does not call the provider destroy API.

bash
mkdir -p ~/terraform-labs/terraform-remove-resource-without-destroy/demos/state-rm/generated
cd ~/terraform-labs/terraform-remove-resource-without-destroy/demos/state-rm
bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"

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

resource "local_file" "persist" {
  filename = "${path.module}/generated/persist-rm.txt"
  content  = "state-rm-lab"
}
EOF

Initialize and apply:

bash
terraform init -input=false
bash
terraform apply -auto-approve -input=false -no-color

Forget the address while leaving the resource block in configuration:

bash
terraform state rm local_file.persist
output
Removed local_file.persist
Successfully removed 1 resource instance(s).

terraform state list prints nothing — the binding is gone. The HCL block is still present, which matters for the next plan.


Verify the real resource still exists

Terraform saying “removed from state” is not enough for production handoffs. Verify the object independently.

After the removed block apply, read the file directly:

bash
cat ~/terraform-labs/terraform-remove-resource-without-destroy/demos/removed-block/generated/persist-removed.txt
output
removed-block-lab
bash
sha256sum ~/terraform-labs/terraform-remove-resource-without-destroy/demos/removed-block/generated/persist-removed.txt
output
26a5c306f3b0a9b4dfc39533ef090b2814f6da46f4ad386110f0cf42268d0fdd  .../persist-removed.txt

After state rm, the state-rm file keeps the same content:

bash
cat ~/terraform-labs/terraform-remove-resource-without-destroy/demos/state-rm/generated/persist-rm.txt
output
state-rm-lab

For provider-backed infrastructure, create a Docker volume, run terraform state rm, then inspect outside Terraform:

bash
mkdir -p ~/terraform-labs/terraform-remove-resource-without-destroy/demos/docker-volume-rm
cd ~/terraform-labs/terraform-remove-resource-without-destroy/demos/docker-volume-rm
bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"

  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

resource "docker_volume" "lab" {
  name = "tf-forget-lab"
}
EOF
bash
terraform init -input=false
bash
terraform apply -auto-approve -input=false -no-color
bash
terraform state rm docker_volume.lab
bash
docker volume inspect tf-forget-lab
output
[
    {
        "Name": "tf-forget-lab",
        "Driver": "local",
        "Mountpoint": "/var/lib/docker/volumes/tf-forget-lab/_data",
        ...
    }
]

The volume survived state rm. Destroy it during cleanup when you finish the lab.


What happens when configuration still contains the resource

Forgetting state does not remove the resource block from HCL. If the block remains, Terraform sees an untracked resource and plans to create it.

After state rm in demos/state-rm/:

bash
terraform plan -no-color -input=false
output
# local_file.persist will be created
  + resource "local_file" "persist" {
      + filename = "./generated/persist-rm.txt"
      + content  = "state-rm-lab"
    }

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

Terraform does not automatically reconnect to the existing file — it plans a fresh create. Applying that plan invokes the provider's create behavior. For local_file with the same path and content, the visible bytes may remain unchanged, but this is not import and should not be generalized as an adoption technique. Cloud resources can fail on name conflicts or create duplicates.

Remove the resource block after state rm so configuration matches the forgotten state. The config-cleanup demo shows the end state:

bash
mkdir -p ~/terraform-labs/terraform-remove-resource-without-destroy/demos/config-cleanup/generated
cd ~/terraform-labs/terraform-remove-resource-without-destroy/demos/config-cleanup
bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"

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

resource "local_file" "persist" {
  filename = "${path.module}/generated/persist-rm.txt"
  content  = "state-rm-lab"
}
EOF
bash
terraform init -input=false
bash
terraform apply -auto-approve -input=false -no-color
bash
terraform state rm local_file.persist

Remove the resource block, leaving only the terraform block:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"

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

Plan reports no changes while the file remains on disk:

bash
terraform plan -no-color -input=false
output
No changes. Your infrastructure matches the configuration.

The forgotten file is still readable:

bash
cat ~/terraform-labs/terraform-remove-resource-without-destroy/demos/config-cleanup/generated/persist-rm.txt
output
state-rm-lab

Compare removed block and state rm

removed { lifecycle { destroy = false } } terraform state rm ADDRESS
Where it lives Configuration in Git One-off CLI
Runs on terraform plan / apply Immediately
Team visibility Same as other HCL changes Easy to omit from runbooks
Provider destroy API Not called when destroy = false Not called
After success Remove removed block once all envs applied Delete or comment the resource block
Best for Declarative “stop managing” in version control Emergency repair when an immediate imperative state operation is required

Both leave real infrastructure in place when used correctly. Prefer removed { lifecycle { destroy = false } } for planned changes because Terraform can preview the state removal before apply and the intent stays in version control. Reserve terraform state rm for cases where an immediate imperative state operation is specifically required and you already have a state backup.

Default destroy on a removed block is true — omitting destroy = false schedules a provider delete. Always set destroy = false explicitly when infrastructure must survive.


Move a resource to another Terraform configuration

Handoff is a two-root problem: stop managing in configuration A, start managing in configuration B, without deleting the real object.

The lab confirmed that local_file does not implement Terraform import. That makes it a poor example of a true cross-root adoption workflow. If the target root declares the same local_file after the source runs state rm, Terraform still sees an untracked resource and plans a create. Applying that plan invokes the provider's create behavior; matching filename and content may leave the file contents unchanged, but this is not import and should not be generalized as an adoption technique.

For a real handoff, use a resource that supports import. The Docker volume lab below demonstrates the sequence you can generalize to cloud resources:

  1. Source root manages the Docker volume
  2. Verify the volume exists outside Terraform
  3. Source runs terraform state rm (or a removed block with destroy = false)
  4. Target root declares docker_volume and an import block
  5. Target plan confirms import; apply records the existing object in target state

Source root — create and forget the volume binding:

bash
mkdir -p ~/terraform-labs/terraform-remove-resource-without-destroy/demos/handoff-new-root/source
cd ~/terraform-labs/terraform-remove-resource-without-destroy/demos/handoff-new-root/source
bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"

  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

resource "docker_volume" "lab" {
  name = "tf-handoff-lab"
}
EOF
bash
terraform init -input=false
bash
terraform apply -auto-approve -input=false -no-color
bash
docker volume inspect tf-handoff-lab
bash
terraform state rm docker_volume.lab

Target root — import the existing volume into a new state file:

bash
mkdir -p ~/terraform-labs/terraform-remove-resource-without-destroy/demos/handoff-new-root/target
cd ~/terraform-labs/terraform-remove-resource-without-destroy/demos/handoff-new-root/target
bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"

  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

import {
  to = docker_volume.lab
  id = "tf-handoff-lab"
}

resource "docker_volume" "lab" {
  name = "tf-handoff-lab"
}
EOF
bash
terraform init -input=false
bash
terraform plan -no-color -input=false
output
# docker_volume.lab will be imported
    resource "docker_volume" "lab" {
        id   = "tf-handoff-lab"
        name = "tf-handoff-lab"
    }

Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
bash
terraform apply -auto-approve -input=false -no-color
output
Apply complete! Resources: 1 imported, 0 added, 0 changed, 0 destroyed.

A follow-up plan reports no drift — the target root now manages the existing volume. Destroy the volume from the target root when you finish the lab.

Never copy or delete whole terraform.tfstate files to split workspaces — use per-address state rm, import, or a controlled state migration when moving backends.


Common safety mistakes

Mistake Why it hurts Safer approach
Deleting terraform.tfstate Loses all resource tracking in that workspace state rm one address or removed with destroy = false
Removing HCL without removed / state rm Next apply destroys real infrastructure Add removed { destroy = false } before deleting the block, or state rm then remove block
state rm but leaving the resource block Plan wants to create again; risk of duplicates Remove the block or re-import where supported
Assuming apply after state rm reconnects Terraform plans create, not adopt Import in target root when the provider supports it
Skipping independent verification State says “gone” while object still exists — or vice versa after partial failure cat, cloud console, docker volume inspect, etc.
removed without destroy = false Default destroys through the provider Set destroy = false explicitly
No state backup before state rm Hard to undo a wrong address terraform state pull first — see terraform state commands

References


Summary

You saw the default path first: delete a resource block without migration metadata and Terraform plans a destroy. That is the behavior you are overriding when real infrastructure must outlive Terraform management.

The two supported forget paths are declarative and imperative. A removed block with lifecycle { destroy = false } records the decision in Git and drops the address on apply without calling the provider delete API. terraform state rm does the same for one address immediately from the CLI. In both cases you verified files with cat and sha256sum, and a Docker volume with docker volume inspect, because Terraform output alone is not proof the object survived.

If the resource block stays in configuration after state rm, the next plan proposes create — remove the block or import into a new root when the provider supports it. A Docker volume handoff with an import block is the generalizable pattern; local_file is not importable and must not be treated as a cross-root adoption example. Do not delete terraform.tfstate to fix a single resource; use address-level tools and backups instead. For broader refactor patterns, continue with Terraform moved and removed blocks and terraform state commands.


Frequently Asked Questions

1. What is the difference between terraform state rm and a removed block?

terraform state rm is an imperative CLI command that drops one address from state immediately without calling the provider. A removed block is declarative configuration that records the same intent in Git and runs on the next plan and apply. Both can leave real infrastructure in place when destroy is false or when the provider is never asked to delete.

2. Does terraform state rm delete the real resource?

No. state rm only removes the binding between a Terraform address and the object in state. The provider destroy API is not called. Always verify the real object independently with provider tools, the cloud console, or filesystem checks.

3. What happens if I remove a resource from state but leave the block in configuration?

Terraform no longer tracks the object but still sees the resource block in configuration. The next plan typically proposes creating the resource again. Remove or comment the block, add a removed block, or re-import if the provider supports import.

4. Should I delete terraform.tfstate to stop managing a resource?

No. Deleting state files loses tracking for every resource in that workspace, invites duplicate creates, and does not cleanly hand off infrastructure. Use removed with destroy false, terraform state rm on one address, or a proper import and state migration instead.
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)