Terraform: modules, variables, and outputs
How to reuse infrastructure: variables as input, outputs on the way out, and modules as functions for resources.
How to reuse infrastructure: variables as input, outputs going out, and modules as functions for resources.
Why reuse
Earlier in the track we described one resource at a time. In a real project there are dozens of
resources, and often identical ones: three nearly identical environments (dev, staging, prod),
five instances of the same VM. Copying resource blocks is bad: an edit in one place doesn't
reach the rest. Terraform solves this with variables and modules, the same way regular code solves
repetition with functions.
Variables as input
A variable (input variable) makes a config configurable: instead of a hardcoded value, you declare an input that gets set at run time.
A fragment for Yandex Cloud (the required resources, boot_disk, and network_interface
blocks are skipped, they were covered in the Terraform basics lesson):
variable "instance_count" {
type = number
default = 2
}
resource "yandex_compute_instance" "web" {
count = var.instance_count # referenced via var.name
name = "web-${count.index}"
}
type protects against typos: if you pass -var instance_count=abc, Terraform stops before the
plan with an a number is required error. default makes the variable optional. A variable
without a default must be set by whoever runs the config or calls the module.
You can set a value several ways:
- with a flag:
terraform plan -var instance_count=10; - with a file:
terraform.tfvarsand*.auto.tfvarsare picked up automatically, any other file is passed with the-var-file=prod.tfvarsflag; - with an environment variable prefixed
TF_VAR_:TF_VAR_instance_count=10.
If the same value is set in several places, the last one by order wins: environment, then
terraform.tfvars, then *.auto.tfvars, then the -var and -var-file flags in the order they
appear on the command line. So a flag beats a file, and a file beats TF_VAR_.
That's how the same code brings up 2 VMs in dev and 10 in prod, differing only in the variable's value. It's the same "one artifact, different parameters" idea from the 12-factor lesson.
Outputs going out
An output returns a value from the configuration: something you'll need after applying (the IP of a VM you created, a database address).
output "web_ip" {
value = yandex_compute_instance.web[0].network_interface[0].nat_ip_address
}
terraform apply
...
Outputs:
web_ip = "51.250.1.42"
Outputs show up at the end of apply. Later, you can read them without applying anything:
terraform output prints all of them, terraform output -raw web_ip prints one value without
quotes, handy for scripts. Outputs pass a result along: into a deploy script, into another module,
into documentation.
Modules as functions
A module is a folder with .tf files. Any configuration is already a module: the folder
where you run terraform is called the root module. You can call another folder from the root
like a function: pass variables in, get outputs out. Resources are described inside the module,
and from outside it's used as a single module block.
module "database" {
source = "./modules/postgres"
instance_count = 1
disk_size = 50
}
# the module's result is available as module.database.connection_string
The arguments in a module block are exactly the variables the module declared for itself with
variable blocks. module.database.connection_string only works if the module has an
output "connection_string". Nothing else inside the module is visible from outside: not
its resources, not its variables. The set of variables and outputs is the module's interface, like
a function's signature.
source points at where the module lives: a local folder (the path starts with ./ or ../),
a git repository, a module registry, and other sources. One postgres module gets called three
times with different variables for dev, staging, and prod, and the infrastructure stays
consistent. In state, the module's resources get an address with the call's prefix:
module.dev.terraform_data.site, module.prod.terraform_data.site.
A change in a local module reaches every call at the next plan and apply. A module from a
registry works differently: its version is pinned with the version argument, and a new version
arrives only when someone explicitly bumps it and runs terraform init -upgrade.
After you add a module block or change its source, you need terraform init: it installs
modules, even local ones. Without it, plan stops with a Module not installed error.
Common mistakes
- A mixed-up input or output name. The module expects
size_gb, but the call hasdisk: Terraform answersUnsupported argument. The reverse happens with outputs: referencingmodule.db.url, which the module never declared, givesUnsupported attribute. The fix is reading thevariableandoutputblocks in the module's folder, not changing the module to fit the call. - A required variable wasn't passed. The module's variable has no
default, and the call doesn't set it:Missing required argument. - A forgotten
terraform initafter a newmoduleblock:Module not installed. - Moving existing resources into a module without
moved. The resource's address in state changes, and Terraform plans to delete the old object and create a new one. For a VM or a database that's data loss. Amoved { from = ... to = ... }block tells Terraform it's the same object under a new address, and the plan turns into an in-place change.
moved {
from = yandex_compute_instance.web
to = module.web.yandex_compute_instance.this
}
- Environment values hardcoded inside the module. If the module has
name = "shop-prod"written in, you can't call it for dev. Anything that differs between environments gets moved into variables.
What you'll need for the tasks
The labs run on your laptop with no cloud and no Docker. Instead of VMs, there's the
terraform_data resource: it's built into Terraform, stores values in state, and doesn't need any
providers downloaded.
- Terraform 1.4 or newer (
terraform_dataappeared in 1.4), check with:terraform version. - Commands from the lab folder with the
-chdirflag:terraform -chdir=infra init,terraform -chdir=infra plan,terraform -chdir=infra apply. - See what's in state:
terraform -chdir=infra state listandterraform -chdir=infra state show <address>. - Read an output:
terraform -chdir=infra output. - Check a module's interface in its own folder: the
variableandoutputblocks.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson iac-basics-04-terraform-modules
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress