Search…

Terraform state management

In this series (10 parts)
  1. Introduction to Infrastructure as Code
  2. Terraform fundamentals
  3. Terraform state management
  4. Terraform modules
  5. Terraform in CI/CD
  6. Ansible fundamentals
  7. Ansible roles and best practices
  8. Packer for machine images
  9. CloudFormation and CDK
  10. Managing drift and compliance

Terraform needs to know what exists before it can decide what to change. That knowledge lives in state. Without it, every terraform apply would attempt to create everything from scratch. State is what makes Terraform declarative rather than purely imperative.

What state actually contains

The state file is JSON. It maps every resource in your configuration to a real object in your cloud provider. Each entry records the resource type, its provider-assigned ID, and every attribute Terraform knows about.

Run terraform show on any project and you will see this mapping laid out. The file also tracks dependency order, metadata about providers, and the version of Terraform that last wrote to it.

Local vs remote state

By default, Terraform writes state to a file called terraform.tfstate in your working directory. This works fine for solo experimentation. It breaks down fast in any team setting.

Problems with local state:

  • No shared access. Each team member has their own copy.
  • No locking. Two people can run apply simultaneously and corrupt the file.
  • Secrets in plaintext sitting on a laptop.

Remote state solves all three. You store the file in a shared backend and let the backend handle access control and locking.

flowchart LR
  A[terraform apply] --> B{Backend Type}
  B --> C[Local File]
  B --> D[S3 + DynamoDB]
  B --> E[Terraform Cloud]
  D --> F[Encrypted at Rest]
  D --> G[State Locking]
  D --> H[Versioning]

S3 backend with DynamoDB locking

This is the most common remote backend for AWS users. S3 stores the state file. DynamoDB provides a lock table so only one operation runs at a time.

First, create the infrastructure for your backend. You can do this manually or with a separate Terraform configuration. Here is the full setup:

provider "aws" {
  region = "us-east-1"
}

resource "aws_s3_bucket" "tfstate" {
  bucket = "myorg-terraform-state"

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_s3_bucket_versioning" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id

  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
  }
}

resource "aws_s3_bucket_public_access_block" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_dynamodb_table" "tflock" {
  name         = "terraform-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

Versioning lets you recover a previous state if something goes wrong. Encryption protects secrets at rest. The public access block is non-negotiable.

Now configure your actual project to use this backend:

terraform {
  backend "s3" {
    bucket         = "myorg-terraform-state"
    key            = "prod/network/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-lock"
    encrypt        = true
  }
}

The key field determines the path inside the bucket. Use a consistent naming scheme like {environment}/{component}/terraform.tfstate to keep things organized as you scale.

Run terraform init after adding or changing a backend block. Terraform will offer to migrate your existing local state to the new backend.

How state locking works

When you run apply or plan, Terraform writes a lock entry to the DynamoDB table. The entry contains a unique ID, the user who acquired it, and a timestamp. If another process tries to acquire the lock, it fails immediately with a clear error message.

The lock is released automatically when the operation finishes. If Terraform crashes mid-apply, the lock stays. You will need to remove it manually:

terraform force-unlock LOCK_ID

Use this sparingly. Only force-unlock when you are certain no other operation is running.

Essential state commands

Terraform provides several commands for inspecting and modifying state directly. These are power tools. Use them carefully.

List all resources in state:

terraform state list

Show details for a specific resource:

terraform state show aws_s3_bucket.tfstate

Move a resource to a new address (useful when refactoring modules):

terraform state mv aws_instance.old aws_instance.new

Remove a resource from state without destroying it:

terraform state rm aws_instance.legacy

This tells Terraform to forget about the resource. The actual infrastructure remains untouched. You would do this when you want to stop managing something with Terraform or hand it off to a different state file.

Import existing infrastructure into state:

terraform import aws_instance.web i-1234567890abcdef0

Import is how you bring pre-existing resources under Terraform management. You still need to write the corresponding resource block in your configuration.

Sensitive values in state

State files contain everything Terraform knows about your resources. That includes database passwords, API keys, and TLS certificates. Even if you mark a variable as sensitive in your configuration, the value still appears in state.

variable "db_password" {
  type      = string
  sensitive = true
}

The sensitive flag only prevents the value from showing in CLI output and logs. It does not encrypt the state file.

Protecting sensitive values requires a layered approach:

  1. Encrypt the backend. S3 with KMS encryption is the baseline.
  2. Restrict access. Use IAM policies to limit who can read the state bucket.
  3. Never commit state to version control. Add *.tfstate and *.tfstate.backup to your .gitignore.
  4. Audit access. Enable S3 access logging and CloudTrail.

For highly sensitive values, consider generating them outside Terraform entirely and referencing them through AWS Secrets Manager or SSM Parameter Store. This keeps the secret out of state altogether.

data "aws_ssm_parameter" "db_password" {
  name            = "/prod/db/password"
  with_decryption = true
}

resource "aws_db_instance" "main" {
  password = data.aws_ssm_parameter.db_password.value
}

The SSM parameter value will still land in state, but the secret itself is managed and rotated outside of Terraform.

State file hygiene

A few practices that prevent headaches:

  • One state file per environment. Do not mix production and staging resources in the same state. Use separate backend keys or workspaces.
  • Small state files. Break large projects into smaller root modules. A state file tracking 500 resources is slow to plan and risky to modify.
  • Regular backups. S3 versioning handles this, but verify you can actually restore from a previous version.
  • Lock down write access. Most team members only need read access to state. Reserve write access for CI/CD pipelines and a small set of operators.

What comes next

State management gives you a reliable foundation for team workflows. The next article covers Terraform modules: how to build reusable, composable infrastructure components that multiple teams can share without duplicating configuration.

Start typing to search across all content
navigate Enter open Esc close