skip to content

Why does Terraform reject an input variable inside a backend block, and how do you point the same configuration at a different state bucket per environment given that restriction?

level: middleimportance: should knowfreq 50%

answer

  1. read before variables are evaluated
  2. literals only in that block
  3. omit arguments, supply at init
  4. one small file per environment
  5. re-point without copying state

basics

~20 s

Backend settings are read at init, before variables, locals or providers are evaluated, so they must be literal values. The workaround is partial configuration: omit the varying arguments and supply them at init with -backend-config files or key=value flags.

solid answer

~40 s

The `backend` block is the very first thing Terraform processes — it has to locate and read state before it can evaluate anything else, so at that moment no variables, locals, data sources or functions exist yet. That is why interpolation is rejected there and every argument must be a literal. The supported answer is *partial configuration*: leave out the arguments that differ, then pass them at init time, either as repeated `-backend-config="bucket=acme-tfstate-prod"` flags or, more commonly, as a file — `terraform init -backend-config=envs/prod.s3.tfbackend`. Those files are plain `key = value` HCL, one per environment, checked into the repo. When switching a working directory between environments you must re-init, and you use `-reconfigure` so Terraform re-points without copying the current state — using `-migrate-state` there would copy one environment's state over another's.

code

hcl · 6 lines
hcl
terraform {
  backend "s3" {
    region  = "eu-west-1"
    encrypt = true
  }
}

go deeper

for a junior

Know that a backend block accepts only literal values — no var. or local. references — and that the missing pieces are passed to terraform init with -backend-config.

for a middle

Explain the ordering reason: the backend is resolved before variables, locals, providers or data sources are evaluated, because Terraform must read state before it can evaluate anything. Then describe partial configuration concretely.

for a senior

Demonstrate the operational discipline: -reconfigure when re-pointing an initialised directory, never -migrate-state; backend-config files that carry location but never credentials; and a wrapper so nobody types the wrong environment by hand.

for a principal

Own the repository shape this implies across many teams — one root module per environment versus one parameterised directory — and defend the choice on how hard it makes a wrong-environment apply, not on how much duplication it removes.

## Why the restriction exists Terraform's evaluation has a bootstrap problem. Variables can come from `.tfvars` files, `TF_VAR_` environment variables, or the CLI; locals derive from variables; data sources require a configured provider; provider configuration can itself reference variables. All of that evaluation happens *within a run*, and a run begins by loading state. So the backend cannot depend on anything Terraform computes, because nothing has been computed yet — the backend is how Terraform finds out what already exists. Concretely, this fails: ```hcl terraform { backend "s3" { bucket = var.state_bucket # Error: Variables not allowed key = "${var.env}/terraform.tfstate" } } ``` The same restriction covers `local.`, `data.`, `path.module`, and function calls. Literals only. ## Partial configuration The designed escape hatch is to omit arguments and supply them at `terraform init`. Any subset can be omitted — including all of them, leaving `backend "s3" {}`. ```hcl terraform { backend "s3" { region = "eu-west-1" # same everywhere encrypt = true } } ``` Then per environment: ``` # envs/prod.s3.tfbackend bucket = "acme-tfstate-prod" key = "platform/network/terraform.tfstate" ``` and `terraform init -backend-config=envs/prod.s3.tfbackend`. The `.tfbackend` extension is a convention, not a requirement — the file is HCL key/value pairs with no surrounding block. You can also pass individual settings as `-backend-config="bucket=acme-tfstate-prod"`, repeated as needed, and mix files with flags where later values win. Credentials should not go in these files: the AWS, Google and Azure backends all pick up the normal provider credential chains and environment variables, so the checked-in file carries only location, never secrets. Because typing the flag every time is error-prone, teams usually wrap it — a `make init ENV=prod` target, a shell wrapper, or the `TF_CLI_ARGS_init` environment variable, which appends arguments to every `init` invocation in that shell. ## Re-pointing an existing working directory Terraform caches the backend configuration it last initialised in `.terraform/terraform.tfstate`. If you re-run `init` with different backend settings, it notices the change and refuses to continue silently. Two flags resolve it, and confusing them is the classic accident: - `-reconfigure` — forget the recorded backend configuration and initialise against the new one, **without** copying any state. This is what you want when switching a working directory from dev to prod, because prod's state already exists and must not be overwritten. - `-migrate-state` — copy the state from the old location to the new one. Correct when genuinely relocating a single state; catastrophic when "switching environments", because it copies dev's state over prod's object. A safer habit than flag discipline is to not share a working directory between environments at all. ## The alternative that avoids the problem Many teams sidestep partial configuration by giving each environment its own root module directory, each with a fully literal backend block: ``` envs/dev/main.tf # backend "s3" { bucket = "...-dev" ... } envs/prod/main.tf # backend "s3" { bucket = "...-prod" ... } modules/network/ # the shared, reusable code ``` The duplication is a few literal lines per environment; the payoff is that `cd envs/prod && terraform init` cannot possibly be pointed at the wrong state, and a plan for prod is unambiguously a plan for prod. Which shape you pick is a real tradeoff — one directory plus backend-config files is DRYer, separate root modules are harder to get wrong — but the constraint driving both is the same: the backend is resolved before the language is. HCP Terraform uses a `cloud` block instead of a `backend` block; it is subject to the same literal-only rule, and the same `-backend-config` mechanism applies to it. ## How to say it in an interview "The backend is read before variables exist, so it takes literals only. You use partial configuration — omit the varying arguments and pass a per-environment `.tfbackend` file to init — and if you reuse one working directory across environments you re-init with `-reconfigure`, never `-migrate-state`."

  • Should backend credentials go into the .tfbackend file?
    No. Those files are checked into the repository, so they should carry only location — bucket, key, region, container. The S3, GCS and azurerm backends all use the same credential chains as their providers, so environment variables, an assumed role, or the ambient CI identity supply authentication. Putting an access key in a committed backend-config file is a secret leak with extra steps.
  • What actually happens if you run init with a different backend config and no flag?
    Terraform compares the new settings against the backend configuration cached in `.terraform/terraform.tfstate`, sees they differ, and stops with an error telling you the backend changed and that you must pass `-migrate-state` or `-reconfigure`. It deliberately refuses to guess, because one choice copies data and the other abandons it.
  • Is one root module per environment better than one directory plus backend-config files?
    It trades duplication for safety. Separate root modules make it impossible to init the wrong state and let environments diverge in provider or version pins; the shared directory is DRYer but relies on every human and pipeline passing the right file. Teams managing production usually prefer separate root modules with the real logic in shared child modules.

saying these in an interview costs you the question

  • Claims TF_VAR_ variables can populate the backend block
  • Suggests generating the backend block with a templating script
  • Uses -migrate-state when switching environments in one directory
  • Puts access keys into the checked-in backend-config file
  • Thinks partial configuration requires omitting every argument

context