Skip to main content
RunBook Academy

TerraformXIII · Variables, Outputs, and LocalsVariables

Variables: The Configuration Interface

Foundation⏱ ~18 min🧪 Lab requiredbashterraform

What you'll learn

  • Declare variables with appropriate types and validation
  • Read the precedence of variable inputs
  • Design a variable interface that supports multi-environment use
  • Mark sensitive variables correctly

Prerequisites

Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-12

Not yet marked complete on this device.

Variables are the configurations interface to the world. They declare the inputs the operator can change between environments and over time. A well-designed variable interface is the difference between a configuration that scales to multiple environments and one that requires a duplication per environment.

Declaring a variable

variable "environment" {
  type        = string
  description = "The environment name (dev, staging, prod)."

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

The fields:

  • type — the expected type. Required.
  • description — human-readable documentation. Required in production.
  • default — the value if no other input is provided.
  • validation — a block that constrains the value.
  • sensitive — if true, the value is suppressed in CLI output.

Variable types

The full set of valid types:

variable "string_var" {
  type = string
}

variable "number_var" {
  type = number
}

variable "bool_var" {
  type = bool
}

variable "list_var" {
  type = list(string)
}

variable "map_var" {
  type = map(string)
}

variable "set_var" {
  type = set(string)
}

variable "object_var" {
  type = object({
    name = string
    port = number
  })
}

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

variable "nullable_var" {
  type     = string
  nullable = true   # allow null in addition to the type
}

The default for nullable is false. A variable without nullable = true cannot be assigned null.

Variable validation

A validation block constrains the value:

variable "instance_type" {
  type        = string
  description = "The EC2 instance type."

  validation {
    condition     = can(regex("^(t3|t3a|m5|m5a|c5|c5a|r5|r5a)\\.", var.instance_type))
    error_message = "Instance type must be in the t3, m5, c5, or r5 family."
  }
}

A validation:

  • Is evaluated at the variables input time (during terraform plan).
  • Fails the plan if the input is invalid.
  • Does not have access to other variables or resources.

Validation is the cheapest way to catch a configuration error. The cost is a few lines of HCL.

The variable interface

The variables are the configurations interface to the operator. A well-designed interface:

  • Has a small number of variables. A configuration with 50 variables has an interface that is hard to understand.
  • Has documented variables. Every variable has a description.
  • Has validated variables. Every variable has a validation block.
  • Has sensible defaults. A variable that always has to be overridden is a sign that the default should be different.
  • Has stable names. Renaming a variable breaks every environment.
# Good: focused interface
variable "environment" {
  type        = string
  description = "The environment name (dev, staging, prod)."
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Must be one of dev, staging, prod."
  }
}

variable "vpc_cidr" {
  type        = string
  description = "The VPC CIDR block."
  default     = "10.0.0.0/16"
  validation {
    condition     = can(cidrnetmask(var.vpc_cidr))
    error_message = "Must be a valid CIDR block."
  }
}

variable "instance_type" {
  type        = string
  description = "The EC2 instance type."
  default     = "t3.medium"
}

The interface is small. Each variable has a clear purpose. The operator can read the configuration and understand the inputs without reading the rest of the file.

Input sources and precedence

A variable can be set from multiple sources. The precedence, from highest to lowest:

  1. CLI flags. terraform apply -var="environment=prod".
  2. .tfvars files specified by -var-file.
  3. *.auto.tfvars files in the working directory (loaded automatically).
  4. terraform.tfvars files in the working directory.
  5. Environment variables. TF_VAR_environment=prod.
  6. Default values in the variable declaration.

The precedence is documented in the official documentation. The common production patterns:

  • Default values for variables that are always the same.
  • .tfvars files per environment to set environment-specific values.
  • Environment variables for CI pipelines.
  • CLI flags for one-off overrides (rare in production).
# Production: use a tfvars file
terraform apply -var-file=production.tfvars

# CI: use environment variables
export TF_VAR_environment=prod
terraform apply

# One-off: use CLI flags (rare)
terraform apply -var="instance_type=t3.large"

Sensitive variables

A variable can be marked sensitive:

variable "db_password" {
  type        = string
  description = "The database password."
  sensitive   = true
}

When sensitive = true is set:

  • The variables value is hidden in the CLI output.
  • The variables value is not hidden in the state.
  • The variables value is not hidden in the plan output (the plan shows (sensitive) instead of the value).
  • The variables value is not hidden from references inside the configuration.

A sensitive variable is a user interface control, not a security control. The variable is still in state; the variable is still in the plan file; the variable is still in the apply log. The hiding is only for the CLIs output.

The course has a dedicated lesson on secrets management. The recommendation is to use a secrets manager (HashiCorp Vault, AWS Secrets Manager, etc.) and read the secret at runtime.

The defaults that should not be defaults

A few patterns that look like defaults but should be explicit:

  • Empty defaults. default = "" is rarely the right default. Either the value should be required, or the default should be a sensible value.
  • Defaults that are environment-specific. default = "prod" in a shared configuration is a recipe for accidental prod deployment.
  • Defaults that are infrastructure-specific. default = "vpc-0abc123..." couples the default to a specific real-world resource.

The courses recommendation: defaults should be safe for every environment. If the default is wrong for some environment, the variable should be required.

The complexity trap

A configuration with 50 variables is a configuration that requires the operator to remember 50 things. The configuration has become the documentation. The documentation has become the configuration.

The principle is: a small number of variables is better than a large number. A configuration with 10 well-designed variables is more maintainable than one with 50 poorly-designed variables.

The way to reduce variables is to group them:

# Less good: 10 separate variables
variable "vpc_cidr" { ... }
variable "vpc_name" { ... }
variable "vpc_enable_dns_hostnames" { ... }
variable "vpc_enable_dns_support" { ... }
variable "vpc_instance_tenancy" { ... }
# ... 6 more

# Better: an object variable
variable "vpc" {
  type = object({
    cidr                   = string
    name                   = string
    enable_dns_hostnames   = optional(bool)
    enable_dns_support     = optional(bool)
    instance_tenancy       = optional(string)
  })
  default = {
    cidr = "10.0.0.0/16"
    name = "production"
  }
}

# Use as:
resource "aws_vpc" "main" {
  cidr_block           = var.vpc.cidr
  enable_dns_hostnames = var.vpc.enable_dns_hostnames
}

The object variable groups the related inputs. The interface is smaller. The default is a single object, not 10 separate values.

What comes next

The next lesson is locals — the configurations internal variables, used for readability and to avoid duplication.

What comes next

The next lesson is locals - the configuration internal variables, used for readability and to avoid duplication.

Verification

Verification

Verification

Knowledge check · 7 questions

  1. Q1. What is the role of variables in Terraform?

  2. Q2. What is the role of locals?

  3. Q3. Variables should be sensitive when they contain secrets.

  4. Q4. What is the highest-priority source of variable values?

  5. Q5. Which of the following are valid variable types? (Select all that apply.)

  6. Q6. What is the role of validation in a variable?

  7. Q7. A team uses .tfvars files in Git for production. What is the risk?

Passing score: 75%. Answers are checked in this browser.