Terraform Backends and Remote State Explained

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
hashicorp/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.

text
Terraform configuration
     Backend
        └── stores state

The 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.

NOTE
Complete install Terraform on Ubuntu and the Terraform lab environment first. Backend changes always go through 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/:

bash
mkdir -p ~/terraform-labs/terraform-backend-remote-state/local-default

Move into that directory — the remaining commands in this section assume you are here:

bash
cd ~/terraform-labs/terraform-backend-remote-state/local-default

Add a minimal resource in main.tf:

hcl
resource "local_file" "demo" {
  content  = "backend remote state lab"
  filename = "${path.module}/demo.txt"
}

Declare the provider in versions.tf without a backend block:

hcl
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}

Initialize the working directory:

bash
terraform init

Sample output:

output
Initializing the backend...
Initializing provider plugins...
Terraform has been successfully initialized!

Apply so Terraform creates managed state:

bash
terraform apply -auto-approve

Sample output:

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:

bash
ls -la terraform.tfstate

Sample output:

output
-rw-r--r-- 1 user user 1635 Aug 12 09:52 terraform.tfstate

That 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.

text
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 API

Remote 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:

hcl
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 a backend block.
  • Changing backend settings requires running terraform init again — usually with -reconfigure or -migrate-state.

Switch to remote-local/ to store state outside the default filename:

bash
cd ~/terraform-labs/terraform-backend-remote-state/remote-local

Use the same main.tf as local-default/. In versions.tf, add an explicit local backend with a subdirectory path:

hcl
terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }

  backend "local" {
    path = "state-store/remote.tfstate"
  }
}

Initialize with the new backend settings:

bash
terraform init

Sample output:

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:

bash
terraform apply -auto-approve

List the state store directory:

bash
ls -la state-store/

Sample output:

output
-rw-r--r-- 1 user user 1635 Aug 12 09:52 remote.tfstate

No 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.

bash
cd ~/terraform-labs/terraform-backend-remote-state/migrate

Start with path = "state-v1.tfstate" in versions.tf:

hcl
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:

bash
terraform init

Apply so the first state file contains a managed resource:

bash
terraform apply -auto-approve

Edit versions.tf to point at a second path:

hcl
backend "local" {
    path = "state-v2.tfstate"
  }

Run migrate init and approve the copy when Terraform prompts:

bash
terraform init -migrate-state

Sample output:

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:

bash
ls -la state-v2.tfstate

Sample output:

output
-rw-r--r-- 1 user user 1635 Aug 12 09:53 state-v2.tfstate

Run plan to verify Terraform still recognizes the managed file:

bash
terraform plan

Sample output:

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.

IMPORTANT
Do not pass -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.

bash
cd ~/terraform-labs/terraform-backend-remote-state/reconfigure

Apply once with path = "state-a.tfstate", then change the path to state-b.tfstate in versions.tf. Run reconfigure init:

bash
terraform init -reconfigure

Sample output:

output
Terraform has been successfully initialized!

Plan immediately after reconfigure:

bash
terraform plan

Sample output:

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:

hcl
terraform {
  backend "local" {}
}

Put environment-specific values in a separate file — HashiCorp recommends the *.tfbackend naming pattern for clarity. In partial-config/backend.hcl:

hcl
path = "partial-state.tfstate"

From partial-config/, change into the lab directory:

bash
cd ~/terraform-labs/terraform-backend-remote-state/partial-config

Pass the external path file during init:

bash
terraform init -backend-config=backend.hcl

Sample output:

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:

hcl
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.

IMPORTANT
Do not hardcode backend credentials or place long-lived secrets in -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:

hcl
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:

bash
cd ~/terraform-labs/terraform-backend-remote-state/remote-state-read/producer

Initialize the producer working directory:

bash
terraform init

Apply so the producer state file exports app_name:

bash
terraform apply -auto-approve

Switch to the consumer directory that reads the producer state:

bash
cd ~/terraform-labs/terraform-backend-remote-state/remote-state-read/consumer

Initialize the consumer so Terraform can load the remote state data source:

bash
terraform init

Plan to see the remote output flow in:

bash
terraform plan

Sample output:

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.

NOTE
For HCP Terraform or Terraform Enterprise, HashiCorp recommends 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:

bash
cd ~/terraform-labs/terraform-backend-remote-state/errors/invalid-backend

versions.tf uses a deliberate typo in the backend type:

hcl
terraform {
  backend "nosuch" {}
}

Run init to surface the unsupported backend message:

bash
terraform init

Sample output:

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


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.


Frequently Asked Questions

1. What is a Terraform backend?

A backend determines where Terraform stores state and which optional features, such as locking, are available. Without an explicit backend block, Terraform uses the built-in local backend and writes terraform.tfstate in the working directory.

2. What is the difference between terraform init -reconfigure and -migrate-state?

-migrate-state copies or moves existing state into a newly configured backend when you changed backend settings and want to keep prior records. -reconfigure accepts the new backend settings without migrating state, which leaves the new backend empty unless you copy state manually.

3. Can I use variables in a Terraform backend block?

No. Backend blocks are processed during initialization and do not support ordinary Terraform expressions such as var or local references. Use partial backend configuration and -backend-config for non-secret environment-specific settings that should not be hardcoded in the main configuration.

4. What is partial backend configuration?

Partial configuration declares the backend type in your .tf files and supplies remaining settings at init time with -backend-config files or key=value arguments. Use it for environment-specific non-secret values such as bucket names, state keys, and regions — not for credentials.

5. Is a backend the same as a provider?

No. Providers talk to infrastructure APIs during plan and apply. Backends store and retrieve state. A root module configures both, but they solve different problems and use different configuration blocks.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)