Remote State and Backends
A backend determines where Terraform stores and locks its state. Standard backends like S3 store state remotely, but plan and apply still run locally on your machine or CI runner — running operations remotely is a separate feature of HCP Terraform, configured with the cloud block, not the S3 backend. The default local backend writes terraform.tfstate next to your code — fine for a solo experiment on a laptop, disqualifying the moment a second person or a CI job needs to touch the same infrastructure.
A remote backend puts state in shared, durable, encrypted storage. On AWS that means an S3 bucket: one object that a team and its pipeline read and write as a single source of truth, with versioning for recovery and encryption at rest. Switching from local to remote state is the line between a personal toy and a project anyone else can safely run.
The Local Backend and Its Limits
Local state is a single file on one machine. The local backend does lock it on that machine, but the lock cannot reach across machines — so the moment you share the file (commit it, pass it around) two laptops clobber each other; no sharing, so a teammate's plan is blind to what you just created; and no durability, so a wiped laptop or a fat-fingered rm takes your only record of production with it. People paper over this by committing the file to git or passing it around in chat, which trades one corruption mode for several worse ones. For anything beyond a throwaway sandbox, local state is the wrong answer.
The S3 Backend
The S3 backend stores the state object in a bucket you control. Turn on bucket versioning and every write becomes a recovery point — a bad apply is one console click from being rolled back. Default encryption (SSE-S3 or SSE-KMS) covers the plaintext secrets state carries, and blocking public access keeps the bucket off the open internet. Modern Terraform (1.10+) can also lock through S3 itself, which the locking topic covers next; older setups paired the bucket with a DynamoDB table.
terraform { backend "s3" { bucket = "acme-tfstate-prod" key = "network/terraform.tfstate" region = "us-east-1" encrypt = true use_lockfile = true } }
Backend Configuration
The backend block is read before the rest of your configuration — before variables exist, before any expression is evaluated. That is why it cannot use var. references or interpolation: at the moment Terraform needs to find your state, the machinery that would resolve a variable has not run yet. The values in the block must be literals or supplied from outside. People hit this constantly, try to template the bucket name with a variable, and get a flat refusal.
Partial Backend Config
To serve dev and prod from the same code without duplicating the backend block, leave the changing values out and supply them at init time. This is partial configuration: the block declares it uses S3, and a per-environment file fills in the bucket and key. You pass it with terraform init -backend-config=prod.s3.tfbackend, swapping one file to point the same code at a different state.
# terraform init -backend-config=prod.s3.tfbackend bucket = "acme-tfstate-prod" key = "network/terraform.tfstate" region = "us-east-1"
Migrating Backends
When you move state from local to S3, or between backends, do it with terraform init -migrate-state. Terraform reads the existing state, writes it to the new backend, and preserves the lineage so the new location is recognized as the same history. Copying the file by hand looks equivalent and is not — a byte-for-byte copy does carry the serial and lineage along, but you skip the consistency check that compares them before writing, you don't rewrite the backend configuration, and you bypass the safety prompt that confirms what is moving where.
Local backend — a single terraform.tfstate on one machine, locked only against other processes on that same machine, with no sharing, no durability, and no recovery. Choose it only for a throwaway sandbox you would not mind losing.
S3 backend — a state object in a versioned, encrypted, access-controlled bucket that a team and CI share. Choose it for any project that more than one person, one machine, or one pipeline will ever touch — which is every real project.
- Running a team off local state passed around or committed to git, which guarantees corruption and conflicting writes.
- Trying to use a variable or interpolation in the
backendblock, which Terraform evaluates before variables exist. - Enabling an S3 backend without bucket versioning, so a bad write has no recovery point and you are restoring from memory.
- Migrating backends by copying the file manually instead of
init -migrate-state, skipping the consistency check and the safety prompt that confirm the move. - Putting the state bucket in the same account with broad access, so anyone who can read the bucket can read every secret in state.
- Use an S3 backend with bucket versioning, default encryption, and blocked public access for every non-trivial project.
- Supply environment-specific settings via
-backend-configfiles rather than duplicating the backend block per environment. - Migrate state with
terraform init -migrate-state, never by hand-copying the file. - Keep the state bucket in a dedicated, tightly controlled account or behind a restrictive bucket policy.
- Give each environment and stack its own state key so a bad apply in one cannot reach another's state.
Knowledge Check
Why does local state fail for a team?
- It is a single file on one machine with no sharing and no durability, and its lock only coordinates that one machine
- It refuses to run more than a single apply per day for each user, throttling the entire team to serial daily changes
- It cannot store more than a few hundred resources before the local file grows too large to parse
- It encrypts state with a per-machine key that other teammates have no way to read
Why can't the backend block reference a variable?
- Terraform reads the backend before the rest of the config, so variables don't exist yet
- Backends only accept literal JSON syntax, whereas variables are written in HCL
- Variables can hold sensitive values, and backends forbid any sensitive input by design
- It can reference a variable, but only for the region argument and never for the bucket name itself
What does enabling S3 bucket versioning give you for state?
- A recovery point for every write, so a bad apply can be rolled back to a prior version
- Automatic locking on every write, which removes the need for any separate lock mechanism
- Client-side encryption of every value written into the state object
- Faster plans, because the older state versions are cached locally on disk
How should you move state from a local backend to S3?
- Run
terraform init -migrate-state, which moves it and preserves lineage - Copy
terraform.tfstatestraight into the bucket with the AWS CLI - Delete the local file and let the next apply rebuild the state in S3
- Run
terraform state pushafter first generating a fresh lineage UUID for it
You got correct