Terraform Variables and Outputs

A comprehensive guide to mastering input variables, output values, local values, and tfvars files in Terraform and OpenTofu for 2026.

Introduction

Variables, outputs, and locals are fundamental to Infrastructure as Code (IaC) with Terraform and OpenTofu. They enable you to:

  • Parameterize configurations for different environments without altering core code
  • Share data between modules and configurations
  • Reduce repetition and improve maintainability
  • Create flexible, reusable infrastructure definitions

This pillar consolidates essential knowledge about these mechanisms, covering input variables, output values, local values, and tfvars file management—everything you need to master variables and outputs in 2026.


Understanding Input Variables

What Are Input Variables?

Input variables are the parameters of your Terraform module. They allow you to pass external values into your configuration, making it reusable across different environments and scenarios. Think of them as function arguments in programming.

Variable Declaration

Variables are declared in .tf files (commonly variables.tf) using the variable block. Key arguments include:

  • type: Specifies the data type (string, number, bool, list, map, object, set, tuple, or any)
  • description: Documents the variable's purpose (highly recommended for clarity)
  • default: Provides a default value, making the variable optional
  • nullable: When false, prevents null values (Terraform 1.1+)
  • sensitive: When true, redacts the value from CLI output (but not from state files)
  • validation: Defines custom rules for validating the variable's value

Basic Variable Example

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

variable "instance_count" {
  type        = number
  description = "Number of EC2 instances to create."
  default     = 1

  validation {
    condition     = var.instance_count > 0 && var.instance_count <= 100
    error_message = "Instance count must be between 1 and 100."
  }
}

variable "instance_type" {
  type        = string
  description = "The EC2 instance type."
  sensitive   = false  # Set to true to redact from CLI output
}

variable "tags" {
  type        = map(string)
  description = "Common tags to apply to all resources."
  default = {
    ManagedBy = "Terraform"
  }
}

Variable Types in Detail

Terraform supports several variable types:

  • Primitive Types: string, number, bool
  • Collection Types: list(type), map(type), set(type)
  • Structural Types: object({...}), tuple([...])
  • Special Type: any (use sparingly; reduces type safety)

Example: Complex Type (Object)

variable "app_service" {
  type = object({
    name          = string
    instance_type = string
    port          = number
    enabled       = bool
  })
  description = "Configuration for the application service."
}

Variable Validation

Use the validation block to enforce business logic constraints:

variable "aws_region" {
  type        = string
  description = "AWS region."

  validation {
    condition     = contains(["us-east-1", "us-west-2", "eu-west-1"], var.aws_region)
    error_message = "Region must be us-east-1, us-west-2, or eu-west-1."
  }
}

Sensitive Variables

Mark variables as sensitive when they contain passwords, API keys, or other confidential data:

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

Important: sensitive = true redacts values from CLI output but does not encrypt them in the state file. Always secure your state file with encryption and backend access controls.


Working with tfvars Files

What Are tfvars Files?

tfvars files (Terraform variables files) assign concrete values to input variables declared in your configuration. They separate configuration values from infrastructure code, enabling environment-specific deployments.

tfvars Syntax

There are two syntax formats for tfvars files:

HCL Format (*.tfvars)

instance_type = "t2.medium"
aws_region    = "us-east-1"
instance_count = 3
enable_monitoring = true

tags = {
  Environment = "production"
  Team        = "platform"
}

JSON Format (*.tfvars.json)

{
  "instance_type": "t2.medium",
  "aws_region": "us-east-1",
  "instance_count": 3,
  "enable_monitoring": true,
  "tags": {
    "Environment": "production",
    "Team": "platform"
  }
}

Complex Types in tfvars

List of Objects Example

Assume variables.tf defines:

variable "app_services" {
  type = list(object({
    name          = string
    instance_type = string
    port          = number
  }))
}

In services.tfvars:

app_services = [
  { name = "frontend", instance_type = "t3.medium", port = 80 },
  { name = "backend", instance_type = "t3.large", port = 8080 },
  { name = "api", instance_type = "t3.large", port = 3000 }
]

