06 · Variables & Outputs¶
Input variables let a configuration accept values from outside itself; outputs let it expose values to whoever ran it (or to other configurations). Together they're what makes a configuration reusable instead of hardcoded.
Declaring an input variable¶
variable "environment" {
description = "Deployment environment name"
type = string
default = "dev"
}
variable "instance_count" {
description = "Number of instances to create"
type = number
default = 1
}
variable "enable_monitoring" {
description = "Whether to enable detailed monitoring"
type = bool
default = false
}
type constrains what values are acceptable — passing a string where
number is declared fails at plan time with a clear type-mismatch error
rather than silently coercing or failing later. description isn't
functional but shows up in terraform plan prompts and generated docs, so
it's worth writing for every variable.
Complex types¶
variable "availability_zones" {
type = list(string)
default = ["us-east-1a", "us-east-1b"]
}
variable "tags" {
type = map(string)
default = {}
}
variable "database_config" {
type = object({
engine = string
instance_class = string
storage_gb = number
})
default = {
engine = "postgres"
instance_class = "db.t3.micro"
storage_gb = 20
}
}
object({...}) describes a structured value with named, typed fields —
useful for grouping related settings instead of passing five separate flat
variables.
Using a variable¶
Reference it anywhere in the configuration as var.<name>:
resource "aws_instance" "web" {
count = var.instance_count
ami = "ami-0c94855ba95c71c99"
instance_type = "t3.micro"
tags = merge(var.tags, {
Environment = var.environment
})
}
Ways to set a variable's value¶
Terraform resolves variable values from several sources, in increasing order of precedence (later wins over earlier):
# 3. *.auto.tfvars — also automatically loaded, alphabetically
# prod.auto.tfvars
environment = "prod"
# 4. -var-file on the command line
terraform apply -var-file="prod.tfvars"
# 5. -var on the command line (highest precedence among file-based options)
terraform apply -var="environment=prod"
# 6. TF_VAR_ environment variables (also read automatically, lower than -var)
export TF_VAR_environment=prod
terraform apply
A common pattern: commit a dev.tfvars with safe defaults, keep
prod.tfvars reviewed carefully (or generated by CI), and never hardcode
the environment name inside main.tf itself.
Marking a variable sensitive¶
sensitive = true tells Terraform to redact the value from plan/apply
console output (shown as (sensitive value)) — it does not encrypt it
in the state file, which is one reason module 07 flags state files as
needing careful handling.
Declaring an output¶
output "instance_ids" {
description = "IDs of the created instances"
value = aws_instance.web[*].id
}
output "bucket_arn" {
value = aws_s3_bucket.reports.arn
}
output "db_password" {
value = var.db_password
sensitive = true # required if the value is (or derives from) something sensitive
}
After terraform apply, outputs print to the console and are queryable
later:
terraform output
# bucket_arn = "arn:aws:s3:::acme-reports-2026"
# instance_ids = [
# "i-0abc123",
# ]
terraform output -json bucket_arn
# "arn:aws:s3:::acme-reports-2026"
terraform output -raw bucket_arn
# arn:aws:s3:::acme-reports-2026 (no surrounding quotes — useful in scripts)
Why outputs matter beyond just printing values¶
Outputs are also how one Terraform configuration hands values to another —
either through terraform_remote_state (Level 2) or by being passed as a
module's output (Level 2's modules topic). A configuration that provisions
a VPC typically outputs its subnet IDs specifically so a different
configuration (managed by a different team, applied separately) can
consume them without needing direct access to the VPC's own state.
How It Actually Works: variable precedence and the evaluation graph¶
Documented mechanics of how Terraform Core resolves a variable's final value and where it fits in the graph — not run against a live workspace here:
- Variables are graph nodes too. Every
variableblock becomes a node in the same dependency graph as resources. Anything referencingvar.namecreates an edge from that resource to the variable node, so the variable's final value must be fully resolved before any dependent resource is planned — this is why a variable can't (directly) depend on a resource attribute; the graph only flows one direction, from inputs toward resources. - Precedence is a strict, documented override order, evaluated once per
run, highest wins: command-line
-var/-var-fileflags, then*.auto.tfvars(alphabetically), then explicit-var-filearguments, then aterraform.tfvarsfile, thenTF_VAR_nameenvironment variables, then thedefaultin thevariableblock itself. Terraform doesn't merge complex-typed values across these sources — whichever source wins for a given variable name supplies the entire value. - Type constraints are enforced by conversion, not just validation.
When you declare
type = list(string), Terraform doesn't just check the input — it attempts to convert the raw value (which may come from environment strings, JSON in a.tfvars.jsonfile, or CLI text) into that exact type, using HCL's type conversion rules, and fails the run at parse-time if conversion isn't possible. - Outputs are graph nodes with the widest dependency fan-in. An
outputblock implicitly depends on every resource attribute it references, and — critically — a root module's outputs are recomputed every single apply even when nothing they reference changed, because Terraform re-evaluates output expressions from final state after the apply completes, rather than caching them. sensitive = trueis a display flag, not encryption. It only suppresses the value from CLI output and plan summaries; the raw value is still written in plaintext inside the state file (module 07 covers why that matters for state file handling).
Exercise¶
Write a variable block for a region string with a sensible default, a
variable block for a tags map, and an output block that exposes a
value merging var.tags with an additional ManagedBy = "Terraform" entry
(hint: the merge() function from module 03's expression syntax — full
function coverage is in Level 2). Then write the -var command-line
invocation that would override region to "eu-west-1" at apply time.