How to Fix Terraform 'Error: no value for required variable' (TF_VAR Missing)
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5 mins
TL;DR
- What broke: Terraform halted at
planorapplybecause avariableblock has nodefaultand no value was injected viaTF_VAR_*,-var, or a.tfvarsfile — Terraform refuses to proceed, full stop. - How to fix it: Supply the missing value via an exported
TF_VAR_<name>environment variable, a-varCLI flag, aterraform.tfvarsfile, or add adefaultto the variable declaration if appropriate. - Fastest path: Use our Client-Side Sandbox below to auto-refactor this — paste your failing
variables.tfand it will generate the corrected variable blocks and a matchingterraform.tfvarsstub locally without sending your config anywhere.
The Incident (What does the error mean?)
Raw error output from terraform plan:
╷
│ Error: No value for required variable
│
│ on variables.tf line 1:
│ 1: variable "db_password" {
│
│ The root module input variable "db_password" is not set, and has no default
│ value. Use a -var or -var-file command line argument to provide a value for
│ this variable.
╵
Immediate consequence: Terraform exits with code 1. No plan is generated. No diff is shown. The entire pipeline — whether local, CI, or CD — is dead in the water until every required variable is satisfied. In a CD pipeline this means a deployment gate is fully blocked, and any downstream terraform apply step never executes.
Terraform's variable resolution order is strict and deterministic:
TF_VAR_<name>environment variablesterraform.tfvarsorterraform.tfvars.jsonin the working directory*.auto.tfvarsor*.auto.tfvars.jsonfiles-varand-var-fileCLI flags (in order passed)defaultvalue in thevariableblock
If none of these yield a value, Terraform throws this error before any provider API call is made.
The Attack Vector / Blast Radius
This is not just a nuisance — the failure mode has real blast radius in automated pipelines:
CI/CD pipeline collapse: A terraform plan step that exits non-zero in GitHub Actions, GitLab CI, or Atlantis will cascade: the plan artifact is never written, the policy-as-code gate (Sentinel, OPA) never fires, and the apply job is either skipped or — worse — if your pipeline is misconfigured with continue-on-error: true, it proceeds with a stale plan from a previous run. You apply yesterday's infrastructure state against today's code.
Secret sprawl from workarounds: Engineers under pressure hardcode values directly into variables.tf as default values to unblock themselves. Secrets (db_password, api_key, private_key_pem) end up committed to version control. This is the most common downstream security incident that originates from this error.
Environment drift: When TF_VAR_* variables are set inconsistently across dev, staging, and prod pipeline runners, the same Terraform module produces different infrastructure. Variable values that exist in one environment's CI secrets store but not another cause intermittent plan failures that are hard to reproduce locally.
Workspace/module confusion: In multi-workspace or root-module-calls-child-module setups, the error message only reports the root module variable. If the missing variable is actually being passed down to a child module, the trace can be misleading and engineers waste time looking at the wrong file.
How to Fix It (The Solution)
Basic Fix — Option A: Export TF_VAR in Shell
The fastest fix for local development. Never do this with secrets in shell history — use -var-file or a secrets manager instead.
- # Variable has no value, plan fails
- terraform plan
+ export TF_VAR_db_password="$(vault kv get -field=password secret/prod/db)"
+ terraform plan
Basic Fix — Option B: terraform.tfvars File
Create a terraform.tfvars file in the working directory. Add this file to .gitignore immediately if it contains secrets.
- # No terraform.tfvars exists, or it is missing the variable
+ # terraform.tfvars
+ db_password = "changeme-local-only"
+ region = "us-east-1"
+ environment = "dev"
Basic Fix — Option C: Add a Default (Non-Secret Variables Only)
Only appropriate for non-sensitive variables like region, environment name, or instance type.
variable "region" {
description = "AWS region to deploy into"
type = string
- # No default — forces caller to always supply it
+ default = "us-east-1"
}
variable "db_password" {
description = "RDS master password"
type = string
+ sensitive = true
- # DO NOT add a default for secrets
}
Enterprise Best Practice — Secrets Manager + Ephemeral Env Injection
In production pipelines, never pass secrets as plaintext TF_VAR values. Use dynamic secret injection at plan/apply time:
- # GitLab CI / GitHub Actions — hardcoded secret in CI variable
- env:
- TF_VAR_db_password: "hardcoded-secret-123"
+ # GitHub Actions — secret fetched from AWS Secrets Manager at runtime
+ - name: Fetch DB password from Secrets Manager
+ run: |
+ export TF_VAR_db_password=$(aws secretsmanager get-secret-value \
+ --secret-id prod/rds/master-password \
+ --query SecretString \
+ --output text | jq -r .password)
+ echo "TF_VAR_db_password=$TF_VAR_db_password" >> $GITHUB_ENV
For HashiCorp Vault:
- # Static secret baked into pipeline environment
- TF_VAR_db_password: ${{ secrets.DB_PASSWORD }}
+ # Vault dynamic secret — leased, auto-rotated, audited
+ - name: Authenticate to Vault
+ uses: hashicorp/vault-action@v2
+ with:
+ url: https://vault.corp.example.com
+ method: jwt
+ secrets: |
+ secret/data/prod/rds password | TF_VAR_db_password ;
For variable validation to catch bad values early (not just missing values):
variable "environment" {
description = "Deployment environment"
type = string
+ validation {
+ condition = contains(["dev", "staging", "prod"], var.environment)
+ error_message = "environment must be one of: dev, staging, prod."
+ }
}
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Checkov — static scan for missing variable defaults on sensitive vars:
checkov -d . --check CKV_TF_1 --framework terraform
Add to your pipeline before terraform plan:
# .github/workflows/terraform.yml
- name: Checkov Scan
uses: bridgecrewio/checkov-action@master
with:
directory: ./infra
framework: terraform
soft_fail: false
2. tflint — catches undeclared variables and missing required inputs:
tflint --init
tflint --var-file=terraform.tfvars
.tflint.hcl config:
plugin "terraform" {
enabled = true
preset = "recommended"
}
rule "terraform_required_providers" {
enabled = true
}
3. OPA/Conftest — policy gate that rejects plans with sensitive variables lacking sensitive = true:
# policy/terraform_vars.rego
package terraform.variables
deny[msg] {
var := input.variables[name]
regex.match("(password|secret|key|token|credential)", lower(name))
not var.sensitive
msg := sprintf("Variable '%v' appears to be sensitive but 'sensitive = true' is not set.", [name])
}
terraform show -json tfplan.binary | conftest test --policy policy/ -
4. Required variable audit script — run before every terraform init:
#!/usr/bin/env bash
# check_tf_vars.sh — exits non-zero if any required TF_VAR is unset
REQUIRED_VARS=("db_password" "api_key" "environment" "region")
MISSING=()
for var in "${REQUIRED_VARS[@]}"; do
if [[ -z "${!var+x}" ]] && [[ -z "$(printenv TF_VAR_${var})" ]]; then
MISSING+=("TF_VAR_${var}")
fi
done
if [[ ${#MISSING[@]} -gt 0 ]]; then
echo "ERROR: Missing required Terraform variables:"
printf ' - %s\n' "${MISSING[@]}"
exit 1
fi
echo "All required TF_VAR_* variables are set."
5. Pre-commit hook — block commits that introduce required variables without defaults:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.83.5
hooks:
- id: terraform_tflint
- id: terraform_validate
- id: checkov
args:
- --args=--check CKV_TF_1