Terraform/OpenTofu Fundamentals
A Type Is a Checkpoint, Not Documentation
In one line
A variable's type blocks wrong values at the boundary, quietly converts values that can be made to fit, and silently discards what is not in the declaration. You must tell these three apart.
Why you should write types
Without writing types, the configuration still runs. So it is easy to skip in a hurry, and on the day you skip it, nothing happens. The problem is the day a wrong value comes in.
Without a type, that value simply passes the variable boundary. The value that passed goes through locals, through functions, flows on to a resource argument, and blows up somewhere in the end. The message then is not "the value you gave is wrong" but "this function call failed." You have to trace the value back up through the configuration file from the top to reach the cause.
If you write the type, the same mistake is blocked like this.
Error: Invalid value for input variable
on v.tfvars line 1:
1: tight = 7
number required.
The line number comes out, and what was expected comes out. The time it takes to fix is completely different.
How it works
Types divide into three branches: primitive types (string, number, bool), collection types (list, set, map), structural types (object, tuple), and any, which accepts anything.
It blocks. When a value that cannot be converted comes in, it rejects it right there. If you give a list where a number belongs, the number of elements of a tuple differs, or a required attribute of an object is missing, the plan is not even made.
It converts. If it can convert, it converts. Even if you write replicas = "3" in tfvars, 3 goes into a number variable, and debug = "true" also becomes true. Quieter still is the inside of a collection.
variable "tags" { type = map(string) }
# tags = { team = "core", rev = 3, tls = true }
# 결과 { "team" = "core", "rev" = "3", "tls" = "true" }
The number 3 became the string "3", and nobody tells you. If you later try to do arithmetic or an equality comparison with that value, it gets strange then.
It discards. Structural types have yet another property. An object accepts only the attributes you declared, and an attribute not in the declaration is not an error but discarded. If you make a typo in an attribute name, that value vanishes, and it looks as if the default was applied. This is the typical hole that type checking does not catch.
The difference between a list and a set is also worth remembering. Given the same ["b", "a", "b"], a list holds three in order, while a set discards duplicates, sorts by its own rules, and holds only two. The length changes, so code that counts is affected.
Optional attributes let you accept short inputs.
variable "service" {
type = object({
name = string
port = optional(number, 8080)
tls = optional(bool, false)
})
}
With this, the caller needs to give only name, and the rest are filled with defaults. If you do not give a default, that attribute becomes null and has to be checked again downstream, so write them together whenever possible.
The declaration that null is not accepted works in two branches. If there is a default, an incoming null is replaced by the default, and if there is no default, it is an error. It is a device that pins down in one place "what happens when there is no value."
Finally, type checking and value checking are different jobs. The type looks at the shape, and the range of the value is looked at by the validation block. The type sees whether the port is a number, but the type does not know whether that number is 1024 or more.
What you see in the field
The most common case is leaving a module's input open as any. It was left open so that callers would have it easy, but the person who actually gets it wrong sees an error inside the module. The type at a module boundary is both documentation and a contract, so it is better for everyone to write it narrowly.
The second is using a map without knowing about the conversion that happened inside it. If you put a number in a tag map and later compare with that value, it is false forever. If the value is to be used as a number, declare the element type of the map as number, or pass it through a conversion function at the place of use.
The third is a typo in an object. You mistyped one attribute name, yet the apply succeeds and only that value behaves as the default. It is hard to see in review, too. To reduce this kind of thing, the habit of exporting the value that actually went in as an output once and checking it helps.
What you will do in the next lab
You go through eight steps. You check what is blocked and what is converted in primitive types, look through an output at the quiet conversion inside a collection, and see the difference when the same input is received as a list, a set, and a tuple. Then you check the missing and extra attributes of an object, accept short inputs with optional attributes and defaults, and see the two branches of the declaration that null is not accepted. In the last two steps, you compare side by side where the error arises when you did not write the type and when you wrote it narrowly, and pin down the input specification with nested types to confirm that the wrong inputs the grader feeds in are rejected.