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, orany)description: Documents the variable's purpose (highly recommended for clarity)default: Provides a default value, making the variable optionalnullable: Whenfalse, preventsnullvalues (Terraform 1.1+)sensitive: Whentrue, 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:
terraform.tfvars(HCL format) - if presentterraform.tfvars.json(JSON format) - if present*.auto.tfvarsfiles in lexical (alphabetical) order*.auto.tfvars.jsonfiles 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):
- Environment Variables:
TF_VAR_name(orOPENTOFU_VAR_namefor OpenTofu) terraform.tfvarsfileterraform.tfvars.jsonfile (if both.tfvarsand.tfvars.jsonexist, JSON overrides for the same variable)*.auto.tfvarsor*.auto.tfvars.jsonfiles (loaded alphabetically; later files override earlier ones)- Command-line flags (
-varand-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_nameis set: OpenTofu uses it - If only
OPENTOFU_VAR_nameis set: OpenTofu uses it - If both are set for the same variable:
OPENTOFU_VAR_nametakes 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:
.tffiles (configuration)*.tfvars.example(templates only).gitignore
Do NOT commit:
*.tfvars(unless example).tfstatefiles.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:
- Account Level: Variables available across all environments and workspaces
- Environment Level: Variables specific to an environment (dev, staging, prod)
- 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:
- Input Variables parameterize your modules for external configuration
- tfvars Files separate values from code, enabling environment-specific deployments
- Variable Precedence matters—understand the loading order to avoid surprises
- Output Values expose infrastructure data for consumption by other modules or external tools
- Local Values reduce repetition and improve readability within modules
- Locals vs. Variables serve different purposes: use variables for external input, locals for internal organization
- Advanced Patterns like
forexpressions and conditional logic enable sophisticated data transformations - Best Practices include clear naming, validation, secure secret handling, and proper documentation
- 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.