Terraform & Infrastructure as Code›02 · State management
Learning Hub / Delivery & Infrastructure as Code / Terraform & Infrastructure as Code

Lesson 02 of 7 · Modules

State management

Understand Terraform state and change it safely: what it stores, local vs remote backends, locking, adopting existing resources with import blocks, refactoring with moved blocks, and letting go with removed blocks.

Practitioner
Key wordsstatebackendslockingimport blocksmoved blocksremoved blocksrefresh-onlysensitive data

What state is

State is Terraform's record of which real object belongs to each resource address, plus their last-known attributes:

aws_s3_bucket.logs  →  bucket "acme-logs" in eu-west-1, arn …, tags …

Terraform needs it to know what it manages, to compute diffs, and to handle dependencies on deletion. It lives in a backend: a local file by default, or remote (S3, GCS, Azure Blob, Terraform Cloud, Kubernetes…) for teams.

State is Terraform's address book: "the bucket I call logs is the one at acme-logs". If you rename a friend in your address book without saying it's the same person, you'd think one friend left and a stranger arrived. moved blocks say "same friend, new name". import blocks say "add this person I already know to my book".

Backends and locking

For anything shared, use a remote backend with locking (see Amazon EKS in Production, lesson 02, for the S3 setup). Locking stops two runs from writing state at the same time. Never commit state files to Git.

Refactor without destroying: moved

# before: resource "aws_s3_bucket" "old" { … }
resource "aws_s3_bucket" "logs" {
  bucket = "acme-logs"
}

moved {
  from = aws_s3_bucket.old
  to   = aws_s3_bucket.logs
}

plan now shows has moved to, with no destroy or create. It also works for moving resources into modules (to = module.logging.aws_s3_bucket.this).

Adopt existing resources: import

import {
  to = aws_s3_bucket.legacy
  id = "acme-legacy-reports"
}

resource "aws_s3_bucket" "legacy" {
  bucket = "acme-legacy-reports"
}

Or let Terraform write a starting configuration for you:

$ terraform plan -generate-config-out=generated.tf

Review and tidy the generated code, then apply. The resource is managed from now on.

Let go without deleting: removed

removed {
  from = aws_s3_bucket.legacy
  lifecycle {
    destroy = false          # forget it, keep the real bucket
  }
}

Useful when handing a resource to another team's state, or back to manual management.

Drift and refresh-only

terraform plan -refresh-only shows what changed outside Terraform, without proposing to change it back. Apply a refresh-only plan to accept reality into state, or run a normal plan to revert reality to code.

State surgery (last resort)

terraform state mv, state rm and state push still exist. Use them only when code blocks can't express the change, and always:

  1. terraform state pull > backup.tfstate first,
  2. hold the lock / pause pipelines,
  3. finish with a clean plan.

Try it: refactor safely (free, local)

With the random/local configuration from lesson 01:

  1. Rename local_file.inventory to local_file.hosts without a moved block, and read the plan (destroy + create). Then add the moved block and plan again.
  2. Use a removed block with destroy = false for local_file.hosts; apply, and confirm the file still exists but is no longer in terraform state list.
  3. Run terraform state list and state show at each step.
  4. (Sandbox cloud account) Create an S3 bucket in the console, adopt it with an import block plus terraform plan -generate-config-out=generated.tf, tidy the generated code, and apply. The plan should show 1 to import, 0 to add.

Going deeper: state at scale

  • Split state along ownership and change rate: network (rarely changes), clusters, platform add-ons, app infrastructure.
  • Sharing outputs: terraform_remote_state is simple but couples consumers to the producer's state access. Publishing outputs to SSM Parameter Store or a similar store decouples them.
  • Keep import and moved blocks for a while after applying (they're harmless), and clean them up once every environment has applied them.
  • Enable state versioning in the backend: it's your undo button (see Amazon EKS in Production, lesson 11).

Recap

  • State maps addresses to real objects; keep it remote, locked, encrypted and restricted.
  • Refactor with moved, adopt with import (and -generate-config-out), release with removed.
  • plan -refresh-only shows drift without changing anything.
  • CLI state surgery is a last resort: back up, lock, verify with a clean plan.

This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.