Locals in Terraform in Terraform, IaC & Cloud Automation
When you're managing infrastructure across multiple environments and teams, you quickly discover that repeating the same expressions throughout your Terraform code leads to maintenance headaches and inconsistency. Locals solve this by letting you define and name complex values once, then reference them throughout your configuration. This article walks you through how to use locals effectively without crossing the line into over-abstraction.
What Are Locals and Why They Matter
Locals are named values computed within your Terraform module. Unlike variables, locals are not inputs—they're derived values you calculate based on other locals, variables, data sources, or static values. They live in your current module scope and aren't passed between modules.
Think of locals as your module's internal constants and computed values. They're especially valuable when you find yourself writing the same expression multiple times, or when you want to make a complex expression readable by giving it a meaningful name.
Basic Syntax: locals { name = ... }
A locals block declares one or more local values. Here's the simplest form:
locals {
environment = var.env
region = var.aws_region
}
resource "aws_s3_bucket" "app_data" {
bucket = "app-data-${local.environment}-${local.region}"
}
resource "aws_s3_bucket" "logs" {
bucket = "app-logs-${local.environment}-${local.region}"
}
You reference a local value with local.name (singular). You can declare multiple locals blocks in a module, but most teams keep them in one place for readability. Order matters only if one local depends on another, so arrange them logically.
Locals can reference variables, data sources, resource attributes, and other locals:
locals {
app_name = var.application_name
env = var.environment
region = data.aws_region.current.name
# Depends on other locals
common_tags = {
Application = local.app_name
Environment = local.env
Region = local.region
ManagedBy = "Terraform"
}
# String interpolation
bucket_prefix = "${local.app_name}-${local.env}"
}
Build Tags and Names Once
One of the most practical uses for locals is centralizing how you build naming conventions and tags. In real-world infrastructure, you'll apply the same set of tags to dozens of resources and use consistent naming across buckets, instances, queues, and databases. A locals block becomes your source of truth for this.
Let's look at a typical e-commerce platform scenario where we manage multiple environments:
locals {
app_name = "checkout-service"
environment = var.environment
region = var.aws_region
cost_center = var.cost_center
owner_email = var.owner_email
# Computed naming convention
resource_name_prefix = "${local.app_name}-${local.environment}"
# Universal tags applied to all resources
common_tags = {
Application = local.app_name
Environment = local.environment
Region = local.region
CostCenter = local.cost_center
Owner = local.owner_email
CreatedBy = "Terraform"
CreatedAt = timestamp()
}
# Service-specific naming
service_tags = merge(local.common_tags, {
Component = "api-gateway"
})
}
resource "aws_s3_bucket" "checkout_logs" {
bucket = "${local.resource_name_prefix}-logs"
tags = local.common_tags
}
resource "aws_sqs_queue" "checkout_events" {
name = "${local.resource_name_prefix}-events"
visibility_timeout_seconds = 300
tags = merge(local.common_tags, {
Component = "event-queue"
})
}
resource "aws_lambda_function" "checkout_handler" {
function_name = "${local.resource_name_prefix}-handler"
tags = local.service_tags
}
By defining common_tags once, you guarantee consistency across all resources. When the finance team asks you to add a new cost center tag or when you move to a new region, you update one place. All resources automatically inherit the change. This also makes auditing and compliance reporting significantly easier.
Keep Expressions Readable
Locals aren't just about reducing repetition—they make complex logic easier to understand. When you have intricate conditionals, data source queries, or computed values, naming them clearly improves code maintainability for your entire team.
Consider a scenario where you're conditioning resource creation and configuration based on deployment tier:
locals {
is_production = var.environment == "prod"
is_staging = var.environment == "staging"
is_dev = var.environment == "dev"
# Derived capacity decisions
instance_count = local.is_production ? 3 : (local.is_staging ? 2 : 1)
instance_type = local.is_production ? "t3.large" : "t3.micro"
# Database settings
db_multi_az = local.is_production
db_backup_retention = local.is_production ? 30 : 7
db_deletion_protected = local.is_production
# Logging and monitoring
enable_detailed_monitoring = local.is_production || local.is_staging
log_retention_days = local.is_production ? 90 : 30
}
resource "aws_rds_cluster" "main" {
cluster_identifier = "app-cluster"
multi_az = local.db_multi_az
backup_retention_days = local.db_backup_retention
deletion_protection = local.db_deletion_protected
}
resource "aws_cloudwatch_log_group" "app_logs" {
name = "/aws/app/${local.environment}"
retention_in_days = local.log_retention_days
}
By naming is_production, instance_count, and log_retention_days, anyone reading your code immediately understands the intent. The conditionals are applied in one place, making future changes predictable and testable.
You can also use locals to make data transformations explicit:
locals {
# Map input list into a usable structure
availability_zones = data.aws_availability_zones.available.names
# Create a map for easy lookup
subnet_configs = {
for idx, az in local.availability_zones : az => {
cidr_block = "10.0.${idx * 16}.0/20"
az = az
}
}
# Filter and transform
public_subnets = {
for az, config in local.subnet_configs :
az => config if can(regex("^us-east-1[ac]$", az))
}
}
Do Not Overuse — Inputs Belong in Variables
The biggest mistake teams make is blurring the line between locals and variables. A good rule: if a value should be configurable by the person or system calling your module, it belongs in a variable, not a local.
Variables are the module's public interface. Locals are internal implementation details. When you hardcode business logic into locals, you remove flexibility from your infrastructure code:
// WRONG: Instance type should be configurable per environment
locals {
instance_type = "t3.xlarge" // Hard-coded, not flexible
}
// RIGHT: Let the caller decide, use locals for derived values
variable "instance_type" {
type = string
description = "EC2 instance type"
}
variable "environment" {
type = string
description = "Deployment environment"
}
locals {
# This is fine: derived from inputs
env_tags = {
Environment = var.environment
InstanceType = var.instance_type
}
}
When you're building a module that other teams will use, expose the knobs they need to turn. Reserve locals for computed values, defaults derived from inputs, and internal shortcuts that improve readability without reducing flexibility.
Another trap: using locals to hide complexity that should live in separate modules or be made explicit through variables:
// RISKY: Complex logic buried in locals
locals {
# What happens if we have 200+ lines here? Hard to test or understand
vpc_config = merge(
var.vpc_base_config,
var.environment == "prod" ? var.prod_vpc_overrides : {},
var.environment == "staging" ? var.staging_vpc_overrides : {},
# ... dozens more conditions
)
}
// BETTER: Make this a separate module or expose it clearly
module "vpc_configuration" {
source = "./modules/vpc-config"
base_config = var.vpc_base_config
environment = var.environment
environment_rules = var.vpc_environment_rules
}
Locals are best kept simple and focused. If your locals block spans more than 20-30 lines or contains deeply nested logic, consider whether you're solving a problem that belongs elsewhere.
Common Mistakes
- Treating locals as variables: Locals can't be overridden at apply time or passed between modules. If you need external control, use a variable instead.
- Over-nesting and over-abbreviating: Names like
local.ctorlocal.pt_env_tagssave typing but cost readability. Spend the extra characters for clarity. - Creating circular dependencies: While Terraform catches this, avoid letting one local depend on another in convoluted ways. Keep the dependency graph simple and top-down.
- Putting all configuration into locals: Your module's public interface (variables) and internal logic (locals) should be clearly separated. Don't hide important decisions in locals.
- Using locals for sensitive values: Locals are not encrypted in state. For secrets, use
sensitive = trueon variables and reference them directly, or use a secrets manager.
Key Takeaways
- Use
locals {}to define named values computed within your module; reference them withlocal.name. - Centralize naming conventions and tags in locals to ensure consistency and reduce maintenance burden across dozens of resources.
- Name complex expressions explicitly to improve readability and make future changes more predictable.
- Keep locals simple and focused on internal implementation; expose configurable values as variables.
- Reserve locals for derived values, computed tags, and internal shortcuts—not for business logic that should be flexible or tested independently.
- A well-designed locals block is usually 10-30 lines; if it grows beyond that, reconsider whether some logic belongs in a separate module.






Comments
Loading comments…