Skip to content
Infrastructure as Code iac variables 4 min read

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 = null is a special case: the variable stays optional, but its value becomes null when 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 kindDefault?Rationale
Identity / placement (VPC, account, region binding)No — requiredWrong value is silently destructive; force a choice.
Sizing & tuning (instance_type, retention days)YesSensible baseline; teams override only when needed.
Feature toggles (enable_logging)Yes (true/false)Most callers want one mode; make it explicit.
Secrets & credentialsNo — requiredShould never live in code; force external injection.
Tags / metadataYes ({} 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 = null for “optional with no fallback” and branch on the null value in your resource logic.
  • Always run automation with -input=false so 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.
Last updated June 14, 2026
Was this helpful?