Defaults & Required Variables
Every input variable in Terraform is either optional or required, and the line between them is drawn by a single argument: default. Declaring a default makes a variable optional and gives it a fallback value; omitting it makes the variable required, forcing the caller to supply a value before any plan or apply can run. Choosing well between these two states is one of the most consequential design decisions in a reusable module, because it shapes how forgiving — or how foolproof — your configuration is.
How defaults decide optional vs. required
A variable with no default is required. Terraform will not proceed until it receives a value from the CLI, a .tfvars file, an environment variable, or an interactive prompt. A variable with a default is optional: if no value is supplied anywhere, Terraform silently uses the default.
# Required — no default, caller MUST provide a value
variable "vpc_id" {
type = string
description = "ID of the VPC the resources are deployed into."
}
# Optional — falls back to the default when omitted
variable "instance_type" {
type = string
description = "EC2 instance size for the application nodes."
default = "t3.micro"
}
The default must be a literal value and must match the variable’s declared type. You cannot reference other variables, resources, locals, or functions inside a default — it is evaluated before the rest of the configuration exists. If you need a value derived from other inputs, use a local value instead.
Setting
default = nullis a special case: the variable stays optional, but its value becomesnullwhen omitted. This is the idiomatic way to express “optional with no fallback,” letting downstream logic detect the absence with a conditional rather than failing outright.
Prompting behavior
When a required variable has no value from any source, Terraform stops and prompts for it interactively:
terraform apply
Output:
var.vpc_id
ID of the VPC the resources are deployed into.
Enter a value:
The prompt shows the variable’s description, which is one more reason to always write good descriptions. Interactive prompting is convenient for ad-hoc local runs but is fatal in automation: a CI pipeline or terraform apply -auto-approve job will hang or error because there is no TTY to answer. Pass -input=false to turn prompting off and fail fast instead:
terraform apply -input=false -auto-approve
Output:
╷
│ Error: No value for required variable
│
│ on variables.tf line 1:
│ 1: variable "vpc_id" {
│
│ The root module input variable "vpc_id" is not set, and has no default value.
│ Use a -var or -var-file command line argument to provide a value for this variable.
╵
This behavior is identical in OpenTofu (tofu apply -input=false), so the same discipline applies regardless of which CLI your team standardizes on.
Choosing good defaults
A default is a promise that this value is safe and sensible for the common case. The art is deciding which variables deserve one.
| Variable kind | Default? | Rationale |
|---|---|---|
| Identity / placement (VPC, account, region binding) | No — required | Wrong value is silently destructive; force a choice. |
Sizing & tuning (instance_type, retention days) | Yes | Sensible baseline; teams override only when needed. |
Feature toggles (enable_logging) | Yes (true/false) | Most callers want one mode; make it explicit. |
| Secrets & credentials | No — required | Should never live in code; force external injection. |
| Tags / metadata | Yes ({} or a base map) | Empty default keeps callers from having to pass it. |
Prefer the most conservative safe value for a default. For sizing, default down (cheaper, smaller) so an accidental omission doesn’t provision an expensive fleet. For security toggles, default to the secure posture (encryption on, public access off):
variable "enable_encryption" {
type = bool
description = "Whether to enable server-side encryption on the bucket."
default = true
}
variable "tags" {
type = map(string)
description = "Additional tags applied to all resources."
default = {}
}
resource "aws_s3_bucket" "data" {
bucket = "devcraftly-data"
tags = var.tags
}
resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
count = var.enable_encryption ? 1 : 0
bucket = aws_s3_bucket.data.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
Complex defaults are allowed too — maps, lists, and objects can all carry literal defaults, which is especially useful with optional() object attributes:
variable "scaling" {
type = object({
min_size = optional(number, 1)
max_size = optional(number, 3)
})
description = "Auto Scaling Group bounds."
default = {}
}
Here both attributes have per-field defaults via optional(), and the whole object defaults to {}, so a caller can omit scaling entirely or override just one field. See variable types for the full object/optional syntax.
Best Practices
- Make a variable required whenever a wrong value is destructive, environment-specific, or a secret — fail loudly rather than guess.
- Give every truly optional variable a default so consumers can adopt your module with minimal configuration.
- Default to the safe, conservative, low-cost option: smaller sizes, encryption on, public access off.
- Use
default = nullfor “optional with no fallback” and branch on the null value in your resource logic. - Always run automation with
-input=falseso missing required variables fail immediately instead of hanging on a prompt. - Keep defaults as literals; move any derived or computed value into a local value.
- Pair every variable — required or not — with a clear
description, since it surfaces in prompts and generated docs.