Skip to content
ExponaBLOG
TERRAFORM, IAC & CLOUD AUTOMATION · DAY 009 / 100

Terraform state explained for Terraform & IaC developers

The file that maps code to real resources.

Expona Admin7 min read

Opening: The Bridge Between Code and Infrastructure

You've written your Terraform configuration, run terraform apply, and watched resources spin up in AWS. But have you stopped to think about how Terraform actually knows what you created, what you changed, or what to destroy? That's the job of state. Terraform state explained for Terraform & IaC developers is essential because state is the single source of truth that maps your code to real infrastructure—without it, Terraform is flying blind.

In this guide, we'll walk through what state stores, why it's critical to your workflow, and how to manage it safely in production environments. By the end, you'll understand state well enough to debug issues, collaborate securely with your team, and avoid the common pitfalls that catch many IaC teams off guard.

What State Stores: Resource IDs, Attributes, and Dependencies

Terraform state is a JSON file that records a snapshot of your infrastructure at a point in time. It's not just a list of what was created—it's a detailed map of every resource Terraform manages, including metadata that Terraform needs to plan and apply future changes.

When you run terraform apply, Terraform creates resources in your cloud provider and immediately records their IDs and current attributes in the state file. Here's what actually gets stored:

  • Resource IDs: The unique identifiers assigned by your cloud provider (e.g., i-0abc123def456 for an EC2 instance).
  • Computed attributes: Values that only exist after creation, like a database endpoint or security group ARN.
  • Resource dependencies: The order and relationships between resources, so Terraform can safely modify or destroy them in the right sequence.
  • Metadata: Timestamps, resource types, and references to other resources in the same configuration.

Let's look at a concrete example. Here's a simple Terraform configuration:

hcl
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
}

resource "aws_subnet" "private" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "us-east-1a"
}

After applying this, the state file will contain the VPC's ID (assigned by AWS), the subnet's ID, the computed DNS name server, and a reference showing that the subnet depends on the VPC. That dependency information is critical—Terraform uses it to know that it must destroy the subnet before the VPC.

You can inspect the state at any time with:

hcl
terraform state show aws_vpc.main

This command reads the state file and outputs the current attributes of that specific resource. It's often the first debugging tool we reach for when something doesn't match our expectations.

Why It Matters: Without State, Terraform Cannot Know What Exists

State is the bridge between your code and reality. Without it, Terraform would have no way to track what it created versus what already existed in your account, or what needs to change on the next apply.

Consider a scenario: You apply Terraform, creating an S3 bucket. A week later, someone manually adds a tag to that bucket in the AWS console. On your next terraform apply, what should happen? Terraform compares your configuration to the state file (not directly to AWS). It sees that your code defines no tags, but the state says there are none either. So Terraform doesn't change the bucket—even though AWS now shows a tag. This is by design. State acts as a checkpoint that lets Terraform work offline, plan changes before applying them, and avoid thrashing resources.

Without state, Terraform would have two problems:

  1. No change detection: Every apply would require querying every resource in your cloud account to see what's different. That's slow and error-prone.
  2. No dependency tracking: Terraform wouldn't know which resources depend on which, so it couldn't safely create or destroy in the right order.

State also enables Terraform's plan step. When you run terraform plan, Terraform compares your configuration to the state file and shows you what will change—without touching your infrastructure. This read-only preview is invaluable for catching mistakes before they go live.

In team environments, a shared state file is non-negotiable. If each developer has their own copy of the state, you'll quickly end up with conflicting changes, orphaned resources, and no single source of truth. That's why we use remote state backends (Terraform Cloud, S3 with DynamoDB locking, Consul, etc.) to ensure everyone works from the same checkpoint.

Never Edit by Hand: Use terraform state Commands Instead

State files are JSON, and they're tempting to edit directly when something goes wrong. Resist that temptation. Hand-editing state is a reliable way to corrupt it, break dependencies, or orphan resources in your account.

Instead, Terraform provides a suite of commands to modify state safely:

  • terraform state list — Show all managed resources.
  • terraform state show — Display attributes of a specific resource.
  • terraform state rm — Remove a resource from state (without destroying it in your cloud provider).
  • terraform state mv — Rename or move a resource to a different address in your configuration.
  • terraform state push/pull — Manually manage remote state (rarely needed, but available).

Here's a real-world example: You've refactored your configuration and moved an EC2 instance resource from aws_instance.app to aws_instance.web_server. The code is identical, but the resource address has changed. Without updating state, Terraform would destroy the old resource and create a new one (causing downtime). Instead, use:

