Variables and Outputs in OpenTofu
Learn how to build modular and dynamic OpenTofu configurations using variables.tf, outputs.tf, terraform.tfvars, custom validations, and complex data types.
Table of Contents
In the previous part, we wrote our entire infrastructure definition in a single main.tf file using static, hardcoded values. While sufficient for getting started, real-world deployments require flexible configurations that can be reused seamlessly across different environments (development, staging, and production).
OpenTofu automatically reads and merges all .tf files within the same working directory. Splitting your configuration across dedicated files makes your codebase clean, modular, and easy to maintain.
Modular File Structure
The standard convention in OpenTofu projects separates input declarations, resource logic, and output values into distinct files:
infra/
├── main.tf # Resource definitions and provider configuration
├── variables.tf # Input variable declarations and validation rules
├── outputs.tf # Output values to expose after apply
└── terraform.tfvars # Concrete variable values for a specific environment (optional)
1. Input Declarations (variables.tf)
Think of variables.tf as function parameters in programming. This file defines what variables the configuration accepts, their expected data types, default values, and custom validation rules.
variable "container_name" {
description = "Name of the Docker container on the host"
type = string
default = "belajar-opentofu-nginx"
}
variable "nginx_image" {
description = "Docker image name and version tag"
type = string
default = "nginx:alpine"
}
variable "external_port" {
description = "Host port forwarded to the internal container port"
type = number
default = 8080
# Custom validation rule to ensure valid port numbers
validation {
condition = var.external_port > 0 && var.external_port <= 65535
error_message = "The external port must be between 1 and 65535."
}
}
variable "enable_ssl" {
description = "Whether to enable HTTPS configuration"
type = bool
default = false
}
Anatomy of a variable Block
type: Restricts the permitted data type (e.g.,string,number,bool,list,map, orobject). While omitting this defaults toany, explicitly typing variables is a best practice.default: Fallback value used when the caller does not supply one. Variables without adefaultare mandatory (required).description: Documents the purpose of the variable.validation: A block containing a boolean expression (condition) and anerror_messagereturned if the user input fails validation.sensitive: When set totrue, OpenTofu redacts the value in console output duringtofu planandtofu apply(essential for passwords and API tokens).
2. Referencing Variables in Resources (main.tf)
Resources in main.tf no longer use hardcoded values; instead, they reference inputs via the var.<variable_name> syntax:
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 4.6.0"
}
}
}
provider "docker" {
host = "unix:///var/run/docker.sock"
}
# Pull the image specified in var.nginx_image
resource "docker_image" "nginx" {
name = var.nginx_image
keep_locally = false
}
# Create the container using input variables
resource "docker_container" "web" {
image = docker_image.nginx.image_id
name = var.container_name
ports {
internal = 80
external = var.external_port
}
}
3. Exposing Return Values (outputs.tf)
The outputs.tf file defines data to expose once tofu apply completes successfully. Outputs are useful for printing values directly to the terminal, feeding downstream CI/CD pipelines, or exposing values to root modules.
output "container_id" {
description = "Unique ID of the created Docker container"
value = docker_container.web.id
}
output "web_url" {
description = "Access URL for the web service in a browser"
value = "http://localhost:${var.external_port}"
}
output "image_digest" {
description = "SHA digest of the Docker image in use"
value = docker_image.nginx.repo_digest
}
You can view all current outputs at any time without running apply again using:
tofu output
Or query a specific output by name:
tofu output web_url
# "http://localhost:8080"
4. Supplying Variable Values (terraform.tfvars)
To override default values without modifying .tf code, create a terraform.tfvars file.
OpenTofu automatically loads terraform.tfvars whenever you run tofu plan or tofu apply:
container_name = "web-custom-staging"
external_port = 9090
nginx_image = "nginx:1.27-alpine"
Variable Precedence Hierarchy
What happens if a variable is assigned in multiple places? OpenTofu resolves conflicts according to a strict priority hierarchy (from highest to lowest):
- Direct CLI flags (
-varor-var-file):tofu apply -var="external_port=8888" - Auto-loaded files (
*.auto.tfvarsor*.auto.tfvars.json), loaded in lexicographical filename order. - The
terraform.tfvarsorterraform.tfvars.jsonfile. - Environment variables prefixed with
TF_VAR_:export TF_VAR_external_port=9000 - The
defaultattribute declared inside thevariableblock invariables.tf.
Best Practice: Add
*.tfvarsfiles containing credentials to.gitignore. Commit a sanitizedterraform.tfvars.examplefile to version control as a reference template for your team.
5. Advanced Data Types in OpenTofu
Beyond primitive types (string, number, bool), OpenTofu supports collection and structural types for complex infrastructure scenarios.
A. List (Ordered Sequence)
A list holds an ordered collection of values sharing the same data type:
variable "allowed_ports" {
description = "List of ports permitted for inbound traffic"
type = list(number)
default = [80, 443, 8080]
}
# Accessing list elements:
# var.allowed_ports[0] -> 80
# var.allowed_ports[1] -> 443
B. Map (Key-Value Pairs)
A map stores lookup tables where every value shares the same type. Great for resource tagging or environment-specific mappings:
variable "container_labels" {
description = "Metadata labels attached to the container"
type = map(string)
default = {
environment = "development"
owner = "devops-team"
project = "updeta-tutorial"
}
}
# Accessing map values:
# var.container_labels["environment"] -> "development"
C. Object (Complex Data Structure)
An object defines a typed schema with named attributes of differing types:
variable "server_config" {
description = "Comprehensive server configuration"
type = object({
hostname = string
memory_mb = number
restart_policy = string
is_production = bool
})
default = {
hostname = "nginx-server"
memory_mb = 512
restart_policy = "unless-stopped"
is_production = false
}
}
# Accessing object attributes:
# var.server_config.hostname -> "nginx-server"
# var.server_config.memory_mb -> 512
D. Combining Collections: list(object)
To manage repeating configuration blocks (such as multiple port forwards or disk mounts), use list(object):
variable "port_mappings" {
description = "Port mappings forwarded from container to host"
type = list(object({
internal = number
external = number
protocol = string
}))
default = [
{
internal = 80
external = 8080
protocol = "tcp"
},
{
internal = 443
external = 8443
protocol = "tcp"
}
]
}
# Accessing elements:
# var.port_mappings[0].internal -> 80
# var.port_mappings[0].external -> 8080
6. Hands-on Testing
Let’s test our modular setup:
1. Test Value Overrides via terraform.tfvars
Ensure terraform.tfvars specifies port 9090, then preview the changes:
tofu plan
OpenTofu reads terraform.tfvars and indicates port 9090 will be bound instead of default 8080.
2. Apply the Configuration
tofu apply -auto-approve
Once provisioning finishes, inspect the exported outputs:
Outputs:
container_id = "5a2f8c4e..."
image_digest = "sha256:..."
web_url = "http://localhost:9090"
Verify the running container via your terminal or browser:
curl http://localhost:9090
3. Verify Custom Validation
Try passing an out-of-range port number to verify that our validation block catches the invalid input:
tofu plan -var="external_port=70000"
OpenTofu halts execution immediately and prints our custom error message:
Error: Invalid value for variable
on variables.tf line 13:
13: variable "external_port" {
The external port must be between 1 and 65535.
Custom validation ensures incorrect parameters never reach target infrastructure APIs.
Summary & Next Steps
In this tutorial, you learned:
- How to structure projects across
variables.tf,main.tf,outputs.tf, andterraform.tfvars. - How to declare variables with explicit types, default fallbacks, and custom
validationrules. - OpenTofu’s variable precedence hierarchy.
- How to utilize complex data types:
list,map,object, andlist(object). - How to expose and inspect output values with
outputs.tfandtofu output.
In Part 3, we will cover State Management and Remote Backends—exploring how engineering teams collaborate safely using OpenTofu without state file conflicts using Amazon S3 or compatible object storage.