Skip to main content

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​

variables.tf
variable "bucket_name" {
type = string
description = "Name of the archive bucket"
default = "campus-library-archive"
}
  • type — one of string, number, bool, list(...), map(...), object({...}), set(...), or any.
  • default — the value used when nothing is passed. A variable without a default must be provided.
  • description — documentation for other readers (and it shows in plan).

Providing values — the precedence order​

#MethodExample
1Command-line flagterraform apply -var="bucket_name=x" (highest)
2.tfvars file on the command lineterraform apply -var-file="prod.tfvars"
3terraform.tfvars or *.auto.tfvarspicked up automatically
4Environment variableexport TF_VAR_bucket_name=x
5default in the declarationlowest

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.

Remember

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.

outputs.tf
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.
Remember

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​

Module or not?

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.