terrakit

terrakit

Terraform state mv and state rm

terraform state mv files an existing object under a new address. terraform state rm drops the entry and leaves the object running, unmanaged. Both write to the state directly, the moment you press return, with nothing in the repository to show it happened. Since Terraform 1.1 a moved block does the first job as code, and since 1.7 a removed block does the second. Use the blocks unless the move crosses a state boundary, which is the one thing no configuration can express.

The two commands, and the two blocks that replace them in a pull request:

terraform state mv aws_db_instance.db aws_db_instance.database
terraform state rm aws_db_instance.db
Terraform
moved {
  from = aws_db_instance.db
  to   = aws_db_instance.database
}
Terraform
removed {
  from = aws_db_instance.db

  lifecycle {
    destroy = false
  }
}

What terraform state mv does

It takes a source address and a destination address, and rewrites one entry in the state file so the same real object is filed under the new one. No provider is called and nothing in your cloud account is touched.

terraform state mv
Move "aws_db_instance.db" to "aws_db_instance.database"
Successfully moved 1 object(s).

What changes is which configuration block Terraform matches that object against on the next plan. That is the whole mechanism, and it is also the trap: the command moves the state entry and does nothing to your configuration. Move the entry without renaming the resource in the code and the next plan reverses you, because now the new address is in the state with no configuration and the old one is in the configuration with no state:

terraform plan, abridged
  # aws_db_instance.database will be destroyed
  # (because aws_db_instance.database is not in configuration)
  - resource "aws_db_instance" "database" {
      ...
    }

  # aws_db_instance.db will be created
  + resource "aws_db_instance" "db" {
      ...
    }

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

Two properties of the command are worth having in your head before you run it. It writes a backup copy of the state before it saves anything and the backup cannot be disabled, which is Terraform being honest about how dangerous the command is. And it takes -dry-run, which prints what would move without moving anything, and which costs nothing to use.

What terraform state rm does

It deletes an entry from the state. The object it referred to is untouched: still running in your cloud account, still being paid for, and now managed by nobody.

terraform state rm
Removed aws_db_instance.db
Successfully removed 1 resource instance(s).

The part people miss is the next plan. The resource is still in your configuration, and there is no longer a state entry matching it, so Terraform does the only thing it can and plans to create one:

terraform plan, abridged
  # aws_db_instance.db will be created
  + resource "aws_db_instance" "db" {
      ...
    }

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

Apply that and you have two. On a database instance that is an expensive surprise; on anything with a unique name it fails at apply, halfway through, which is the better outcome of the two. Deleting the resource from the configuration in the same change avoids it, and leaves you with an object nothing is tracking.

The command is blunter than it looks in two more ways. An address with count or for_each removes every instance of that resource, not one. A module address removes everything in that module and in every module underneath it. Both of those are documented and neither is obvious at three in the morning, which is when this command gets typed. It also takes -dry-run.

Choosing between a command and a block

There are four options and they are a two-by-two: keep the object or forget it, as a code change or as state surgery.

 Where it livesWho it runs forThe real objectRun it twice
moved blockThe configuration, in the pull requestEveryone, on their next plan, in every workspaceUntouched, under a new addressHarmless, because the second run has nothing to move
terraform state mvOne person's shell historyWhoever ran it, against the one state they pointed atUntouched, under a new addressErrors, because the source address is gone
removed blockThe configuration, in the pull requestEveryone, on their next plan, in every workspaceLeft running, unmanagedHarmless, because the entry is already gone
terraform state rmOne person's shell historyWhoever ran it, against the one state they pointed atLeft running, unmanagedErrors, because the address is no longer in state

The column that decides it is the second one. A state command runs once, for one person, against one state. A block runs for everyone, on their next plan, without anybody being told to run anything, and it is in the diff beside the change it belongs to where a reviewer can object to it. A rename is a code change; so is deciding to stop managing something. Neither is an operation on a file.

The removed block is the newer half of that and the less well known. It takes a from address and a lifecycle block, and with destroy = false it forgets the object rather than destroying it. What it adds over terraform state rm is that it says so out loud, in the plan, before anything happens:

terraform plan, abridged
  # aws_db_instance.db will no longer be managed by Terraform, but will not be destroyed
  # (destroy = false is set in the configuration)
  . resource "aws_db_instance" "db" {
        ...
    }

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 delete them:
 - aws_db_instance.db

A warning naming every object about to stop being managed is exactly what terraform state rm does not give you, and it is the reason to prefer the block even when you are the only person who will ever run it.

Why a rename needs any of this at all, and where a moved block goes, is the subject of the moved block page.

Check the plan before you trust it

A state command that reports success has told you it did what you typed. It has told you nothing about whether the state is now correct, and those are different questions. The next plan is the test, and it should be run before anybody applies anything.

What a correct plan looks like after a move. Nothing. The object is the same object, the configuration describes it the same way, and the only thing that changed was the address it is filed under:

terraform plan
No changes. Your infrastructure matches the configuration.

If the change also altered an attribute, an update in place is fine too. What must not be there is a destroy.

What a still-wrong plan looks like. A destroy and a create of the same resource type, hundreds of lines apart, as in the output further up this page. That means the state and the configuration disagree about the address: either the entry moved and the code did not follow, or the code was renamed and the entry was not.

