Skip to content
Infrastructure as Code iac variables 4 min read

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 runs terraform plan without 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):

SourceExampleNotes
Default in the blockdefault = "t3.micro"Lowest priority; the fallback.
Environment variableTF_VAR_region=us-east-1Prefix the name with TF_VAR_.
terraform.tfvars / *.auto.tfvarsregion = "us-east-1"Loaded automatically.
-var-file flag-var-file=prod.tfvarsExplicit 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 description and an explicit type; never rely on Terraform inferring the type.
  • Make a variable required (omit default) when there is no safe universal value, and optional (provide a default) only when there genuinely is one.
  • Keep variable values out of source control by using .tfvars files 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 .tfvars files 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.
Last updated June 14, 2026
Was this helpful?