Skip to content
Infrastructure as Code iac state 5 min read

Remote State

By default Terraform keeps its state in a local terraform.tfstate file in your working directory. That works fine for a solo experiment, but the moment a second person — or a CI pipeline — needs to run terraform apply, local state falls apart. Remote state moves that file into shared, durable storage (S3, Azure Blob, GCS, Terraform Cloud) and adds locking so concurrent runs cannot corrupt it. It is the single most important step in turning a personal Terraform project into a team-ready one.

Why local state breaks for teams

Local state assumes exactly one source of truth lives on one machine. As soon as you have a team, every assumption breaks:

  • No sharing. State lives on your laptop. A teammate running apply from their own checkout has an empty or stale state and will try to recreate everything you already built.
  • No locking. If two people apply at the same time, both read the same starting state, both write back, and the last write wins — silently clobbering the other’s changes.
  • No durability. A lost laptop or a deleted directory means the mapping between your config and your real cloud resources is gone, leaving orphaned infrastructure Terraform no longer tracks.
  • Tempting to commit. Teams sometimes commit terraform.tfstate to git to “share” it. This leaks secrets (state stores attribute values in plaintext) and produces merge conflicts on the serial counter that are effectively unresolvable.

Remote backends solve all four problems at once: a shared, encrypted, durable store with a coordinated lock.

What a remote backend provides

A backend is the component that determines where state is stored and how operations interact with it. A remote backend gives you two things that local storage cannot:

CapabilityWhat it doesTypical mechanism
Shared storageOne canonical state every operator reads and writesS3 bucket, Azure Blob container, GCS bucket, Terraform Cloud
State lockingPrevents concurrent writes that would corrupt stateS3 native lockfile, DynamoDB table, Azure lease, TFC managed
Encryption at restProtects the plaintext secrets inside stateSSE-S3/KMS, Azure SSE, GCS CMEK
VersioningLets you roll back a bad applyBucket object versioning

This applies equally to OpenTofu, which supports the same backend types and configuration syntax, so everything below is portable to tofu commands.

The backend block

You configure a backend inside the top-level terraform block. A backend block holds only the location and access settings — it must not reference variables, locals, or data sources, because Terraform reads it before any of those are evaluated.

Here is a complete S3 backend using S3-native locking (Terraform 1.11+ / OpenTofu 1.10+), which removes the need for a separate DynamoDB table:

terraform {
  required_version = ">= 1.11"

  backend "s3" {
    bucket       = "devcraftly-tfstate-prod"
    key          = "networking/terraform.tfstate"
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true
  }
}

The key is the path to the state object inside the bucket. Give each component or environment a distinct key (for example networking/, app/, data/) so they live in separate, independently locked state files.

Tip: Keep credentials out of the backend block. Terraform resolves backend authentication through the same environment as the AWS provider — AWS_PROFILE, AWS_ACCESS_KEY_ID, or an assumed role — so the configuration stays free of secrets and safe to commit.

Partial configuration

Because the backend block cannot use variables, the common pattern for reusing one configuration across environments is partial configuration: leave the changing values out of the block and pass them at init time.

terraform {
  backend "s3" {
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true
  }
}
terraform init \
  -backend-config="bucket=devcraftly-tfstate-prod" \
  -backend-config="key=app/terraform.tfstate"

You can also place those values in a prod.s3.tfbackend file and run terraform init -backend-config=prod.s3.tfbackend.

Migrating local state to remote

When you add a backend block to a project that already has local state, Terraform detects the change on the next init and offers to copy your existing state up to the new backend. Nothing is lost — the migration is a copy, and your local file is left behind as a backup.

Start with a project that has been applied locally, then add the backend block shown above and run:

terraform init

Output:

Initializing the backend...

Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend
  to the newly configured "s3" backend. No existing state was found in the
  newly configured "s3" backend. Do you want to copy this state to the new
  "s3" backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value: yes

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Confirm the migration worked by listing resources from the new remote state:

terraform state list

Output:

aws_s3_bucket.assets
aws_vpc.main

Once verified, delete the local terraform.tfstate and terraform.tfstate.backup files so no one accidentally edits a stale copy. To move off a backend later, remove the backend block and run terraform init -migrate-state to pull state back down.

Warning: never run a migration with an apply in progress, and make sure the state lock is free. Migrating while another operator holds the lock can produce divergent copies that are painful to reconcile.

Best Practices

  • Adopt remote state before the second person (or CI job) ever touches the project — retrofitting under contention is much harder.
  • Always enable both encryption at rest and locking; a shared backend without locking still allows silent overwrites.
  • Turn on object versioning on the backend bucket so a bad apply can be rolled back to a previous state version.
  • Use one backend bucket with a distinct key per environment and component, rather than one giant shared state file.
  • Keep the backend block free of secrets and variables; pass environment-specific values via partial configuration or .tfbackend files.
  • Restrict backend access with IAM/RBAC — anyone who can read the bucket can read every plaintext secret in your state.
  • Verify with terraform state list immediately after migrating, and remove leftover local state files.
Last updated June 14, 2026
Was this helpful?