If a destroy is still there, work through these in order:

  • Compare the two addresses character by character. Copy them out of the plan rather than retyping them. An instance key is part of the address, so this[0] and this["app"] are different objects and look nearly identical in a diff.
  • Check you are looking at the right state. terraform state list prints every address the current workspace knows about. A move applied to the wrong workspace reports success and changes nothing that matters.
  • Move it back. The command is its own inverse: run it with the source and destination the other way round, then plan again. The state backup it wrote is beside the current state if you need to go further back than that.
  • Do not apply to see what happens. The destroy in that plan is real. Nothing about a plan is a rehearsal once it is applied.

The one case that still needs state mv

A moved block names two addresses inside one configuration. It cannot express a move between two state files, because the second address is not in the configuration doing the moving. So splitting one root module into two, or merging two into one, is still the command's job and there is no block that competes for it.

It is also the case that deserves the most care, because the options that name a source and a destination state file are legacy and work with the local backend only. Between two remote states the route is terraform state pull for each, the work on the local copies, and terraform state push back, which is as dangerous as it sounds. The command writes a backup for each state file involved, and those backups are worth keeping until both configurations have planned clean.

The ways this goes wrong

Moving the entry and forgetting the code

The commonest one, and the plan above is what it looks like. The state command and the code change are two halves of one job, and only the block keeps them together.

Moving onto an address that is already occupied

Terraform refuses, which makes this the cheapest failure on the page:

terraform state mv
Error: Invalid target address

Cannot move to aws_db_instance.database: there is already a resource
instance at that address in the current state.

Removing from state and applying anyway

A second object, built beside the one you told Terraform to forget. Where the resource type enforces a unique name it fails at apply instead, partway through the change.

Using state rm as an undo

It is not one. Removing an entry does not remove the object, so a state rm run to get out of a bad apply leaves the bad object in place and Terraform no longer aware of it. The next plan then proposes to build its replacement alongside.

Pointing a move at the wrong resource

The expensive one, and it is the same failure a wrong moved block causes. Terraform believes the addresses you gave it: the old object's state entry is filed under the new address, and the next plan compares your configuration against whatever real object that entry pointed at. You have adopted one resource's state under another resource's name, which is worse than the destroy you were avoiding, because the destroy would at least have been in the plan.

Reading a plan somebody else made

None of this is visible in a diff. A state command leaves no trace in the repository at all, and a rename that should have carried a moved block looks in the plan like an unrelated destroy and an unrelated create, hundreds of lines apart, neither of them resembling the other.

terrakit reads the plan JSON and pairs them up:

terrakit plan.json
terrakit  2 findings  terraform 1.16.1
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

HIGH ──────────────────────────────────────────────────────────────────────  1

  azurerm_subnet.app
  destroy
  └ possible missed moved block
      5 of 5 attributes match azurerm_subnet.application
      moved { from = azurerm_subnet.app  to = azurerm_subnet.application }
      verify the pairing before using that block

INFO ──────────────────────────────────────────────────────────────────────  1

  azurerm_subnet.application
  create
  ├ these values are not known until apply, so no claim about them can be
  │ checked in review
  │   etag
  │   id
  └ possible missed moved block
      paired with azurerm_subnet.app, shown above

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1 high  1 info

It compares the top-level attributes of every delete against every create of the same resource type, needs at least three comparable attributes and an 80 per cent match, and prints the count it matched over the count it compared so you can judge the pairing yourself. The suggested block is marked as needing verification, and that is not a formality: pasting a wrong pairing is the last failure in the list above. The rules and their edges are on the moved block page.

A resource being forgotten rather than destroyed has its own line in that report, because the two are not the same event and a plan summary counts neither of them as a change.

Questions about state mv and state rm

What is the difference between terraform state mv and a moved block?
They end with the same state entry under a new address. A moved block is configuration: it is reviewed in the pull request, versioned with the change it belongs to, and applied by everyone on their next plan, in every workspace. terraform state mv is one command, run once, by one person, against the one state they pointed at, needing direct write access to it and leaving nothing in the repository to show it happened.
What does terraform state rm do to the real resource?
Nothing. It removes the entry from state and leaves the object running in your cloud account, unmanaged and still being paid for. The part people miss is what the next plan does: with the resource still in your configuration and no state entry to match it, Terraform plans to create a second one. Delete the configuration too and the plan is clean, and the object is orphaned with nothing tracking it.
When do I still need terraform state mv?
When the move crosses a state boundary. A moved block names two addresses inside one configuration, so it cannot express moving an object from one state file to another, which is what splitting a root module in two or merging two into one comes down to. That case is still the command's, and it is the only one where a block is not the better answer.
How do I undo a terraform state mv?
Move it back, with the source and destination the other way round. The command also writes a state backup before it saves anything and the backup cannot be disabled, so the previous state is on disk beside the current one if the move went somewhere you did not expect. Read the next plan either way: a state that is correct plans nothing.
What is a removed block, and is it the same as terraform state rm?
A removed block, added in Terraform 1.7, drops an object from state as a code change rather than as a command. With destroy set to false in its lifecycle block, the plan says the object will no longer be managed by Terraform but will not be destroyed, and it raises a warning naming everything it is about to forget. That is the same outcome as terraform state rm, arriving in a pull request where somebody can object to it.
Does terraform state mv change anything in my cloud account?
No. It rewrites an entry in the state file and makes no API call to any provider. What it changes is which configuration block Terraform matches that object against on the next plan, which is why moving the entry without renaming the resource in the configuration produces a plan that destroys the new address and creates the old one.

terrakit uses Google Analytics to count how many people read these pages. No cookie is set unless you accept, and every page works exactly the same either way.