Variables, Outputs & Locals
Hard-coding values into Terraform resources — environment names, region strings, instance sizes — makes configurations brittle and impossible to reuse. Variables, outputs, and locals are Terraform's three mechanisms for parameterizing configurations, exposing results, and computing reusable values. Together they turn a single-purpose script into a flexible, team-shareable module.
Input Variables
An input variable declares a parameter for a configuration. It is the Terraform equivalent of a function argument — callers supply values, the configuration uses them without caring where they come from.
variable "environment" {
description = "Deployment environment (dev, staging, prod)"
type = string
default = "dev"
}
variable "instance_count" {
description = "Number of EC2 instances to create"
type = number
default = 1
}
variable "enable_monitoring" {
description = "Whether to enable detailed CloudWatch monitoring"
type = bool
default = false
}
Reference a variable anywhere in your configuration with var.<NAME>:
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
count = var.instance_count
monitoring = var.enable_monitoring
tags = {
Environment = var.environment
Name = "web-${var.environment}"
}
}
Variables declared without a default are required — Terraform will prompt for their values if not supplied through another method.
Variable Types
Terraform has a rich type system for variables. Declaring types catches mismatches early — before an apply fails mid-way through creating resources.
Primitive types
variable "region" {
type = string
default = "us-east-1"
}
variable "replicas" {
type = number
default = 3
}
variable "multi_az" {
type = bool
default = true
}
Collection types
# list — ordered sequence of values, all the same type
variable "availability_zones" {
type = list(string)
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
# map — key/value pairs, all values the same type
variable "tags" {
type = map(string)
default = {
Team = "platform"
Project = "learniac"
}
}
# set — unordered unique values
variable "allowed_cidr_blocks" {
type = set(string)
default = ["10.0.0.0/8", "172.16.0.0/12"]
}
Structural types
# object — named attributes with potentially different types
variable "database" {
type = object({
engine = string
instance_class = string
multi_az = bool
storage_gb = number
})
default = {
engine = "postgres"
instance_class = "db.t3.micro"
multi_az = false
storage_gb = 20
}
}
# Access object attributes with dot notation
resource "aws_db_instance" "main" {
engine = var.database.engine
instance_class = var.database.instance_class
multi_az = var.database.multi_az
allocated_storage = var.database.storage_gb
}
# tuple — ordered sequence with mixed types
variable "subnets" {
type = tuple([string, string, number])
default = ["10.0.1.0/24", "us-east-1a", 1]
}
any sparingly
type = any disables type checking for that variable. It can be useful for module interfaces that need to accept heterogeneous values, but in general you should declare the most specific type possible — it makes errors much easier to diagnose.
Setting Variables
Terraform accepts variable values through several mechanisms, evaluated in this order (later sources override earlier ones):
1. Default values
Defined in the variable block. The fallback when nothing else is provided.
2. Environment variables
Prefix any variable name with TF_VAR_:
export TF_VAR_environment="prod"
export TF_VAR_instance_count="5"
terraform apply
This is the standard pattern for CI/CD pipelines — set secrets as environment variables in your CI system rather than writing them to files.
3. .tfvars files (automatically loaded)
Terraform automatically loads terraform.tfvars and any file ending in .auto.tfvars:
# terraform.tfvars
environment = "prod"
instance_count = 5
enable_monitoring = true
tags = {
Team = "platform"
Project = "learniac"
}
Use separate .tfvars files per environment:
# dev.tfvars
environment = "dev"
instance_count = 1
# prod.tfvars
environment = "prod"
instance_count = 10
enable_monitoring = true
4. -var-file flag
Explicitly load a .tfvars file:
terraform apply -var-file="prod.tfvars"
5. -var flag
Set individual values on the command line:
terraform apply -var="environment=prod" -var="instance_count=5"
6. Interactive prompt
If a required variable (no default) is not set by any other method, Terraform prompts for it. Avoid relying on this in CI/CD — unprovided required variables should fail fast with a clear error.
.tfvars files that contain secrets
It is fine to commit .tfvars files with non-sensitive configuration (instance sizes, region names). Files containing API keys, passwords, or tokens should be in .gitignore. Use your CI/CD system's secret store (GitHub Secrets, GitLab CI variables) and inject values via TF_VAR_ environment variables instead.
Variable Validation
Validation rules catch invalid values before Terraform calls any provider APIs — giving users clear error messages rather than cryptic API errors:
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_type" {
type = string
description = "EC2 instance type"
validation {
condition = can(regex("^t[23]\\.", var.instance_type))
error_message = "Instance type must be a t2 or t3 class (e.g., t3.micro)."
}
}
variable "cidr_block" {
type = string
description = "VPC CIDR block"
validation {
condition = can(cidrnetmask(var.cidr_block))
error_message = "Must be a valid CIDR block (e.g., 10.0.0.0/16)."
}
}
A variable can have multiple validation blocks. All conditions must be true or Terraform reports the corresponding error_message.
Sensitive Variables
Mark a variable as sensitive = true to prevent Terraform from printing its value in plan/apply output or in error messages:
variable "db_password" {
type = string
description = "Database master password"
sensitive = true
}
variable "api_key" {
type = string
sensitive = true
}
# CLI output redacts sensitive values:
# var.db_password
# (sensitive value)
Marking a variable sensitive prevents it from appearing in CLI output, but Terraform still stores its value in the state file in plaintext. Protect the state file with encryption and access controls — sensitive variable values there are not masked.
Output Values
Output values expose information from a configuration — to the terminal after an apply, to other Terraform configurations via remote state data sources, or to calling modules.
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "load_balancer_dns" {
description = "DNS name of the application load balancer"
value = aws_lb.main.dns_name
}
output "db_endpoint" {
description = "Database connection endpoint"
value = aws_db_instance.main.endpoint
sensitive = true # Hides from CLI output; still in state
}
After an apply, Terraform prints all non-sensitive outputs:
Outputs:
load_balancer_dns = "my-app-lb-1234567890.us-east-1.elb.amazonaws.com"
vpc_id = "vpc-0abc123"
Access outputs at any time with:
# Show all outputs
terraform output
# Show a specific output value (useful in scripts)
terraform output -raw load_balancer_dns
# Show outputs as JSON
terraform output -json
Reading outputs from another configuration
Use terraform_remote_state to read outputs from a separate configuration's state:
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "network/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_instance" "app" {
subnet_id = data.terraform_remote_state.network.outputs.private_subnet_id
# ...
}
Local Values
Local values (locals) are named expressions computed once and reused throughout a configuration. Think of them as constants or intermediate computed values — like named variables in a programming language, but scoped to the configuration.
locals {
# Computed from variables
name_prefix = "${var.project}-${var.environment}"
# Common tags applied to all resources
common_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
CreatedAt = "2026"
}
# Derived values
is_prod = var.environment == "prod"
instance_type = local.is_prod ? "t3.large" : "t3.micro"
min_capacity = local.is_prod ? 3 : 1
}
Reference locals with local.<NAME> (no trailing s):
resource "aws_instance" "web" {
instance_type = local.instance_type
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web"
})
}
resource "aws_autoscaling_group" "web" {
min_size = local.min_capacity
# ...
}
Use variable for values that callers (users or parent modules) should be able to override. Use local for values that are always computed from other values and should never be overridden externally. A common pattern: accept raw inputs as variables, derive computed values as locals, and use locals in resources.
File Organization
By convention, split your configuration into these files:
| File | Contains |
|---|---|
main.tf | Resources (the core of the configuration) |
variables.tf | All variable blocks |
outputs.tf | All output blocks |
locals.tf | All locals blocks (or inline in main.tf) |
providers.tf | The terraform {} block and all provider {} blocks |
terraform.tfvars | Variable values for the default environment (do not commit secrets) |
prod.tfvars | Variable values for production (committed if no secrets) |
Here is what a minimal real-world configuration looks like with this layout:
# variables.tf
variable "project" {
type = string
default = "learniac"
}
variable "environment" {
type = string
validation {
condition = contains(["dev", "prod"], var.environment)
error_message = "Must be dev or prod."
}
}
variable "region" {
type = string
default = "us-east-1"
}
# locals.tf
locals {
name_prefix = "${var.project}-${var.environment}"
common_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
}
}
# outputs.tf
output "bucket_name" {
value = aws_s3_bucket.data.bucket
description = "Name of the S3 data bucket"
}
# terraform.tfvars (committed)
project = "learniac"
region = "us-east-1"
# dev.tfvars
environment = "dev"
# prod.tfvars
environment = "prod"
Deploy to different environments by selecting the right .tfvars file:
terraform apply -var-file="dev.tfvars"
terraform apply -var-file="prod.tfvars"
Key Takeaways
- Input variables (
variable {}) parameterize a configuration. Reference them withvar.name. Declare types and validation to catch errors early. - Set variables via defaults,
TF_VAR_environment variables,.tfvarsfiles,-var-file, or-varflags — in ascending priority order. - Mark variables
sensitive = trueto suppress CLI output, but note the value still appears in state. - Output values (
output {}) expose results after apply. Use them to share data between configurations viaterraform_remote_state. - Local values (
locals {}) are named computed expressions. Use them to avoid repetition. Reference withlocal.name. - Convention: put
variableblocks invariables.tf,outputblocks inoutputs.tf, resources inmain.tf.