Intermediate

Terraform: Variables and Outputs

Input variables, data types, local values and output values via the Burrito Barn AWS scenario.

Practical scenario: Burrito Barn application deployed on AWS (network, EC2, Secrets Manager, S3)


Table of Contents

  1. Overview
  2. Module 1 — Input Variables
  3. Module 2 — Data Types
  4. Module 3 — Local Values
  5. Module 4 — Output Values
  6. Flow Diagrams
  7. Reference Tables
  8. Best Practices and Common Pitfalls

1. Overview

Terraform relies on a structured information model in three layers:

LayerTerraform ConstructFunction Analogy
Inputvariable (input variable)Parameter / argument
Internallocals (local value)Local variable
Outputoutput (output value)Return value

Input variables make a configuration reusable and dynamic. Local values avoid repetition and enable data transformations. Output values expose information to the terminal, parent modules, and other configurations via terraform_remote_state.


2. Module 1 — Input Variables

Variable Syntax

variable "<NAME>" {
  description = "..."
  type        = <TYPE>
  default     = <VALUE>
  sensitive   = false
  nullable    = true

  validation {
    condition     = <BOOLEAN_EXPRESSION>
    error_message = "Error message shown to the user."
  }
}

Reference in configuration: var.<NAME>

Available Arguments

ArgumentRequiredDescription
descriptionRecommendedAutomatic documentation (terraform-docs, Terraform Registry)
typeRecommendedData type validation; error if incompatible
defaultOptionalMakes variable optional; if absent, variable is required
sensitiveOptionalHides value in logs and plan; false by default
nullableOptionalAllows null as a valid value; true by default
validationOptionalCustom validation rule with error message

Ways to Provide a Value

Terraform accepts variable values from multiple sources, applied in a strict precedence order.

1. Default value (default)

variable "region" {
  type    = string
  default = "us-east-1"
}

2. terraform.tfvars file (auto-loaded)

# terraform.tfvars
region        = "us-west-2"
instance_type = "t3.micro"

3. *.auto.tfvars file (auto-loaded)

# production.auto.tfvars
environment = "prod"

4. -var-file CLI option

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

5. -var CLI option

terraform plan -var "instance_type=t3.nano" -var "region=eu-west-1"

6. TF_VAR_<name> environment variable

# Bash / Zsh
export TF_VAR_api_key="MySecretAPIKey"

# PowerShell
$env:TF_VAR_api_key = "MySecretAPIKey"

7. Interactive prompt (last resort)

If no value is provided and there is no default, Terraform prompts the user at plan or apply time.

Variable Precedence

Rule: the source with the highest priority overrides all others.

flowchart TD
    A[default in variable block\nLowest priority] --> B[Environment variable\nTF_VAR_name]
    B --> C[terraform.tfvars\nor terraform.tfvars.json]
    C --> D[*.auto.tfvars files\nor *.auto.tfvars.json\nalphabetical order]
    D --> E[-var-file\nCLI option]
    E --> F[-var\nCLI option\nHighest priority]
    
    style A fill:#f9f,stroke:#333
    style F fill:#9f9,stroke:#333

Burrito Barn Demo — Module 1

Refactored variables with type, description, validation:

# variables.tf — m1_solution

variable "api_key" {
  description = "API key to be stored in Secrets Manager."
  type        = string
  sensitive   = true
}

variable "environment" {
  description = "Deployment environment (e.g., dev, prod)."
  type        = string
  default     = "dev"
}

variable "instance_type" {
  description = "Instance type for the EC2 instance."
  type        = string
}

variable "region" {
  description = "AWS Region to deploy resources in."
  type        = string
  default     = "us-west-2"
}

variable "sg_port_number" {
  description = "Port number for the security group."
  type        = number
  default     = 80
}

Passing api_key value via environment variable (PowerShell):

$env:TF_VAR_api_key = "MySecretAPIKey"
terraform plan

Referencing a variable in a resource:

resource "aws_instance" "web" {
  instance_type = var.instance_type
  user_data = templatefile("${path.module}/templates/user_data.sh", {
    environment = var.environment
  })
}

Passing a value to a child module:

# In the root module (main.tf)
module "storage" {
  source        = "./modules/s3_bucket"
  bucket_prefix = var.bucket_prefix
}

3. Module 2 — Data Types

Primitive Types

TypeDescriptionExample
stringSequence of Unicode characters"us-east-1"
numberInteger or decimal80, 3.14
boolBooleantrue, false

Collection Types

All elements of a collection type must be of the same data type.

TypeDescriptionExample
list(<TYPE>)Ordered group of same-type elements["10.0.0.0/24", "10.0.1.0/24"]
set(<TYPE>)Unordered group of unique same-type elementstoset(["a", "b", "c"])
map(<TYPE>)Key-value pairs with unique keys, same-type values{ subnet1 = "10.0.0.0/24" }

Structural Types

Structural types allow mixing data types.

TypeDescription
tuple([<TYPE>, ...])Ordered group of elements of different types; exact structure required
object({ <KEY> = <TYPE>, ... })Key-value pairs with different types per key

Type Expressions

# Primitives
variable "region" {
  type = string
}

variable "instance_count" {
  type = number
}

variable "create_nat_gateway" {
  type = bool
}

# Collections
variable "allowed_ports" {
  type = list(number)   # [80, 443, 8080]
}

variable "subnet_cidrs" {
  type = map(list(string))
  # {
  #   public  = ["10.0.0.0/24", "10.0.1.0/24"]
  #   private = ["10.0.2.0/24", "10.0.3.0/24"]
  # }
}

# Tuple
variable "mixed_data" {
  type = tuple([string, list(number), bool])
  # ["us-east-1", [80, 443], true]
}

# Object
variable "vpc_config" {
  type = object({
    name               = string
    subnets            = map(string)
    create_nat_gateway = bool
  })
}

Burrito Barn Demo — Module 2

vpc_network_info variable of type object:

variable "vpc_network_info" {
  description = "Values for VPC module."
  type = object({
    vpc_cidr       = string
    public_subnets = map(string)  # subnet name => CIDR
  })
}

Value in terraform.tfvars:

vpc_network_info = {
  vpc_cidr = "10.0.0.0/16"
  public_subnets = {
    subnet1 = "10.0.0.0/24"
    subnet2 = "10.0.1.0/24"
  }
}

4. Module 3 — Local Values

Why Use locals?

  1. Avoid repetition (DRY) — define a value once, reference it everywhere.
  2. Complex data transformations — isolate transformation logic in a locals block rather than inlining it in each resource.

Locals Syntax

locals {
  # Simple value
  naming_prefix = "${var.project}-${var.environment}"

  # Dynamically constructed map
  default_tags = merge(var.tags, {
    Environment = var.environment
    Project     = var.project
  })

  # List
  environments = ["dev", "staging", "prod"]

  # Inline object
  subnet_properties = {
    cidr   = "10.0.0.0/24"
    az     = "us-east-1a"
    public = true
  }
}

Reference in configuration:

resource "aws_vpc" "main" {
  tags = local.default_tags
}

resource "aws_instance" "web" {
  tags = {
    Name = "${local.naming_prefix}-web"
  }
}

Useful Transformation Functions

FunctionDescriptionExample
merge(map1, map2, ...)Merge multiple maps; last map’s keys winmerge(var.tags, { Env = "prod" })
lower(str)Convert to lowercaselower("BurritoBarn")"burritobarn"
upper(str)Convert to uppercaseupper("dev")"DEV"
format(fmt, ...)String formattingformat("%s-%s", var.project, var.env)
join(delim, list)Join list elements with separatorjoin("-", ["a","b"])"a-b"
split(delim, str)Split a stringsplit(",", "a,b,c")["a","b","c"]
toset(list)Convert list to set (deduplicates)toset(["a","a","b"]){"a","b"}
tolist(set)Convert set to listtolist(toset(["b","a"]))
keys(map)Return map keys as a listkeys({ a=1, b=2 })["a","b"]
values(map)Return map values as a listvalues({ a=1, b=2 })[1,2]
length(val)Length of string, list, map, or setlength(["a","b"])2
lookup(map, key, default)Look up key in map with defaultlookup(var.tags, "Env", "dev")
contains(list, val)Check if list contains elementcontains(["dev","prod"], var.env)
slice(list, start, end)Sublist from start (inclusive) to end (exclusive)slice(azs, 0, 2)
templatefile(path, vars)Render template with variable substitutiontemplatefile("user_data.sh", { env = var.env })
nonsensitive(val)Remove sensitive marker to allow displaynonsensitive(data.aws_ssm_parameter.amzn2.value)

Burrito Barn Demo — Module 3

# locals.tf — m3_solution / m4_solution

locals {
  # Default tags merged with user-provided ones
  default_tags = merge(var.tags, {
    Environment = var.environment
    Project     = var.project
  })

  # Common naming prefix for all resources
  naming_prefix = "${var.project}-${var.environment}"
}

5. Module 4 — Output Values

Why Use outputs?

Use CaseDescription
Terminal displayAfter terraform apply, root module outputs are displayed and stored in state
Parent ↔ child module communicationParent accesses child data via module.<name>.<output>
Cross-configuration sharingVia terraform_remote_state, one config can read another’s outputs

Useful commands:

terraform output                    # Display all outputs
terraform output ec2_public_ip      # Display a specific output
terraform output -json              # JSON output for automation
terraform output -raw ec2_public_ip # Raw output (string only)

Output Syntax

output "<NAME>" {
  description = "..."
  value       = <EXPRESSION>
  sensitive   = false
  ephemeral   = false
  depends_on  = [<RESOURCE_OR_MODULE>]
}

Output Arguments

ArgumentRequiredDescription
valueYesThe expression whose value will be exposed
descriptionRecommendedDocumentation for terraform-docs and Terraform Registry
sensitiveOptionalHides value in terminal (false by default); stored in plain text in state
ephemeralOptionalDon’t write to state; only for child modules (false by default)
depends_onOptionalExplicit dependency when Terraform cannot infer it automatically

Important: sensitive = true in a root module output hides the terminal display but the value is still written to state. Protect access to the state file.

Output Use Cases

1. Display information in terminal

output "ec2_public_ip" {
  description = "The public IP address of the EC2 instance."
  value       = aws_instance.web.public_ip
}

2. Expose child module data to parent module

# In modules/s3_bucket/outputs.tf
output "bucket_id" {
  description = "The ID of the S3 bucket."
  value       = aws_s3_bucket.bucket.id
}

# In root module (main.tf or outputs.tf)
output "bucket_info" {
  value = {
    id = module.storage.bucket_id
  }
}

3. Sensitive output

output "vpc_id" {
  description = "The ID of the VPC."
  value       = module.networking.vpc_id
  sensitive   = true
}

4. Composite output (map/object)

output "bucket_info" {
  description = "Information about the S3 bucket."
  value = {
    arn    = module.storage.bucket_arn
    id     = module.storage.bucket_id
    policy = module.storage.bucket_policy
  }
}

6. Flow Diagrams

Global Flow: Variables → Locals → Outputs

flowchart LR
    subgraph Sources["Value Sources"]
        D[default]
        ENV[TF_VAR_*\nenv vars]
        TFVARS[terraform.tfvars\n*.auto.tfvars]
        CLI[-var / -var-file]
        PROMPT[Interactive prompt]
    end

    subgraph RootModule["Root Module"]
        IV[Input Variables\nvar.*]
        DS[Data Sources\ndata.*]
        RES[Resources\nresource.*]
        LOC[Local Values\nlocal.*]
        OUT[Output Values\noutput.*]
    end

    subgraph ChildModule["Child Module"]
        CIV[Input Variables]
        CRES[Resources]
        COUT[Output Values]
    end

    subgraph Consumers["Consumers"]
        TERM[Terminal / CLI]
        PARENT[Parent Module\nmodule.name.output]
        REMOTE[Remote State\nterraform_remote_state]
    end

    Sources --> IV
    IV --> LOC
    IV --> RES
    IV --> CIV
    DS --> LOC
    DS --> RES
    LOC --> RES
    LOC --> OUT
    RES --> OUT
    CIV --> CRES
    CRES --> COUT
    COUT --> OUT
    OUT --> TERM
    OUT --> PARENT
    OUT --> REMOTE

    style IV fill:#ddf,stroke:#33f
    style LOC fill:#ffd,stroke:#fa0
    style OUT fill:#dfd,stroke:#080

7. Reference Tables

Variable Types Table

CategoryTypeExample
Primitivestring"us-east-1"
Primitivenumber80, 3.14
Primitivebooltrue, false
Collectionlist(string)["a", "b", "c"]
Collectionset(string)toset(["a", "b"])
Collectionmap(string){ key = "value" }
Structuraltuple([string, number])["us-east-1", 3]
Structuralobject({ name = string }){ name = "app", count = 2 }
AnyanyNo type validation

Value Source Precedence Table

PrioritySourceHow to Set
1 (lowest)defaultIn variable block
2Environment variableTF_VAR_name=value
3terraform.tfvarsKey-value file
4*.auto.tfvarsAlphabetical order
5-var-fileterraform plan -var-file=file.tfvars
6 (highest)-varterraform plan -var "name=value"

8. Best Practices and Common Pitfalls

Best Practices

PracticeWhy
Always specify type and descriptionAutomatic validation + generated documentation
Sort variables and outputs alphabeticallyConsistency, easier navigation
Use sensitive = true for secretsAvoids accidental exposure in CI/CD logs
Don’t put secrets in committed terraform.tfvarsRisk of leaking in version control
Use TF_VAR_ or a vault for secretsProduction security
Define default only if reasonably universalAvoids surprises in production
Use locals to avoid repetitionDRY — one change updates the entire module
Add description to each outputterraform-docs and Terraform Registry documentation
Mark sensitive outputs with sensitive = truePrevents accidental terminal display
Use depends_on on outputs only when necessaryImplicit dependency graph is preferred

Common Pitfalls

PitfallCauseSolution
Required variable without value → Terraform blocksNo value source available, no defaultProvide value via -var, TF_VAR_, or .tfvars
Sensitive output visible in statesensitive = true hides display, not stateProtect access to Terraform backend (e.g., S3 + encryption)
Conflict between auto.tfvarsTwo auto.tfvars files define the same variableAlphabetical order applies; last one wins
Type mismatchVariable typed number but string value passedCheck consistency between .tfvars value and type
Circular reference in localsLocal A references local B which references ARefactor; Terraform returns a clear error
ephemeral = true on root module outputRoot outputs are by definition written to stateUse ephemeral only in child modules
module/
├── main.tf         # Main resources
├── variables.tf    # All variable blocks (alphabetical order)
├── outputs.tf      # All output blocks (alphabetical order)
├── locals.tf       # All locals blocks
├── terraform.tf    # required_providers, terraform block
└── terraform.tfvars  # Values (DO NOT commit if contains secrets)

Search Terms

terraform · variables · outputs · infrastructure · ci/cd · devops · output · types · variable · barn · burrito · locals · syntax · value · arguments · auto-loaded · cli · data · flow · option · pitfalls · precedence · values

Interested in this course?

Contact us to book it or get a custom training plan for your team.