11.3 Remote State and the backend Block

Key Takeaways

  • Configure remote state with one terraform { backend "TYPE" { ... } } block; you cannot combine it with a cloud block
  • Backend arguments cannot use variables, locals, or data source interpolations; use partial configuration and terraform init -backend-config instead
  • Terraform 1.12 ships built-in backends including s3, azurerm, gcs, http, consul, kubernetes, oss, plus local, remote, cos, oci, and pg
  • terraform init -migrate-state copies existing state to the new backend; -reconfigure ignores the old backend and does not copy
  • terraform_remote_state reads another state's root outputs; for HCP Terraform, HashiCorp recommends the tfe_outputs data source because it does not require full state access
Last updated: August 2026

11.3 Remote State and the backend Block

Quick Answer: Put one backend block inside terraform { }. Arguments are literal — no var, local, or data-source interpolations. Finish the rest with terraform init -backend-config. A cloud block (HCP Terraform) cannot sit next to a backend block. After a backend change, init -migrate-state copies state; init -reconfigure does not. terraform_remote_state reads another state's root outputs.

Objective 6c on Terraform Associate (004) is configuration, not philosophy. Official references: Backend block configuration, the terraform block (backend vs cloud), terraform init, and terraform_remote_state.

One backend block, literal arguments

terraform {
  required_version = ">= 1.12.0"

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

Official limitations:

  • A configuration can provide only one backend block.
  • A backend block cannot refer to named values (input variables, locals, or data source attributes).
  • You cannot reference values declared inside a backend block elsewhere in the configuration.
  • You cannot load additional backends as plugins. The type must ship in the Terraform 1.12 binary.
  • If the configuration includes a cloud block, it cannot include a backend block. HCP Terraform and Terraform Enterprise manage state in the workspace; you do not also point at S3 from the same root module.

The cloud block is the current HCP Terraform / Terraform Enterprise integration (the product formerly marketed as Terraform Cloud). It is mutually exclusive with backend. The older backend "remote" type still exists in 1.12 for compatibility; new HCP work is the cloud block.

# Legal: HCP Terraform holds state. No backend block.
terraform {
  cloud {
    organization = "example_corp"
    workspaces {
      name = "network-prod"
    }
  }
}

Built-in backend types on Terraform 1.12

The v1.12.x docs directory publishes these backend types: local, remote, s3, azurerm, gcs, http, consul, kubernetes, oss, cos, oci, and pg. 004 will not ask you to memorize every argument of every type. It will ask you to recognize that the type is the block label and that arguments are type-specific.

TypeWhere the snapshot lives (1.12 docs)
s3Object at bucket / key (non-default workspaces under workspace_key_prefix, default env:)
azurermBlob key in container_name on storage_account_name
gcsObject in a Google Cloud Storage bucket
httpREST endpoint (GET to read, configurable write method)
consulConsul KV
kubernetesKubernetes secret
ossAlibaba Cloud Object Storage Service

HashiCorp recommends not putting access keys in the block. Leave credential arguments unset and use the environment variables or credential files that platform already uses (AWS_PROFILE, ARM_USE_OIDC, and so on). If you hardcode secrets or pass them with -backend-config, Terraform copies them in plain text into .terraform/terraform.tfstate and into saved plan files.

Partial configuration and -backend-config

You may omit arguments and finish them at init time. That is a partial configuration. Terraform still needs an empty-or-partial backend "TYPE" block in a root .tf file so it knows the type.

# backend.tf — committed. No secrets.
terraform {
  backend "s3" {
    key          = "network/terraform.tfstate"
    use_lockfile = true
  }
}
# backend.hcl or config.s3.tfbackend — not committed if it has secrets
bucket = "tfstate-prod-004"
region = "us-east-1"
terraform init -backend-config=backend.hcl
terraform init -backend-config="bucket=tfstate-prod-004" -backend-config="region=us-east-1"

Ways to supply the rest, from the official backend page:

  1. File-backend-config=PATH. Recommended filename pattern: *.backendname.tfbackend (for example config.s3.tfbackend). The file is the body of the backend block as top-level attributes, not wrapped in terraform { }.
  2. Command-line key/value pairs-backend-config="KEY=VALUE". Shells keep history; do not put secrets here.
  3. Interactive prompts — Terraform asks for missing required values unless input is disabled. It does not prompt for optional values.

Command-line options override the configuration file, and later -backend-config flags override earlier ones. The merged result is stored under .terraform/ and must not be committed.

Changing or removing a backend

You can change arguments or change types (local → s3, consul → s3). Terraform detects the change and requires reinitialization.

Init flagState data
-migrate-stateCopy existing state to the new backend; may prompt per workspace
-force-copySame copy, answers yes; implies -migrate-state
-reconfigureDisregard existing backend configuration; do not migrate
-backend=falseSkip backend configuration (already-initialized dir; useful so validate can run)

Back up first (cp terraform.tfstate … or terraform state pull > backup.tfstate). If you have multiple CLI workspaces, Terraform can copy all of them and will ask. If you are only fixing an argument on the same backend, it still asks; you can answer no when there is nothing to copy.

To go back to local: delete the backend block and re-init. Terraform prompts to migrate the remote snapshot onto the default local backend.

terraform_remote_state versus HCP workspace data

terraform_remote_state is a built-in data source (terraform.io/builtin/terraform). It reads the latest snapshot from a specified backend and exports root module outputs only. Nested module outputs are invisible unless the other root re-exports them. It does not create resources in the other configuration.

data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "tfstate-prod-004"
    key    = "network/terraform.tfstate"
    region = "us-east-1"
  }
}

