TerraformPilot

OpenTofu

OpenTofu Early Variable Evaluation: Dynamic Backends and Module Sources

OpenTofu's early evaluation lets you use variables and locals in backend configuration, module sources, and required_providers — features Terraform doesn't...

LLuca Berton2 min read

Early variable evaluation is OpenTofu's answer to one of Terraform's oldest pain points: backend blocks, module sources, and required_providers versions cannot reference variables or locals. OpenTofu lifts this restriction, enabling fully parameterised, environment-driven configuration without code generation or terragrunt.

The Terraform limitation

#

In Terraform you cannot write:

# Terraform: ERROR — variables not allowed in backend
terraform {
  backend "s3" {
    bucket = var.state_bucket
    key    = "${var.env}/terraform.tfstate"
    region = var.region
  }
}

The standard workaround is partial config + -backend-config= files:

terraform init \
  -backend-config="bucket=tfstate-prod" \
  -backend-config="key=prod/terraform.tfstate" \
  -backend-config="region=eu-west-1"

Functional, but verbose, hard to template, and unfriendly to GitOps.

OpenTofu: the same code works

#

OpenTofu evaluates variables and locals early enough for backend, module, and provider blocks to consume them:

variable "env"           { type = string }
variable "state_bucket"  { type = string }
variable "region"        { type = string default = "eu-west-1" }
 
terraform {
  backend "s3" {
    bucket = var.state_bucket
    key    = "${var.env}/terraform.tfstate"
    region = var.region
  }
}

Run with:

tofu init -var="env=prod" -var="state_bucket=tfstate-prod"
tofu plan

Or via a tfvars file:

# prod.tfvars
env          = "prod"
state_bucket = "tfstate-prod"
region       = "eu-west-1"
tofu init -var-file=prod.tfvars

Dynamic module sources

#

A common Terraform anti-pattern is duplicating wrapper modules per environment. With OpenTofu:

variable "module_version" { type = string default = "1.4.0" }
 
module "vpc" {
  source  = "git::ssh://git@github.com/acme/tf-modules.git//vpc?ref=${var.module_version}"
  cidr    = "10.0.0.0/16"
}

Roll forward by bumping module_version in a tfvars file — no code change.

Dynamic required_providers

#

Pin different provider versions per environment without forking the module:

variable "aws_provider_version" {
  type    = string
  default = "~> 5.70"
}
 
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = var.aws_provider_version
    }
  }
}

Useful for canarying a new provider release in staging while prod stays on the known-good version.

What's allowed and what isn't

#

Early evaluation works for:

  • terraform.backend.<type> arguments
  • module.<name>.source and version
  • terraform.required_providers.<name>.source and version
  • terraform.cloud (HCP Terraform integration)

It does not work for:

  • Resource attributes that other resources depend on at parse time (still needs the regular evaluation order).
  • Provider configuration that depends on resource outputs (cycle).

The constraint: early-evaluated values must come from variable defaults, -var/-var-file, environment variables (TF_VAR_*), or pure locals that don't reference resources/data sources.

Migration: removing partial-config workarounds

#

Before (Terraform):

terraform {
  backend "s3" {}     # everything in -backend-config files
}
terraform init -backend-config=envs/prod.s3.tfbackend

After (OpenTofu):

terraform {
  backend "s3" {
    bucket = var.state_bucket
    key    = var.state_key
    region = var.region
  }
}
tofu init -var-file=envs/prod.tfvars

A single *.tfvars per environment now drives everything — backend, module versions, providers, resources. No more shadow .tfbackend files.

Combined example

#
variable "env"            { type = string }
variable "module_version" { type = string }
variable "aws_version"    { type = string default = "~> 5.70" }
 
locals {
  state_bucket = "tfstate-${var.env}"
  state_key    = "${var.env}/network/terraform.tfstate"
}
 
terraform {
  required_version = ">= 1.8.0"
 
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = var.aws_version
    }
  }
 
  backend "s3" {
    bucket  = local.state_bucket
    key     = local.state_key
    region  = "eu-west-1"
    encrypt = true
  }
}
 
module "vpc" {
  source = "git::ssh://git@github.com/acme/tf-modules.git//vpc?ref=${var.module_version}"
  env    = var.env
}

One module, one workflow, every environment.

Caveat: Terraform compatibility

#

If you ever plan to migrate back to HashiCorp Terraform, keep the backend block static — Terraform 1.x will reject var.* references with a parse error. Many teams gate this with feature flags or maintain a backend.terraform.tf and backend.opentofu.tf pair. See Terraform vs OpenTofu: Key Differences for the full divergence list.

#

Conclusion

#

Early evaluation removes the single most-asked-for missing feature in Terraform. Combined with state encryption and an open registry, it makes OpenTofu the more flexible choice for teams that have hit the limits of partial backend config and per-environment file duplication.

#OpenTofu#Variables#Backends#Modules#HCL

Share this article