Chapter 5: Variables, Outputs & Locals#
🎯 Learning Objectives#
- Define and use input variables with proper type constraints
- Understand variable definition precedence
- Use output values to share information
- Create local values for cleaner configurations
- Implement variable validation and error handling
- Handle sensitive data correctly
5.1 Input Variables#
Input variables make Terraform configurations parameterizable and reusable.
Variable Declaration#
# variables.tf
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
}
variable "environment" {
description = "Deployment environment"
type = string
}Variable Attributes (Exam Critical)#
| Attribute | Required | Description |
|---|---|---|
type |
No (defaults to any) |
Type constraint (string, number, bool, list, map, object, etc.) |
description |
No | Human-readable description |
default |
No | Default value if none provided (variable is optional if default is set) |
validation |
No | Custom validation rules |
sensitive |
No | Marks value as sensitive in logs/outputs (default: false) |
nullable |
No | Whether variable can be null (default: false) |
Variable Definition Precedence (Exam Critical)#
Variables can be set in multiple ways. The order of precedence (highest to lowest) is:
1. Command-line flag: -var="key=value" or -var-file="custom.tfvars"
2. *.auto.tfvars files: terraform.auto.tfvars, *.auto.tfvars
3. terraform.tfvars file
4. Environment variables: TF_VAR_variable_name
5. Default value in variable declarationSetting Variable Values#
# method 1: terraform.tfvars (highest file priority)
instance_type = "t3.large"
environment = "production"
# method 2: *.auto.tfvars (alphabetically sorted)
# dev.auto.tfvars
instance_type = "t2.micro"
environment = "dev"
# method 3: Environment variables
# export TF_VAR_instance_type="t2.micro"
# export TF_VAR_environment="dev"
# method 4: Command line
# terraform plan -var="instance_type=t2.micro" -var="environment=dev"
# method 5: Custom var file
# terraform plan -var-file="production.tfvars"Variable Files Example#
# terraform.tfvars (committed to git with example values)
instance_type = "t2.micro"
environment = "dev"
region = "us-east-1"
# production.tfvars (NOT committed to git — contains real values)
instance_type = "t3.large"
environment = "production"
region = "eu-west-1"
# terraform.tfvars.example (committed to git — template for others)
instance_type = "CHANGE_ME"
environment = "CHANGE_ME"
region = "CHANGE_ME"5.2 Variable Types (Advanced)#
Collection Types#
# List
variable "availability_zones" {
type = list(string)
description = "List of AZs"
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
# Map
variable "instance_tags" {
type = map(string)
description = "Tags for all instances"
default = {
Owner = "DevOps"
ManagedBy = "Terraform"
Project = "WebApp"
}
}
# Set (unordered, unique)
variable "allowed_ports" {
type = set(number)
description = "Set of allowed ports"
default = [80, 443, 22]
# Duplicates are automatically removed
}Structural Types#
# Object (structured data with named fields)
variable "instance_config" {
type = object({
name = string
instance_type = string
ami_id = string
root_volume = optional(number, 20) # optional with default
environment = optional(string) # optional, no default → null
subnets = list(string)
tags = map(string)
})
description = "EC2 instance configuration"
}
# Tuple (sequence of different types)
variable "api_endpoint_config" {
type = tuple([
string, # endpoint URL
number, # port
bool, # https enabled
list(string) # allowed origins
])
default = [
"api.example.com",
443,
true,
["https://app.example.com"]
]
}Optional Attributes in Objects#
variable "database_config" {
type = object({
engine = string
engine_version = string
instance_class = string
storage = optional(number, 100) # optional with default 100
multi_az = optional(bool, false) # optional with default false
backup_retention = optional(number) # optional, nullable
kms_key_arn = optional(string, null) # optional, defaults to null
tags = optional(map(string), {}) # optional map
})
}
# Usage — only required fields need to be provided
database_config = {
engine = "postgres"
engine_version = "15.3"
instance_class = "db.r6g.large"
# storage, multi_az use defaults
}5.3 Variable Validation#
Basic Validation#
variable "environment" {
type = string
description = "Deployment environment"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be one of: dev, staging, prod."
}
}
variable "instance_count" {
type = number
description = "Number of EC2 instances"
validation {
condition = var.instance_count >= 1 && var.instance_count <= 100
error_message = "Instance count must be between 1 and 100."
}
}Advanced Validation Patterns#
# CIDR validation
variable "vpc_cidr" {
type = string
description = "VPC CIDR block"
validation {
condition = can(regex("^(10\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.|192\\.168\\.)", var.vpc_cidr))
error_message = "Must be a private IP range: 10.x.x.x, 172.16-31.x.x, or 192.168.x.x."
}
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "Must be a valid CIDR block."
}
}
# Regex validation
variable "domain_name" {
type = string
description = "Domain name"
validation {
condition = can(regex("^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$", var.domain_name))
error_message = "Must be a valid domain name."
}
}
# Map key validation
variable "allowed_services" {
type = map(object({
port = number
protocol = string
}))
validation {
condition = alltrue([
for k, v in var.allowed_services : v.port >= 1 && v.port <= 65535
])
error_message = "All ports must be between 1 and 65535."
}
}
# Length validation
variable "name_prefix" {
type = string
description = "Resource name prefix"
validation {
condition = length(var.name_prefix) >= 2 && length(var.name_prefix) <= 20
error_message = "Name prefix must be between 2 and 20 characters."
}
}5.4 Sensitive Variables#
variable "db_password" {
description = "Database master password"
type = string
sensitive = true
# Password will be hidden in logs and CLI output
validation {
condition = length(var.db_password) >= 16
error_message = "Database password must be at least 16 characters."
}
}
variable "api_keys" {
description = "API keys for external services"
type = map(string)
sensitive = true
}
# Usage in resource
resource "aws_db_instance" "main" {
engine = "postgres"
master_username = "admin"
master_password = var.db_password # Sensitive value
}
# Output sensitive values carefully
output "db_password" {
value = aws_db_instance.main.master_password
sensitive = true
# This output will be marked as sensitive and not displayed in plaintext
}Best Practices for Secrets#
# NEVER do this
variable "db_password" {
default = "password123" # ❌ Hardcoded secret!
}
# Instead, use:
# 1. Environment variables: TF_VAR_db_password
# 2. AWS Secrets Manager
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/db/password"
}
resource "aws_db_instance" "main" {
master_password = data.aws_secretsmanager_secret_version.db_password.secret_string
}
# 3. HashiCorp Vault
# 4. Prompt during apply (terraform apply asks for variable)5.5 Output Values#
Output values display information about your infrastructure after terraform apply.
Basic Outputs#
# outputs.tf
output "instance_id" {
description = "ID of the EC2 instance"
value = aws_instance.web.id
}
output "instance_public_ip" {
description = "Public IP of EC2 instance"
value = aws_instance.web.public_ip
}
output "instance_arn" {
description = "ARN of the EC2 instance"
value = aws_instance.web.arn
}Output Commands#
# Show all outputs
terraform output
# Show specific output
terraform output instance_public_ip
# Show output in JSON format
terraform output -json
# Show in raw format (no quotes)
terraform output -raw instance_public_ip
# Use output in shell scripts
INSTANCE_IP=$(terraform output -raw instance_public_ip)Output Types#
# Output from count resources
output "instance_ids" {
description = "IDs of all EC2 instances"
value = aws_instance.web[*].id
}
# Output from for_each resources
output "instance_map" {
description = "Map of instance names to IDs"
value = { for k, v in aws_instance.web : k => v.id }
}
# Output with description
output "vpc_info" {
description = "VPC ID and CIDR block"
value = {
id = aws_vpc.main.id
cidr = aws_vpc.main.cidr_block
subnet_ids = aws_subnet.public[*].id
}
}
# Sensitive output
output "db_password" {
value = random_password.db.result
sensitive = true
description = "Database master password"
# Optional: set to false to disable sensitive behavior
# depends_on = [aws_db_instance.main]
}Output Preconditions (Terraform 1.2+)#
output "instance_public_ip" {
value = aws_instance.web.public_ip
precondition {
condition = aws_instance.web.public_ip != ""
error_message = "Instance must have a public IP assigned."
}
}
output "lb_dns_name" {
value = aws_lb.web.dns_name
precondition {
condition = can(regex("^.*\\.elb\\.amazonaws\\.com$", aws_lb.web.dns_name))
error_message = "Load balancer must have a valid DNS name."
}
}5.6 Local Values#
Local values (or locals) assign names to expressions, making configurations DRY and more readable.
Basic Locals#
# locals.tf
locals {
# Simple value
service_name = "web-app"
# Computed value
instance_type = var.environment == "production" ? "t3.large" : "t2.micro"
# Combined string
name_prefix = "${var.project}-${var.environment}"
# Computed from resources
vpc_id = var.create_vpc ? aws_vpc.main[0].id : data.aws_vpc.existing[0].id
}Advanced Locals#
locals {
# Common tags for all resources
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "Terraform"
Owner = var.owner
CreatedAt = timestamp()
}
# Security group rules from variable
ingress_rules = [
for rule in var.ingress_rules : {
port = rule.port
protocol = rule.protocol
cidr_blocks = rule.cidr_blocks
description = rule.description
} if rule.enabled
]
# Subnet CIDR calculations
public_subnet_cidrs = [
for i in range(var.az_count) :
cidrsubnet(var.vpc_cidr, 8, i)
]
private_subnet_cidrs = [
for i in range(var.az_count) :
cidrsubnet(var.vpc_cidr, 8, i + var.az_count)
]
# Merged tags
all_tags = merge(
local.common_tags,
var.additional_tags,
{ Name = "${local.name_prefix}-instance" }
)
}Using Locals#
# Using computed tags
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = local.instance_type
tags = local.all_tags
}
resource "aws_s3_bucket" "logs" {
bucket = "${local.name_prefix}-logs"
tags = local.all_tags
}
# Using computed subnet CIDRs
resource "aws_subnet" "public" {
count = var.az_count
vpc_id = aws_vpc.main.id
cidr_block = local.public_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
tags = local.all_tags
}5.7 Variable vs Locals — When to Use Which#
| Scenario | Variable | Locals | Explanation |
|---|---|---|---|
| User-provided value | ✅ | ❌ | Variables accept user input |
| Computed value | ❌ | ✅ | Locals can use expressions |
| Default value | ✅ | ❌ | Variables have default support |
| Used across modules | ✅ | ❌ | Variables passed as arguments |
| File scoped | ❌ | ✅ | Locals are module-scoped |
| Sensitive values | ✅ (sensitive) | ❌ | Variables support sensitive flag |
| Validation | ✅ | ❌ | Variables support validation blocks |
| Type constraints | ✅ | ❌ | Variables support type constraints |
Practical Example: When to Use Each#
# Variables: Things the user should set
variable "environment" {
type = string
description = "Deployment environment"
}
variable "instance_count" {
type = number
default = 2
description = "Number of instances"
}
# Locals: Things computed from variables/resources
locals {
name_prefix = "myapp-${var.environment}"
# ↑ Computed from variable
tags = merge(
var.common_tags, # From variable (user input)
{ Name = local.name_prefix, Environment = var.environment }
# ↑ Locals can reference other locals and variables
)
}5.8 Complete Variable Patterns#
Pattern 1: Simple Reusable Module#
# variables.tf
variable "name" {
type = string
description = "Resource name"
}
variable "environment" {
type = string
description = "Environment"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Invalid environment."
}
}
variable "instance_config" {
type = object({
instance_type = string
ami_id = optional(string)
root_volume = optional(number, 20)
user_data = optional(string)
})
description = "EC2 instance configuration"
}
# locals.tf
locals {
name_prefix = "${var.name}-${var.environment}"
common_tags = {
Name = local.name_prefix
Environment = var.environment
ManagedBy = "Terraform"
}
}
# outputs.tf
output "instance_id" {
value = aws_instance.this.id
}
output "instance_public_ip" {
value = aws_instance.this.public_ip
}Pattern 2: Multi-Environment Configuration#
# environments/prod/terraform.tfvars
environment = "production"
instance_count = 5
instance_type = "t3.large"
enable_monitoring = true
vpc_cidr = "10.0.0.0/16"
# environments/dev/terraform.tfvars
environment = "dev"
instance_count = 1
instance_type = "t2.micro"
enable_monitoring = false
vpc_cidr = "10.1.0.0/16"
# Deploy different environments
cd environments/prod && terraform apply
cd environments/dev && terraform apply📝 Exam Tips#
- Variable precedence:
-varflag >*.auto.tfvars>terraform.tfvars>TF_VAR_> default - Variables are optional if they have a
default— Required if no default is set sensitive = truehides values in logs and plan output- Outputs display after apply — Use
terraform outputto view them later - Locals are not user-configurable — They’re computed values within a module
optional()makes object attributes optional (Terraform 1.3+)validationblocks useconditionanderror_messagecan()function checks if an expression would succeed- Outputs with
preconditionvalidate that conditions are met - Never hardcode secrets — Use env vars, secrets manager, or prompt
✅ Chapter 5 Quiz#
-
What is the highest precedence for setting a variable value?
- a) Default value
- b)
terraform.tfvars - c)
-varcommand line flag - d)
TF_VAR_environment variable
-
Which attribute makes a variable’s value hidden in logs?
- a)
hidden - b)
private - c)
sensitive - d)
secret
- a)
-
How do you make an object field optional in Terraform 1.3+?
- a)
optional(type) - b)
nullable(type) - c)
default = null - d)
optional(type, default)
- a)
-
True or False: Local values can be set by users using
.tfvarsfiles. -
Which command shows output values after apply?
- a)
terraform show - b)
terraform output - c)
terraform plan - d)
terraform state list
- a)
📌 Answers
- c —
-varcommand line flag has the highest precedence - c —
sensitive = truehides values in logs and output - d —
optional(type, default)makes object fields optional - False — Locals are module-scoped computed values, not user-configurable
- b —
terraform outputdisplays output values
Continue to → Chapter 6: State Management