Hedronite · Cert Lesson · HashiCorp · Sat 2026-09-26

Terraform Associate: type constraints optional() attributes and nullable

Converts before it checks. Unknown attributes drop. Null keeps null unless nullable = false.

Lesson Class: Cert (T2 · Terraform Associate 003 Obj. 8 · Pro-depth)
Vendor: HashiCorp
Verified: every output real · Terraform 1.14.3 · provider-free module
Paired Ops: ECR registry scanning + Rust census
Paired Dev: Rust serde over plan JSON
Grounding: Brikman pp.115-117 · Lab 16 · Lab 02 referenced
Typo Tripwire
Make security-relevant attributes required; keep optional() for tuning knobs.
A default can quietly absorb a typo.

<!-- hal:authoritative:yaml -->

A type constraint converts before it checks. optional() fills gaps. nullable decides what null means. Each one can accept an input you meant to reject.

§I. Frame: Associate objectives for tonight

09-23 covered data sources and depends_on. 09-20 covered dynamic blocks, for_each, count and splat. 09-17 covered ephemeral and write-only values. 09-14 covered import and moved. 09-11 covered backends. Leave all of that.

Tonight's stems test how a variable block treats the value it is given: the type constraint, the optional() modifier inside object(...), and the nullable argument. Ops tonight declares ECR repositories. A module wrapping them would take map(object({...})) input, and that is the shape below.

Every output here is real, from Terraform 1.14.3 on the lab Mac, in a root module with variables and outputs only. No provider, no cloud calls.

§II. The test module

variable "repos" {
  type = map(object({
    mutability   = optional(string, "IMMUTABLE")
    scan_on_push = optional(bool, true)
    kms_key_arn  = optional(string)
    keep_images  = optional(number, 50)
  }))
}

variable "region" {
  type     = string
  default  = "us-east-1"
  nullable = false
}

variable "extra_tags" {
  type    = map(string)
  default = { team = "platform" }
}

Input file:

repos = {
  api    = {}
  worker = { mutability = "MUTABLE", keep_images = "20" }
  legacy = { scan_on_push = "false", kms_key_arn = null }
}
region     = null
extra_tags = null

§III. Five claims, each from a real run

Claim one. optional(type, default) fills a missing attribute; optional(type) fills it with null. api = {} came back as keep_images = 50, mutability = "IMMUTABLE", scan_on_push = true, kms_key_arn = tostring(null). The null is typed: it is a string-typed null, and the output shows it that way.

Claim two. Conversion runs before any check. keep_images = "20" became the number 20. scan_on_push = "false" became the bool false. Terraform converts a string to a number or bool whenever the text parses. Brikman lists the constraint types (string, number, bool, list, map, set, object, tuple, any) and advises always declaring one (PDF p. 115). Declaring a type is not the same as refusing strings. Only a value that cannot convert fails:

Error: Invalid value for input variable
  on bad1.tfvars line 1:
   1: repos = { api = { keep_images = "fifty" } }
The given value is not suitable for var.repos declared at main.tf:1,1-17: a
number is required.

Claim three. An object type drops attributes it does not declare, without a warning. Input repos = { api = { mutable = "MUTABLE" } } has a typo: mutable, not mutability. The plan succeeded with no error and no warning. The result:

> var.repos.api
{
  "keep_images" = 50
  "kms_key_arn" = tostring(null)
  "mutability" = "IMMUTABLE"
  "scan_on_push" = true
}

The typo vanished and the default applied. Here that default happens to be the safe value. Invert the default and the same typo ships a mutable registry.

Claim four. A required attribute turns that typo into an error. Same input, with mutability = string (no optional):

Unsuitable value for var.repos set using -var="repos=...": element "api":
attribute "mutability" is required.

Typo Tripwire (named technique): make the attributes that carry a security decision required, and leave optional() for tuning knobs. Bootcamp Lab 16 already has this split: versioning = bool is required, while lifecycle_days = optional(number) and tags = optional(map(string), {}) are optional.

Claim five. nullable decides whether null means "use the default". region has nullable = false, so passing null produced the default: output region = "us-east-1". extra_tags leaves nullable at its default of true, so passing null set it to null and ignored the default. terraform console confirmed var.extra_tags == null is true, and after apply the output was simply missing: terraform output extra_tags returned Error: Output "extra_tags" not found. Terraform does not store an output whose value is null.

§IV. Exam traps

  1. **"A number type rejects the string \"20\"."** False. It converts. Only non-numeric text fails.
  2. "Unknown object attributes cause a validation error." False for input variables in Terraform 1.14. They are discarded.
  3. **"Passing null to a variable with a default uses the default."** Only with nullable = false. The default is nullable = true, which keeps the null.
  4. **"optional(string) means the attribute defaults to an empty string."** False. It defaults to null.
  5. **"A validation block will catch the typo."** Not after conversion. On the lab Mac, condition = !can(var.x.mutable) passed with mutable in the input, because the key was already gone when the condition ran.

§V. Practice

Question 1
A variable is declared type = object({ size = optional(number, 10) }) and receives { size = "25" }. What does var.x.size hold?
tap to reveal
The number 25. The string converts because it parses as a number. The default 10 applies only when size is absent.
Question 2
A variable has default = "eu-west-1" and no nullable argument. A caller passes null. What is the value inside the module?
tap to reveal
null. nullable defaults to true, so null is a valid value and the default is not used. Set nullable = false to make null fall back to the default.
Question 3
A module input is map(object({ encrypted = optional(bool, false) })). A caller writes { db = { encrypt = true } }. What happens at plan?
tap to reveal
No error. encrypt is not a declared attribute, so it is dropped, and encrypted takes its default false. Making encrypted a required bool would have failed the plan instead.
Question 4
Which declaration makes a missing attribute an error while still allowing callers to omit tags?
tap to reveal
object({ name = string, tags = optional(map(string), {}) }). name is required, and tags becomes an empty map when omitted.

§VI. Drill (Lab 16 primary)

  1. Open the study notes/tfpro-labs/labs/16-filtered-foreach-outputs-broken/. Read the buckets variable and explain why versioning is required while lifecycle_days is optional.
  2. In a scratch root module, reproduce claim three: misspell an optional attribute and confirm the plan passes. Then make it required and confirm the plan fails.
  3. Add nullable = false to a variable with a default, pass -var 'x=null', and check the value with terraform console.

Success: you can predict, before running, whether a given input converts, drops, defaults, or errors.

§VII. Close instruction

File three flash lines: converts before it checks; unknown attributes drop silently; null keeps null unless nullable = false. Maghrib owns quiz.html. Pair: Ops ECR registry scanning and census; Dev Rust serde over plan JSON.

Related