Skip to content

Variables, Outputs and Locals


A Terraform configuration becomes much more useful when you stop hard-coding every value. The same configuration may need to work for different environments, projects, regions, or naming conventions. Variables let you provide input, locals let you calculate and reuse values, and outputs let you expose useful results.

The simplest mental model is:

Input
  │
  ▼
Variable
  │
  ▼
Terraform expressions
  │
  ▼
Local values / resources
  │
  ▼
Output

Input Variables

An input variable defines a value that a Terraform configuration expects from outside the configuration.

For example:

variable "environment" {
  type        = string
  description = "Deployment environment."
}

The variable declaration says:

There is an input called "environment".
It should be a string.

You reference it using:

var.environment

This is useful because the configuration no longer needs to hard-code the environment everywhere.

Without a variable:

name = "dev"

With a variable:

name = var.environment

The second configuration can receive a different value without changing the configuration itself.

Defaults

A variable can have a default:

variable "environment" {
  type        = string
  description = "Deployment environment."
  default     = "dev"
}

Now Terraform can use dev when no other value is supplied.

A variable without a default is required unless a value is supplied through another supported input mechanism.

This gives you a useful distinction:

variable with default
    = optional input

variable without default
    = required input

Variable Types

You can constrain the type of a variable.

For example:

variable "instance_count" {
  type        = number
  description = "Number of instances."
  default     = 2
}

Or:

variable "enabled" {
  type        = bool
  description = "Whether the feature is enabled."
  default     = true
}

You can also use complex types:

variable "tags" {
  type = map(string)

  default = {
    Environment = "dev"
    Owner       = "platform"
  }
}

Type constraints make configuration interfaces clearer and allow Terraform to report invalid input earlier.

Validation

A variable can also define validation rules.

For example:

