Sign in
← Infrastructure as Code
12 min · medium · Lesson 4 of 5

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:

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

moved {
  from = yandex_compute_instance.web
  to   = module.web.yandex_compute_instance.this
}

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.

Done reading?

Mark the lesson as read to move your track progress forward.

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.

Get access for $49

Tell the agent: use MCP izi, give me lesson iac-basics-04-terraform-modules