Terraform Variables Outputs
Terraform variables, outputs, locals, type constraints, validation, and data flow patterns
You are an expert in Terraform variables, outputs, and locals for infrastructure as code. ## Key Points 1. Default value in variable block 2. `terraform.tfvars` or `terraform.tfvars.json` 3. `*.auto.tfvars` or `*.auto.tfvars.json` (alphabetical order) 4. `-var-file` flag (in order specified) 5. `-var` flag (in order specified) 6. `TF_VAR_<name>` environment variable - **Always set `description`** on every variable and output. It is the primary documentation. - **Use specific types** instead of `any`. Type constraints catch errors at plan time. - **Mark sensitive values** with `sensitive = true` on both variables and outputs. - **Use `optional()` with defaults** for object attributes (Terraform 1.3+) to simplify caller configuration. - **Validate early.** Put `validation` blocks on variables to give clear error messages before Terraform contacts any API. - **Keep locals readable.** If a local expression is complex, break it into multiple locals with descriptive names.
skilldb get terraform-skills/Terraform Variables OutputsFull skill: 354 linesVariables, Outputs, and Locals — Terraform
You are an expert in Terraform variables, outputs, and locals for infrastructure as code.
Overview
Variables, outputs, and locals are the data flow mechanisms in Terraform. Input variables parameterize configurations, outputs expose values for consumption by other configurations or users, and locals compute intermediate values to reduce repetition. Together, they define the interface and internal wiring of every module.
Core Concepts
Input Variables
variable "project" {
description = "Project name, used for resource naming and tagging"
type = string
}
variable "environment" {
description = "Deployment environment"
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be one of: dev, staging, prod."
}
}
variable "instance_count" {
description = "Number of application instances"
type = number
default = 1
validation {
condition = var.instance_count > 0 && var.instance_count <= 20
error_message = "Instance count must be between 1 and 20."
}
}
variable "enable_monitoring" {
description = "Whether to enable detailed monitoring"
type = bool
default = false
}
variable "db_password" {
description = "Database master password"
type = string
sensitive = true
}
Type Constraints
# Simple types
variable "name" { type = string }
variable "port" { type = number }
variable "enabled" { type = bool }
# Collection types
variable "availability_zones" {
type = list(string)
}
variable "tags" {
type = map(string)
default = {}
}
variable "cidr_blocks" {
type = set(string)
}
# Structural types
variable "ingress_rules" {
description = "List of ingress rules for the security group"
type = list(object({
port = number
protocol = string
cidr_blocks = list(string)
description = optional(string, "Managed by Terraform")
}))
default = []
}
variable "notification_config" {
description = "SNS notification configuration"
type = object({
topic_arn = string
events = set(string)
filter = optional(map(string), {})
})
default = null
}
# Tuple type (fixed length, mixed types)
variable "health_check" {
type = tuple([string, number, number])
# Example: ["/health", 30, 5] — path, interval, threshold
}
# any type (use sparingly)
variable "extra_tags" {
type = any
default = {}
}
Outputs
output "vpc_id" {
description = "ID of the created VPC"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "List of public subnet IDs"
value = [for s in aws_subnet.public : s.id]
}
output "db_endpoint" {
description = "Database connection endpoint"
value = aws_db_instance.main.endpoint
}
output "db_password" {
description = "Database master password"
value = aws_db_instance.main.password
sensitive = true
}
# Conditional output
output "nat_gateway_ip" {
description = "NAT gateway public IP, if created"
value = var.create_nat_gateway ? aws_eip.nat[0].public_ip : null
}
Locals
Locals compute values from variables, data sources, and other locals to reduce repetition and improve readability.
locals {
# Computed naming
name_prefix = "${var.project}-${var.environment}"
# Merged tags
common_tags = merge(var.extra_tags, {
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
})
# Derived data
private_subnets = { for az in var.availability_zones :
az => cidrsubnet(var.vpc_cidr, 8, index(var.availability_zones, az))
}
public_subnets = { for az in var.availability_zones :
az => cidrsubnet(var.vpc_cidr, 8, index(var.availability_zones, az) + length(var.availability_zones))
}
# Conditional logic
is_production = var.environment == "prod"
backup_enabled = local.is_production
multi_az = local.is_production
deletion_protection = local.is_production
}
Implementation Patterns
Variable Precedence
Terraform evaluates variables in this order (last wins):
- Default value in variable block
terraform.tfvarsorterraform.tfvars.json*.auto.tfvarsor*.auto.tfvars.json(alphabetical order)-var-fileflag (in order specified)-varflag (in order specified)TF_VAR_<name>environment variable
# Using environment variables
export TF_VAR_db_password="supersecret"
terraform apply
# Using var-file and overrides
terraform apply \
-var-file=environments/prod.tfvars \
-var="instance_count=5"
Complex Validation
variable "cidr_block" {
type = string
validation {
condition = can(cidrhost(var.cidr_block, 0))
error_message = "Must be a valid CIDR notation (e.g., 10.0.0.0/16)."
}
validation {
condition = tonumber(split("/", var.cidr_block)[1]) <= 24
error_message = "CIDR prefix must be /24 or larger (smaller number)."
}
}
variable "name" {
type = string
validation {
condition = can(regex("^[a-z][a-z0-9-]{2,28}[a-z0-9]$", var.name))
error_message = "Name must be 4-30 characters, lowercase alphanumeric and hyphens, starting with a letter."
}
}
Dynamic Security Group Rules from Variable
variable "ingress_rules" {
type = list(object({
port = number
protocol = string
cidr_blocks = list(string)
description = optional(string, "Managed by Terraform")
}))
default = [
{
port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS from anywhere"
},
{
port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTP from anywhere"
}
]
}
resource "aws_security_group" "web" {
name = "${local.name_prefix}-web"
vpc_id = var.vpc_id
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.port
to_port = ingress.value.port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
description = ingress.value.description
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
Transforming Data with Locals
variable "services" {
type = map(object({
port = number
cpu = number
memory = number
replicas = number
}))
}
locals {
# Flatten services into a list for use with for_each on related resources
service_ports = { for name, svc in var.services :
name => svc.port
}
# Create listener rules from services
listener_rules = { for name, svc in var.services :
name => {
path = "/${name}/*"
port = svc.port
priority = index(keys(var.services), name) + 1
}
}
# Total resource requirements
total_cpu = sum([for svc in var.services : svc.cpu * svc.replicas])
total_memory = sum([for svc in var.services : svc.memory * svc.replicas])
}
Best Practices
- Always set
descriptionon every variable and output. It is the primary documentation. - Use specific types instead of
any. Type constraints catch errors at plan time. - Mark sensitive values with
sensitive = trueon both variables and outputs. - Use
optional()with defaults for object attributes (Terraform 1.3+) to simplify caller configuration. - Validate early. Put
validationblocks on variables to give clear error messages before Terraform contacts any API. - Keep locals readable. If a local expression is complex, break it into multiple locals with descriptive names.
- Use
terraform.tfvarsfor defaults and-var-filefor environment overrides. Do not commit sensitive.tfvarsfiles. - Prefer structured variables (objects and lists of objects) over flat variables when values are logically grouped.
Core Philosophy
Variables, outputs, and locals define the contract between a module and its consumers. A well-designed variable interface is the difference between a module that is a joy to use and one that requires reading every line of source code to understand. Think of variables as the public API of your infrastructure component: they should be minimal, well-documented, and stable across versions.
Type constraints and validation blocks are your first line of defense against misconfiguration. A variable that accepts any string when it should only accept "dev", "staging", or "prod" is a bug waiting to happen. By encoding constraints directly in the variable definition, you shift error detection from plan-time (or worse, apply-time) to the moment the caller provides the value. Clear error messages in validation blocks save hours of debugging.
Locals are the private helper functions of Terraform. They reduce repetition, give meaningful names to complex expressions, and centralize derived values so they can be changed in one place. However, locals can also become a maze of indirection if overused. A local that is referenced once and does nothing more than rename a variable adds noise without value. Use locals when they genuinely improve readability or eliminate duplication.
Anti-Patterns
-
Exposing every possible knob as a variable. A module with 40 variables is not flexible; it is unusable. Most callers want sensible defaults. Expose only the variables that genuinely vary across use cases and provide defaults for everything else.
-
Using
anytype as the default choice. Theanytype bypasses Terraform's type checking entirely, allowing callers to pass values that will fail at plan or apply time with cryptic errors. Use specific types and lean onoptional()for object attributes that may not always be needed. -
Sensitive values without
sensitive = true. Forgetting to mark database passwords, API keys, or private keys as sensitive causes them to appear in plain text in CLI output, plan files, and CI logs. Always mark sensitive variables and any outputs derived from them. -
Complex expressions inlined everywhere. Repeating
"${var.project}-${var.environment}"in ten resource blocks instead of defining alocal.name_prefixcreates maintenance burden and inconsistency risk. If you use the same expression more than twice, it belongs in a local. -
Relying on interactive prompts for required variables. Variables without defaults that are not supplied via
.tfvarsfiles,-varflags, or environment variables cause Terraform to prompt for input. In CI/CD, this hangs the pipeline indefinitely. Always supply all required variables explicitly.
Common Pitfalls
- Using
anytype and losing validation. Theanytype bypasses Terraform's type checking. Use it only when the type genuinely varies. - Circular references in locals. Locals cannot reference themselves or create cycles with other locals.
- Forgetting
sensitiveon outputs that derive from sensitive variables. Terraform will error if you pass a sensitive variable to a non-sensitive output. - Overusing variables. Not everything needs to be a variable. If a value will never change (like a protocol or a well-known port), use a local or a literal.
- Variable default with
nullcan be useful for optional resources, but forgetting to handle thenullcase in the configuration causes runtime errors. - Confusing
setandlist. Sets are unordered and deduplicated. If you need ordering, uselist. If you use a list where a set is expected, Terraform converts it automatically, but duplicates are silently removed. - Not using
.tfvarsfile and instead relying on interactive prompts. In CI/CD, interactive prompts cause the pipeline to hang. Always supply all required variables via files, flags, or environment variables.
Install this skill directly: skilldb add terraform-skills
Related Skills
Terraform Basics
Terraform fundamentals including providers, resources, data sources, and core workflow
Terraform CI CD Pipeline
Running Terraform in CI/CD pipelines with automated plan, approval gates, and safe apply workflows
Terraform Modules
Terraform module design, composition, versioning, and reuse patterns
Terraform Provisioners
Terraform provisioners, null resources, triggers, and when to use alternatives
Terraform State Management
Remote state backends, state locking, import, migration, and state surgery techniques
Terraform Testing
Testing Terraform configurations with native tests, Terratest, plan validation, and policy-as-code