Automatic Loading

Terraform automatically loads tfvars files from the root module directory in a specific order:

  1. terraform.tfvars (HCL format) - if present
  2. terraform.tfvars.json (JSON format) - if present
  3. *.auto.tfvars files in lexical (alphabetical) order
  4. *.auto.tfvars.json files in lexical order

This automatic loading is convenient for default or environment-specific overrides. However, the scope is limited to the root module only—child modules cannot directly read values from tfvars files.

Explicit Loading with -var-file

To load specific tfvars files, use the -var-file command-line option:

terraform plan -var-file="dev.tfvars"
terraform apply -var-file="prod.tfvars"
tofu apply -var-file="environments/staging.tfvars"

Multiple -var-file options can be specified, and they are loaded in the order given:

terraform apply -var-file="base.tfvars" -var-file="prod.tfvars"

Organizing tfvars Files

A recommended directory structure for multi-environment management:

project/
├── main.tf
├── variables.tf
├── outputs.tf
├── terraform.tfvars.example     # Commit this, not secrets
├── environments/
│   ├── dev.tfvars
│   ├── staging.tfvars
│   └── prod.tfvars
└── .gitignore                   # Exclude actual tfvars files

Corresponding .gitignore:

# Exclude actual tfvars files (may contain secrets)
*.tfvars
!*.tfvars.example

Variable Precedence and Loading

The Variable Precedence Order

If a variable is specified in multiple places, Terraform applies values in order of increasing precedence (later values override earlier ones):

  1. Environment Variables: TF_VAR_name (or OPENTOFU_VAR_name for OpenTofu)
  2. terraform.tfvars file
  3. terraform.tfvars.json file (if both .tfvars and .tfvars.json exist, JSON overrides for the same variable)
  4. *.auto.tfvars or *.auto.tfvars.json files (loaded alphabetically; later files override earlier ones)
  5. Command-line flags (-var and -var-file): Processed in the order given; highest precedence

OpenTofu Environment Variables

OpenTofu introduces OPENTOFU_VAR_name environment variables while maintaining backward compatibility with TF_VAR_name:

  • If only TF_VAR_name is set: OpenTofu uses it
  • If only OPENTOFU_VAR_name is set: OpenTofu uses it
  • If both are set for the same variable: OPENTOFU_VAR_name takes precedence

For new OpenTofu projects, adopt the OPENTOFU_VAR_ prefix.

Practical Precedence Example

Given this setup:

variables.tf:

variable "region" {
  type    = string
  default = "us-west-2"
}

terraform.tfvars:

region = "us-east-1"

auto.tfvars:

region = "eu-west-1"

Environment:

export TF_VAR_region="ap-south-1"

Command:

terraform plan -var="region=ca-central-1"

Result: The final value is ca-central-1 (command-line flag has highest precedence).

Debugging Precedence Issues

Use terraform console to inspect variable values:

terraform console
> var.region
"ca-central-1"

Or examine the plan output:

terraform plan -var-file="dev.tfvars" | grep "region"

Output Values: Exposing Infrastructure Data

What Are Output Values?

Terraform outputs are named values that expose data from your infrastructure after terraform apply. They serve as the "return values" of your module, making critical information accessible to:

  • End users via CLI display
  • Other modules as dependencies
  • External tools and CI/CD pipelines
  • State files for cross-configuration references

Basic Output Example

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "public" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
}

output "vpc_id" {
  value       = aws_vpc.main.id
  description = "The ID of the VPC."
}

output "public_subnet_id" {
  value       = aws_subnet.public.id
  description = "The ID of the public subnet."
}

After terraform apply, the outputs are displayed:

Outputs:

public_subnet_id = "subnet-0123456789abcdef"
vpc_id = "vpc-0123456789abcdef"

Output Arguments

value (required)

The expression to expose as an output. Can be a resource attribute, local value, or computed expression.

output "s3_bucket_name" {
  value = aws_s3_bucket.app_data.id
}

description (optional, highly recommended)

Human-readable explanation of what the output represents.