variable "environment" {
  type        = string
  description = "Deployment environment."

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

The type constraint answers:

Is this a string?

The validation rule answers:

Is this particular string acceptable?

That difference becomes valuable as configurations become reusable.

Where Variable Values Come From

Terraform supports several ways to provide variable values.

A common approach is a .tfvars file.

For example, create:

terraform.tfvars

with:

environment = "staging"
instance_count = 3

Terraform automatically loads terraform.tfvars and similarly named automatic variable files.

You can also explicitly supply a variable file:

terraform plan -var-file="staging.tfvars"

Another option is supplying a value directly from the CLI:

terraform plan -var="environment=staging"

Terraform also supports environment variables using the TF_VAR_ prefix.

For example:

export TF_VAR_environment="staging"

Terraform can then use that value for the environment variable.

The important idea is not to memorize a precedence table yet. It is to understand that variable declarations define the interface, while the actual values can come from different input mechanisms.

When multiple sources provide the same variable, Terraform follows a defined precedence order. More specific inputs override less specific ones. This is why you should avoid supplying the same variable through several mechanisms unless you understand which value will win.

Sensitive Variables

A variable can be marked sensitive:

variable "database_password" {
  type        = string
  description = "Database password."
  sensitive   = true
}

This tells Terraform to treat the value as sensitive for display purposes.

For example, Terraform will avoid displaying the actual value in many CLI messages.

But this does not mean the value magically disappears.

Warning

sensitive = true is not a secret-storage mechanism. A sensitive value can still be stored in Terraform state and other Terraform artifacts. Protect the underlying state and use appropriate secret-handling mechanisms.

Think of sensitivity as:

sensitive = true
    │
    └── "Be careful about displaying this value."

not:

sensitive = true
    │
    └── "Terraform no longer stores this value."

Outputs

Variables bring values into a configuration.

Outputs expose values from a configuration.

For example:

output "environment" {
  description = "Current environment."
  value       = var.environment
}

After applying the configuration, Terraform can display the output:

terraform output

You can also request a specific output:

terraform output environment

An output’s value is an expression:

output "environment" {
  value = var.environment
}

It does not have to be a simple variable. It can reference a resource attribute or a calculated expression.

For example:

output "generated_name" {
  value = random_pet.application.id
}

The mental model is:

variable
    │
    └── input to configuration

local
    │
    └── reusable calculated value inside configuration

output
    │
    └── value exposed by configuration

Local Values

A local value gives a reusable name to an expression.

For example:

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

You reference it with:

local.name_prefix

Locals are useful when the value is calculated or reused.

For example:

locals {
  application_name = "${var.environment}-application"
  is_production    = var.environment == "prod"
}

Now several resources can use the same calculated values without repeating the expressions.

The important distinction is:

variable
    = value supplied to the configuration

local
    = value calculated/reused inside the configuration

output
    = value exposed from the configuration

A local is not a second input mechanism.

A Complete Example

Create:

terraform-values/
├── main.tf
├── variables.tf
├── locals.tf
├── outputs.tf
└── terraform.tfvars

Create main.tf:

terraform {
  required_providers {
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
  }
}

provider "random" {}

resource "random_pet" "application" {
  length    = var.name_length
  separator = "-"
}

Create variables.tf:

variable "environment" {
  type        = string
  description = "Deployment environment."

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

variable "name_length" {
  type        = number
  description = "Number of words in the generated name."
  default     = 2

  validation {
    condition     = var.name_length >= 1 && var.name_length <= 5
    error_message = "Name length must be between 1 and 5."
  }
}

Create locals.tf:

locals {
  application_name = "${var.environment}-${random_pet.application.id}"
}

Create outputs.tf:

output "environment" {
  description = "Deployment environment."
  value       = var.environment
}

output "application_name" {
  description = "Generated application name including the environment."
  value       = local.application_name
}

Create terraform.tfvars:

environment = "dev"
name_length = 3

Initialize:

terraform init

Validate:

terraform validate

Plan:

terraform plan

Apply:

terraform apply

Confirm with:

yes

Display the outputs:

terraform output

You should see the environment and the generated application name.

Now change terraform.tfvars:

environment = "staging"
name_length = 3

Run:

terraform plan

You have changed an input without changing the resource configuration itself.

Terraform can therefore calculate a different desired result from the same configuration.

When finished:

terraform destroy

Variables vs Locals vs Outputs

When you are unsure which construct to use, ask where the value comes from and where it needs to go.

ConstructMain purposeTypical source
VariableAccept inputUser, file, CLI, environment
LocalCalculate/reuse a valueVariables, resources, expressions
OutputExpose a valueResources, variables, locals

Consider this example:

environment = "prod"
       │
       ▼
   variable
       │
       ▼
local.application_name
       │
       ▼
resource configuration
       │
       ▼
output.application_name

The value starts as external input, is transformed into a useful local value, used by infrastructure, and then exposed as an output.

Variable Precedence in Practice

It is possible to provide the same variable in several places. That can be useful for defaults and environment-specific configuration, but it can also become confusing.

PrecedenceSourceExampleNotes
1 — Highest-var / -var-file CLI optionsterraform apply -var="region=us-east-1"CLI values override other local sources. Multiple CLI values are processed in the order provided.
2*.auto.tfvars / *.auto.tfvars.jsonprod.auto.tfvarsMultiple files are loaded in lexical (alphabetical) order.
3terraform.tfvars.jsonterraform.tfvars.jsonAutomatically loaded.
4terraform.tfvarsterraform.tfvarsAutomatically loaded.
5Environment variablesTF_VAR_region=us-east-1Uses the TF_VAR_<name> convention.
6 — Lowestdefault in variable blockdefault = "us-east-1"Used only when no higher-precedence value is supplied.

Generally, the more specific value can override a less specific one.

Rather than memorizing the entire precedence system immediately, adopt a practical rule:

Give each important variable a clear source of truth.

If you find yourself wondering which of three different files is supplying a value, your configuration has become harder to operate than it needs to be.

The Input, Calculation, Output Model

You can now look at a Terraform configuration as a small data flow:

    flowchart LR
    A[External input] --> B[Input variable]
    B --> C[Terraform expression]
    C --> D[Local value]
    D --> E[Resource configuration]
    E --> F[Resource attributes]
    F --> G[Output]
  

Not every configuration follows exactly this path. A resource can consume a variable directly, and an output can expose a resource attribute directly.

But the model gives you a useful vocabulary:

  • Variables make configuration configurable.
  • Expressions calculate values.
  • Locals give useful names to calculations.
  • Resources use those values to describe infrastructure.
  • Outputs expose useful results.

Once expressions and types become more complex, these relationships become even more powerful. That is the subject of the next chapter.

Last updated on