linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
Intro
Lessons
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Knowledge base
  • Cheat sheet
  • Capstone
  • Interview prep
home/terraform/kb/State/tf-state

kb/state ── State ── beginner

State: Terraform's memory of what it created

State is the JSON file `terraform.tfstate` where Terraform records what it created in the cloud. Without it, Terraform would have no way to tell which bucket is "its own" and which belongs to something else. The file holds resource IDs, all attributes, and often secrets. It is the most sensitive part of any project.

view as markdown

Why state exists

Imagine you wrote HCL with a single S3 bucket and ran apply. The bucket was created with the name my-bucket-12345. You run apply again. How does Terraform know that bucket is the same bucket?

  • From HCL? No, HCL only describes the desired state.
  • From the AWS API? No, AWS has millions of buckets, and Terraform has no way to know which one is yours.

The answer is the state file. After the first apply, Terraform stored this in terraform.tfstate: "Resource aws_s3_bucket.demo maps to the real bucket named my-bucket-12345, with this ARN, created at this time."

Without that record, Terraform would try to create everything from scratch on every run and fail with "bucket already exists."

Where state lives

By default, terraform.tfstate sits in the same directory as your HCL. This is the local backend.

my-project/
├── main.tf
├── terraform.tfstate         # ← here it is
└── terraform.tfstate.backup  # copy from the previous apply

In real projects, state is moved to a remote backend: S3, Terraform Cloud, and so on. This lets a team share a single state without conflicts. See tf-init-backends.

What is inside

The file is JSON. You can inspect it by hand, but do not edit it directly:

json
{
  "version": 4,
  "terraform_version": "1.9.8",
  "serial": 5,
  "lineage": "abc-123-...",
  "resources": [
    {
      "mode": "managed",
      "type": "aws_s3_bucket",
      "name": "demo",
      "instances": [
        {
          "attributes": {
            "id": "my-bucket-12345",
            "bucket": "my-bucket-12345",
            "arn": "arn:aws:s3:::my-bucket-12345",
            "region": "us-east-1",
            "tags": { "Owner": "student" }
          }
        }
      ]
    }
  ]
}

Key fields:

  • serial is a counter. Every apply increments it by 1. Used to verify that state is current.
  • lineage is the unique UUID of this state. It protects against accidentally swapping the file with another.
  • resources is an array of all managed resources: type, name, attributes.

Commands for working with state

Do not touch the file by hand. Use the CLI:

bash
# List all resources in state
terraform state list
# Show attributes of one resource
terraform state show aws_s3_bucket.demo
# Full state as JSON (for scripts)
terraform show -json

Dangerous commands (use only in exceptional cases):

bash
# Remove a resource from state (NOT from the cloud)
terraform state rm aws_s3_bucket.demo
# Rename a resource in state
terraform state mv aws_s3_bucket.demo aws_s3_bucket.renamed
# Import an existing resource into state (the resource must already be described in HCL)
terraform import aws_s3_bucket.demo my-existing-bucket

These commands modify terraform.tfstate directly. Back up the file before running them.

Drift: when state and reality diverge

State is a snapshot of what Terraform believes. The actual cloud infrastructure can change underneath it.

Examples of drift:

  • Someone deleted a bucket manually through the AWS Console.
  • Auto Scaling changed desired_capacity on its own.
  • An ALB Auto Scaling target was enabled.

On the next plan, Terraform reads state and compares it with HCL. When reality differs from state, plan -refresh=true (the default) first updates state from the API, then shows the diff.

This is expected behavior, but it requires attention. See tf-resource-lifecycle for ignore_changes, which handles attributes that change outside Terraform.

Secrets in state

This is critical to understand: state contains all attributes, including sensitive ones.

  • A database password (aws_db_instance.password) is in state.
  • Tokens, keys, and secret values are in state.
  • sensitive = true masks values only in CLI output. In the JSON file they appear as plain text.

Because of this:

  • Local state in git is not allowed. Never. Even in a private repository, it will eventually leak.
  • Local state on a shared server's disk is also a bad idea. Any user with sudo can read it.
  • A remote backend with encryption is required for production. S3 with SSE-KMS, Terraform Cloud with server-side encryption, and similar options.

Locking: protection against concurrent apply

If two people run apply on the same project at the same time, state can become corrupted. Locking solves this:

  • The local backend has no lock. This is a risk even on a single machine (two terminals open at once).
  • S3 backend with DynamoDB for locking is the standard. One apply holds a record in DynamoDB; the second waits.
  • Terraform Cloud handles locking automatically.

When a concurrent apply is attempted, you will see:

Error: Error acquiring the state lock
Lock Info:
  ID:        abc-123
  Path:      s3://my-bucket/terraform.tfstate
  Operation: OperationTypeApply
  Who:       user@host
  Created:   2026-05-20 14:00:00 +0000 UTC

If you know the other process is dead, run terraform force-unlock <lock-id>.

Common pitfalls

  • Do not edit state by hand. The JSON looks simple, but Terraform validates internal invariants (lineage, serial, hashes). Break those and you will spend a long time recovering.

  • Do not commit state to git. Never. Even local state. Add to .gitignore:

    terraform.tfstate
    terraform.tfstate.backup
    .terraform/
    .terraform.lock.hcl  # ← this one, on the other hand, should be committed
  • State reflects a single point in time. Between applies it does not watch the cloud. To get fresh data, run terraform refresh or terraform plan (which also does a refresh).

  • state rm does not delete the resource in the cloud. It only removes it from state. The resource continues to exist outside Terraform's control. Useful during migrations, dangerous when done by mistake.

  • state mv is required when renaming. If you change "demo" to "main" in HCL without running state mv, Terraform will recreate the resource. After state mv, it only updates the record without touching the cloud.

  • Lineage protects against file substitution, but it can be broken. If you accidentally overwrote state with a copy from another project, the lineage will not match and Terraform will refuse to run. That is a protection, not a bug.

§ команды

bash
terraform state list

List all managed resources. The first command to run when exploring an unfamiliar project.

bash
terraform state show aws_s3_bucket.demo

Show all attributes of one resource: what Terraform currently knows about it.

bash
terraform show -json | jq '.values.root_module.resources'

All resources in state via jq: useful for scripting and analysis.

bash
terraform refresh

Update state from the real cloud. Useful before a potentially destructive plan.

bash
terraform state pull > backup.tfstate

Download the current state to a file (works with any backend). Back up before dangerous operations.

§ см. также

  • tf-initterraform init: the first command in any projectterraform init downloads the provider plugins (AWS, GCP, and so on), creates a lockfile that pins their versions, and prepares the working directory. Without it, neither plan nor apply will run.
  • tf-init-backendsBackends in Terraform: where state livesA backend is where the state file is stored. The default is local, next to your HCL. Remote backends (S3, GCS, Terraform Cloud, http) give you shared access and locking. This course uses only local; remote is covered as an overview.
  • tf-planterraform plan: see what Terraform is about to doplan is a dry run: Terraform reads your HCL, reads the state, and shows the diff between them. It changes nothing in the cloud. This is your main tool for not breaking prod by mistake.
  • tf-applyterraform apply: apply a plan to a real cloudapply takes the result of plan and actually calls the cloud API: it creates, changes, and deletes resources. After apply, the state is updated. This is the command that turns money into infrastructure.
  • tf-resource-blockResource block: the main building block of TerraformA resource block tells Terraform "create this thing in the cloud." It has three parts: the resource type (what it is), the name (how you refer to it internally), and the arguments (how to configure it). Writing these blocks is what you spend 90% of your time doing in Terraform.
Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies