TerraformPilot

DevOps

Terraform Variables vs Outputs: Inputs, Outputs, and Data Flow

Understand Terraform variables (inputs) and outputs. Learn types, validation, defaults, sensitive values

LLuca Berton1 min read

Variables are inputs — values passed into your configuration. Outputs are results — values exposed after terraform apply. Together they define how data flows in and out of Terraform modules.

Variables (Inputs)

#

Basic Variable

#
variable "instance_type" {
  type        = string
  description = "EC2 instance type"
  default     = "t3.micro"
}
 
resource "aws_instance" "web" {
  instance_type = var.instance_type
}

Variable Types

#
# String
variable "environment" {
  type    = string
  default = "dev"
}
 
# Number
variable "instance_count" {
  type    = number
  default = 3
}
 
# Boolean
variable "enable_monitoring" {
  type    = bool
  default = true
}
 
# List
variable "availability_zones" {
  type    = list(string)
  default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
 
# Map
variable "instance_types" {
  type = map(string)
  default = {
    dev  = "t3.micro"
    prod = "t3.xlarge"
  }
}
 
# Object
variable "database" {
  type = object({
    engine         = string
    engine_version = string
    instance_class = string
    multi_az       = bool
  })
  default = {
    engine         = "postgres"
    engine_version = "16.3"
    instance_class = "db.t3.micro"
    multi_az       = false
  }
}
 
# Tuple
variable "subnet_config" {
  type = tuple([string, number, bool])
  # ["10.0.1.0/24", 1, true]
}

Setting Variables

#
# 1. CLI flag
terraform apply -var="instance_type=t3.large"
 
# 2. tfvars file
terraform apply -var-file="prod.tfvars"
 
# 3. Environment variable
export TF_VAR_instance_type="t3.large"
terraform apply
 
# 4. Auto-loaded files
# terraform.tfvars
# terraform.tfvars.json
# *.auto.tfvars
# *.auto.tfvars.json
# prod.tfvars
environment    = "prod"
instance_type  = "t3.xlarge"
instance_count = 5
enable_monitoring = true

Variable Precedence (Highest to Lowest)

#
  1. -var and -var-file on CLI
  2. *.auto.tfvars (alphabetical order)
  3. terraform.tfvars
  4. TF_VAR_* environment variables
  5. Variable default value
  6. Interactive prompt (if no default)

Validation

#
variable "environment" {
  type = string
 
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}
 
variable "instance_type" {
  type = string
 
  validation {
    condition     = can(regex("^t3\\.", var.instance_type))
    error_message = "Only t3 instance types are allowed."
  }
}
 
variable "cidr_block" {
  type = string
 
  validation {
    condition     = can(cidrhost(var.cidr_block, 0))
    error_message = "Must be a valid CIDR block."
  }
}

Sensitive Variables

#
variable "db_password" {
  type      = string
  sensitive = true   # Hidden in plan/apply output
}
terraform plan:
~ password = (sensitive value)

Note: sensitive = true hides the value in CLI output but does NOT encrypt it in state.

Required vs Optional

#
# Required — no default, must be provided
variable "vpc_id" {
  type        = string
  description = "VPC ID to deploy into"
}
 
# Optional — has a default
variable "enable_logging" {
  type    = bool
  default = true
}

Outputs

#

Basic Output

#
output "instance_id" {
  description = "EC2 instance ID"
  value       = aws_instance.web.id
}
 
output "public_ip" {
  description = "Public IP address"
  value       = aws_instance.web.public_ip
}
terraform apply
# Outputs:
# instance_id = "i-0abc123def456"
# public_ip = "54.210.167.99"
 
# Query specific output
terraform output public_ip
# "54.210.167.99"
 
# JSON format (for scripts)
terraform output -json

Sensitive Outputs

#
output "db_password" {
  value     = random_password.db.result
  sensitive = true
}
terraform output db_password
# (sensitive — use terraform output -raw db_password to see)
terraform output -raw db_password
# MyS3cur3P@ss

Conditional Outputs

#
output "bastion_ip" {
  value       = var.create_bastion ? aws_instance.bastion[0].public_ip : null
  description = "Bastion host IP (null if bastion disabled)"
}

Data Flow Between Modules

#

This is where variables and outputs work together:

Root Module
├── variables.tf     (inputs from user)
├── main.tf          (calls child modules)
├── outputs.tf       (exposes results)

├── modules/networking/
│   ├── variables.tf   (inputs from root)
│   ├── main.tf        (creates resources)
│   └── outputs.tf     (exposes VPC ID, subnet IDs)

└── modules/compute/
    ├── variables.tf   (inputs from root + networking outputs)
    ├── main.tf        (creates resources)
    └── outputs.tf     (exposes instance IPs)

Module Variables (Inputs)

#
# modules/networking/variables.tf
variable "vpc_cidr" {
  type = string
}
 
variable "environment" {
  type = string
}

Module Outputs

#
# modules/networking/outputs.tf
output "vpc_id" {
  value = aws_vpc.main.id
}
 
output "private_subnet_ids" {
  value = aws_subnet.private[*].id
}

Wiring Modules Together

#
# Root main.tf
module "networking" {
  source = "./modules/networking"
 
  vpc_cidr    = var.vpc_cidr         # Root variable → module input
  environment = var.environment
}
 
module "compute" {
  source = "./modules/compute"
 
  vpc_id     = module.networking.vpc_id              # Module output → module input
  subnet_ids = module.networking.private_subnet_ids
}
 
# Root outputs.tf
output "vpc_id" {
  value = module.networking.vpc_id    # Module output → root output
}

Data flow:

User (tfvars) → Root variables → Module inputs → Resources

User ← Root outputs ← Module outputs ← Resource attributes

Best Practices

#
  1. Always add description — it shows up in terraform plan and documentation
  2. Always add type — catches errors early
  3. Use validation for constrained values (environments, CIDR blocks, instance types)
  4. Mark secrets sensitive — prevents accidental exposure in logs
  5. Group by file: variables.tf, outputs.tf, locals.tf
  6. Use objects for related values instead of many individual variables
# ❌ Too many individual variables
variable "db_engine" { ... }
variable "db_version" { ... }
variable "db_instance_class" { ... }
variable "db_multi_az" { ... }
 
# ✅ One object variable
variable "database" {
  type = object({
    engine         = string
    version        = string
    instance_class = string
    multi_az       = bool
  })
}

Hands-On Courses

#

Conclusion

#

Variables are inputs (what goes into your config), outputs are results (what comes out). They form the interface of every Terraform module — variables define what the caller must provide, outputs define what the module exposes. Use types and validation to catch errors early, mark secrets sensitive, and use object types to group related inputs.

#Terraform#DevOps#IaC#HCL

Share this article