Input Variable
A Terraform input variable is a parameter that makes a module or configuration reusable by letting callers pass in different values at runtime. Variables are declared with a variable block and can have a type, default value, description, and validation rules.
What Is a Terraform Input Variable?
A Terraform input variable is the way you parameterise your configuration. Instead of hardcoding "ap-south-1" in ten places, you declare it once as variable "aws_region" and reference it everywhere as var.aws_region. When you want to change the region, you change one value — not ten.
Think of variables as the blanks in a template. Your Terraform configuration is the template — write it once. The .tfvars file fills in the blanks differently for dev, staging, and production.
At Zerodha, the same Terraform configuration provisions the payments API infrastructure in three environments. The only difference between environments is the variable values: smaller instances in dev, larger in prod, deletion protection off in dev, on in prod. Variables make this zero-duplication.
+------------------------------------------+| dev.tfvars || environment = "dev" || instance_type = "t3.micro" |+------------------------------------------+ |+------------------------------------------+| prod.tfvars || environment = "prod" || instance_type = "t3.large" |+------------------------------------------+ | v+------------------------------------------+| variable "environment" { type = string } || variable "instance_type" { type = string}|+------------------------------------------+ | v+------------------------------------------+| resource "aws_instance" "app" { || instance_type = var.instance_type || } |+------------------------------------------+Declaring Variables
Every variable must be declared in a variable block before it can be used:
# variables.tf # Minimum declaration — just a name and typevariable "aws_region" { type = string} # Full declaration — best practicevariable "environment" { description = "Deployment environment — controls naming, sizing, and protection" type = string default = "dev" # used if no value is provided by the caller validation { condition = contains(["dev", "staging", "prod"], var.environment) error_message = "Must be dev, staging, or prod." }}Variable Types
# Primitive typesvariable "region" { type = string } # "ap-south-1"variable "port" { type = number } # 8080variable "enable_logs" { type = bool } # true or false # Collection typesvariable "azs" { type = list(string) default = ["ap-south-1a", "ap-south-1b"]} variable "instance_types" { type = map(string) default = { dev = "t3.micro" prod = "t3.large" }} variable "allowed_ports" { type = set(number) # set = no duplicates default = [80, 443, 8080]} # Structural types — named attributes with different typesvariable "database" { type = object({ engine = string instance_class = string storage_gb = number multi_az = bool }) default = { engine = "postgres" instance_class = "db.t3.medium" storage_gb = 20 multi_az = false }} # List of objects — for dynamic blocksvariable "ingress_rules" { type = list(object({ port = number description = string cidr_blocks = list(string) })) default = []} # any — Terraform infers the type from the valuevariable "flexible" { type = any}Variable Validation
variable "project_name" { type = string # Multiple validation blocks are allowed validation { condition = can(regex("^[a-z0-9-]+$", var.project_name)) error_message = "Project name: only lowercase letters, numbers, hyphens." } validation { condition = length(var.project_name) <= 20 error_message = "Project name must be 20 characters or fewer." }} variable "vpc_cidr" { type = string validation { # can() returns true if the expression succeeds without error condition = can(cidrnetmask(var.vpc_cidr)) error_message = "Must be a valid CIDR block (e.g., 10.0.0.0/16)." }}Sensitive Variables
variable "db_password" { description = "Database master password — pass via TF_VAR_db_password env var" type = string sensitive = true # value is hidden in plan output and logs # IMPORTANT: sensitive = true hides the value in terminal output # It does NOT prevent the value being stored in the state file # Always use an encrypted remote backend with restricted access}Referencing Variables
resource "aws_instance" "app" { # Reference with var.<name> instance_type = var.instance_type # Access list item availability_zone = var.azs[0] # Access map value instance_type = var.instance_types[var.environment] # Access object attribute tags = { Engine = var.database.engine }}Passing Values to Variables
# 1. Default value in variable block — used if nothing else provided# 2. terraform.tfvars (auto-loaded if it exists)# 3. *.auto.tfvars (auto-loaded, alphabetical order)# 4. -var-file flagterraform plan -var-file=prod.tfvars # 5. TF_VAR_ environment variablesexport TF_VAR_db_password="S3cr3tP@ss"export TF_VAR_environment="prod" # 6. -var flag — highest priority, overrides everythingterraform plan -var="environment=prod" -var="instance_type=t3.large"Troubleshooting Variables
| Error | Root Cause | Fix |
|---|---|---|
Error: No value for required variable |
Variable has no default and no value was provided | Add a default or pass a value via tfvars or -var |
Error: Invalid value for variable |
Validation condition returned false | Read the error_message in the validation block |
Error: Variables not allowed |
Using var.x inside a backend block | Backend blocks cannot use variables — hardcode or use partial config |
Error: Unsupported attribute "x" |
Wrong key name in object type | Check the object type definition |
Value shows (sensitive value) in plan |
Variable has sensitive = true | Expected — use terraform output -raw to read sensitive outputs |
RememberVariable declarations in
variable {}blocks are like function signatures — they define what can be passed. Values in.tfvarsfiles orTF_VAR_env vars are like function arguments — they provide the actual data.
Common MistakePutting database passwords in
terraform.tfvarsand committing it to Git. Anyone with repo access then has the production database password. UseTF_VAR_environment variables for all secrets, or read them from AWS Secrets Manager at apply time.
Frequently Asked Questions
What's the practical benefit of declaring type and validation on a Terraform input variable versus leaving it untyped?
An untyped variable accepts anything, so a typo like passing a string where a list was expected only surfaces as a confusing downstream error deep in resource creation. Declaring `type = string` or a structured object type makes Terraform reject bad input at plan time with a clear message, and `validation` blocks let you enforce domain rules (e.g., instance type must match a regex) before any API call is made, catching mistakes minutes earlier in the workflow.
What's a common mistake when using default values on Terraform variables?
Setting a 'safe-looking' default (like a small instance size or a specific region) on a variable meant to be explicitly supplied per environment — it silently works in dev, then someone forgets to override it and it ships to production unnoticed. For anything environment-critical (region, environment name, sizing), it's often safer to omit the default entirely so Terraform forces the caller to supply a value rather than falling back silently.