The Error
You add a variable reference to your backend configuration โ something that seems perfectly reasonable โ and Terraform throws this:
Error: Variables not allowed
on main.tf line 4, in terraform:
4: bucket = var.state_bucket
Variables may not be used here.
Engineers hit this constantly, especially when wiring up shared modules or multi-environment setups where the backend bucket, prefix, or region needs to differ per deployment.
Why This Happens
Terraform processes the backend block before it evaluates anything else โ before variables, locals, data sources, or any other expressions. The backend initializes first. That's how Terraform knows where to read and write state. By the time everything else loads, var.*, local.*, and data.* haven't been resolved yet.
Deliberate design, not a bug. The backend block accepts only literal values โ strings, numbers, booleans. No interpolation, period.
Common Trigger Pattern
Here's the setup that trips people up:
variable "environment" {
type = string
}
variable "state_bucket" {
type = string
}
terraform {
backend "s3" {
bucket = var.state_bucket # โ Not allowed
key = "${var.environment}/terraform.tfstate" # โ Not allowed
region = "ap-southeast-1"
}
}
Both lines fail. Even string interpolation with ${} syntax is blocked inside backend blocks.
Fix 1: Use -backend-config Flag (Quick)
Strip the dynamic values out of the backend block entirely. Keep only static values โ or leave an empty skeleton:
terraform {
backend "s3" {
region = "ap-southeast-1" # static values only
}
}
Pass the dynamic values at init time via -backend-config:
terraform init \
-backend-config="bucket=my-tfstate-prod" \
-backend-config="key=prod/terraform.tfstate"
Or use a separate config file per environment:
# backends/prod.hcl
bucket = "my-tfstate-prod"
key = "prod/terraform.tfstate"
region = "ap-southeast-1"
terraform init -backend-config=backends/prod.hcl
Most CI/CD setups handle this by setting ENV as a pipeline variable, then calling terraform init with the matching .hcl file for that environment.
Fix 2: Partial Backend Configuration (Recommended for Teams)
Put only the truly static parts inside the backend block. Leave dynamic fields empty โ Terraform will prompt you at init time or accept them via flags.
terraform {
backend "s3" {
# Only what never changes goes here
region = "ap-southeast-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
Wire it up in your init script:
#!/bin/bash
ENV=${1:-staging}
terraform init \
-backend-config="bucket=myapp-tfstate-${ENV}" \
-backend-config="key=${ENV}/terraform.tfstate"
No duplicated backend blocks across environments โ just one clean main.tf and separate .hcl files per environment.
Fix 3: Use Terragrunt (for Complex Multi-Environment Setups)
Managing many environments with heavy backend variation? Terragrunt solves this natively. It generates the backend config dynamically before calling Terraform:
# terragrunt.hcl
remote_state {
backend = "s3"
config = {
bucket = "myapp-tfstate-${local.environment}"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "ap-southeast-1"
}
}
Terragrunt handles the dynamic generation before Terraform ever sees the backend block. Clean separation, zero workarounds.
Verification
After the fix, re-run terraform init and check the output:
terraform init -backend-config=backends/prod.hcl
# Expected output:
# Initializing the backend...
# Successfully configured the backend "s3"!
Confirm the state backend is connected:
terraform state list
An existing workspace returns your resources. A new workspace returns an empty list. Either way, no error means the backend is working.
To inspect the resolved backend config directly:
cat .terraform/terraform.tfstate | python3 -m json.tool | grep -A10 '"backend"'
Structuring Your Backend Files
For multi-environment backends, a layout like this scales well:
infra/
โโโ main.tf # backend block with static values only
โโโ variables.tf
โโโ backends/
โ โโโ dev.hcl
โ โโโ staging.hcl
โ โโโ prod.hcl
โโโ scripts/
โโโ init.sh # wrapper: terraform init -backend-config=backends/$ENV.hcl
Each .hcl file holds the environment-specific backend values. Commit them to version control โ they don't contain secrets, just bucket names and key paths.
Common Mistakes After the Fix
- Running
terraform planwithout re-runninginit: Changed your backend config approach? Always re-runterraform initfirst. - Leaving
-reconfigureout when switching backends: Moving an existing workspace to a new backend location requiresterraform init -reconfigure. - Mixing backend config sources: Don't specify the same key in both the
backendblock and a-backend-configfile. The flag-provided value wins, but split responsibilities cleanly to avoid confusion.
Tip: Validate Your HCL Files Before Init
Hand-editing .hcl or .tfvars files is error-prone. Before burning a terraform init run on a syntax mistake, sanity-check the structure first. The YAML โ JSON Converter on ToolCraft is useful here โ HCL structure maps closely to JSON, so it catches mismatched brackets and bad nesting quickly. Everything runs in-browser, nothing gets uploaded.

