| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.15.8-1hashicorp/local 2.9.0 |
| Applies to | Any host with Terraform installed |
| Lab environment | Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu |
| Privilege | Normal user |
| Scope | Backend concept — default local backend, local vs remote storage, backend block syntax, terraform init backend workflow, -reconfigure vs -migrate-state, partial backend configuration, credentials guidance, locking capability overview, brief terraform_remote_state, backend vs provider distinction, and common init errors. Does not cover full S3 or cloud backend setup, HCP Terraform workspaces, state subcommands, force-unlock depth, or provider configuration. |
| Related guides | Terraform state explained terraform init command Configure S3 bucket as Terraform backend Terraform sensitive data Terraform Associate certification course |
A Terraform backend decides where state is stored and whether the storage layer can offer features such as locking. Configuration flows through Terraform Core; the backend sits underneath and persists the state snapshot Terraform reads during plan and apply.
Terraform configuration
│
▼
Backend
│
└── stores stateThe Terraform state lesson explains what state contains and why Terraform needs it. This lesson focuses on where that state lives, how you declare a backend, and how to change backends safely. Every example runs under ~/terraform-labs/terraform-backend-remote-state/ with the hashicorp/local provider — no AWS account required. Production teams often use S3, Azure Blob, GCS, or HCP Terraform; the workflow is the same even when the storage system changes.
terraform init; see terraform init for the full init flag reference.
What is a Terraform backend?
A backend is Terraform's state storage layer. During terraform init, Terraform configures the backend declared in your root module (or falls back to the built-in local backend). On each plan and apply, Terraform reads the latest state snapshot from that backend and writes updates back through it.
Without an explicit backend block, Terraform uses the local backend and creates terraform.tfstate in the working directory where you run commands. That default is fine for solo experiments; it becomes fragile when several people need the same state file or when the machine disk is not your source of truth.
Start in local-default/:
mkdir -p ~/terraform-labs/terraform-backend-remote-state/local-defaultMove into that directory — the remaining commands in this section assume you are here:
cd ~/terraform-labs/terraform-backend-remote-state/local-defaultAdd a minimal resource in main.tf:
resource "local_file" "demo" {
content = "backend remote state lab"
filename = "${path.module}/demo.txt"
}Declare the provider in versions.tf without a backend block:
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}Initialize the working directory:
terraform initSample output:
Initializing the backend...
Initializing provider plugins...
Terraform has been successfully initialized!Apply so Terraform creates managed state:
terraform apply -auto-approveSample output:
Plan: 1 to add, 0 to change, 0 to destroy.
local_file.demo: Creating...
local_file.demo: Creation complete after 0s [id=...]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.List the state file Terraform wrote with the default backend:
ls -la terraform.tfstateSample output:
-rw-r--r-- 1 user user 1635 Aug 12 09:52 terraform.tfstateThat JSON file is your state snapshot on disk. The local backend also maintains terraform.tfstate.backup after updates — a one-generation safety net on the same machine, not a team-wide backup strategy.
Local vs remote Terraform state
Local state means the configured backend keeps state on the filesystem accessible to the machine running Terraform. The default backend writes terraform.tfstate in the module root; you can also point the local backend at another path.
Remote state means state is stored through a remote system — object storage, a Terraform Cloud or HCP Terraform workspace, Consul, and others. Remote storage is what makes shared workflows practical: one canonical state file, access control on the storage layer, and optional versioning or locking depending on the backend type.
Local backend (default)
→ terraform.tfstate in working directory
Local backend (custom path)
→ state file at configured path
Remote backend (S3, Azure, GCS, HCP Terraform, …)
→ state stored through remote service APIRemote backends matter for collaboration and durability, not because local state is invalid. Many engineers learn on the default local backend first, then move state to shared storage before the team grows. The migration steps later in this guide apply whether you move from default local storage to a custom path or from one path to object storage — the init flags are the same.
Configure a Terraform backend
Declare a backend inside a terraform block in the root module:
terraform {
backend "TYPE" {
# backend-specific arguments
}
}Replace TYPE with the backend name (local, s3, azurerm, gcs, remote, and others documented by HashiCorp). Each type accepts different arguments. The local backend only needs a path; S3 needs bucket and key; HCP Terraform needs organization and workspace names.
Backend configuration is evaluated during initialization, not during ordinary expression evaluation in resources. That distinction matters for two rules:
- You cannot reference
var,local, resource attributes, or other dynamic expressions inside abackendblock. - Changing backend settings requires running
terraform initagain — usually with-reconfigureor-migrate-state.
Switch to remote-local/ to store state outside the default filename:
cd ~/terraform-labs/terraform-backend-remote-state/remote-localUse the same main.tf as local-default/. In versions.tf, add an explicit local backend with a subdirectory path:
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
backend "local" {
path = "state-store/remote.tfstate"
}
}Initialize with the new backend settings:
terraform initSample output:
Successfully configured the backend "local"!
Terraform has been successfully initialized!Apply and confirm state lands at the configured path instead of terraform.tfstate in the root:
terraform apply -auto-approveList the state store directory:
ls -la state-store/Sample output:
-rw-r--r-- 1 user user 1635 Aug 12 09:52 remote.tfstateNo terraform.tfstate appears in the working directory root — only the path you configured. For a real remote backend, Terraform would upload the same JSON structure to your storage service instead of a local folder.
Initialize and change backend configuration
Any backend change starts with terraform init. Terraform compares the backend settings in your configuration with the backend metadata recorded under .terraform/. When they differ, init stops and asks how to proceed.
Two flags solve different problems:
| Flag | When to use it | Effect on existing state |
|---|---|---|
terraform init -migrate-state |
You changed backend settings and want Terraform to copy existing state into the new backend | Prompts to copy state when a prior snapshot exists |
terraform init -reconfigure |
You want the new backend settings without copying state | Points Terraform at the new backend; leaves prior state file in place but unused |
HashiCorp documents these as alternatives for a backend change — not sequential steps. If you run -reconfigure first, Terraform already accepts the new backend and a follow-up -migrate-state no longer demonstrates migration from the original backend.
Migrate state to a new backend path
The migrate/ directory walks through -migrate-state using two paths on the local backend — the same prompts you see when moving state to S3 or another remote type.
cd ~/terraform-labs/terraform-backend-remote-state/migrateStart with path = "state-v1.tfstate" in versions.tf:
terraform {
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
backend "local" {
path = "state-v1.tfstate"
}
}Initialize the working directory with the first backend path:
terraform initApply so the first state file contains a managed resource:
terraform apply -auto-approveEdit versions.tf to point at a second path:
backend "local" {
path = "state-v2.tfstate"
}Run migrate init and approve the copy when Terraform prompts:
terraform init -migrate-stateSample output:
Backend configuration changed!
Terraform has detected that the configuration specified for the backend
has changed. Terraform will now check for existing state in the backends.
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 "local" backend. No existing state was found in the newly
configured "local" backend. Do you want to copy this state to the new "local"
backend? Enter "yes" to copy and "no" to start with an empty state.
Successfully configured the backend "local"!
Terraform has been successfully initialized!Type yes at the prompt (or pipe echo yes | when you are automating a lab script). Terraform copies the snapshot into state-v2.tfstate.
Confirm the new file exists:
ls -la state-v2.tfstateSample output:
-rw-r--r-- 1 user user 1635 Aug 12 09:53 state-v2.tfstateRun plan to verify Terraform still recognizes the managed file:
terraform planSample output:
No changes. Your infrastructure matches the configuration.No changes means migration preserved the resource mapping — exactly what you want before decommissioning the old state file.
-input=false to terraform init -migrate-state when you need the copy prompt — Terraform refuses to migrate silently. Approve the copy interactively or pipe yes explicitly.
Reconfigure without migrating state
Use reconfigure/ when you deliberately want new backend metadata without copying the old snapshot — for example when pointing at a fresh empty backend.
cd ~/terraform-labs/terraform-backend-remote-state/reconfigureApply once with path = "state-a.tfstate", then change the path to state-b.tfstate in versions.tf. Run reconfigure init:
terraform init -reconfigureSample output:
Terraform has been successfully initialized!Plan immediately after reconfigure:
terraform planSample output:
# local_file.demo will be created
Plan: 1 to add, 0 to change, 0 to destroy.Terraform proposes a create because state-b.tfstate is empty even though demo.txt still exists on disk from the first apply and state-a.tfstate still holds the old mapping. That is the practical difference between migrate and reconfigure: migrate keeps continuity; reconfigure abandons the prior backend binding.
Choose -migrate-state when you are moving production state. Choose -reconfigure when you intentionally start fresh on a new backend or you will copy state through another process.
Partial backend configuration
Teams often commit only non-sensitive, static backend settings and supply the rest at init time. Declare the backend type with minimal arguments:
terraform {
backend "local" {}
}Put environment-specific values in a separate file — HashiCorp recommends the *.tfbackend naming pattern for clarity. In partial-config/backend.hcl:
path = "partial-state.tfstate"From partial-config/, change into the lab directory:
cd ~/terraform-labs/terraform-backend-remote-state/partial-configPass the external path file during init:
terraform init -backend-config=backend.hclSample output:
Successfully configured the backend "local"! Terraform will automatically
use this backend unless the backend configuration changes.You can also pass individual settings: terraform init -backend-config=path=partial-state.tfstate. For remote backends such as S3, the same pattern suits environment-specific non-secret settings you do not want hardcoded in the main configuration:
bucket = "terraform-state-prod"
key = "app/prod.tfstate"
region = "us-east-1"Use -backend-config for environment-specific backend settings that you do not want hardcoded in the main configuration. For credentials or other sensitive authentication data, prefer the backend's supported environment variables, workload identity, IAM roles, or equivalent authentication mechanism. Terraform may persist values passed with -backend-config under .terraform/ and in plan files.
-backend-config files. Prefer environment-based or identity-based authentication supported by the backend. State files themselves can contain sensitive values — see manage secrets and sensitive data in Terraform.
Remote state and state locking
Backend capabilities differ by type. All backends store state; only some provide state locking — an automatic mutex that blocks concurrent writes while one apply is in flight. Without locking, two operators applying at the same time can corrupt or overwrite state.
The built-in local backend does not coordinate locks across machines. Remote backends such as HCP Terraform, Consul, and several cloud storage integrations support locking when configured correctly.
For Amazon S3, the backend supports native state locking with use_lockfile = true. DynamoDB-based locking is deprecated and retained primarily for migration compatibility with older configurations — HashiCorp plans to remove it in a future minor version. Full AWS wiring lives in Configure S3 bucket as Terraform backend. Deeper lock troubleshooting and force-unlock belong in the Terraform state locking lesson on this course track.
When a backend supports locking, Terraform prints Acquiring state lock and Releasing state lock around plan and apply. A lock error means another process holds the lock or a prior run ended abruptly — resolve that before forcing an unlock in production.
Access data from another Terraform state
Sometimes one stack needs values exported by another stack's state — VPC identifiers, subnet IDs, or application names from an upstream module. The terraform_remote_state data source reads outputs from a remote state snapshot:
data "terraform_remote_state" "producer" {
backend = "local"
config = {
path = "${path.module}/../producer/terraform.tfstate"
}
}
output "remote_app_name" {
value = data.terraform_remote_state.producer.outputs.app_name
}The producer stack in remote-state-read/producer/ declares output "app_name" in outputs.tf. Start in the producer directory:
cd ~/terraform-labs/terraform-backend-remote-state/remote-state-read/producerInitialize the producer working directory:
terraform initApply so the producer state file exports app_name:
terraform apply -auto-approveSwitch to the consumer directory that reads the producer state:
cd ~/terraform-labs/terraform-backend-remote-state/remote-state-read/consumerInitialize the consumer so Terraform can load the remote state data source:
terraform initPlan to see the remote output flow in:
terraform planSample output:
data.terraform_remote_state.producer: Reading...
data.terraform_remote_state.producer: Read complete after 0s
Changes to Outputs:
+ remote_app_name = "producer-app"
Plan: 0 to add, 0 to change, 0 to destroy.terraform_remote_state is a read path into another backend's stored snapshot. In production you configure the same data source with backend = "s3" (or another remote type) and the appropriate config map. Although terraform_remote_state exposes only root-module outputs to your configuration, the identity reading those outputs must be able to read the entire state snapshot. Therefore, anyone granted backend access for terraform_remote_state may also be able to retrieve sensitive data stored elsewhere in that state. HashiCorp warns against using this data source when the remote state contains data you consider sensitive.
tfe_outputs when possible because it can expose outputs without requiring full workspace-state access.
This article focuses on backend storage and migration. Output design and cross-stack contracts are covered in Terraform output values and module inputs and outputs.
Common Terraform backend problems
| Symptom | Likely cause | Fix |
|---|---|---|
Error: Backend initialization required |
plan or apply before init, or backend block added after last init |
Run terraform init in the module root |
Backend configuration changed |
backend arguments differ from .terraform/ metadata |
Run terraform init and choose -migrate-state or -reconfigure deliberately |
Unsupported backend type |
Typo in backend "TYPE" |
Use a supported backend type |
| Authentication failure on remote backend | Missing credentials, wrong role, or expired token | Fix cloud auth outside Terraform; use environment variables, workload identity, or IAM roles supported by the backend |
| Plan wants to recreate everything after backend change | Used -reconfigure on an empty new backend |
Re-run with -migrate-state or restore state from backup |
| Remote backend unreachable | Network, DNS, or storage outage | Restore connectivity; verify bucket/workspace names match configuration |
| Credentials embedded in Git | Secrets committed in backend blocks or -backend-config files |
Rotate secrets; use identity-based authentication; keep only non-secret settings in partial config |
| Confusing backend with provider | provider block added where backend belongs |
Providers call APIs; backends store state — separate blocks, separate init concerns |
Reproduce the unsupported backend error in errors/invalid-backend/. Change into the error demo directory:
cd ~/terraform-labs/terraform-backend-remote-state/errors/invalid-backendversions.tf uses a deliberate typo in the backend type:
terraform {
backend "nosuch" {}
}Run init to surface the unsupported backend message:
terraform initSample output:
Error: Unsupported backend type
on versions.tf line 8, in terraform:
8: backend "nosuch" {}
There is no backend type named "nosuch".For AWS-specific bucket policies, use_lockfile locking, and console setup, continue with Configure S3 bucket as Terraform backend. That article owns the implementation steps; this one owns the backend concept and init workflow.
References
- Backend configuration
- terraform init command
- Partial configuration
- terraform_remote_state data source
- State locking
Summary
A Terraform backend determines where state snapshots live and which optional features, such as locking, your workflow can rely on. Without configuration, the local backend writes terraform.tfstate in your working directory — fine for learning, limiting for teams. Declaring backend "TYPE" { ... } moves storage to a configured path or remote service; every backend change flows through terraform init.
You practiced the distinction that matters on exams and in production: terraform init -migrate-state copies existing state when backend settings change, while terraform init -reconfigure accepts new settings without migration — which leaves an empty backend unless you copy state yourself. Partial configuration keeps static settings in .tf files and supplies environment-specific non-secret values through -backend-config; credentials belong in environment variables, workload identity, or IAM roles — not in committed backend blocks or -backend-config files, because Terraform may persist those values under .terraform/ and in plan files.
Backends are not providers. Providers reach infrastructure APIs; backends persist state. When stacks need values from one another, terraform_remote_state exposes only root-module outputs to expressions, but the reader must access the full state snapshot — so grant backend read access only when that exposure is acceptable. For AWS S3 implementation detail, follow the dedicated S3 backend guide; for lock errors and force-unlock, continue with the Terraform state locking lesson on this course track.

