How to Fix Terraform "Variables Not Allowed" Error During Plan
When you run terraform plan, you might encounter an error saying "Variables not allowed" in certain contexts like backend configuration, provider configuration, or terraform blocks. This happens because Terraform evaluates some parts of your configuration before it processes variables, making them unavailable in these early-evaluation contexts.
Understanding where variables can and cannot be used is crucial for structuring your Terraform configurations correctly.
TLDR: Variables cannot be used in backend configuration blocks, terraform blocks, or provider aliases because these are evaluated before Terraform loads variables. Use hard-coded values, environment variables, -backend-config flags, or partial configuration files instead. For dynamic provider configuration, use locals or separate configuration files per environment. The error occurs because Terraform needs to know backend and provider details before it can process variables.
Where Variables Cannot Be Used
Variables are not allowed in these contexts:
# INVALID: Variables in terraform block
terraform {
required_version = var.terraform_version # ERROR: Variables not allowed
}
# INVALID: Variables in backend configuration
terraform {
backend "s3" {
bucket = var.state_bucket # ERROR: Variables not allowed
key = var.state_key # ERROR: Variables not allowed
region = var.aws_region # ERROR: Variables not allowed
}
}
# INVALID: Variables in provider version constraints
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = var.aws_provider_version # ERROR: Variables not allowed
}
}
}
These blocks are evaluated during Terraform's initialization phase, before variables are loaded.
Understanding Terraform Evaluation Order
Terraform evaluates configuration in this order:
1. Terraform block (required_version, required_providers)
├─> Backend configuration
└─> Provider source and version constraints
2. Provider configuration
└─> Some provider settings can't use variables
3. Variables loaded
└─> Default values from variables.tf
└─> terraform.tfvars files
└─> -var flags
4. Resources and modules
└─> Variables available here
This is why variables aren't available in early-evaluation contexts.
Solution 1: Backend Configuration With Partial Configuration
Instead of using variables in the backend block, use partial configuration:
# backend.tf - only specify the backend type
terraform {
backend "s3" {}
}
Provide the configuration via command-line flags:
terraform init \
-backend-config="bucket=my-terraform-state" \
-backend-config="key=prod/terraform.tfstate" \
-backend-config="region=us-east-1"
Or create a backend configuration file:
# backend-prod.hcl
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
Reference it during init:
terraform init -backend-config=backend-prod.hcl
For multiple environments:
config/
├── backend-dev.hcl
├── backend-staging.hcl
└── backend-prod.hcl
# Initialize for production
terraform init -backend-config=config/backend-prod.hcl
# Initialize for dev
terraform init -backend-config=config/backend-dev.hcl
Solution 2: Using Environment Variables
Backend configuration can read from environment variables:
# Set environment variables
export TF_CLI_ARGS_init="-backend-config=bucket=my-state-bucket -backend-config=key=terraform.tfstate"
# Or use AWS environment variables
export AWS_DEFAULT_REGION=us-east-1
terraform init
For the S3 backend specifically:
export AWS_REGION=us-east-1
export TF_BACKEND_BUCKET=my-terraform-state
export TF_BACKEND_KEY=prod/terraform.tfstate
terraform init \
-backend-config="bucket=$TF_BACKEND_BUCKET" \
-backend-config="key=$TF_BACKEND_KEY" \
-backend-config="region=$AWS_REGION"
Solution 3: Provider Configuration Workarounds
Some provider settings can't use variables. Here's how to work around it:
Problem: Can't use variables in provider alias:
# INVALID
provider "aws" {
alias = var.provider_alias # ERROR
region = var.aws_region
}
Solution: Use separate provider blocks:
# Define providers with hard-coded aliases
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
}
provider "aws" {
alias = "us_west_2"
region = "us-west-2"
}
# Use variables to choose which provider
resource "aws_instance" "app" {
provider = var.use_west_region ? aws.us_west_2 : aws.us_east_1
ami = var.ami_id
instance_type = "t3.medium"
}
Problem: Can't use variables in provider version constraints:
# INVALID
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = var.aws_version # ERROR
}
}
}
Solution: Hard-code version or use version files:
# versions.tf - committed to Git
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
For environment-specific versions, use separate configuration directories:
terraform/
├── environments/
│ ├── dev/
│ │ ├── versions.tf # AWS provider ~> 4.0
│ │ └── main.tf
│ └── prod/
│ ├── versions.tf # AWS provider ~> 5.0
│ └── main.tf
Solution 4: Using Locals Instead of Variables
For values computed from variables that you need early in configuration:
# Variables are loaded
variable "environment" {
type = string
}
variable "aws_region" {
type = string
}
# Locals are evaluated after variables
locals {
state_key = "${var.environment}/terraform.tfstate"
common_tags = {
Environment = var.environment
Region = var.aws_region
ManagedBy = "terraform"
}
}
# But you still can't use locals in backend config
# This won't work:
# backend "s3" {
# key = local.state_key # ERROR: Not allowed
# }
Locals don't solve the backend configuration problem but are useful elsewhere.
Solution 5: Dynamic Provider Configuration
While you can't use variables in some provider settings, you can use them in most:
variable "aws_region" {
type = string
}
variable "assume_role_arn" {
type = string
}
# This is fine - most provider arguments accept variables
provider "aws" {
region = var.aws_region
assume_role {
role_arn = var.assume_role_arn
}
default_tags {
tags = {
Environment = var.environment
ManagedBy = "terraform"
}
}
}
What you can't do is make the provider block itself conditional or use variables in the provider alias.
Solution 6: Separate Configuration Per Environment
For significantly different environments, use separate directories:
infrastructure/
├── global/
│ └── versions.tf
├── dev/
│ ├── backend.tf
│ ├── provider.tf
│ ├── main.tf
│ └── terraform.tfvars
├── staging/
│ ├── backend.tf
│ ├── provider.tf
│ ├── main.tf
│ └── terraform.tfvars
└── prod/
├── backend.tf
├── provider.tf
├── main.tf
└── terraform.tfvars
Each environment has its own backend configuration:
# dev/backend.tf
terraform {
backend "s3" {
bucket = "company-terraform-state-dev"
key = "dev/terraform.tfstate"
region = "us-east-1"
}
}
# prod/backend.tf
terraform {
backend "s3" {
bucket = "company-terraform-state-prod"
key = "prod/terraform.tfstate"
region = "us-east-1"
}
}
This eliminates the need for dynamic backend configuration.
Solution 7: Terraform Wrapper Scripts
Create scripts that set up the environment before running Terraform:
#!/bin/bash
# deploy.sh
ENVIRONMENT=$1
if [ -z "$ENVIRONMENT" ]; then
echo "Usage: $0 <environment>"
exit 1
fi
# Set backend config based on environment
case $ENVIRONMENT in
dev)
BACKEND_BUCKET="terraform-state-dev"
BACKEND_KEY="dev/terraform.tfstate"
;;
staging)
BACKEND_BUCKET="terraform-state-staging"
BACKEND_KEY="staging/terraform.tfstate"
;;
prod)
BACKEND_BUCKET="terraform-state-prod"
BACKEND_KEY="prod/terraform.tfstate"
;;
*)
echo "Unknown environment: $ENVIRONMENT"
exit 1
;;
esac
# Initialize with dynamic backend config
terraform init \
-backend-config="bucket=$BACKEND_BUCKET" \
-backend-config="key=$BACKEND_KEY" \
-backend-config="region=us-east-1"
# Apply with environment-specific vars
terraform apply -var-file="environments/${ENVIRONMENT}.tfvars"
Use it:
./deploy.sh prod
Common Scenarios and Solutions
Scenario: Different backend per environment
# Don't use variables in backend block
# Instead, use -backend-config
terraform init -backend-config=backend-${ENV}.hcl
Scenario: Different AWS accounts per environment
# Variables work fine in provider configuration
variable "aws_account_id" {
type = string
}
provider "aws" {
region = var.aws_region
assume_role {
role_arn = "arn:aws:iam::${var.aws_account_id}:role/TerraformRole"
}
}
Scenario: Need to compute backend key from variables
# Use shell variables to construct backend config
ENV="production"
BACKEND_KEY="${ENV}/terraform.tfstate"
terraform init -backend-config="key=$BACKEND_KEY"
Debugging Variables Not Allowed Errors
When you see this error, check:
- Where the variable is used:
# Find variable usage
grep -r "var\." *.tf
- The exact error message:
Error: Variables not allowed
on backend.tf line 4, in terraform:
4: bucket = var.state_bucket
Variables may not be used here.
The error shows exactly which file and line has the problem.
- Check Terraform block contents:
# Review your terraform blocks
terraform {
# No variables allowed anywhere in here
required_version = ">= 1.5"
backend "s3" {
# No variables allowed in backend config
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # Must be hard-coded
}
}
}
Terraform Cloud Workspaces Alternative
If using Terraform Cloud, workspace variables can replace backend configuration:
# No backend configuration needed with Terraform Cloud
terraform {
cloud {
organization = "my-org"
workspaces {
name = "production" # Hard-coded workspace name
}
}
}
Configure different workspaces for each environment in the Terraform Cloud UI, avoiding the need for dynamic backend configuration.
Using Data Sources for Dynamic Values
While you can't use variables in terraform blocks, you can use data sources to fetch dynamic values for use in resources:
# Fetch current AWS account info
data "aws_caller_identity" "current" {}
# Fetch current region
data "aws_region" "current" {}
resource "aws_s3_bucket" "state" {
bucket = "terraform-state-${data.aws_caller_identity.current.account_id}"
tags = {
Region = data.aws_region.current.name
}
}
This doesn't help with backend configuration but is useful for making other parts of your config dynamic.
The "Variables not allowed" error is Terraform enforcing its evaluation order. Backend and provider configuration happen before variables are loaded, so they must use hard-coded values, command-line flags, or environment variables. Structure your configuration to work with these constraints rather than trying to work around them.
Found an issue?