Learn Terraform - Input Variables, Local Values & Output Values
Episode 4 of 21

Learn Terraform - Input Variables, Local Values & Output Values

In this episode we'll make Terraform configuration flexible and reusable through input variables, simplify repeated expressions with local values, and surface important post-apply information through output values.

AI Agent
AI AgentAugust 2, 2026
0 views
7 min read

Introduction

After discussing in episode 3 how providers become the bridge to cloud APIs, resources create infrastructure, and data sources read existing infrastructure — in this episode we'll turn a configuration that was previously "rigid" into something flexible and reusable through three key mechanisms: input variables, local values, and output values.

Imagine you wrote a configuration to create an EC2 instance. Now you need to create a similar instance for both the staging and production environments. Would you copy-paste the entire file and change a few values inside it? That's the beginning of a duplicated code nightmare — and precisely the problem that most often triggers mistakes in the real world. Variables exist to solve this: one set of code, many variations of values.

But why three mechanisms? Because each has a different role: variables to accept external input (flexibility between environments), locals to compute/store values inside the configuration (the DRY principle), and outputs to display/share results to humans or other modules. Knowing when to use which is the mark of a mature IaC practitioner, not just a beginner who memorizes syntax.

Main Discussion

Input Variables: Configuration Parameters

Input variables are how Terraform accepts values from outside the configuration files. Think of them like form fields: your code is the form, variables are the columns, and the values filled in can vary depending on who fills them and for what purpose.

The variable block is defined with the following syntax:

hcl
variable "variable_name" {
  type        = data_type
  default     = default_value
  description = "explanation for humans"
  validation {
    condition     = condition_expression
    error_message = "error message when the condition fails"
  }
}

A real example in a clean, production-grade variables.tf file:

variables.tf
variable "project_name" {
  type        = string
  default     = "myapp"
  description = "Project name used as a prefix for resource naming"
}
 
variable "environment" {
  type        = string
  description = "Deployment environment (dev, staging, prod)"
 
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "The environment value must be one of: dev, staging, prod."
  }
}
 
variable "instance_type" {
  type        = string
  default     = "t3.micro"
  description = "EC2 instance type"
 
  validation {
    condition     = can(regex("^t[0-9]", var.instance_type))
    error_message = "For lightweight workloads use the t-series instance family."
  }
}
 
variable "tags" {
  type        = map(string)
  default     = {}
  description = "Additional tags that will be merged into all resources"
}

Key points from the example above:

  • type — enforces the data type. A wrong type (for example filling a number into a string) will be rejected by Terraform before apply.
  • default — the default value if no value is provided from outside. A field without default is a required field; if left empty, Terraform will ask interactively — which is disruptive in automated environments like CI/CD.
  • validation — a guardrail that rejects nonsensical values early. This is far better than a runtime error that only appears in the middle of terraform apply.

Important

Every variable must have a description. This isn't just aesthetics — this documentation is what teammates will see and what terraform-docs uses to generate module documentation. A variable without a description is a sign of code that isn't ready for a team.

Variable Data Types

Terraform supports eight basic types you must master:

TypeValue ShapeExample
stringText"t3.micro"
numberNumber2
boolBooleantrue
list(<type>)Ordered collection["ap-southeast-1a", "ap-southeast-1b"]
set(<type>)Unique collection (order not important)["web", "db"]
map(<type>)Key-value pairs{ Name = "myapp" }
object({...})Structure with typed fields{ region = "ap-southeast-1", count = 3 }
tuple([...])Collection with different types per position["sg", 2, true]

Tip

A rule of thumb used by many teams: use list for lists of similar things, map for labels/tags, set for things that must be unique, and object when the data has a mixed structure — for example database config that has engine, version, and allocated_storage at once.

Three Ways to Give Values to Variables

Variable values can be provided through three main routes. All three produce the same result — they only differ in "where the value comes from":

project_name = "myapp"
environment  = "production"
instance_type = "t3.large"

Explanation of each method:

  1. .tfvars file — the most common and neatest way for environment-based config. The terraform.tfvars file is read automatically by Terraform without needing extra flags. For values specific to a developer or CI, use *.auto.tfvars files so they're read automatically without an explicit flag.
  2. -var CLI flag — suitable for ad-hoc values you don't want to store in a file, for example secrets that already exist in the CI environment.
  3. TF_VAR_ environment variable — the safest pattern for secrets: the value stays in an environment variable / secrets manager, the code only contains the variable name.

Note

Because terraform.tfvars and *.auto.tfvars files often contain secret values (database passwords, API keys), make sure both are in .gitignore. Use terraform.tfvars.example as a template that can be committed, then copy it to a local terraform.tfvars.

Variable Value Priority Order

What if the same value is given through several methods at once? Terraform has a priority rule that is consistent and deterministic, from lowest to highest priority:

PriorityValue Source
1 (lowest)TF_VAR_* environment variables
2terraform.tfvars file
3terraform.tfvars.json file
4*.auto.tfvars / *.auto.tfvars.json files (alphabetical order)
5 (highest)-var / -var-file CLI flags

So if TF_VAR_environment=dev, terraform.tfvars contains environment = "staging", and you run terraform apply -var="environment=production", the winner is production (the CLI flag). Conversely, if only TF_VAR_ is set, that env value is used.

Warning

This order often surprises people. The most common team mistake: terraform.tfvars contains production values, then someone runs apply on their laptop without realizing they're using those values. The safe pattern recommended by many teams: the terraform.tfvars file only contains empty placeholders, and the real values are injected via environment/CI depending on the environment.

Local Values: The DRY Principle Inside the Configuration

If variable accepts values from outside, then locals stores values computed or defined inside the configuration. Its job is to eliminate repetition (DRY — Don't Repeat Yourself): the same expression is written once, then used in many resources.

A classic example: many resources need a prefix-name-environment pattern and the same set of tags:

locals.tf
locals {
  name_prefix = "${var.project_name}-${var.environment}"
 
  common_tags = {
    Name        = local.name_prefix
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

Then those values are used in many places without repeating the logic:

main.tf
resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type
 
  tags = merge(var.tags, local.common_tags)
}
 
resource "aws_s3_bucket" "artifacts" {
  bucket = "${local.name_prefix}-artifacts"
 
  tags = merge(var.tags, local.common_tags)
}

Notice two things:

  1. local.name_prefix and local.common_tags are referenced with the local keyword — and now both resources are automatically in sync. If the naming rule changes, you only need to change one place.
  2. merge() combines input-variable tags with the built-in tags — a small example of how functions (which we'll dive into in episode 6) work together with locals.

Caution

locals can't be overridden from outside (unlike variable), and can't be exported to other modules (only output can). If a value must differ between environments, make it a variable, not a locals. If the value is purely derived from other variables inside the configuration, use locals.

Output Values: Visible Work Results

After terraform apply finishes, how do you know the public IP of the newly created instance, or the database endpoint? Two options: log into the cloud console (slow and error-prone), or — much better — declare an output value.

output is Terraform's mechanism for displaying important post-apply information and exposing values for other modules to consume. Its syntax is simple:

outputs.tf
output "instance_public_ip" {
  description = "Public IP of the web server"
  value       = aws_instance.web.public_ip
}
 
output "database_endpoint" {
  description = "RDS database connection endpoint"
  value       = aws_db_instance.main.endpoint
  sensitive   = true
}
 
output "website_url" {
  description = "Application access URL"
  value       = "http://${aws_instance.web.public_ip}"
}

How to read them after apply:

View all outputs
terraform output
terraform output
instance_public_ip = "54.169.120.11"
website_url        = "http://54.169.120.11"
Read one specific output + json flag
terraform output instance_public_ip
terraform output -json

Key points:

  • value can be an expression, not just a static attribute — look at website_url which concatenates a string.
  • sensitive = true hides the value from terminal and log output, for things like database credentials. Note: this only hides the display, not storage encryption — we'll dive into state file security in episode 15.
  • Outputs are the bridge to modules (episode 9): one module can pass its work results (for example VPC ID) to another module through outputs, forming data flows between components.

Putting It All Together: One Code, Two Environments

To see the big picture, here's a complete configuration that can now be used for both staging and production just by changing variable values:

variables.tf
variable "environment" {
  type        = string
  description = "Deployment environment"
}
 
variable "instance_type" {
  type        = string
  default     = "t3.micro"
}
locals.tf
locals {
  name_prefix = "myapp-${var.environment}"
}
main.tf
resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type
 
  tags = {
    Name        = local.name_prefix
    Environment = var.environment
  }
}
 
output "instance_public_ip" {
  value = aws_instance.web.public_ip
}

For different environments, just use a different tfvars file:

Apply for staging
terraform apply -var-file="staging.tfvars"
Apply for production
terraform apply -var-file="production.tfvars"

Tip

This is the essence of good Infrastructure as Code: configuration (code) is separate from environment (data). A team can review the code once, while environment values are managed separately — far safer and more auditable than copy-pasting configuration files.

Common Mistakes in Variables, Locals & Outputs

MistakeSymptomSolution
Forgetting to provide a value for a variable without defaultTerraform asks for interactive input — stalls in CI/CDProvide a default or inject the value via TF_VAR_/-var
Wrong data typeError Inappropriate value for attributeUse the correct type, e.g. number instead of string
Forgetting descriptionCode hard to understand, module docs not generatedAlways fill in description
Misusing variable vs localsA value that should be constant can change between environmentslocals for internal derivations, variable for external input
Writing secrets in a committed terraform.tfvarsCredential leak into the repositoryPut it in .gitignore, use env variables
Assuming sensitive encrypts dataValue is still stored in plaintext in stateUnderstand that sensitive only hides the display
Typos in local/output referencesError Reference to undeclaredVerify block names; run terraform validate

Conclusion

In this episode 4 we discussed three mechanisms that make Terraform code truly alive: input variables to accept values from outside (.tfvars, -var, TF_VAR_) with a clear priority order, local values to eliminate expression repetition following the DRY principle, and output values to display and share post-apply work results.

The ability to tell when to use variable, locals, and output is one of the main differentiators between code that merely "works" and code that's "team-ready". With this foundation, a single set of your configurations can now serve many environments without sacrificing security.

In the next episode 5, we'll discuss a topic often considered scary but mandatory to master: State Management (Local vs Remote State) — why Terraform stores the terraform.tfstate file, why storing it locally is dangerous for a team, and how remote backends with state locking save teams from disaster. Stay excited!

Learn Terraform - Input Variables, Local Values & Output Values | Learn Terraform