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" { instance_type = var.instance_type availability_zone = var.azs[0] instance_type = var.instance_types[var.environment] 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 |
REMEMBER THIS**Remember:** Variable declarations in `variable {}` blocks are like function signatures — they define what can be passed. Values in `.tfvars` files or `TF_VAR_` env vars are like function arguments — they provide the actual data.
COMMON MISTAKE / WARNING**Common Mistake:** Putting database passwords in `terraform.tfvars` and committing it to Git. Anyone with repo access then has the production database password. Use `TF_VAR_` environment variables for all secrets, or read them from AWS Secrets Manager at apply time.