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
applyfrom 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.tfstateto 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:
| Capability | What it does | Typical mechanism |
|---|---|---|
| Shared storage | One canonical state every operator reads and writes | S3 bucket, Azure Blob container, GCS bucket, Terraform Cloud |
| State locking | Prevents concurrent writes that would corrupt state | S3 native lockfile, DynamoDB table, Azure lease, TFC managed |
| Encryption at rest | Protects the plaintext secrets inside state | SSE-S3/KMS, Azure SSE, GCS CMEK |
| Versioning | Lets you roll back a bad apply | Bucket 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
keyper 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
.tfbackendfiles. - Restrict backend access with IAM/RBAC — anyone who can read the bucket can read every plaintext secret in your state.
- Verify with
terraform state listimmediately after migrating, and remove leftover local state files.