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
11.3 Remote State and the backend Block
Quick Answer: Put one
backendblock insideterraform { }. Arguments are literal — novar,local, or data-source interpolations. Finish the rest withterraform init -backend-config. Acloudblock (HCP Terraform) cannot sit next to abackendblock. After a backend change,init -migrate-statecopies state;init -reconfiguredoes not.terraform_remote_statereads 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
cloudblock, it cannot include abackendblock. 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.
| Type | Where the snapshot lives (1.12 docs) |
|---|---|
s3 | Object at bucket / key (non-default workspaces under workspace_key_prefix, default env:) |
azurerm | Blob key in container_name on storage_account_name |
gcs | Object in a Google Cloud Storage bucket |
http | REST endpoint (GET to read, configurable write method) |
consul | Consul KV |
kubernetes | Kubernetes secret |
oss | Alibaba 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:
- File —
-backend-config=PATH. Recommended filename pattern:*.backendname.tfbackend(for exampleconfig.s3.tfbackend). The file is the body of the backend block as top-level attributes, not wrapped interraform { }. - Command-line key/value pairs —
-backend-config="KEY=VALUE". Shells keep history; do not put secrets here. - 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 flag | State data |
|---|---|
-migrate-state | Copy existing state to the new backend; may prompt per workspace |
-force-copy | Same copy, answers yes; implies -migrate-state |
-reconfigure | Disregard existing backend configuration; do not migrate |
-backend=false | Skip 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-statecopies;-reconfiguredoes not.terraform_remote_stateis outputs-only and still implies full-state read access.- Prefer
tfe_outputswhen the other workspace lives in HCP Terraform. - Credentials in the block or in
-backend-configland in.terraform/and in plan files.
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?
Can a Terraform 1.12 root module include both a cloud block for HCP Terraform and a backend "s3" block?
How should a second Terraform configuration read a subnet ID that another configuration stored as a root output?