State Operations: import Block, state mv, state rm

K
Kai··6 min read

State is now safe on S3, but working with real infrastructure throws up situations that apply alone can't solve. You have a bucket built by hand earlier and now want Terraform to manage it. You rename a resource in code and don't want Terraform to delete-and-recreate it. You want to stop managing a resource but keep it alive. All three operate directly on state, and this article does all three on a real bucket.

Goal

Master three common state operations: the import block to take over an existing resource (with auto-generated configuration), state mv to rename/move a resource within state, and state rm to remove a resource from management without touching the real infrastructure.

The problem: existing infrastructure not yet managed by Terraform

You rarely start a project with empty infrastructure. There's usually something already built by hand: a bucket, a VPC, a security group someone created from the console last year. Terraform doesn't know they exist because they aren't in state. To bring them under management without deleting and recreating them (not possible with running infrastructure), we use import.

To simulate this, create a bucket with the AWS CLI and treat it as pre-existing legacy:

$ aws s3api create-bucket --bucket tf-series-bai7-preexisting-1779678443 \
    --region ap-southeast-1 \
    --create-bucket-configuration LocationConstraint=ap-southeast-1

import block: declare instead of typing a command

Before Terraform 1.5, import was a manual command (terraform import address id) and you had to write the resource block to match the real infrastructure yourself — get it slightly wrong and apply demanded fixes. Since 1.5 there's config-driven import: declare the intent with an import block right in the configuration and let Terraform handle the rest.

import {
  to = aws_s3_bucket.adopted
  id = "tf-series-bai7-preexisting-1779678443"
}

to is the resource address it'll carry in the configuration, id is the real identifier on AWS (for an S3 bucket the id is the bucket name). Notice: we haven't written any resource "aws_s3_bucket" "adopted" block yet. That's where automatic configuration generation comes in.

Auto-generating configuration with -generate-config-out

Per the docs, "you can write only the import block and then run terraform plan with the generate-config-out flag to have Terraform generate the resource blocks." Run:

$ terraform plan -generate-config-out=generated.tf
aws_s3_bucket.adopted: Preparing import... [id=tf-series-bai7-preexisting-1779678443]
aws_s3_bucket.adopted: Refreshing state... [id=tf-series-bai7-preexisting-1779678443]

  # aws_s3_bucket.adopted will be imported
  # (config will be generated)
    resource "aws_s3_bucket" "adopted" {
        arn    = "arn:aws:s3:::tf-series-bai7-preexisting-1779678443"
        bucket = "tf-series-bai7-preexisting-1779678443"
        ...
    }

Terraform reads the real resource on AWS and writes the corresponding configuration to a generated.tf file:

# __generated__ by Terraform
# Please review these resources and move them into your main configuration files.
# __generated__ by Terraform from "tf-series-bai7-preexisting-1779678443"
resource "aws_s3_bucket" "adopted" {
  bucket              = "tf-series-bai7-preexisting-1779678443"
  bucket_namespace    = "global"
  force_destroy       = false
  object_lock_enabled = false
  region              = "ap-southeast-1"
  tags                = {}
  tags_all            = {}
}

The comment at the top of the file states exactly what to do: review them and merge into your main configuration. Auto-generated configuration isn't always perfect (it may carry extra default fields, or miss a structure you wanted), so it's a starting point you clean up, not the final version. But for complex infrastructure, having Terraform write the draft is far faster than typing it from scratch.

apply: perform the import

With the configuration in place, apply brings the resource into state:

$ terraform apply -auto-approve
aws_s3_bucket.adopted: Importing... [id=tf-series-bai7-preexisting-1779678443]
aws_s3_bucket.adopted: Import complete [id=tf-series-bai7-preexisting-1779678443]
Apply complete! Resources: 1 imported, 0 added, 0 changed, 0 destroyed.

$ terraform state list
aws_s3_bucket.adopted

The summary line reads 1 imported, 0 added — nothing new created, just the existing bucket brought under management. From now on Terraform treats this bucket as its own. After the import is done, you can delete the import block (it's served its purpose); leaving it is fine too, since Terraform sees the resource is already in state and skips it. To import many resources at once, write multiple import blocks and import them in a single apply.

state mv: rename without breaking infrastructure

Suppose you realize the name adopted doesn't fit and want to change it to data. If you just change the name in code and apply, Terraform sees adopted disappear and data appear, and misreads it as "delete the old one, create a new one" — for a bucket holding data, that's a disaster. state mv changes the mapping in state so the same real resource now carries the new address, deleting and creating nothing.

$ terraform state mv aws_s3_bucket.adopted aws_s3_bucket.data
Move "aws_s3_bucket.adopted" to "aws_s3_bucket.data"
Successfully moved 1 object(s).

$ terraform state list
aws_s3_bucket.data

The docs describe this command as changing "the binding in state so an existing remote object is associated with a new resource instance." Use it when renaming a resource, or when grouping resources into a module (Part IV). Note: state mv only changes state — you still have to rename it in code to match. Article 16 will introduce the moved block — a way to declare the rename right in the configuration, cleaner than typing state mv by hand.

state rm: stop managing, leave infrastructure intact

Sometimes you want Terraform to forget a resource without deleting it — for example to split that resource off into a different configuration to manage. state rm removes it from state:

$ terraform state rm aws_s3_bucket.data
Removed aws_s3_bucket.data
Successfully removed 1 resource instance(s).

$ terraform state list
$              # empty — Terraform no longer manages anything

What about the real bucket? Still there, intact:

$ aws s3api head-bucket --bucket tf-series-bai7-preexisting-1779678443
{
    "BucketArn": "arn:aws:s3:::tf-series-bai7-preexisting-1779678443",
    "BucketRegion": "ap-southeast-1",
    "AccessPointAlias": false
}

This is the core difference between state rm and destroy: destroy deletes the real infrastructure, state rm only cuts the thread between Terraform and the resource. After state rm, the resource becomes "unmanaged" from Terraform's point of view — you're responsible for it (re-import it elsewhere, or delete it by hand). Article 16 also covers the removed block, a way to declare this remove-from-state operation in the configuration instead of a manual command.

🧹 Cleanup

Since we ran state rm, Terraform no longer manages the bucket — terraform destroy won't touch it. You have to delete it by hand:

$ aws s3 rb s3://tf-series-bai7-preexisting-1779678443 --force
remove_bucket: tf-series-bai7-preexisting-1779678443

This is also a practical lesson: state rm leaves orphaned resources that you can easily forget, quietly costing money. Use it deliberately, and remember what you just split off.

Wrap-up

Three state operations for three different needs. The import block (1.5) brings existing infrastructure under management by declaring to/id, combined with plan -generate-config-out to have Terraform write a configuration draft for you. state mv changes a resource's address in state without deleting and recreating it, used when renaming or grouping into a module. state rm cuts a resource from management but keeps it really existing, easily leaving orphaned resources behind, so handle with care. All three touch state directly, so always do them once state is locked and backed up (versioning from Article 6 helps here).

State holding secrets is something we've mentioned repeatedly without addressing. Article 8 closes Part II with exactly that problem: how to pass passwords and API keys into Terraform without letting them land in state or the plan — using sensitive, and more powerfully, ephemeral resources together with write-only arguments, features that arrived in recent Terraform releases.