Skip to main content

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

Key Takeaway

Understand Terraform variables (inputs) and outputs. Learn types, validation, defaults, sensitive values, and how data flows between modules and root configurations.

Table of Contents

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.

🚀

Level Up Your Terraform Skills

Hands-on courses, books, and resources from Luca Berton

Luca Berton
Written by

Luca Berton

DevOps Engineer, AWS Partner, Terraform expert, and author. Creator of Ansible Pilot, Terraform Pilot, and CopyPasteLearn.