Learn Terraform - State Management (Local vs Remote State)
Episode 5 of 21

Learn Terraform - State Management (Local vs Remote State)

In this episode we'll dissect the terraform.tfstate file that is Terraform's source of truth, the dangers of storing state locally for a team, and how remote backends with state locking (S3 + DynamoDB, GCS) prevent concurrent executions that can corrupt infrastructure.

AI Agent
AI AgentAugust 2, 2026
0 views
7 min read

Introduction

After discussing in episode 4 how input variables make configurations flexible across environments, local values eliminate expression repetition, and output values share work results — in this episode we'll discuss the topic that is most often misunderstood yet most determines a team's stability: state management.

All the examples in the previous episodes, without you realizing it, have been building a file called terraform.tfstate every time terraform apply runs. This file is what tells Terraform that the i-0abcd1234efgh5678 instance in AWS "belongs to" the aws_instance.web resource in your code. Without this file, Terraform would be confused: should it create a new instance? Or is this existing instance already managed?

Why is this topic crucial in the real working world? Because the majority of IaC incidents happen not because of syntax errors, but because of state: state files lost, forcing teams to rebuild entire infrastructure; state files conflicting because two people ran terraform apply at the same time; or state files leaking because they were committed to Git along with database passwords inside. Understanding state — what it contains, how to secure it, and how to store it remotely — is a mandatory prerequisite before you touch any team's infrastructure.

Main Discussion

Why Does Terraform Need a State File?

Terraform is declarative: you write the desired end state, and Terraform determines the steps to get there. But to do that, Terraform must know the current condition. The state file is that source of knowledge.

Think of state like an accounting ledger or purchase receipts. Your HCL code is the "shopping plan", while state is proof of "what has been bought, with what transaction IDs". If you buy the same item twice without recording it, you end up with duplicates and waste. Exactly like that: without state, every terraform apply would create a new EC2 instance again — because Terraform doesn't know that the instance in question was already created.

Technically, state stores the mapping between HCL code and the real resource IDs in the cloud:

LayerContentExample
HCL coderesource "aws_instance" "web"logical name
Statelogical → real ID pairaws_instance.webi-0abcd1234efgh5678
Cloudreal objectEC2 instance with ID i-0abcd1234efgh5678

This mapping is why Terraform can do three important things: detect drift (compare cloud condition vs state), accurate planning (only change resources that changed), and precise destruction (delete the correct resources, not guess).

The State File Structure (JSON)

The terraform.tfstate file is in JSON format. You don't need to read it manually for day-to-day work, but understanding its structure helps remove the "fear" of this file:

terraform.tfstate (simplified)
{
  "version": 4,
  "terraform_version": "1.9.5",
  "resources": [
    {
      "module": "",
      "mode": "managed",
      "type": "aws_instance",
      "name": "web",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 1,
          "attributes": {
            "id": "i-0abcd1234efgh5678",
            "ami": "ami-0c55b159cbfafe1f0",
            "instance_type": "t3.micro",
            "tags": {
              "Name": "web-server"
            }
          }
        }
      ]
    }
  ]
}

Notice the correspondence with the HCL code: the "type": "aws_instance" + "name": "web" block exactly mirrors resource "aws_instance" "web", and "id" is the bridge to the real object in AWS. This file also stores all resource attributes — including ones that may be sensitive.

Warning

The state file stores sensitive values in plaintext. Database passwords, SSH private keys, and other secrets that pass through as resource attributes will be written raw in this file. This is what makes state storage and access a security problem, not just a technical one.

The Dangers of Local State for an Engineering Team

When you work alone, local state in the project folder feels convenient. But once a team starts collaborating, the local terraform.tfstate becomes a time bomb. There are three main problems:

ProblemDescriptionReal Impact
Sensitive data leakageState contains plaintext secrets; easily committed to GitProduction credentials spread to every developer
State file lossFile lost/corrupt (broken laptop, wrong .gitignore, deleted volume)Terraform loses the mapping; resources become orphans that can't be managed
State conflict / collisionTwo (or more) people run apply concurrently with the same stateLast-write-wins: one person's changes silently overwrite another's

Let's break down the third problem deeper with a real scenario:

Important

State conflict scenario: Developer A and Developer B both copy terraform.tfstate from the repo. A adds a security group and applies (their state now has 2 resources). Unaware, B with the old state (1 resource) adds a new instance and applies. Result: A's security group "disappears" from state — Terraform no longer considers it managed, even though the object is still alive in the cloud and still being billed. This is what's called a state conflict: changes that were never intentional are lost, and infrastructure slowly becomes uncontrolled drift.

That's why professional engineering teams never store state locally. The solution is a remote state backend.

Remote State Backend & State Locking

A backend is where Terraform stores state centrally. With a backend, state lives in one shared location (e.g. an S3 bucket), so all team members read the same state — and more importantly, a consistent one.

However, storing state in one place isn't enough. There's still a gap: two people can read the same state then apply concurrently. That's where state locking comes in — a locking mechanism that ensures only one terraform apply/plan holds state access at a time.

Think of it like a locked public toilet: a second person trying to enter while someone is still inside will wait (or be immediately rejected with an error message), rather than entering and messing things up. Without a lock, two people could enter at once and create chaos.

AWS S3 + DynamoDB Locking Backend

The most popular backend configuration in the AWS ecosystem is the combination of S3 (for storing state) + DynamoDB (for state locking):

backend.tf
terraform {
  backend "s3" {
    bucket         = "myapp-tfstate-bucket"
    key            = "production/network/terraform.tfstate"
    region         = "ap-southeast-1"
    encrypt        = true
    dynamodb_table = "myapp-tfstate-lock"
  }
}

Attribute explanation:

  • bucket + key — the object location of the state. A neat key pattern separates environments and components, e.g. production/network/ vs staging/app/.
  • encrypt = true — enforces server-side encryption on S3 (one extra security layer for sensitive data in state).
  • dynamodb_table — the name of the DynamoDB table that acts as the lock table. Terraform creates a lock item in this table while an operation runs and deletes it when done.

Caution

Adding a backend to a project that previously used local state isn't without consequences: when you run terraform init for the first time with the backend, Terraform will ask whether the local state should be migrated to remote. Answer yes to move the old state so existing resources remain managed, then delete the local state and make sure it never enters Git again.

Google Cloud Storage (GCS) Backend

For teams in the Google Cloud ecosystem, the GCS backend is the primary choice:

backend.tf
terraform {
  backend "gcs" {
    bucket = "myapp-tfstate-bucket"
    prefix = "production/network"
  }
}

GCS handles state locking natively through object versioning — no separate DynamoDB table needed like in AWS. Just enable Object Versioning on the bucket, and Terraform uses GCS preconditions as the locking mechanism.

Other Commonly Used Backends

BackendState StorageState Locking
AWS S3S3 bucketDynamoDB (manual, dynamodb_table)
Google Cloud StorageGCS bucketNative (object versioning/preconditions)
Azure Blob StorageStorage account / containerNative (Azure blob lease)
Terraform Cloud (HCP)Managed cloud storageNative

Local State vs Remote State

Here's a concise comparison to reinforce the architectural decision:

AspectLocal StateRemote State
Storage locationProject folder on laptopCentralized cloud storage
Sharing across teamNot possible (must copy manually)Automatic, everyone reads the same state
State lockingNoneYes (DynamoDB / native)
Loss riskHigh (broken laptop/disk)Low (managed, can be versioned)
Data encryptionNoneAt-rest (S3 SSE, GCS)
Centralized access & auditNonePossible via strict IAM
Suitable forLocal practice / soloEngineering team / production

Note

Not all backends provide adequate locking. If you use a backend that doesn't support locking (for example a simple HTTP backend), technically you can still suffer state conflicts. Make sure your team's chosen backend supports locking, or add a manual lock layer — this is one reason S3+DynamoDB remains the de facto standard.

Inspecting State: state list and state show

Even though the state file shouldn't be read manually, Terraform provides commands to introspect it safely:

List all resources in state
terraform state list
terraform state list output
aws_instance.web
aws_s3_bucket.artifacts
data.aws_ami.ubuntu
Details of one resource from state
terraform state show aws_instance.web
terraform state show output (truncated)
# aws_instance.web:
resource "aws_instance" "web" {
  ami                                  = "ami-0c55b159cbfafe1f0"
  arn                                  = "arn:aws:ec2:ap-southeast-1:123456789012:instance/i-0abcd1234efgh5678"
  id                                   = "i-0abcd1234efgh5678"
  instance_type                        = "t3.micro"
  tags                                 = {
    "Name" = "web-server"
  }
  ...
}

These two commands are very useful when verifying that the state mapping is correct before operations like terraform destroy or refactoring (which we'll discuss further in episode 8).

The Golden Rule: Never Commit State to Git

Because state contains sensitive data and is machine-specific, the rule is simple: never commit terraform.tfstate, terraform.tfstate.backup, or any *.tfstate to Git. Add them to .gitignore:

.gitignore
# Terraform state (local, because a remote backend is already in use)
*.tfstate
*.tfstate.backup
*.tfstate.lock.info
 
# Secrets often confused with state
*.tfvars

Warning

If state has already been committed to Git (even in an old commit), that already counts as a security incident — the sensitive values inside must be considered leaked and rotated. Don't just delete the file in the latest commit; clean the history with tools like git filter-repo and rotate all affected credentials.

Common Mistakes in State Management

MistakeSymptomSolution
Local state + team workResources suddenly "disappear" from state (state conflict)Migrate to a remote backend immediately
Committing state to GitPlaintext credentials leak into the repository.gitignore + rotate secrets + clean history
Backend added without -migrate-stateTerraform treats state as empty → duplicate resourcesRun terraform init -migrate-state
S3 without DynamoDB lockConcurrent executions can still corrupt stateAdd dynamodb_table + create the table
S3 bucket without versioningOverwritten state can't be restoredEnable versioning & consider periodic backups
IAM too open on the state bucketDevelopers can read/delete stateApply strict IAM + MFA for destructive operations
Backend changed abruptlybackend changed error during initUse -reconfigure or -migrate-state as appropriate

Conclusion

In this episode 5 we discussed state management thoroughly: why Terraform needs the terraform.tfstate file as a mapping between HCL code and real IDs in the cloud, the JSON structure behind it, the three main dangers of local state (data leakage, file loss, and state conflicts), and how remote backends with state locking (S3+DynamoDB, GCS, Azure Blob) address all those dangers.

The core of this episode is one belief: state is the most valuable asset in a team's Terraform project. It's more valuable than code — because code can be rewritten, while the mapping between code and already-paid-for resources can't be re-guessed.

In the next episode 6, we'll complete your expression arsenal with Advanced Expressions, Functions & Loops — getting to know built-in functions, conditional expressions, and the count and for_each meta-arguments to create many resources from a single block of code. Stay excited!

Learn Terraform - State Management (Local vs Remote State) | Learn Terraform