Back to Blog

Terraform Variables, Outputs, and Locals: A Complete Guide for the Terraform Associate Exam

Master Terraform input variables, outputs, and locals for the Terraform Associate exam — type constraints, validation, variable precedence, sensitive values, and real HCL examples.

By Sailor Team , June 20, 2026

Introduction

If there is one area of the Terraform Associate exam that separates people who use Terraform from people who only follow tutorials, it’s how they handle configuration values. Input variables, outputs, and locals are the three constructs that turn a hard-coded .tf file into reusable, composable infrastructure — and they show up across multiple objectives on the exam, especially “Read, generate, and modify configuration.”

The tricky part isn’t the syntax. It’s the details: the exact precedence order when the same variable is set in three different places, when a value can be overridden and when it can’t, how sensitive = true actually behaves, and why a validation block fails at plan time instead of apply time. Those details are exactly what the exam likes to test, because they’re the things you only learn by getting burned in production.

This guide covers all three constructs from a practitioner’s perspective, with the HCL you’ll actually write and the gotchas the exam actually asks about. By the end you’ll know not just how to declare a variable, but when to reach for a variable versus a local versus an output — which is the real skill.

If you’re earlier in your prep and want the big picture first, start with the complete Terraform Associate exam guide, then come back here to go deep.

The Mental Model: Inputs, Internals, and Outputs

Before the syntax, get the model straight. These three constructs map to three different directions of data flow:

ConstructDirectionWho sets itCan it be overridden?
Input variable (variable)Into a moduleThe caller (CLI, tfvars, env, parent module)Yes — from outside
Local value (locals)Inside a moduleThe module author onlyNo — internal only
Output value (output)Out of a moduleThe module authorRead by the caller

A useful one-liner: variables are arguments, outputs are return values, and locals are the temporary variables in between. If you’ve written a function in any language, that analogy will carry you a long way — and it’s a clean way to remember the difference if a scenario question tries to trip you up.

Input Variables

Input variables (often just called “variables”) are how a Terraform configuration or module accepts values from the outside world. You declare them once and reference them anywhere with var.<name>.

Declaring a Variable

variable "instance_type" {
  type        = string
  default     = "t3.micro"
  description = "EC2 instance size for the web tier"
}

Every argument inside the block is optional, but in real code you should always include type and description. The full set of supported arguments:

ArgumentPurpose
typeType constraint (string, number, bool, or a complex type)
defaultValue used when none is supplied. Omitting it makes the variable required
descriptionDocumentation shown in terraform plan and generated docs
sensitiveRedacts the value from CLI output when true
nullableWhen false, the variable cannot be set to null
validationOne or more custom rules that must pass before plan continues

Reference it anywhere in the same module:

resource "aws_instance" "web" {
  ami           = var.ami_id
  instance_type = var.instance_type
}

Type Constraints

This is heavily tested. Terraform supports primitive, collection, and structural types:

# Primitives
variable "region"     { type = string }
variable "node_count" { type = number }
variable "enabled"    { type = bool }

# Collections (all elements share one type)
variable "azs"        { type = list(string) }
variable "tags"       { type = map(string) }
variable "ports"      { type = set(number) }

# Structural (mixed element types)
variable "app" {
  type = object({
    name     = string
    replicas = number
    public   = bool
  })
}

variable "pair" {
  type = tuple([string, number])
}

# Escape hatch — accept anything
variable "metadata" { type = any }

A few exam-relevant distinctions:

  • list vs set — a list is ordered and allows duplicates; a set is unordered and deduplicates. This matters when you pair them with for_each (which requires a set or map). The count vs for_each deep dive explains why that distinction causes the dreaded index-shifting problem.
  • object vs map — an object has a fixed set of named attributes with individual types; a map has arbitrary keys that all share one type.
  • Type conversion — Terraform will convert compatible types automatically. A number will coerce to a string where needed, but an invalid conversion (string "abc" to number) fails at plan time.

Required vs Optional Variables

A variable with no default is required. If you don’t supply a value, Terraform prompts for it interactively (or errors in automation):

variable "db_password" {
  type        = string
  sensitive   = true
  # no default → required
}

Adding default = null does not make a variable optional in the way people expect — it makes null the default value, which your code then has to handle. Use this pattern deliberately, not by accident.

How Variables Get Their Values — and the Precedence Order

This is one of the most commonly missed exam questions. The same variable can be set in several places, and Terraform applies them in a strict order where later sources override earlier ones:

Order (low → high priority)Source
1Environment variables (TF_VAR_name)
2terraform.tfvars
3terraform.tfvars.json
4*.auto.tfvars / *.auto.tfvars.json (in lexical filename order)
5-var and -var-file on the command line (highest)

So if region is set to us-east-1 via TF_VAR_region and to eu-west-1 in terraform.tfvars, and you also pass -var="region=ap-south-1", the apply uses ap-south-1 — the command-line flag wins.

# Environment variable (lowest priority)
export TF_VAR_region="us-east-1"

# Auto-loaded files
echo 'region = "eu-west-1"' > terraform.tfvars