output "database_endpoint" {
  value       = aws_db_instance.main.endpoint
  description = "The endpoint address of the RDS database instance."
}

sensitive (optional, default: false)

When true, the CLI redacts the output value from display (useful for passwords, API keys, etc.). Note: The value is still stored in the state file.

output "db_password" {
  value       = aws_db_instance.main.password
  sensitive   = true
  description = "The database root password. Value is redacted from logs."
}

depends_on (optional)

Explicitly declares dependencies on resources or modules. Rarely needed because Terraform infers dependencies from value references.

resource "aws_instance" "app" {
  # ...
}

resource "aws_security_group_rule" "allow_ssh" {
  type                     = "ingress"
  from_port                = 22
  to_port                  = 22
  protocol                 = "tcp"
  cidr_blocks              = ["0.0.0.0/0"]
  security_group_id        = aws_instance.app.vpc_security_group_ids[0]
}

output "app_public_ip" {
  value       = aws_instance.app.public_ip
  description = "The public IP of the application server."
  depends_on  = [aws_security_group_rule.allow_ssh]
}

ephemeral (optional, Terraform 1.10+)

When true, prevents the output value from being persisted in the state file. Useful for temporary values only needed during a specific run (e.g., bootstrapping credentials).

resource "random_password" "temp_creds" {
  length = 16
}

output "bootstrap_password" {
  value       = random_password.temp_creds.result
  sensitive   = true
  ephemeral   = true
  description = "Temporary password for bootstrapping. Not stored in state."
}

Complex Outputs: Maps and Lists

Outputs can return structured data:

output "instance_details" {
  value = {
    id        = aws_instance.web.id
    public_ip = aws_instance.web.public_ip
    arn       = aws_instance.web.arn
  }
  description = "Details of the web server instance."
}

output "subnet_ids" {
  value       = [aws_subnet.public.id, aws_subnet.private.id]
  description = "List of subnet IDs."
}

Using For Expressions in Outputs

For expressions transform and restructure data before exposing it:

resource "aws_instance" "web" {
  count         = 3
  ami           = "ami-0abcdef1234567890"
  instance_type = "t2.micro"

  tags = {
    Name = "web-server-${count.index + 1}"
  }
}

output "web_server_public_ips" {
  description = "A map of web server names to public IPs."
  value = {
    for instance in aws_instance.web :
      instance.tags.Name => instance.public_ip
  }
}

Result:

web_server_public_ips = {
  "web-server-1" = "192.0.2.101"
  "web-server-2" = "192.0.2.102"
  "web-server-3" = "192.0.2.103"
}

Accessing Outputs with terraform output

The terraform output command retrieves output values programmatically:

# Display all outputs
terraform output

# Get a specific output
terraform output vpc_id

# Get output as JSON
terraform output -json
terraform output -json | jq '.vpc_id.value'

# Get raw output (useful for scripts)
terraform output -raw vpc_id

This is essential for CI/CD integration:

#!/bin/bash
APP_URL=$(terraform output -raw app_url)
echo "Deploying to $APP_URL"

Child Module Outputs

Access outputs from child modules using the module. syntax:

module "network" {
  source = "./modules/network"
  # ...
}

output "vpc_id_from_module" {
  value = module.network.outputs.vpc_id
}

Outputs Across Multiple State Files

Use terraform_remote_state to reference outputs from other Terraform configurations:

Network Configuration (network/main.tf)

output "vpc_id" {
  value = aws_vpc.main.id
}

output "public_subnet_id" {
  value = aws_subnet.public.id
}

Application Configuration (application/main.tf)

data "terraform_remote_state" "network" {
  backend = "s3"  # Or "local", "azurerm", etc.
  config = {
    bucket = "my-terraform-state"
    key    = "network/terraform.tfstate"
    region = "us-east-1"
  }
}

resource "aws_instance" "app" {
  subnet_id = data.terraform_remote_state.network.outputs.public_subnet_id
}

Local Values: Internal Helpers

What Are Local Values?

Local values (or "locals") are a way to assign a name to an expression within a module. They are internal to the module—not meant for external configuration—and serve to:

  • Reduce Repetition (DRY): Define complex expressions once and reuse them
  • Improve Readability: Assign meaningful names to "magic strings" or computed values
  • Centralize Logic: Encapsulate transformations or conditional logic

Declaring Locals

Locals are defined in locals blocks (note the plural). A module can have multiple locals blocks, which Terraform merges.

locals {
  # Simple assignments
  project_prefix = "mycorp"
  environment    = "dev"
  region         = "us-east-1"

  # Derived values (referencing other locals)
  common_name_prefix = "${local.project_prefix}-${local.environment}"

  # Complex structures
  common_tags = {
    Project     = local.project_prefix
    Environment = local.environment
    ManagedBy   = "Terraform"
    CreatedAt   = timestamp()
  }

  # Conditional logic
  instance_type = local.environment == "prod" ? "t3.large" : "t2.medium"
}

Referencing Locals

Access locals using the local.name syntax (note the singular "local"):

resource "aws_instance" "web" {
  ami           = "ami-0c55b31ad20599c04"
  instance_type = local.instance_type  # Use the conditional local

  tags = merge(
    local.common_tags,
    {
      Name = "${local.common_name_prefix}-web-server"
    }
  )
}

resource "aws_s3_bucket" "app_data" {
  bucket = "${local.common_name_prefix}-data-storage"

  tags = local.common_tags
}

Common Use Cases for Locals

Consistent Naming Conventions

Enforce standardized names across resources:

locals {
  app_name = "inventory"
  env      = "prod"
  base     = "${local.app_name}-${local.env}"
}

resource "aws_s3_bucket" "data" {
  bucket = "${local.base}-data"
}

resource "aws_dynamodb_table" "logs" {
  name = "${local.base}-logs"
}

Common Tags

Apply consistent tags to all resources:

locals {
  common_tags = {
    Environment = var.environment
    Project     = var.project_name
    Owner       = "infra-team"
    Terraform   = "true"
  }
}

resource "aws_instance" "web" {
  # ...
  tags = local.common_tags
}

resource "aws_s3_bucket" "assets" {
  # ...
  tags = local.common_tags
}

Conditional Logic

Simplify conditional resource configuration:

locals {
  # Determine instance type based on environment
  instance_type = var.environment == "prod" ? "t3.xlarge" : "t2.medium"

  # Build a list of AZs based on environment
  availability_zones = var.environment == "prod" ? ["us-east-1a", "us-east-1b", "us-east-1c"] : ["us-east-1a"]
}

resource "aws_instance" "app" {
  instance_type         = local.instance_type
  availability_zone     = local.availability_zones[0]
}

Complex Data Transformations

Pre-process data from variables:

variable "server_configs" {
  type = map(object({
    name          = string
    instance_type = string
    enabled       = bool
  }))
}

locals {
  # Filter to only enabled servers
  enabled_servers = {
    for name, config in var.server_configs :
      name => config if config.enabled
  }

  # Create a list of server names (for iteration)
  server_names = keys(local.enabled_servers)
}

resource "aws_instance" "servers" {
  for_each      = local.enabled_servers
  instance_type = each.value.instance_type

  tags = {
    Name = each.value.name
  }
}

Locals vs. Variables: Key Distinctions

The Core Differences

Understanding when to use locals versus input variables is crucial:

Feature Local Value Input Variable Output Value
Purpose Internal naming, reduce repetition Parameterize modules, accept external input Expose module results, link modules
Scope Internal to the module Module's input API Exports from module for external use
Assignment Defined via expression within module Set via CLI, env vars, tfvars, or calling module Expression value, usually resource attributes
User Input No direct external input; derived internally Primary mechanism for external configuration Not for input; displays/returns data
Analogy Function's temporary local variables Function arguments Function return values
Reference local.name var.name module.child.output_name or resource attributes

The Common Mistake: Using Locals for Module Configuration

❌ Incorrect: Locals cannot be set externally

# This doesn't work!
locals {
  environment = "production"  # Hardcoded—can't be changed per environment
}

# Or worse:
locals {
  environment_prefix = substr(var.environment, 0, 1)
}

variable "resource_name" {
  default = "${local.environment_prefix}-resource"  # ERROR: Can't reference locals here
}

✅ Correct: Use variables for external configuration

variable "environment" {
  type        = string
  description = "The environment name."
}

variable "resource_name_prefix" {
  type        = string
  description = "Prefix for resource names."
  default     = "resource"
}

locals {
  environment_prefix = substr(var.environment, 0, 1)
  resource_name      = "${local.environment_prefix}-${var.resource_name_prefix}"
}

resource "aws_s3_bucket" "example" {
  bucket = local.resource_name
}

Variable Scope: Root vs. Child Modules

Tfvars files apply only to the root module. Child modules receive values through variable definitions:

Root Module (main.tf)

variable "instance_count" {
  type = number
}

module "app" {
  source = "./modules/app"
  instance_count = var.instance_count  # Pass root module variable to child
}

Child Module (modules/app/main.tf)

variable "instance_count" {
  type = number
}

resource "aws_instance" "web" {
  count = var.instance_count
  # ...
}

terraform.tfvars only sets variables in the root module:

instance_count = 3  # This value flows to root, then to child module

When to Use Each

Use variable when:

  • You need to accept external input (from tfvars, CLI, or calling modules)
  • You want to make your module reusable across different configurations
  • The value might change per environment or deployment

Use local when:

  • You're deriving a value from variables or resource attributes
  • You want to avoid repetition of complex expressions
  • You need to apply conditional logic or transformations internally
  • The value is for internal use only and doesn't need external configuration

Advanced Patterns: Loops and Dynamic Data

Using Locals with for_each

Locals are powerful when combined with for_each for iteration:

variable "services" {
  type = map(object({
    port    = number
    enabled = bool
  }))
}

locals {
  # Filter to only enabled services
  enabled_services = {
    for name, config in var.services :
      name => config if config.enabled
  }
}

resource "aws_security_group_rule" "service_ingress" {
  for_each                   = local.enabled_services
  type                       = "ingress"
  from_port                  = each.value.port
  to_port                    = each.value.port
  protocol                   = "tcp"
  cidr_blocks                = ["0.0.0.0/0"]
  security_group_id          = aws_security_group.main.id
}

Handling Dynamic Index Issues with count

When using count, reference specific elements explicitly:

resource "aws_instance" "web" {
  count         = 3
  ami           = "ami-0abcdef1234567890"
  instance_type = "t2.micro"

  tags = {
    Name = "web-server-${count.index + 1}"
  }
}

output "first_web_server_ip" {
  value       = aws_instance.web[0].public_ip  # Explicitly index [0]
  description = "The public IP of the first instance."
}

output "all_web_server_ips" {
  value       = [for instance in aws_instance.web : instance.public_ip]
  description = "IPs of all web servers."
}

Complex Outputs with for Expressions

Transform resource collections into user-friendly outputs:

resource "aws_instance" "web" {
  count = 3
  ami   = "ami-0abcdef1234567890"

  tags = {
    Name = "web-${count.index + 1}"
  }
}

output "instance_summary" {
  value = {
    for i, instance in aws_instance.web :
      "server-${i + 1}" => {
        id  = instance.id
        ip  = instance.public_ip
        az  = instance.availability_zone
      }
  }
  description = "Summary of all instances with ID, IP, and AZ."
}

Combining Locals with Conditional Logic

Use locals to centralize complex conditions:

variable "environment" {
  type = string
}

variable "enable_detailed_monitoring" {
  type    = bool
  default = false
}

locals {
  # Determine feature flags based on environment
  is_production = var.environment == "prod"

  # Enable detailed monitoring in production or if explicitly requested
  monitoring_enabled = local.is_production || var.enable_detailed_monitoring

  # Scale configuration based on environment
  autoscaling_config = local.is_production ? {
    min_capacity = 5
    max_capacity = 20
  } : {
    min_capacity = 1
    max_capacity = 5
  }
}

resource "aws_autoscaling_group" "app" {
  min_size         = local.autoscaling_config.min_capacity
  max_size         = local.autoscaling_config.max_capacity
  desired_capacity = local.autoscaling_config.min_capacity
}

resource "aws_cloudwatch_metric_alarm" "cpu" {
  count               = local.monitoring_enabled ? 1 : 0
  alarm_name          = "app-cpu-alarm"
  comparison_operator = "GreaterThanThreshold"
  threshold           = local.is_production ? 70 : 80
}

Best Practices

1. Variable Documentation

Always include detailed descriptions:

variable "instance_type" {
  type        = string
  description = "The EC2 instance type. Examples: t2.micro, t3.small, m5.large."
  default     = "t2.micro"
}

2. Validation for Data Quality

Use validation blocks to catch errors early:

variable "environment" {
  type        = string
  description = "Environment name."

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

variable "instance_count" {
  type = number

  validation {
    condition     = var.instance_count >= 1 && var.instance_count <= 100
    error_message = "Instance count must be between 1 and 100."
  }
}

3. Secure Sensitive Data

Never commit plain-text secrets to version control:

# ❌ Don't do this
# prod.tfvars
db_password = "SuperSecretPassword123!"

# ✅ Do this: Use secure secret management
# prod.tfvars
db_password = var.db_password_from_vault

# Inject via environment variable or CI/CD
export TF_VAR_db_password=$(vault kv get -field=password secret/prod/db)

Alternatives for secret management:

  • HashiCorp Vault
  • AWS Secrets Manager / Parameter Store
  • Azure Key Vault
  • Google Secret Manager
  • Terraform Cloud/Spacelift secure variables
  • Mozilla SOPS for file-level encryption

4. Organize tfvars by Environment

Structure for scalability:

infrastructure/
├── variables.tf
├── outputs.tf
├── main.tf
├── terraform.tfvars.example
├── environments/
│   ├── dev.tfvars
│   ├── staging.tfvars
│   └── prod.tfvars
└── .gitignore

5. Use Example tfvars Files

Commit example files (not actual ones with secrets):

terraform.tfvars.example:

# Example configuration for development
environment     = "dev"
instance_count  = 2
instance_type   = "t2.micro"
enable_monitoring = false

# Uncomment and update as needed
# aws_region = "us-east-1"
# db_engine_version = "13.7"

6. Clear Naming Conventions

Use consistent, descriptive names:

# ✅ Good: Clear intent
variable "max_retries"
variable "enable_auto_recovery"
variable "db_backup_retention_days"

# ❌ Avoid: Ambiguous or cryptic
variable "max_retry"
variable "ar"
variable "backup"

7. Minimize Output Exposure

Don't output sensitive data unnecessarily:

# ❌ Risky: Exposes plaintext password
output "db_password" {
  value = aws_db_instance.main.password
}

# ✅ Better: Mark as sensitive
output "db_password" {
  value     = aws_db_instance.main.password
  sensitive = true
}

# ✅ Best: Output only the endpoint, not credentials
output "db_endpoint" {
  value       = aws_db_instance.main.endpoint
  description = "Database endpoint for application connection."
}

8. Use Locals to Reduce Duplication

Avoid repeating the same expression:

# ❌ Repetitive
resource "aws_security_group" "web" {
  name = "${var.project}-${var.environment}-web-sg"
  tags = {
    Name = "${var.project}-${var.environment}-web-sg"
  }
}

resource "aws_s3_bucket" "logs" {
  bucket = "${var.project}-${var.environment}-logs"
  tags = {
    Name = "${var.project}-${var.environment}-logs"
  }
}

# ✅ Cleaner with locals
locals {
  name_prefix = "${var.project}-${var.environment}"
  common_tags = {
    Project     = var.project
    Environment = var.environment
  }
}

resource "aws_security_group" "web" {
  name = "${local.name_prefix}-web-sg"
  tags = merge(local.common_tags, { Name = "${local.name_prefix}-web-sg" })
}

resource "aws_s3_bucket" "logs" {
  bucket = "${local.name_prefix}-logs"
  tags   = merge(local.common_tags, { Name = "${local.name_prefix}-logs" })
}

9. Version Control Strategy

.gitignore for sensitive files:

# Terraform files
*.tfstate*
*.tfvars
!*.tfvars.example
*.auto.tfvars

# Secrets
.env
.env.local
secret/
vault/

Commit:

  • .tf files (configuration)
  • *.tfvars.example (templates only)
  • .gitignore

Do NOT commit:

  • *.tfvars (unless example)
  • .tfstate files
  • .terraform/ directory
  • Files with passwords, API keys, or PII

10. Handle Complex Types Carefully

Use object types for clarity:

# ✅ Clear structure
variable "database_config" {
  type = object({
    engine         = string
    version        = string
    instance_class = string
    allocated_storage = number
  })
}

# ❌ Too flexible; loses type safety
variable "database_config" {
  type = any
}

Scalr Integration

Native Variable Management in Scalr

Scalr provides a unified approach to managing Terraform variables across multiple workspaces and environments. This simplifies variable handling at scale:

Scalr Variable Hierarchy:

  1. Account Level: Variables available across all environments and workspaces
  2. Environment Level: Variables specific to an environment (dev, staging, prod)
  3. Workspace Level: Variables specific to a single workspace

Variables can be:

  • Terraform variables: Passed to Terraform as terraform.tfvars.json
  • Shell variables: Exported as environment variables during runs
  • Environment variables: Set as OS-level environment variables

Setting Variables in Scalr

Example: Creating an account-level variable in Scalr

# Variable defined in Scalr UI or API
variable "region" {
  type        = string
  description = "AWS region for all resources"
  default     = "us-east-1"
}

In Scalr, you would set:

  • Type: terraform
  • Key: region
  • Value: us-east-1
  • Scope: account (available to all workspaces)

Shared Output Example with Scalr

Use terraform_remote_state to reference outputs from other Scalr workspaces:

data "terraform_remote_state" "network" {
  backend = "remote"

  config = {
    hostname     = "your-scalr-instance.scalr.io"
    organization = "your-environment"
    workspaces = {
      name = "network-prod"
    }
  }
}

resource "aws_instance" "app" {
  subnet_id = data.terraform_remote_state.network.outputs.public_subnet_id
  # ...
}

output "app_instance_id" {
  value = aws_instance.app.id
}

Scalr Policy Enforcement

Scalr can enforce variable naming conventions and structure validation. For example:

# Scalr policy to enforce variable naming
def check_variable_names(context):
    variables = context.tf_vars
    for var_name in variables:
        if not var_name.islower():
            return False, f"Variable '{var_name}' must be lowercase"
    return True, "All variables follow naming convention"

Benefits of Using Scalr for Variable Management

  • Centralized Control: Manage variables across environments from a single interface
  • Audit Trail: Track all variable changes with full history
  • Sensitive Storage: Securely store and manage sensitive variables without committing to Git
  • Hierarchy Support: Define variables at different scopes (account, environment, workspace)
  • Policy Enforcement: Use Scalr policies to validate variable structure and values
  • No .gitignore Needed: Secrets never leave Scalr; they're injected at run time

Conclusion

Mastering variables, outputs, and locals is essential for writing scalable, maintainable Terraform and OpenTofu configurations. Key takeaways:

  1. Input Variables parameterize your modules for external configuration
  2. tfvars Files separate values from code, enabling environment-specific deployments
  3. Variable Precedence matters—understand the loading order to avoid surprises
  4. Output Values expose infrastructure data for consumption by other modules or external tools
  5. Local Values reduce repetition and improve readability within modules
  6. Locals vs. Variables serve different purposes: use variables for external input, locals for internal organization
  7. Advanced Patterns like for expressions and conditional logic enable sophisticated data transformations
  8. Best Practices include clear naming, validation, secure secret handling, and proper documentation
  9. Scalr Integration simplifies variable management across multiple environments and workspaces at scale

By following these patterns and best practices, you'll build flexible, secure, and maintainable infrastructure code that scales with your organization.


Further Reading


Updated for 2026 with Terraform 1.10+ features and modern IaC practices.