linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
Intro
Lessons
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Knowledge base
  • Cheat sheet
  • Capstone
  • Interview prep
home/terraform/kb/State/tf-state-manipulation

kb/state ── State ── intermediate

state mv, state rm, state pull/push: manual operations

`terraform state mv` renames a resource address in state without destroy/recreate. `terraform state rm` removes a resource from state but not from the cloud. `terraform state pull/push` downloads or uploads state as a file. All four are sharp operations; do them with a backup and a clear reason. For declarative alternatives, see [[tf-moved-block]] and [[tf-removed-block]].

view as markdownaka: terraform-state-mv, terraform-state-cli

When to touch state manually

Ideally, never. Terraform favors a declarative approach: you change HCL, run plan, see what will happen, then apply. Direct state operations are for cases where the declarative path does not work:

  • Renaming a resource without destroy and create (historically via state mv; with TF 1.1+ use a moved block, see tf-moved-block).
  • Removing a resource from Terraform management without deleting it in the cloud (with TF 1.7+ use a removed block, see tf-removed-block).
  • Emergency repair of a broken state: state pull, edit the JSON by hand, state push. Rare and risky; usually handled through provider support.
  • Merging two states into one using state mv with cross-state notation.

Declarative blocks (moved, removed, import) are the preferred path. They leave a trail in HCL and git and are applied automatically for every team member. CLI operations are invisible: only the person who ran them knows what happened.

terraform state list

The entry point: what is in state?

bash
terraform state list
# aws_s3_bucket.demo
# module.logs.aws_s3_bucket.this
# random_id.suffix

Filter by address:

bash
terraform state list 'module.logs.*'
# module.logs.aws_s3_bucket.this

Run this before any state mv/rm to confirm you are targeting the right resource.

terraform state show

Attributes of a specific resource:

bash
terraform state show aws_s3_bucket.demo

Prints an HCL-like form with all attributes, including sensitive ones (unlike terraform plan/apply, which masks them). This is why state must be protected as carefully as secrets.

terraform state mv

The most common manual operation. It changes a resource address in state without destroy.

Case 1: rename a resource

Before:

hcl
resource "aws_s3_bucket" "logs" { ... }

After:

hcl
resource "aws_s3_bucket" "log_storage" { ... }

Without state mv, Terraform sees "logs is gone from HCL, log_storage appeared" and does destroy+create. The bucket is destroyed along with its data.

With state mv:

bash
terraform state mv aws_s3_bucket.logs aws_s3_bucket.log_storage

The resource is now called log_storage in state. Update HCL at the same time. terraform plan → No changes.

Case 2: move into a module

A resource lives in the root module and you want to move it into ./modules/buckets.

bash
# first add to HCL:
# module "buckets" { source = "./modules/buckets" }
# inside the module: resource "aws_s3_bucket" "logs" { ... }
terraform state mv aws_s3_bucket.logs module.buckets.aws_s3_bucket.logs

plan → No changes. This is a typical refactoring scenario; see tf-refactor-patterns.

Case 3: between different states

You are splitting one root into two. Some resources migrate:

bash
# in the source root:
terraform state mv -state-out=../other/terraform.tfstate \
                   aws_s3_bucket.logs aws_s3_bucket.logs

This is a rare operation, typically done when splitting a monolith into services.

terraform state rm

Removes a resource from state. The cloud resource remains.

bash
terraform state rm aws_s3_bucket.demo

Terraform now "forgets" the bucket. The next apply (if the resource is still in HCL) will see "not in state, present in HCL", try to create it again, and fail with "bucket already exists". To recapture it, use import; see tf-state-import.

Use cases:

  • A resource ended up in the wrong state and should live in another. After state rm, run import in the correct state.
  • Switching from count = 1 to for_each. The addresses differ, so without help Terraform does destroy+create. state rm the old address and import the new one to avoid recreation. With TF 1.1+ a moved block is better.

-dry-run

bash
terraform state rm -dry-run aws_s3_bucket.demo

Shows what would happen, without making changes. Always run with -dry-run first, then repeat without it.

terraform state pull / push

An emergency mechanism. Download state as a file:

bash
terraform state pull > terraform.tfstate.dump

Works with any backend: local, S3, remote. Saves to the current directory.

Upload it back:

bash
terraform state push terraform.tfstate.dump

Terraform checks lineage and serial. If the serial is lower than the current one, it asks for confirmation (this is a potential rollback). If lineage differs, it refuses, protecting against replacing a state from another project.

Use this when:

  • You need to edit the JSON by hand (for example, a provider upgrade broke the state format). This is a last resort; usually terraform refresh or talking to the provider author is better.
  • Migrating state between backends by hand instead of init -migrate-state.
  • Creating a backup before a risky operation: state pull > backup-$(date).json.

Backup before any operation

Before any state mv/rm/push, take a dump first:

bash
terraform state pull > backup-pre-refactor-$(date +%s).json

An S3 backend with versioning does this automatically, but an extra copy does not hurt. With a local backend, it is mandatory and manual.

Pitfalls

  • state mv does not update HCL. If you rename in state, rename in HCL too. Otherwise the next plan shows destroy+create again.

  • state rm has no undo. There is genuinely no rollback. If the cloud resource matters, run state pull > backup.json first, then rm. If something goes wrong, state push backup.json.

  • With a remote backend, locking only works when you go through the Terraform CLI. state pull acquires a lock; state push does too. But if you copy the state file directly with aws s3 cp tfstate ./, there is no lock and you can conflict with someone else's apply.

  • state mv for count/for_each resources requires the exact address. Use aws_iam_user.user[0] or aws_iam_user.user["alice"], not aws_iam_user.user. A wrong index moves the wrong resource.

  • push will not work between states with different lineage. This is intentional protection. If you genuinely need it (for example, bootstrapping a new state by importing from an old one), add -force to push. But answer to yourself why first.

  • moved and removed blocks are better when applicable. They are declarative, visible in diffs, and run automatically for everyone. state mv/rm is a one-time manual act; others find out only when conflicts appear.

§ команды

bash
terraform state list

All resources under management. The starting point for any state operation.

bash
terraform state show <ADDRESS>

Resource attributes including sensitive ones. Use this to confirm what you are about to touch.

bash
terraform state mv <SRC> <DST>

Rename or move an address. Does not touch the cloud resource.

bash
terraform state rm -dry-run <ADDRESS>

Shows what rm would do. Always run this first, then repeat without -dry-run.

bash
terraform state pull > backup-$(date +%s).json

Backup state before a risky operation.

bash
terraform state push <FILE>

Upload state from a file. Checks lineage/serial. An emergency mechanism.

§ см. также

  • tf-stateState: Terraform's memory of what it createdState is the JSON file `terraform.tfstate` where Terraform records what it created in the cloud. Without it, Terraform would have no way to tell which bucket is "its own" and which belongs to something else. The file holds resource IDs, all attributes, and often secrets. It is the most sensitive part of any project.
  • tf-moved-blockThe moved block: rename without destroy`moved { from = ..., to = ... }` in HCL declaratively tells Terraform: "this resource used to live at one address and now lives at another, the cloud object is the same." The plan shows a "move", not a "destroy + create". It arrived in TF 1.1. It replaces the manual `terraform state mv`, leaves a trace in git, repeats for everyone on the team, and shows up in the diff.
  • tf-removed-blockremoved block: drop a resource from state, keep it in the cloud`removed { from = ..., lifecycle { destroy = false } }` tells Terraform declaratively: remove this resource from management, but do not touch it in the cloud. The block was introduced in TF 1.7 and replaces the manual `terraform state rm` command. With `destroy = true` it behaves like an ordinary resource deletion from HCL.
  • tf-remote-backend-s3Remote state in S3: bucket, DynamoDB lock, encryptionS3 backend stores `terraform.tfstate` in a bucket. A DynamoDB table provides locking so only one apply runs at a time. Configuration goes in the `backend "s3"` block inside `terraform { ... }`. State lives in S3. It is the single source of truth; there is no local file anymore. Migrate from local to S3 with `terraform init -migrate-state`.
  • tf-refactor-patternsRefactoring patterns: count to for_each, split files, extract moduleLarge configs turn into spaghetti. The core refactoring patterns are: count to for_each (stable keys), splitting files by domain (network/compute/storage), extracting a repeated block into a module, merging small resources into a composite one, and removing dead imports. Each pattern is covered step by step, with a `plan` check at every step.
Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies