Good configuration is never hardcoded. This episode covers input variables for parameterization, local values for DRY expressions, and output values for extracting information post-apply. This is the foundation for writing OpenTofu code that's reusable across teams and multi-environment setups.

In episode 3 we declared providers, resources, and data sources. There's one problem deliberately planted in those examples: values like region and instance type are written hard into the code. Now imagine a project with 20 resources and two environments — hardcoding every value is a recipe for disaster.
Episode 4 answers that problem with three HCL tools: input variables for parameters, local values for intermediate computations, and output values for final results. Together they form the foundation for writing reusable OpenTofu code that works across different environments while staying easy for teams to review.
The variable block is the way to add "fill-in-the-blank slots" to your configuration. Its declaration:
variable "region" {
type = string
default = "ap-southeast-1"
description = "AWS region where resources are created"
}
variable "instance_type" {
type = string
description = "Instance type for the web server"
validation {
condition = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
error_message = "instance_type must be one of t3.micro, t3.small, t3.medium."
}
}The four attributes you'll use most often:
| Attribute | Purpose |
|---|---|
type | Data type: string, number, bool, list, map, object |
default | Fallback value when the caller provides none |
description | Documentation for humans and tofu console |
validation | Custom rules; plan fails when the condition isn't met |
Then var.region and var.instance_type are referenced in resources:
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
tags = {
Name = "${var.region}-web"
}
}Notice that variable references spread across many resources, but the value is set only once — that's parameterization at work.
Variable values can come from many sources. OpenTofu selects them by priority order, from lowest to highest:
| Order | Source |
|---|---|
| 1 | default value in the declaration |
| 2 | TF_VAR_<name> environment variable |
| 3 | terraform.tfvars file |
| 4 | terraform.tfvars.json file |
| 5 | *.auto.tfvars files (alphabetical order) |
| 6 | -var / -var-file arguments on the command line |
.tfvars files store values in HCL format without variable declarations:
region = "ap-southeast-1"
instance_type = "t3.micro"Then used with tofu apply -var-file=dev.tfvars. This is the basic pattern that lets one codebase serve many environments — just swap the -var-file.
Tip
Combine this with *.auto.tfvars files (e.g. prod.auto.tfvars) so they're read automatically without writing the -var-file flag. With per-environment naming, one codebase can be used for dev, staging, and prod.
locals are temporary variables computed from other values and used as intermediaries. They can't be set from outside — they're only computed inside the module. Their purpose is to avoid repeating the same long expression across many resources.
locals {
name_prefix = "devvnull-${var.region}"
common_tags = {
Project = "learn-opentofu"
Env = var.environment
}
}Then used as local.name_prefix and local.common_tags:
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
tags = local.common_tags
}The golden rule: if an expression is used more than twice, turn it into locals. This DRY principle keeps configuration consistent and easy to change in one place.
The output block exposes important information after tofu apply finishes — for example, the public IP, resource IDs, or endpoint URLs. Imagine an instance IP that can only be known after creation; without outputs, you'd have to open the cloud console to find it.
output "public_ip" {
value = aws_instance.web.public_ip
description = "Public IP address of the web server"
}
output "db_password" {
value = aws_db_instance.main.password
sensitive = true
description = "Database password (sensitive)"
}After apply, tofu output shows all values and tofu output public_ip shows a single value. The sensitive = true attribute hides the value from normal display, preventing it from leaking into CI logs.
Warning
A sensitive output hides the value from display, but the value still lives in the state file. Don't treat it as full protection — for true secrets, combine it with the state encryption covered in episode 6.
Summary of episode 4:
variable parameterizes configuration, complete with type, default, description, and validation attributes.TF_VAR, terraform.tfvars, *.auto.tfvars, then the -var flag..tfvars files store per-environment values; one codebase for many environments.locals for DRY expressions and computed constants.output for extracting post-apply values, with sensitive = true to hide secrets.Your code is now parameterized and reusable. In the next episode, episode 5, we cover the most important part of collaboration: remote state management and state locking — how to store opentofu.tfstate in a central backend like S3, GCS, and Azure Blob so the whole team works on the same state, safely protected from conflicts. See you there!