Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Explainer

What Is an S3 Backend? Terraform State That Two People Can Share

Sean

Platform Writer

Aug 30, 2026
8 min read

An S3 backend keeps Terraform state in a bucket rather than in a local file, so more than one person can run Terraform against the same infrastructure without overwriting each other.

What Is an S3 Backend? Terraform State That Two People Can Share

Terraform records what it has created in a state file. By default that file sits in your working directory, which works exactly until a second person runs terraform apply — at which point they have no idea what already exists and Terraform proposes creating all of it again.

A remote backend fixes that. S3 is the common choice, and the setup got noticeably simpler recently because locking no longer needs a separate DynamoDB table.

Table of contents

Why local state stops working

terraform.tfstate maps your configuration to real resource IDs. It is how Terraform knows that aws_instance.web means instance i-0abc123 rather than something to create.

Three ways local state fails, in order of how soon you hit them:

  • A second person. Their state file is empty, so Terraform plans to create everything. Applying that duplicates your infrastructure or fails on name collisions.
  • A second machine. Same problem with one person and a laptop plus a desktop.
  • CI. A pipeline runner starts with no state at all and does the same thing.
  • Loss. The state file is on one disk with no backup. Losing it means Terraform no longer knows anything it created, and reconciliation is a manual import of every resource.

Committing state to Git is the instinctive fix and a bad one: it contains secrets in plain text — database passwords, generated keys — and Git has no locking, so two concurrent applies produce a merge conflict in a file you must never hand-merge.

The configuration

terraform {
  backend "s3" {
    bucket       = "acme-terraform-state"
    key          = "production/network/terraform.tfstate"
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true
  }
}

Field by field: bucket is the S3 bucket, key is the object path within it, region is the bucket’s region, encrypt turns on server-side encryption, and use_lockfile enables native S3 locking.

The key deserves thought, because it defines your state boundaries. One giant state file for all infrastructure means every plan reads and locks everything, and a mistake has a wide blast radius. Split by environment and by component — production/network, production/database, staging/network — so each is small, fast to plan, and independently lockable.

Note that backend configuration cannot use variables or interpolation. It is read before Terraform evaluates anything else. For values that differ per environment, use partial configuration:

terraform init -backend-config=environments/production.hcl

Locking, and what changed

Without locking, two concurrent applies both read the same state, both make changes, and the second write overwrites the first — leaving state that does not match reality. Terraform is now confidently wrong about your infrastructure, which is worse than being ignorant of it.

For years the S3 backend needed a DynamoDB table for locks, and most existing configurations still have this:

terraform {
  backend "s3" {
    bucket         = "acme-terraform-state"
    key            = "production/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"   # the old way
    encrypt        = true
  }
}

That works and needs a DynamoDB table with a LockID partition key created and maintained separately. Terraform now supports locking natively in S3 with use_lockfile = true, which writes a lock object beside the state and uses S3’s conditional writes.

For new configurations, use use_lockfile and skip DynamoDB entirely — one less resource, one less IAM policy, one less thing to get wrong. Existing setups can migrate by enabling use_lockfile, running with both for a transition period, then removing dynamodb_table.

When a lock is genuinely stuck — a crashed CI job, usually — terraform force-unlock <LOCK_ID> clears it. Verify nothing is actually running first; forcing a live lock is how you get the corruption the lock existed to prevent.

The bucket needs to be set up properly

State is your most sensitive infrastructure artifact. The bucket should have:

  • Versioning enabled. This is the recovery path when a state file is corrupted or truncated — roll back to a previous object version. Turn it on before you need it, because it does not apply retroactively.
  • Encryption at rest, ideally with a customer-managed key so access is separately auditable.
  • Public access blocked, at both the bucket and account level.
  • A bucket policy denying unencrypted uploads, so a misconfigured client cannot write plaintext state.
  • A lifecycle rule expiring noncurrent versions after some months, or versioning will accumulate every state write forever.

There is a bootstrapping wrinkle: the bucket holding your Terraform state cannot be created by the Terraform configuration that uses it. Common solutions are a small separate configuration with local state committed for that one bucket, or creating the bucket by hand and importing it. Either is fine; just do it deliberately.

On secrets: state files contain sensitive values in plain text regardless of sensitive = true in your configuration, which only redacts them from CLI output. Bucket access is therefore equivalent to access to every secret Terraform manages, and the IAM policy should reflect that.

Migrating to it

Moving from local state is one command:

# After adding the backend block
terraform init -migrate-state

Terraform detects the change, asks whether to copy existing state to the new backend, and does it. Back up your local terraform.tfstate first anyway — it takes a second and it is the only copy.

Afterwards, confirm the state landed and delete the local file so nobody uses it by accident:

terraform state list         # should show your resources
aws s3 ls s3://acme-terraform-state/production/
rm terraform.tfstate terraform.tfstate.backup

Add *.tfstate* to .gitignore if it is not there already.

Whether you need any of this

Worth asking directly. Terraform plus remote state plus locking plus a bucket policy is a real amount of machinery, and it is machinery for managing infrastructure that you are defining, provisioning and maintaining yourself.

It is the right answer when you have genuinely complex infrastructure across multiple services and environments, a team that needs to coordinate changes to it, and requirements that make the infrastructure itself the interesting part.

It is a lot when what you actually need is a web service, a database, and a domain with a certificate. On RunxBuild those are a repository connection and a plan — a service builds from GitHub with a build log, a live route and rollback, and a managed Postgres or MySQL with backups and private networking comes with the platform rather than with a module. There is no state file because there is no infrastructure graph to track.

How this fits the rest of the stack

An S3 backend replaces a local state file with a shared, versioned, lockable one, and use_lockfile = true means you no longer need DynamoDB to get the locking. Split state by environment and component, enable bucket versioning before you need it, and treat bucket access as access to every secret you manage. If the infrastructure is a service, a database and a domain, the RunxBuild hosting calculator shows what that costs without a state file in sight.

Useful related references:

FAQ

What is an S3 backend in Terraform?

A remote backend that stores the Terraform state file in an S3 bucket rather than on your local disk, so multiple people and CI systems can run Terraform against the same infrastructure with a shared, versioned record of what exists.

Do I still need DynamoDB for Terraform state locking?

No. Terraform’s S3 backend now supports native locking with use_lockfile = true, which writes a lock object beside the state using S3 conditional writes. Existing DynamoDB setups still work, and can migrate by enabling the lockfile and then removing dynamodb_table.

How do I migrate Terraform state to S3?

Add the backend block to your configuration and run terraform init -migrate-state. Terraform offers to copy the existing state to the new backend. Back up the local state file first, then verify with terraform state list and delete the local copy.

Is Terraform state sensitive?

Yes. State files contain resource attributes in plain text, including generated passwords and keys, and sensitive = true only redacts them from CLI output rather than from state. Access to the bucket is equivalent to access to every secret Terraform manages.

Why can I not use variables in the backend block?

Backend configuration is read before Terraform evaluates variables or expressions, so interpolation is not available. Use partial configuration and pass the differing values with terraform init -backend-config=file.hcl.

#terraform s3 backend#terraform state#state locking#infrastructure as code#Terraform