# Explicit file
terraform apply -var-file="prod.tfvars"

# Inline flag (highest priority)
terraform apply -var="region=ap-south-1"

Two facts worth memorizing: terraform.tfvars and *.auto.tfvars are loaded automatically, while any other filename (like prod.tfvars) requires an explicit -var-file. And when multiple *.auto.tfvars files exist, they’re processed in alphabetical order, so a later filename can override an earlier one.

Variable Validation

validation blocks let you reject bad input before Terraform touches your infrastructure. This catches mistakes at plan time instead of halfway through an apply:

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

  validation {
    condition     = var.instance_count >= 1 && var.instance_count <= 10
    error_message = "instance_count must be between 1 and 10."
  }
}

The condition must evaluate to true for the value to be accepted. You can declare multiple validation blocks per variable, and each is checked independently. This is a favorite exam topic because it’s a clean way to enforce guardrails in a self-service module.

Sensitive Variables

Marking a variable sensitive = true tells Terraform to redact it from plan and apply output:

variable "api_token" {
  type      = string
  sensitive = true
}

Two things the exam wants you to understand clearly:

  1. Sensitivity is about display, not encryption. A sensitive value is still stored in plaintext in the state file. Protecting the value at rest is a job for a secure remote backend with encryption and access control — covered in the Terraform state management guide.
  2. Sensitivity propagates. If you use a sensitive variable to build a local value or an output, Terraform marks that derived value sensitive too, unless you explicitly opt out.

Local Values

Local values (locals) assign a name to an expression so you can reuse it within a module without repeating yourself. Think of them as constants computed once and referenced many times.

Declaring and Using Locals

locals {
  name_prefix = "${var.project}-${var.environment}"

  common_tags = {
    Project     = var.project
    Environment = var.environment
    ManagedBy   = "terraform"
  }

  is_production = var.environment == "prod"
}

Reference them with local.<name> (note: local, singular, even though the block is locals):

resource "aws_s3_bucket" "assets" {
  bucket = "${local.name_prefix}-assets"
  tags   = local.common_tags
}

When to Use a Local Instead of a Variable

This is the design question the exam loves, and the answer follows directly from the mental model:

  • Use an input variable when the value should be supplied or overridden from outside the module (region, instance size, environment name).
  • Use a local when the value is derived from other values and should never be set externally (a computed naming convention, a merged tag map, a conditional flag).

A common anti-pattern is exposing a variable for something that’s really just a derived expression — that hands callers a knob that can break your naming scheme. If a value is computed from inputs and shouldn’t be overridden, it’s a local. If you find yourself repeating the same merge() or interpolation in five resources, that’s a local waiting to happen.

locals {
  # Derived once, used everywhere — not something a caller should override
  bucket_name = lower(replace("${var.project}-${var.environment}-data", "_", "-"))
}

One caveat worth knowing: locals are evaluated when needed and can reference other locals, but they cannot reference themselves, and you should keep them readable — a 40-line nested local expression is harder to debug than a small module.

Output Values

Outputs expose information from a module after it’s applied. They serve two jobs: surfacing useful values on the command line, and passing data between modules.

Declaring an Output

output "instance_public_ip" {
  value       = aws_instance.web.public_ip
  description = "Public IP of the web server"
}

output "db_endpoint" {
  value       = aws_db_instance.main.endpoint
  description = "RDS connection endpoint"
  sensitive   = true
}

Arguments you can set on an output: value (required), description, sensitive, depends_on, and precondition blocks.

Reading Outputs

Outputs are only populated after a successful apply. From the CLI:

# Show all outputs
terraform output

# Show one output
terraform output instance_public_ip

# Machine-readable, e.g. for scripts
terraform output -json

# Raw string with no quotes — handy for piping
terraform output -raw db_endpoint

The -raw and -json flags come up in automation scenarios — for example, feeding a Terraform output into a shell variable in a CI pipeline. The Terraform commands cheat sheet has the full output command reference.

Outputs as the Interface Between Modules

This is the highest-value use of outputs. When a parent module calls a child module, it reads the child’s outputs with module.<name>.<output>:

module "network" {
  source = "./modules/network"
  cidr   = "10.0.0.0/16"
}

module "app" {
  source     = "./modules/app"
  subnet_ids = module.network.private_subnet_ids   # output from network module
}

A child module’s resources are encapsulated — the parent can only see what the child explicitly exposes via outputs. This is why outputs are central to module design, a topic covered in depth in the Terraform modules complete guide.

Sensitive Outputs

If an output references sensitive data, mark it sensitive = true, or Terraform will error at plan time. The value is then redacted from CLI output — but, as with variables, it remains in plaintext in state. You can still read a sensitive output explicitly with terraform output -raw <name> when you genuinely need the value.

Putting It All Together

Here’s a compact example that uses all three constructs the way you would in a real module:

# --- variables.tf ---
variable "project" {
  type        = string
  description = "Project identifier"
  validation {
    condition     = can(regex("^[a-z][a-z0-9-]+$", var.project))
    error_message = "project must be lowercase alphanumeric with hyphens."
  }
}

