Input Variables
Input variables are the parameters of a Terraform module. Instead of hard-coding values like an instance size or a region directly into your resources, you declare a variable once and let callers supply the value at plan time. This is what turns a one-off configuration into a reusable module that can describe development, staging, and production environments from the same code. Everything here applies equally to OpenTofu, which shares Terraform’s HCL2 language and variable semantics.
Declaring a variable
You declare an input variable with a variable block. The label after the keyword is the variable’s name, and the most useful arguments inside the block are type, default, and description.
variable "instance_type" {
type = string
default = "t3.micro"
description = "EC2 instance size for the application tier."
}
variable "region" {
type = string
description = "AWS region to deploy into."
}
The type argument constrains what values are accepted and lets Terraform catch mistakes early. The description is surfaced in terraform plan prompts and by documentation tools, so treat it as part of the module’s public API. The default makes a variable optional — if you omit it, the variable becomes required and Terraform will refuse to run until a value is provided.
Tip: Always write a
description, even for “obvious” variables. It is the cheapest documentation you will ever produce, and it shows up automatically when someone runsterraform planwithout a value.
Referencing a variable
Inside the same module, you read a variable’s value with the var. prefix followed by the variable name. References work anywhere an expression is allowed — resource arguments, locals, outputs, and string interpolation.
provider "aws" {
region = var.region
}
resource "aws_instance" "app" {
ami = "ami-0c7217cdde317cfec"
instance_type = var.instance_type
tags = {
Name = "app-${var.region}"
}
}
Note that var. only refers to declared input variables. Trying to reference an undeclared name is a configuration error, not a silent empty string.
Why variables make configs reusable
Without variables, deploying the same stack to two environments means copying the code and editing values by hand — a recipe for drift. With variables, the structure lives in one place and only the values change. A single module can be instantiated with different inputs:
module "web_dev" {
source = "./modules/web"
region = "us-east-1"
instance_type = "t3.micro"
}
module "web_prod" {
source = "./modules/web"
region = "us-west-2"
instance_type = "m6i.large"
}
The same idea applies at the root: you keep one configuration and feed it environment-specific values through .tfvars files or CLI flags, rather than maintaining parallel copies.
Supplying values
Terraform collects variable values from several sources, applied in a defined order of precedence (later wins):
| Source | Example | Notes |
|---|---|---|
| Default in the block | default = "t3.micro" | Lowest priority; the fallback. |
| Environment variable | TF_VAR_region=us-east-1 | Prefix the name with TF_VAR_. |
terraform.tfvars / *.auto.tfvars | region = "us-east-1" | Loaded automatically. |
-var-file flag | -var-file=prod.tfvars | Explicit file. |
-var flag | -var="region=us-east-1" | Highest priority. |
If a required variable has no value from any source, Terraform prompts for it interactively (in automation, that becomes a hard failure instead):
terraform apply -var="instance_type=m6i.large"
Output:
var.region
AWS region to deploy into.
Enter a value: us-east-1
Terraform will perform the following actions:
# aws_instance.app will be created
+ resource "aws_instance" "app" {
+ ami = "ami-0c7217cdde317cfec"
+ instance_type = "m6i.large"
+ id = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Complex types
Variables are not limited to single strings. You can declare structured inputs with collection and object types, which is how you pass rich configuration into a module cleanly.
variable "subnet_cidrs" {
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24"]
description = "CIDR blocks for application subnets."
}
variable "tags" {
type = map(string)
default = {}
description = "Tags applied to every resource."
}
You then index or iterate over these values — for example, var.subnet_cidrs[0] or for_each = var.tags. See the dedicated page on variable types for the full type system, including object, tuple, and optional attributes.
Best Practices
- Give every variable a clear
descriptionand an explicittype; never rely on Terraform inferring the type. - Make a variable required (omit
default) when there is no safe universal value, and optional (provide adefault) only when there genuinely is one. - Keep variable values out of source control by using
.tfvarsfiles for secrets and ignoring them in.gitignore. - Name variables for intent (
instance_type,environment) rather than provider mechanics, so the module reads well at the call site. - Prefer one root module fed by per-environment
.tfvarsfiles over copy-pasted directories per environment. - Use complex types (
object,map) to group related inputs instead of a long flat list of loosely connected strings.