When designing a reusable Terraform module, how do you decide which input variables get a default and which are left required?
answer
- no default means required
- -input=false turns it into a hard failure
- identity, cost, security stay required
- default to the safe end
- changing a default is a behaviour change
basics
~20 sOmit the default when the caller must decide — identity, placement, and anything with security or cost consequences — so Terraform refuses to run without it. Give a default only where one value is correct for every caller and being wrong is cheap and visible.
solid answer
~50 sA `variable` with no `default` is required: Terraform prompts for it interactively, and in CI with `-input=false` it fails the run outright. That failure is a feature, and it is how you decide. Leave required anything that is environment identity or placement — `vpc_id`, `environment`, `name` — and anything where a wrong value is expensive or unsafe, such as instance sizing, retention, or allowed CIDR ranges. Give a default only when one value is genuinely right for every caller and a wrong choice is cheap and visible. When you do default, default to the *safe* end: an empty list of ingress CIDRs rather than `0.0.0.0/0`, encryption on, deletion protection on. Use `default = null` for "leave it to the provider's own default", since passing `null` to a resource argument makes Terraform omit it. And remember defaults are part of your published API: changing one produces a plan diff for every caller who never set that variable, without any of them editing a line.
code
hcl · 17 linesvariable "vpc_id" {
description = "VPC the cluster is created in"
type = string
# no default: the caller must decide, and CI fails without it
}
variable "ingress_cidrs" {
description = "CIDR blocks allowed to reach the service"
type = list(string)
default = [] # safe end of the range, not 0.0.0.0/0
}
variable "kms_key_arn" {
description = "Customer-managed key ARN; null uses the AWS-managed key"
type = string
default = null # let the provider decide
}go deeper
Know that a variable without a default is required and that Terraform will prompt for it or fail in CI, and that description and type belong on every variable you declare.
Explain the mechanics that make the choice matter: -input=false converts prompts into hard failures, and passing null makes Terraform fall back to the provider's own default.
Demonstrate the judgment — identity, cost and security inputs stay required so mistakes stop the pipeline, defaults sit at the safe end of the range, and a default change is treated as a behaviour change for every silent caller.
Own where organisational policy lives: whether retention, encryption and sizing defaults belong inside general-purpose modules or in a thin standards wrapper, and how default changes are communicated across every consuming team.
## The mechanic first In Terraform, a `variable` block that omits `default` is **required**. If the caller does not supply a value: - Interactively, Terraform prompts on the terminal for it. - With `-input=false` — which is how every CI pipeline should run — the run fails immediately with an error naming the variable. So the choice between required and defaulted is really the choice between "the pipeline stops and asks a human" and "the module quietly picks for you". Everything below is about which of those you want. ```hcl variable "vpc_id" { description = "VPC the cluster is created in" type = string # no default — the caller must decide } ``` ## Leave it required when the caller must decide Three categories belong on the required side: **Identity and placement.** `name`, `environment`, `vpc_id`, `subnet_ids`, `account`, `region`. There is no universally correct value; a default here is either meaningless or actively dangerous, because a caller who forgets the argument gets a *working* plan pointed at the wrong place. The failure mode of a missing required variable is an error message. The failure mode of a wrong default is production traffic in the wrong VPC. **Anything with a cost or capacity consequence.** Instance types, node counts, provisioned throughput, backup retention, multi-AZ toggles. If getting it wrong shows up on an invoice or in a capacity incident, make someone type it. **Anything with a security consequence.** Which CIDR ranges may reach the service, which principals may assume a role, whether a bucket is public. A module that defaults these is deciding another team's security posture in a file they never read. ## Default when there is one right answer Give a default when the value is genuinely uninteresting to most callers and cheap to get wrong: a `tags` map that starts empty, a log retention period matching your organisation's standard, a naming suffix, a feature toggle that is off. Every default you add removes a decision from a caller — that is its value, and the test is whether the decision was ever theirs to make. When you do default, **default to the safe end of the range**, because the default is what an inattentive caller ships: ```hcl variable "ingress_cidrs" { description = "CIDR blocks allowed to reach the service" type = list(string) default = [] # deny by default, never 0.0.0.0/0 } ``` Defaults that are convenient (`0.0.0.0/0`, encryption off, deletion protection off, a public subnet) are the ones that show up in incident reviews, because the module made them easy and invisible. ## The null default `default = null` is a distinct and useful third state. Passing `null` as a resource argument makes Terraform behave as if the argument were not written at all, so the provider applies its own default. That lets a module say "optional, and if you say nothing I will not force a choice": ```hcl variable "kms_key_arn" { description = "Customer-managed key ARN; null uses the AWS-managed key" type = string default = null } ``` It is also the idiom for optional blocks and for conditionally-created resources, and it keeps the module from encoding a guess about what the provider's default should be. ## Defaults are published API This is the point most candidates miss. A default is not a private implementation choice — it is the value every caller who stayed silent is running with. Change `instance_type` from `t3.small` to `t3.medium` in a module and, on the next upgrade, every silent caller's plan shows a replacement they did not ask for. Change a default retention from 7 days to 1 and you have quietly deleted a compliance control across an estate. Practical consequences: - Treat a default change as a behaviour change, not a tidy-up, and call it out in the changelog. - Prefer *adding* a new optional variable with a default that preserves today's behaviour over changing an existing default. - Where a default is genuinely a policy decision (retention, encryption, sizing), consider making it required in the module and defaulting it once in a thin organisation-level wrapper, so the policy lives in one reviewable place instead of inside a general-purpose module. ## The failure you are choosing between Every input is a choice about *where the mistake surfaces*. Required means it surfaces at plan time, in CI, with the variable's name in the error. Defaulted means it surfaces later, in the infrastructure, with no error at all. Modules age well when the expensive, irreversible and security-relevant decisions are the ones that stop the pipeline.
- What actually happens in CI when a required variable is not supplied?Terraform normally prompts on the terminal, but pipelines run with `-input=false`, which turns the prompt into an immediate error naming the missing variable. That is the desired outcome: the run stops before touching infrastructure, and the message tells the caller exactly which decision they skipped.
- Why use default = null rather than picking a value yourself?Passing `null` to a resource argument makes Terraform omit it entirely, so the provider's own default applies. That keeps the module from hard-coding a guess about provider behaviour, and it gives you a clean way to express an optional setting or to drive a conditional without inventing a sentinel value.
- A module maintainer wants to change a default from t3.small to t3.medium. What do you tell them?That it is a behaviour change, not a cleanup. Every caller who never set the variable gets a replacement in their next plan without editing anything. Prefer leaving the default alone and letting callers opt in, or if the change is genuinely required, flag it prominently as breaking so consumers upgrade deliberately.
saying these in an interview costs you the question
- Defaults every variable so the module is easy to call
- Defaults ingress CIDRs to 0.0.0.0/0 for convenience
- Thinks a missing required variable is caught at apply, not plan
- Treats changing a default as a non-breaking cleanup
- Defaults environment or vpc_id to a production value