Variables, Outputs & Modules — Reusable Blueprints
A blueprint that says "the kitchen is here" is only useful if the kitchen size can change per house. Terraform makes blueprints flexible with variables (adjustable settings), returns what you built with outputs, and packages whole blueprints into modules — the reusable blueprint libraries shared across projects.
Variables — The Adjustable Dimensions
Variables let the same configuration produce different results without editing files.
Declaring a variable
variable "bucket_name" {
type = string
description = "Name of the archive bucket"
default = "campus-library-archive"
}
type— one ofstring,number,bool,list(...),map(...),object({...}),set(...), orany.default— the value used when nothing is passed. A variable without a default must be provided.description— documentation for other readers (and it shows inplan).
Providing values — the precedence order
| # | Method | Example |
|---|---|---|
| 1 | Command-line flag | terraform apply -var="bucket_name=x" (highest) |
| 2 | .tfvars file on the command line | terraform apply -var-file="prod.tfvars" |
| 3 | terraform.tfvars or *.auto.tfvars | picked up automatically |
| 4 | Environment variable | export TF_VAR_bucket_name=x |
| 5 | default in the declaration | lowest |
Validation — rejecting bad inputs
variable "env" {
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.env)
error_message = "env must be dev, staging, or prod."
}
}
Sensitive values — secrets never printed
variable "db_password" {
type = string
sensitive = true
}
Marking a variable sensitive keeps it out of CLI output, plan output, and terraform output. But it is still stored in plaintext in state — see the golden rules in State.
Variables are the inputs to your blueprint. If a value might change between environments, it belongs in a variable, not hard-coded.
Outputs — The Delivered Keys and Addresses
After building, the builder hands over the keys. Outputs expose chosen values from a configuration — useful for the person running it and for other modules.
output "bucket_arn" {
description = "The ARN of the archive bucket"
value = aws_s3_bucket.archive.arn
}
- Printed at the end of
terraform apply. - Read anytime with
terraform output bucket_arn. - Also usable by other modules that depend on this one.
Outputs are the results of your blueprint. Expose anything a person or another module needs — ARNs, IP addresses, IDs — and keep the noisy details private.
Modules — The Reusable Blueprint Library
A module is a self-contained folder of .tf files — a complete, reusable blueprint. Instead of copying the villa design into a hundred projects, the builder keeps one master design and calls it wherever it's needed.
Module structure
modules/villa/
├── main.tf # the resources
├── variables.tf # what callers must provide
└── outputs.tf # what callers get back
The three files are a convention: inputs in, outputs out, the resources in the middle. A module with none of these files is called a root module — your main configuration.
Calling a module
module "dev_villa" {
source = "./modules/villa"
bucket_name = "library-dev-archive"
tags = { Env = "dev" }
}
source— where the module comes from: a local path (./modules/villa), or the Terraform Registry ("terraform-aws-modules/vpc/aws").- Every argument is passed to the module's input variables.
- Access results with
module.dev_villa.shed_arn.
Versioning modules
Modules from the Registry are versioned:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
}
Pin versions for the same reason you pin providers — reproducible builds. For local modules, keep them in version control so every team uses the same blueprint.
When to build a module
Build a module when the same resources are needed more than once — in several places in one config, or across several projects. The sweet spot is a handful of small, focused modules (a network, a bucket, an instance) rather than one giant module that does everything.
If you're only creating a resource once, a module just adds indirection. Start with plain resources, and extract a module when the repetition appears.
Data Sources — Reading Reality (Refresher)
Data sources complement modules nicely — a module can read existing infrastructure:
data "aws_caller_identity" "current" {}
output "account_id" {
value = data.aws_caller_identity.current.account_id
}
See Core Commands for the full resource vs data source distinction.
Next, what happens when things go wrong: the Problem Desk.