Why variables exist
Without variables, each environment needs its own HCL file. With variables, one HCL file handles multiple environments through separate value sets.
Without variables:
resource "aws_s3_bucket" "demo" {bucket = "my-app-prod-logs" # hardcoded
}
With variables:
variable "env" {type = string
default = "dev"
}
resource "aws_s3_bucket" "demo" { bucket = "my-app-${var.env}-logs"}
Now the same HCL runs with -var="env=prod" for production, with no code changes.
A minimal variable
variable "bucket_name" {}This is a valid variable. No type is declared (defaults to any), no description, no default, so you must supply a value from outside. If you omit it, apply fails with a prompt.
All variable fields
variable "bucket_name" {type = string # expected type (see below)
description = "S3 bucket name. Must be globally unique."
default = "my-default-bucket-12345" # used when no external value is set
nullable = false # disallow null (default true)
sensitive = false # mark to hide from log output
ephemeral = false # in-memory only, not written to state (1.10+)
validation {condition = length(var.bucket_name) >= 3 && length(var.bucket_name) <= 63
error_message = "Bucket name must be between 3 and 63 characters."
}
validation { condition = can(regex("^[a-z0-9][a-z0-9.-]*[a-z0-9]$", var.bucket_name))error_message = "Bucket name: lowercase letters, digits, dots, and hyphens only."
}
}
Not all fields are required. At minimum you need type and, when there is no default, a value supplied from outside.
Variable types
Variables accept primitive and complex types:
# primitives
variable "region" { type = string }variable "count" { type = number }variable "enabled" { type = bool }# collections
variable "tags" { type = map(string) }variable "azs" { type = list(string) }variable "values" { type = set(string) } # unique, unordered# object (like map, but with a fixed structure)
variable "db" { type = object({instance_class = string
allocated_storage = number
multi_az = bool
})
}
# no type constraint, anything is accepted (poor practice for production)
variable "anything" {}If a value does not match the declared type, Terraform fails with a clear error. See hcl-types.
Validation: correctness checks
Conditions that Terraform checks before using a variable:
variable "env" {type = string
description = "Environment (dev/staging/prod)"
validation {condition = contains(["dev", "staging", "prod"], var.env)
error_message = "env must be one of: dev, staging, prod."
}
}
Validation runs at plan time. If condition returns false, apply does not start, and Terraform shows the error_message.
You can add multiple validation blocks to one variable. All of them are checked.
sensitive: protection from log output
variable "db_password" {type = string
sensitive = true
}
Effect:
- In
planandapplyoutput, the value is replaced with(sensitive value). - Any output that references this variable is also masked (unless the output is explicitly marked sensitive on its own).
- The state file still stores the value in plain text.
sensitiveaffects CLI output, not encryption.
For real secrecy, use Vault, AWS Secrets Manager, or at minimum an encrypted remote state with restricted access.
Using a variable
variable "env" {type = string
default = "dev"
}
resource "aws_s3_bucket" "demo" { bucket = "my-app-${var.env}-bucket" tags = {Environment = var.env
}
}
The var. prefix is required. Without it, Terraform treats the name as a resource or data reference.
Common pitfalls
-
Always declare a type. Omitting it works, but type errors only surface late. A type annotation catches the problem at validation time.
-
defaultis not a fallback value. It is the value used when nothing is supplied from outside. When an external value is present,defaultis ignored. -
sensitiveis not encryption. It masks output in the CLI. In state, the value is stored in plain text. -
Variables cannot reference each other.
variable "b" { default = var.a }is invalid. For dependent expressions, use tf-locals. -
Validation only runs at plan time. When a value comes from a data source and is "known after apply", validation may miss the error.
-
Variables are global to the configuration. There are no block-scoped variables. When you need a local expression, use
locals { ... }.