Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Terraform By James Joyner IV · · 9 min read

Terraform Error Guide: 'Variables not allowed' — Use Literals or Partial Backend Config

Quick answer

Fix 'Variables not allowed' in Terraform: replace var./local. references in backend and terraform{} blocks with literals or a -backend-config partial config.

  • #terraform
  • #iac
  • #troubleshooting
  • #errors
Free toolkit

Fixing errors like this? Get 500 free DevOps AI prompts

500 copy-paste AI prompts for the stack you actually run — one PDF, free.

Overview

A handful of places in a Terraform configuration are read during the very early initialization and module-loading phase, long before variables, locals, or resource attributes have values. The terraform settings block, the backend/cloud configuration, module source and version, and required-provider version constraints all fall into this category. When you put a var., local., path., or resource reference in one of them, Terraform rejects it at parse time:

Error: Variables not allowed

  on backend.tf line 4, in terraform:
   4:     bucket = var.state_bucket

Variables may not be used here.

The message is the same regardless of the exact spot — backend block, terraform.required_version, a provider’s version, or a module source. It is not saying your variable is undefined; it is saying that this location categorically cannot contain any expression that depends on evaluation. These settings must be static, constant values so Terraform can act on them before it has a chance to compute anything.

Symptoms

  • terraform init or terraform validate fails with Variables not allowed / Variables may not be used here and points at a terraform {}, backend, provider, or module line.
  • The referenced expression contains var.something, local.something, path.module, data.*, or a resource attribute.
  • It occurs even though the variable is declared and has a default — the value is irrelevant here; the reference is what is disallowed.
  • Common trigger lines: bucket = var.… / key = var.… inside a backend block, version = var.… inside required_providers, count/version on a provider block, or source = var.… on a module.
  • Replacing the reference with a hard-coded literal makes the error vanish.

Common Root Causes

  • Parameterized backend blocks — trying to set bucket, key, region, or dynamodb_table from var. or local..
  • Dynamic terraform.required_version or provider version — pulling a version constraint from a variable.
  • Variable-driven module sourcesource = var.module_source or building a registry path with interpolation.
  • var./local. in the terraform {} or cloud {} block — organization, workspace name, or hostname sourced from a variable.
  • Meta-argument confusion — putting count/for_each on a provider block, or expecting provider blocks to be dynamic; provider configuration cannot be conditionally generated this way.
  • Reusing one config for many environments by trying to interpolate the state key or bucket per environment inside the backend block.

Diagnostic Workflow

Reproduce with validate, which parses the configuration without contacting a backend:

terraform init -backend=false
terraform validate
terraform fmt -check    # confirms it is a semantic, not formatting, error

The most common offender — a backend block reading variables:

# backend.tf  -- INVALID: variables may not be used here
terraform {
  backend "s3" {
    bucket = var.state_bucket
    key    = "${var.env}/terraform.tfstate"
    region = var.region
  }
}

The provider/version variant is just as common:

# INVALID: version constraint cannot come from a variable
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = var.aws_provider_version
    }
  }
  required_version = var.tf_version
}

For the backend case, the supported pattern is partial configuration: leave the dynamic keys out of the block and supply them at init time via -backend-config:

# backend.tf  -- VALID partial config: only static, or omitted, values
terraform {
  backend "s3" {
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true
  }
}
# supply the per-environment values at init time
terraform init \
  -backend-config="bucket=acme-tfstate" \
  -backend-config="key=prod/network/terraform.tfstate"

# or from a file
terraform init -backend-config=backend-prod.hcl

Example Root Cause Analysis

An engineer wanted one root module to serve dev, staging, and prod, so they tried to make the state key follow an env variable:

terraform {
  backend "s3" {
    bucket = "acme-tfstate"
    key    = "${var.env}/network/terraform.tfstate"
    region = "us-east-1"
  }
}

terraform init immediately failed with Variables not allowed ... Variables may not be used here, flagging the key line. The backend is resolved before variables exist, so it can never interpolate var.env. The fix was to move the dynamic part out of the block and into partial config supplied per environment. The static parts stayed inline; the changing key came from a small per-env file:

# backend.tf
terraform {
  backend "s3" {
    bucket  = "acme-tfstate"
    region  = "us-east-1"
    encrypt = true
  }
}
# backend-prod.hcl
key = "prod/network/terraform.tfstate"
terraform init -backend-config=backend-prod.hcl

For the sibling problem — a version constraint pulled from var.aws_provider_version — there is no partial-config equivalent; version constraints simply must be literals. The team hard-coded version = "~> 5.0" in required_providers and centralized the value through a .terraform.lock.hcl file and a shared module instead.

Prevention Best Practices

  • Treat terraform {}, backend, cloud, and provider version as constants — never reference var., local., path., data., or resources inside them.
  • Parameterize backends with partial config: keep static keys inline and pass environment-specific ones via -backend-config files or KEY=VALUE pairs at init.
  • Pin provider and Terraform versions as literals and manage them through .terraform.lock.hcl and shared modules, not variables.
  • Keep module source literal; if you need different implementations, use count/for_each over multiple module blocks or restructure, rather than interpolating the source.
  • Use a wrapper (Terragrunt, a Makefile, or a CI script) to inject per-environment -backend-config consistently, so no one is tempted to reintroduce var. into the backend.
  • Run terraform validate in CI to catch a stray variable reference in a static block before it reaches anyone else.

Quick Command Reference

# Reproduce without touching a real backend
terraform init -backend=false
terraform validate

# Initialize a partial backend with inline key=value overrides
terraform init \
  -backend-config="bucket=acme-tfstate" \
  -backend-config="key=prod/network/terraform.tfstate"

# Initialize a partial backend from a per-environment file
terraform init -backend-config=backend-prod.hcl

# Re-point an already-initialized directory to new partial config
terraform init -reconfigure -backend-config=backend-staging.hcl

# Format and validate before committing
terraform fmt
terraform validate

Conclusion

Variables not allowed is not about a missing variable — it is Terraform telling you that a setting is read before any evaluation happens and therefore must be a static literal. The classic offenders are backend blocks, required_version, provider version constraints, and module source. For backends, the sanctioned escape hatch is partial configuration supplied through -backend-config at init time; for version constraints and module sources, use literals and manage variation through lock files, shared modules, or a CI wrapper. Keep those early-phase blocks constant and the error disappears for good.

Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.