locals: Stop Repeating Yourself
The Problem Locals Solve
You have a project name, an environment tag, and a naming convention. They appear in resource names, tags, bucket prefixes, DNS entries, and log group paths — repeated fifteen times across your config. Then the naming convention changes. You update it in twelve places, miss three, and now your tags are inconsistent. The monitoring dashboard shows resources from “payments-prod” alongside resources from “prod-payments” because someone applied the convention in reverse order in two places.
That’s the problem locals solves. It’s the DRY principle applied to Terraform — Don’t Repeat Yourself.
What a locals Block Is — And What It Isn’t
A local value is a named expression you define once and reference anywhere in the same module. It’s not an input (that’s what variables are for — configurable from outside). It’s not exposed externally (that’s what outputs do). It’s internal computation — a way to name, compute, and reuse derived values.
locals {
project = "payments"
environment = var.environment
region = "us-east-1"
# Computed value — built from other locals and variables
name_prefix = "${local.project}-${local.environment}"
# Common tags applied to every resource
common_tags = {
Project = local.project
Environment = local.environment
ManagedBy = "terraform"
}
}
Then use them:
resource "aws_s3_bucket" "data" {
bucket = "${local.name_prefix}-data"
tags = local.common_tags
}
resource "aws_instance" "app" {
...
tags = merge(local.common_tags, { Role = "app-server" })
}
Change local.environment once — through the variable it references — and every resource that uses local.name_prefix or local.common_tags updates automatically on the next apply. Single source of truth.
local. vs var. — The Key Distinction
var. (Variables) | local. (Locals) | |
|---|---|---|
| Set by | External caller (.tfvars, CLI, environment) | Defined inside the config |
| Purpose | Accept input from outside | Compute and reuse internal values |
| Visible externally | Yes — the module’s input interface | No — internal to the module |
| Can be overridden | Yes — that’s the point | No — fixed computation at evaluation time |
The decision rule: If a value needs to be configurable from outside the module — use a variable. If it’s derived from variables, computed from other values, or just needs to be reused internally — use a local. Don’t make a variable for something that has one correct value derived from other inputs. That’s what locals are for.
Locals for Complex Expressions — Readability That Scales
Locals really earn their keep when expressions get complex. Without locals, you’d embed these computations inline in resource blocks, making the code unreadable:
locals {
# Compute availability zones from the region
azs = ["${var.region}a", "${var.region}b", "${var.region}c"]
# Build an environment-specific config map
instance_config = {
dev = { type = "t3.micro", count = 1 }
prod = { type = "t3.xlarge", count = 3 }
}
# Select the right config for this environment
current_config = local.instance_config[var.environment]
}
resource "aws_instance" "app" {
count = local.current_config.count
instance_type = local.current_config.type
}
Without the locals, the resource block would need to contain the full map lookup inline: var.instance_config[var.environment].count. Nested once, it’s manageable. Referenced in five places across the config? It becomes a maintenance nightmare. The local gives you a readable alias that you define once and reference by name.
This pattern is also how you implement environment-specific behavior without conditionals everywhere. The map contains the per-environment configuration. The local selects the right entry. Resources consume the selected values without knowing anything about environments.
Locals for Consistent Tagging — The Most Common Real-World Use
Every organization has a tagging policy. Cost allocation, team ownership, environment classification, compliance tracking — tags drive all of these. And they need to be consistent across every resource.
locals {
tags = {
Project = var.project_name
Environment = var.environment
Owner = var.team_name
CostCenter = var.cost_center
ManagedBy = "terraform"
Repository = "github.com/company/infra"
}
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = local.tags
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
tags = merge(local.tags, { Tier = "public" })
}
resource "aws_security_group" "web" {
vpc_id = aws_vpc.main.id
tags = merge(local.tags, { Role = "web-tier" })
}
Every resource gets the full standard tag set. No copy-paste. merge() adds resource-specific tags without overwriting the common ones. If the compliance team adds a required tag — say, DataClassification — you add it to the local.tags map once. Every resource inherits it on the next apply.
Without this pattern, adding a new tag to 50 resources means editing 50 resource blocks. With it, it’s a one-line change in the locals block.
Locals Can Reference Other Locals
locals {
base_name = "${var.project}-${var.environment}"
bucket_name = "${local.base_name}-assets"
log_bucket = "${local.base_name}-logs"
domain = "${local.base_name}.example.com"
}
Locals that reference other locals don’t need to be declared in order. Terraform evaluates the dependency graph internally and resolves them correctly regardless of declaration order.
One constraint: locals cannot be circular. If local.a references local.b and local.b references local.a, Terraform errors immediately. This is the same DAG constraint that applies to resources — no cycles allowed.
Where Locals Live — Convention and Practice
The standard convention is a dedicated locals.tf file:
project/
├── main.tf ← resource definitions
├── variables.tf ← input variables
├── outputs.tf ← output values
├── locals.tf ← local values
└── terraform.tfvars ← variable values
There’s no technical requirement to use a separate file — locals can be defined in any .tf file. But the convention exists for maintainability. When someone new joins the team and needs to understand the naming convention or tagging strategy, they know to look in locals.tf. Predictable file organization reduces the time-to-understanding.
Key Takeaways
The declaration block is locals {} (plural). The reference is local. (singular). This trips people up constantly — you write locals { name = "web" } but reference it as local.name. The asymmetry is just HCL syntax.
If you find yourself writing the same expression — even a simple string concatenation — in more than two places, pull it into a local. Locals are free. They have zero performance cost. Repeated expressions are technical debt with interest that compounds every time the value needs to change.
The tagging pattern (local.tags + merge()) is probably the single most impactful Terraform best practice for production environments. It ensures consistency, simplifies compliance, and turns a 50-resource change into a 1-line edit. If you take nothing else from this article, implement this pattern.

