NovuSpark
All articles

April 3, 2026 · NovuSpark Team

Managing Terraform State in Teams: Remote Backends and Locking

This is the fourth post in our Terraform fundamentals series. It builds directly on the state concepts introduced in Terraform 101.

We flagged this problem back in the first post in this series and deliberately left it unresolved: local state — a terraform.tfstate file sitting on whoever's laptop ran apply first — works fine right up until a second person needs to run Terraform against the same infrastructure. This post is the fix.

Why local state breaks down with more than one person

Picture two engineers, both with a local clone of the same Terraform configuration, both with permission to run apply. Engineer A runs it Monday morning and provisions a new subnet. Their local terraform.tfstate now reflects that subnet's existence. Engineer B, who hasn't pulled a state file that was never committed to version control in the first place, runs apply Monday afternoon — and as far as their state file is concerned, that subnet doesn't exist. Terraform tries to create it again.

Best case, this fails loudly with a naming collision. Worse case, it partially succeeds, and now two different state files each have a partially-incompatible picture of the same real infrastructure. This isn't a hypothetical edge case — it's the default outcome of local state the moment more than one person, or more than one CI job, touches the same configuration.

The fix: a remote backend

A backend tells Terraform where to store state — instead of a local file, a shared location every team member and every CI job reads from and writes to.

terraform {
  backend "s3" {
    bucket         = "novuspark-terraform-state"
    key            = "web-app/production/terraform.tfstate"
    region         = "eu-west-2"
    dynamodb_table = "terraform-state-lock"
    encrypt        = true
  }
}

Run terraform init after adding this block to an existing local-state configuration, and Terraform offers to migrate your existing state into the new backend automatically — it doesn't need to start from scratch.

A few things in that block are doing real work, not just configuration boilerplate:

  • encrypt = true ensures the state file — which, as covered in the first post, can contain plaintext secrets — is encrypted at rest in S3.
  • key is the path within the bucket. The pattern <project>/<environment>/terraform.tfstate is worth adopting from day one: it's what lets one S3 bucket safely hold state for many projects and environments without collisions.
  • dynamodb_table is the part most tutorials skip, and it's the single most important line in this block.

State locking: the problem a backend alone doesn't solve

Moving state to S3 solves the "two people, two different pictures of reality" problem. It does not, by itself, solve a narrower but still serious problem: two people running apply at the exact same moment, both reading the same state, both computing a plan, both writing back — with the second write silently clobbering the first.

That's what the DynamoDB table is for. Before writing to state, Terraform acquires a lock by writing a row to that table; a second apply attempting to run concurrently sees the lock and blocks — or fails clearly — instead of racing.

resource "aws_dynamodb_table" "terraform_lock" {
  name         = "terraform-state-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"
 
  attribute {
    name = "LockID"
    type = "S"
  }
}

(This table itself is usually provisioned once, by hand or via a small separate "bootstrap" Terraform configuration with its own local state — a rare, deliberate exception to "always use a remote backend," since this resource has to exist before your main configuration's remote backend can use it.)

With locking in place, a second apply attempted mid-run gets a clear, immediate error instead of a silent race:

Error: Error acquiring the state lock

Lock Info:
  ID:        7f3a2b91-...
  Path:      novuspark-terraform-state/web-app/production/terraform.tfstate
  Operation: OperationTypeApply
  Who:       jane@novuspark.com

That error is the system working correctly, not a malfunction — it's telling you exactly who's currently holding the lock and what they're doing.

What this means for how a team actually works

Once state is remote and locked, a few practices become both possible and necessary:

  • CI can run Terraform safely. Local state made CI genuinely dangerous — a pipeline run and a developer's local apply would fight over the same file with no coordination at all. Remote state with locking is what makes "Terraform runs in CI, humans don't apply from their laptops" a safe default instead of a risk.
  • State access itself becomes something to control. Whoever can read the state bucket can potentially read every secret embedded in it. IAM policies on the state bucket deserve the same scrutiny as IAM policies on the infrastructure the state describes.
  • One state file per environment, not one giant shared file. Splitting dev, staging, and production into separate state files (via separate key values, as shown above) means a mistake in one environment's plan can't accidentally touch another's — the blast radius of any single apply is contained to the state file it's actually locking.

What to actually remember from this post

  • Local state is a single-person, single-laptop tool — it stops being safe the moment a second person or a CI job needs to run against the same infrastructure.
  • A remote backend (S3, Terraform Cloud, or similar) gives everyone the same, current picture of what exists.
  • Locking (DynamoDB, or a backend's built-in equivalent) is what prevents two concurrent applies from corrupting state — a backend without locking only solves half the problem.
  • Encrypt state at rest, always — it frequently contains secrets whether you intended it to or not.

Next in the series: Terraform Workspaces and Multi-Environment Strategies, where we look at workspaces as one specific — and sometimes over-used — answer to managing dev/staging/production with the same configuration.

Ready when you are

Want training built around your team's real work?

Tell us about your team and what you're trying to solve — we'll recommend a program that fits.