resource "aws_instance" "app" {
  subnet_id = data.terraform_remote_state.network.outputs.public_subnet_id
}

The config object uses the same arguments the backend block would use. Nested backend blocks become objects (workspaces = { name = "vpc-prod" }, not a workspaces { } block).

Security warning from HashiCorp: although only outputs are exposed in HCL, anyone who can read those outputs can usually read the entire state snapshot over the network. Do not use terraform_remote_state as a secrets channel.

For HCP Terraform, HashiCorp recommends the tfe_outputs data source in the hashicorp/tfe provider instead. It is more secure because it does not require full workspace-state access to fetch outputs. terraform_remote_state still works with backend = "remote" and an organization/workspace config; that is the older pattern, not the preferred HCP pattern on 004.

Even better when you can: publish the shared value somewhere purpose-built (SSM Parameter Store, DNS, a Consul KV key) and read it with a normal data source so consumers never need state permissions.

Scenario: network stack and app stack

The network root uses backend "s3" with key = "network/terraform.tfstate" and use_lockfile = true. It outputs public_subnet_id. The app root has its own backend (key = "app/terraform.tfstate") and a terraform_remote_state data source pointing at the network key. Each stack has one writer and one lock. The app cannot var.bucket its way into the backend block; bucket names that differ per account come from -backend-config at init. When the team later moves the app stack to HCP Terraform, they remove the app backend block, add a cloud block, and run terraform init -migrate-state. They do not leave both blocks in the file.

004 traps for objective 6c

  • Backend arguments are not interpolable. Partial config is the escape hatch.
  • One backend block. No cloud + backend.
  • -migrate-state copies; -reconfigure does not.
  • terraform_remote_state is outputs-only and still implies full-state read access.
  • Prefer tfe_outputs when the other workspace lives in HCP Terraform.
  • Credentials in the block or in -backend-config land in .terraform/ and in plan files.
Test Your Knowledge

A teammate wants the S3 bucket name in a backend block to come from var.state_bucket so staging and production share one .tf file. What does Terraform 1.12 allow?

A
B
C
D
Test Your Knowledge

Can a Terraform 1.12 root module include both a cloud block for HCP Terraform and a backend "s3" block?

A
B
C
D
Test Your Knowledge

How should a second Terraform configuration read a subnet ID that another configuration stored as a root output?

A
B
C
D