hcl
terraform state mv aws_instance.app aws_instance.web_server

Terraform updates the state file so that the EC2 instance is now tracked under its new address. No destruction, no recreation—just a rename in the state.

Another common scenario: You've adopted Terraform for an existing resource (manual AWS creation). To avoid destroying it, import it:

hcl
terraform import aws_instance.existing i-0abc123def456

This fetches the live resource from AWS and writes it to state, effectively telling Terraform "this is mine now, manage it going forward." This is especially useful during lift-and-shift migrations where you're gradually moving manual infrastructure under Terraform control.

The golden rule: if you need to change state, use the Terraform CLI. Never hand-edit the JSON file.

State Contains Secrets: Encrypt It and Restrict Access

Here's a fact that catches many teams: state files contain plaintext secrets. Database passwords, API keys, SSH private keys—if you've created them with Terraform and stored them in state, they're visible to anyone who can read the state file.

Let's say you create a database with a randomly generated password:

hcl
resource "aws_db_instance" "postgres" {
  engine               = "postgres"
  engine_version       = "15.1"
  instance_class       = "db.t3.micro"
  allocated_storage    = 20
  username             = "admin"
  password             = random_password.db_password.result
  skip_final_snapshot  = true
}

resource "random_password" "db_password" {
  length  = 32
  special = true
}

After terraform apply, that password is stored in plain text in the state file. If someone gains access to your state file (via Git, S3, or a team member's laptop), they have the password. This is a compliance and security risk.

Protect state with these practices:

  • Use remote state: Store state in a managed backend like Terraform Cloud, AWS S3 with encryption, or HashiCorp Consul. Never commit terraform.tfstate to Git.
  • Enable encryption at rest: If using S3, enable server-side encryption (SSE-S3 or SSE-KMS) and version control. If using Terraform Cloud, encryption is built-in.
  • Enable encryption in transit: Use HTTPS for remote state backends. Terraform Cloud enforces this; S3 backends should too.
  • Restrict IAM access: Use IAM policies to ensure only the CI/CD pipeline and authorized team members can read/write state. Example for S3:
hcl
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::ACCOUNT_ID:role/TerraformRole"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-terraform-state/prod/*"
    },
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::ACCOUNT_ID:role/TerraformRole"
      },
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::my-terraform-state/prod/*"
    }
  ]
}
  • Enable state locking: Use DynamoDB (for S3) or native locking (Terraform Cloud) to prevent concurrent writes that could corrupt state.
  • Audit access: Enable S3 access logging or CloudTrail to track who accessed the state file and when.
  • Rotate secrets regularly: Even with restricted state access, rotate database passwords, API keys, and other secrets on a schedule. Terraform's terraform taint or module updates can force recreation.

For teams handling highly sensitive infrastructure, consider using a secrets management tool (AWS Secrets Manager, HashiCorp Vault) to generate and rotate secrets outside of Terraform, then reference them in your configuration. This keeps long-lived credentials out of state altogether.

Common Mistakes

Committing state to Git: It's the most common security mistake. Add terraform.tfstate* to your .gitignore immediately. Remote state backends solve this.

Sharing state manually via email or Slack: State files are snapshots that go stale. Always use a remote backend so the team pulls the latest state automatically.

Editing state JSON directly to "fix" issues: This almost always breaks dependencies or orphans resources. Use terraform state commands or re-plan the configuration instead.

Losing state in a migration: When moving to a new backend, use terraform state push after configuring the new backend, or use state migration features provided by your backend. Test in a non-production environment first.

Ignoring state locking: In a team, concurrent applies can corrupt state. Always enable locking (DynamoDB for S3, or use Terraform Cloud).

Key Takeaways

  • State is a JSON snapshot of your infrastructure: resource IDs, attributes, dependencies, and metadata. Terraform uses it to track what it manages and plan changes.
  • Without state, Terraform cannot detect changes, track dependencies, or provide a plan preview. State is the single source of truth.
  • Use terraform state commands (list, show, mv, rm, import) to safely modify state. Never hand-edit the JSON file.
  • State contains plaintext secrets: database passwords, API keys, SSH keys. Encrypt it at rest and in transit, restrict IAM access, enable locking, and audit access logs.
  • Always use a remote backend (Terraform Cloud, S3, Consul) for team environments. Never commit terraform.tfstate to version control.
  • When refactoring, use terraform state mv to rename resources without destruction. Use terraform import to adopt manually created resources.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles