Terraform / Language, providers, and managed identity / Lesson 3
Input contracts, validation, and outputs
Define usable module inputs and intentionally published outputs rather than relying on defaults or empty values to hide ambiguity.
On this page
Lesson goals
- Choose explicit types, defaults, nullability, and validation conditions.
- Explain root inputs, child-module arguments, and output publication.
A variable is an interface promise
A variable declaration defines an input name and can specify its type, description, default, nullability, validation, and sensitivity. The type describes value structure; validation expresses additional domain rules. A number type alone does not state that replicas must be a positive integer within an operational budget. A useful interface explains why a value exists and which combinations are supported. Keep defaults conservative and explicit, and avoid silently selecting a real account, region, backend, or destructive behavior through an undocumented default.
Validate the entire declared domain
The complete example below accepts a synthetic replica count from one through five and rejects fractions. nullable=false distinguishes this required known quantity from optional absence. Validation messages should describe the caller’s mistake without exposing secrets. These conditions are part of a teaching interface, not universal production sizing. Terraform must still evaluate the full configuration in its own context before operational use; the lesson does not run validation or any stateful command.
terraform {
required_version = ">= 1.16.0, < 1.17.0"
}
variable "replicas" {
type = number
default = 2
nullable = false
validation {
condition = var.replicas >= 1 && var.replicas <= 5 && floor(var.replicas) == var.replicas
error_message = "Replicas must be an integer from 1 to 5."
}
}
output "replicas" {
value = var.replicas
}
The default 2 satisfies every condition. Zero,6, and 2.5 violate the declared domain for different reasons. The output exposes the selected value intentionally; an output declaration does not itself create managed objects. No actual Terraform diagnostic or successful run is claimed.
Input sources depend on execution context
Root-module values can come from documented CLI input mechanisms, variable files, environment variables, and defaults under their precedence rules. Child modules receive arguments from their calling module rather than automatically reading a parent’s variable files as a separate interface. Automation systems can supply values differently again. Record the effective source and revision of important inputs. A developer’s successful local defaults do not prove that a reviewed automation run used the same values or the same workspace.
Check declared cases without invoking Terraform
The Python model checks only four finite teaching values against the stated numeric rule. It deliberately does not emulate Terraform type conversion, unknown values, null handling, or variable-source precedence. This narrow scope makes the expected results independently verifiable while keeping every operational context untouched. A real test suite should also consider missing and null input, boundary values, and meaningful error reporting using Terraform’s documented validation and testing facilities in a separately authorized environment.
for value in (0, 2, 2.5, 6):
valid = 1 <= value <= 5 and value == int(value)
print(f'{value}: {valid}')
0: False
2: True
2.5: False
6: False
The model accepts only 2 among the supplied values. Its inputs are explicitly finite numeric literals, so the conversion is bounded and does not parse untrusted text or access a workspace. It is arithmetic evidence, not a Terraform validation result.
Outputs are deliberate publication boundaries
Outputs expose values to callers, automation, or operators. Publish the smallest stable interface needed rather than an entire resource object whose shape can change with a provider upgrade. A sensitive output changes ordinary presentation but does not automatically encrypt state or remove the value from every machine-readable path. Document ownership and consumers before changing output names or types. Removing an output used elsewhere can break another configuration even if no remote resource is deleted. Interface compatibility therefore matters alongside infrastructure lifecycle.
Common errors and fixes
Invalid value for variable.
- Why it happens
- A value satisfies the broad type but violates the declared domain rule.
- How to fix it
- Correct the input or review the requirement; do not remove validation just to proceed.
- Prevent it next time
- Test boundaries and communicate the reason for each constraint.
A child module receives the wrong assumed default.
- Why it happens
- The caller did not pass an explicit argument or confused root variable sources with child inputs.
- How to fix it
- Inspect the module call and effective argument rather than editing an unrelated variable file.
- Prevent it next time
- Version module interfaces and document important caller-supplied values.
A sensitive output is treated as encrypted storage.
- Why it happens
- Redaction behavior was confused with state confidentiality and access control.
- How to fix it
- Protect the state and artifacts according to their actual sensitive contents.
- Prevent it next time
- Review storage and machine-readable consumers separately from console presentation.
Check your understanding
1. Why does 2.5 fail even though type=number accepts numbers?
Show worked solution
The additional validation requires floor(value) to equal the original value, which expresses the integer rule for this declared domain. Type and domain validation solve different problems. A valid number can still be invalid as a replica count; keep both requirements explicit rather than relying on provider rejection later.
2. Should a module output every attribute of its internal resource?
Show worked solution
Usually publish only the contract consumers need. A broad object couples callers to provider-specific details and can expose sensitive or unstable fields. Select meaningful outputs, document types and sensitivity, and review downstream compatibility when the module evolves. This decision is about interface ownership, not merely shorter code.
Key takeaways
- Type, domain validation, nullability, and defaults are separate choices.
- Publish outputs as stable interfaces with explicit sensitivity.