variable "environment" {
  type    = string
  default = "dev"
}

# --- locals.tf ---
locals {
  name_prefix = "${var.project}-${var.environment}"
  common_tags = {
    Project     = var.project
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

# --- main.tf ---
resource "aws_s3_bucket" "data" {
  bucket = "${local.name_prefix}-data"
  tags   = local.common_tags
}

# --- outputs.tf ---
output "bucket_name" {
  value       = aws_s3_bucket.data.id
  description = "Name of the created S3 bucket"
}

Note the conventional file split — variables.tf, locals.tf, main.tf, outputs.tf. Terraform loads all .tf files in a directory regardless of name, but this convention makes modules far easier to read and is what reviewers (and exam scenarios) expect.

Exam Tips and Common Gotchas

  • Memorize the precedence order. The most common variable question gives you the same variable set in three places and asks which wins. Command line (-var) beats auto-loaded files beats terraform.tfvars beats TF_VAR_ environment variables.
  • terraform.tfvars and *.auto.tfvars auto-load; other filenames don’t. A file named prod.tfvars is ignored unless you pass -var-file=prod.tfvars.
  • sensitive hides values from output but not from state. State is plaintext — secure the backend.
  • Required variables have no default. In automation, a missing required variable is a hard error, not a prompt.
  • local.name is singular even though the block is locals. Same trap as each inside a for_each block.
  • Outputs only appear after apply, and a sensitive value used in a non-sensitive output causes a plan error.
  • Validation runs at plan time, giving fast feedback before any resource is touched.

Practice This Until It’s Reflexive

Reading about variable precedence is one thing; recognizing it instantly under a 60-minute timer is another. The exam rewards candidates who’ve written enough HCL that the syntax is automatic and who can spot the trick in a “which value wins?” question without slowing down.

Drill these patterns hands-on. The 25 free Terraform Associate practice questions include variable, output, and locals scenarios with explanations, and the free interactive TF-004 practice set lets you check your accuracy against the clock.

When the mechanics feel automatic, pressure-test yourself with full-length, timed mocks. Sailor.sh’s HashiCorp Terraform Associate Mock Exam Bundle includes eight 60-question exams with detailed explanations across every objective — including the variable precedence, validation, sensitive-value, and module-output scenarios this guide covered — so you find your weak spots before exam day instead of during it. If you want a structured runway, the 30-day Terraform Associate study plan slots configuration topics into the first two weeks, and if you’re still deciding whether to sit the exam at all, is the Terraform Associate worth it in 2026? breaks down the ROI.

Frequently Asked Questions

What is the difference between a variable and a local in Terraform?

An input variable is set from outside the module — by the CLI, a .tfvars file, an environment variable, or a parent module — and is meant to be configurable. A local value is computed inside the module from an expression and cannot be overridden externally. Use variables for things callers should control (region, size, environment) and locals for derived values like naming conventions or merged tag maps.

What is the order of precedence for Terraform variables?

From lowest to highest priority: environment variables (TF_VAR_name), then terraform.tfvars, then terraform.tfvars.json, then *.auto.tfvars / *.auto.tfvars.json files in lexical order, and finally -var and -var-file flags on the command line, which override everything else.

Does marking a Terraform variable sensitive encrypt it?

No. sensitive = true only redacts the value from terraform plan and terraform apply console output. The value is still stored in plaintext inside the state file. To protect secrets at rest, use a secure remote backend with encryption and strict access control, and avoid committing state to version control.

How do I pass a value from one Terraform module to another?

Declare an output in the child module exposing the value, then reference it from the parent as module.<module_name>.<output_name>. A child module’s resources are encapsulated, so outputs are the only way to read values out of it. This is the standard pattern for wiring modules together, such as passing subnet IDs from a network module into an application module.

When are Terraform outputs available?

Output values are only populated after a successful terraform apply. Before the first apply they are unknown. You can view them with terraform output, retrieve a single value with terraform output <name>, get JSON with terraform output -json, or get an unquoted string with terraform output -raw <name> for use in scripts.

Can a Terraform variable be required?

Yes. A variable is required whenever you omit the default argument. If no value is provided, Terraform prompts for it interactively, and in non-interactive automation (like CI/CD) a missing required variable causes the run to fail. Adding any default — including default = null — makes the variable optional.

Conclusion

Input variables, locals, and outputs are small constructs that carry a disproportionate amount of weight, both on the Terraform Associate exam and in real infrastructure code. Variables let modules accept input, locals keep computed logic DRY and internal, and outputs expose results and wire modules together. Master the precedence order, understand that sensitive is about display rather than encryption, and internalize when each construct is the right tool, and you’ll handle the configuration questions on exam day with confidence.

From here, the natural next steps are the Terraform modules complete guide — where outputs really earn their keep — and the Terraform state management guide, which explains where all those sensitive values actually live. Then put it under timed pressure with the Terraform Associate practice questions and full mock exams until the syntax is second nature.

Limited Time Offer: Get 80% off all Mock Exam Bundles | Sale ends in 7 days. Start learning today.

Claim Now