Variables, Locals, and Outputs
Variables, Locals, and Outputs
Variables are the module’s arguments. Locals are its private names. Outputs are the only values a parent should read. This chapter is that contract.
Learning goals
- Declare a variable with a type, a default, and a validation
- Load values from a tfvars file without committing a secret
- Use locals to name a repeated expression once
- Pass outputs from a child into a parent without reaching into the child’s resources
Input Variables: Types, Defaults, Validation Rules, and Sensitive Values
variable "pet_length" {
description = "Words in the generated name."
type = number
default = 2
validation {
condition = var.pet_length >= 1 && var.pet_length <= 4
error_message = "pet_length must be from 1 to 4."
}
}
variable "name_prefix" {
description = "Optional prefix. Null means no prefix."
type = string
default = null
nullable = true
}
type is what stops a string from arriving where a number belongs. default makes the variable optional. No default means the caller must supply it. description is the documentation. nullable = false rejects an explicit null when a default exists and null would be a mistake.
validation runs on the value the caller passed. It can see var.pet_length. It cannot see other resources’ computed attributes in a useful way, and it should not try. Validate shape and policy here: length, prefix, membership in a list. Validate the live infrastructure with a plan or a test, not with a variable block.
sensitive = true marks the value so plans redact it. The value is still in state and still in memory. Sensitivity is a display flag. Chapter 12’s rule stands: do not put the long-lived credential in a variable that gets stored at all if you can pass it only to a provider from the environment.
Variable Files (.tfvars): Environment-Specific Configuration Management
terraform.tfvars and *.auto.tfvars load automatically. Any other name loads with -var-file=prod.tfvars. A variable file is assignment, not logic:
pet_length = 3
Typical layout once you have more than one environment: a root module per environment, each with its own backend and a small tfvars file, rather than one root module and a pile of var-files you forget to pass. Chapter 9 explains why workspaces are a weak substitute for that split. Chapter 14 draws the directories.
Precedence, from lowest to highest: defaults, environment variables TF_VAR_pet_length, terraform.tfvars, *.auto.tfvars in name order, -var-file, -var. A value supplied later wins. In automation, pass -var-file explicitly so the file you reviewed is the file that ran. Do not rely on an auto.tfvars nobody mentioned in the pipeline.
Secrets do not go in a tfvars file that is committed. TF_VAR_state_passphrase from the pipeline’s secret store is the ordinary path. A local secret.auto.tfvars that is gitignored works on a laptop and will eventually be copied into a ticket. Prefer the environment.
Locals: Organizing Complex Logic and Avoiding Repetition
Locals are names for expressions. They are not inputs. Callers cannot set them.
locals {
prefix = var.name_prefix == null ? "" : "${var.name_prefix}-"
pet_words = var.pet_length
}
resource "random_pet" "service" {
length = local.pet_words
prefix = local.prefix == "" ? null : trimsuffix(local.prefix, "-")
}
Use a local when the same expression appears twice, or when a conditional is too long to sit inside a resource. Do not use a local as a second, undocumented input. If a caller should change it, it is a variable.
Locals can refer to resources. That reference is unknown until apply when the resource is being created, and everything downstream inherits the unknown. Keep locals that feed for_each keys on variables and static data.
Outputs: Exposing Information and Connecting Modules
output "name" {
description = "Generated pet name."
value = random_pet.service.id
}
output "state_passphrase_in_use" {
description = "Never the passphrase. Only whether one was set."
value = var.state_passphrase != null
sensitive = false
}
Outputs are the root module’s result (tofu output) and the child module’s return values (module.pets.name). Mark an output sensitive = true when its value is sensitive, or OpenTofu will refuse to let a sensitive value flow into it. The flag still does not encrypt the output. Root outputs end up available to anyone who can read the state.
Export the minimum: an id, an ARN, a name. Do not export an entire resource object. Callers will depend on attributes you did not mean to support, and a provider upgrade will break them.
Passing Data Between Modules: The Parent-Child Contract
A parent passes variables. A child returns outputs. That is the whole contract.
module "pets" {
source = "./modules/pets"
pet_length = 2
}
output "pet_name" {
value = module.pets.name
}
The parent does not read module.pets.random_pet.service.id. That path reaches into the child’s resources, couples the parent to names that are none of its business, and breaks the day the child renames a resource. If the parent needs the value, the child grows an output.
The same rule applies sideways. Two root modules do not read each other’s variables. They read each other’s outputs through remote state (chapter 7), and only the outputs. Remote state is a dependency you should notice in review, because it is an apply-order dependency between projects.
Document the contract next to the variables: units, whether null is allowed, and what the module will create. A variable named name that sometimes is a prefix is how two teams apply two different conventions into one account.
Summary
- Variables are the inputs, typed and validated. Sensitive only redacts the UI
- tfvars assign values. The pipeline should name the file it passes
- Locals name expressions. They are not a back door for inputs
- Outputs are the only supported way out of a module
- Parents pass variables and read outputs. They do not reach into child resources