Terraform Local Values
Learn how Terraform local values work, when to use locals instead of variables or data sources, and how to simplify repetitive expressions in your infrastructure code.
What Are Local Values in Terraform?
Terraform local values, usually just called locals, are named expressions defined inside a locals block. They let you store a calculated value once and then reuse it throughout your configuration.
If input variables are the values that come into your Terraform code, local values are the values you compute and organize inside it.
A basic example looks like this:
variable "environment" {
type = string
}
variable "project_name" {
type = string
}
locals {
name_prefix = "${var.project_name}-${var.environment}"
}
resource "aws_s3_bucket" "logs" {
bucket = "${local.name_prefix}-logs"
}
Here, local.name_prefix saves you from repeating the same string construction everywhere.
Locals do not accept input from the user. They are not part of the public interface of a module. They are strictly an internal convenience that improves readability and removes duplication.
Why Local Values Matter
Beginners sometimes skip locals and write everything inline. Terraform allows that, but the configuration becomes noisy very quickly.
For example:
resource "aws_cloudwatch_log_group" "app" {
name = "/aws/${var.project_name}/${var.environment}/app"
}
resource "aws_s3_bucket" "logs" {
bucket = "${var.project_name}-${var.environment}-logs"
}
resource "aws_iam_role" "app" {
name = "${var.project_name}-${var.environment}-app-role"
}
The repeated project_name + environment pattern appears everywhere. If your naming convention changes, you have to update many places.
With locals:
locals {
name_prefix = "${var.project_name}-${var.environment}"
}
resource "aws_cloudwatch_log_group" "app" {
name = "/aws/${local.name_prefix}/app"
}
resource "aws_s3_bucket" "logs" {
bucket = "${local.name_prefix}-logs"
}
resource "aws_iam_role" "app" {
name = "${local.name_prefix}-app-role"
}
This is easier to read and easier to change.
Defining Locals with the locals Block
A locals block can define one or many local values:
locals {
environment_upper = upper(var.environment)
common_tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "Terraform"
}
enable_backups = var.environment == "prod"
}
You reference them with local.<name>:
tags = local.common_tags
Note the syntax carefully:
var.namefor input variableslocal.namefor local valuesmodule.name.output_namefor module outputs
When to Use Locals vs Variables vs Data Sources
This is one of the most important conceptual distinctions in Terraform.
Use a variable when the value should come from outside
Variables are for values that should be supplied by:
- the person running Terraform
- a parent module
- a
.tfvarsfile - a CI/CD pipeline
Example:
variable "region" {
type = string
}
The region is a decision the caller should make, so it belongs in a variable.
Use a local when the value is internal logic
Locals are for values derived from variables, resources, or expressions inside the module.
Example:
locals {
bucket_name = "${var.project_name}-${var.environment}-logs"
}
This does not need to be exposed to the caller. It is implementation detail.
Use a data source when Terraform must query an external system
Data sources fetch existing information from a provider.
Example:
data "aws_availability_zones" "available" {}
This value comes from AWS, not from the user and not from pure local computation.
Quick decision guide
Ask this question:
- Should a caller choose this value? Use a variable.
- Can Terraform calculate this value from what it already knows? Use a local.
- Must Terraform ask a provider for this value? Use a data source.
Understanding this boundary keeps your module interfaces clean.
Using Expressions in Locals
Local values can contain almost any Terraform expression. This makes them very powerful.
String expressions
locals {
name_prefix = "${var.project_name}-${var.environment}"
log_group = "/aws/${var.project_name}/${var.environment}/app"
}
Arithmetic expressions
locals {
desired_capacity = var.environment == "prod" ? 4 : 2
total_disk_size = var.instance_count * var.disk_size_gb
}
Map and list expressions
locals {
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "Terraform"
}
private_subnet_names = [
for index, az in var.availability_zones :
"${var.project_name}-${var.environment}-private-${index + 1}"
]
}
Referencing resources
Locals can use resource attributes too:
locals {
alb_dns_name = aws_lb.app.dns_name
}
This is valid, but use it thoughtfully. If the value is only used once, creating a local may add unnecessary indirection.
Using Functions in Locals
Terraform includes many built-in functions, and locals are a great place to use them.
merge()
A classic pattern is combining default tags with user-supplied tags.
variable "tags" {
type = map(string)
default = {}
}
locals {
common_tags = merge(
{
Project = var.project_name
Environment = var.environment
ManagedBy = "Terraform"
},
var.tags
)
}
Now every resource can reuse local.common_tags.
lower(), upper(), replace()
Useful for naming standards:
locals {
normalized_project = replace(lower(var.project_name), "_", "-")
name_prefix = "${local.normalized_project}-${var.environment}"
}
concat() and distinct()
Useful for combining lists:
locals {
all_cidrs = distinct(concat(var.office_cidrs, var.vpn_cidrs))
}
lookup()
A common way to choose environment-specific values:
locals {
instance_size = lookup(
{
dev = "t3.micro"
staging = "t3.small"
prod = "t3.medium"
},
var.environment,
"t3.micro"
)
}
jsonencode()
Great for IAM policies or ECS task definitions:
locals {
bucket_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["s3:GetObject"]
Resource = "${aws_s3_bucket.assets.arn}/*"
}
]
})
}
This keeps complex JSON generation readable.
Using Conditionals in Locals
Locals often centralize environment-specific decisions.
locals {
instance_type = var.environment == "prod" ? "t3.medium" : "t3.micro"
backup_days = var.environment == "prod" ? 30 : 7
multi_az = var.environment == "prod" ? true : false
}
Now you express the rule once and reuse it everywhere.
You can also make conditionals more expressive by grouping them in locals rather than scattering ternary operators across resource blocks.
Common Patterns for Local Values
1. Common tags
This is probably the most common real-world use of locals.
locals {
common_tags = merge(
{
Project = var.project_name
Environment = var.environment
ManagedBy = "Terraform"
Owner = "platform-team"
},
var.extra_tags
)
}
Then:
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = var.instance_type
tags = local.common_tags
}
This keeps tags consistent across your infrastructure.
2. Name prefixes
locals {
name_prefix = "${var.project_name}-${var.environment}"
}
Then:
resource "aws_security_group" "app" {
name = "${local.name_prefix}-sg"
}
resource "aws_s3_bucket" "artifacts" {
bucket = "${local.name_prefix}-artifacts"
}
If the naming convention changes later, you update one local value instead of many resources.
3. Environment-specific values
locals {
desired_count = {
dev = 1
staging = 2
prod = 4
}[var.environment]
}
This is cleaner than repeating the same environment branching logic in multiple places.
4. Standardized subnet names
locals {
public_subnet_names = [
for index, az in var.availability_zones :
"${var.project_name}-${var.environment}-public-${index + 1}"
]
}
5. Feature flags derived from environment
locals {
enable_waf = var.environment == "prod"
enable_monitoring = contains(["staging", "prod"], var.environment)
}
This keeps business rules in one place.
A Complete Example
variable "project_name" {
type = string
}
variable "environment" {
type = string
}
variable "availability_zones" {
type = list(string)
}
variable "extra_tags" {
type = map(string)
default = {}
}
locals {
normalized_project = replace(lower(var.project_name), "_", "-")
name_prefix = "${local.normalized_project}-${var.environment}"
common_tags = merge(
{
Project = var.project_name
Environment = var.environment
ManagedBy = "Terraform"
},
var.extra_tags
)
instance_type = {
dev = "t3.micro"
staging = "t3.small"
prod = "t3.medium"
}[var.environment]
backup_retention_days = var.environment == "prod" ? 30 : 7
}
resource "aws_s3_bucket" "logs" {
bucket = "${local.name_prefix}-logs"
tags = local.common_tags
}
resource "aws_db_instance" "app" {
allocated_storage = 20
engine = "postgres"
instance_class = local.instance_type == "t3.medium" ? "db.t3.medium" : "db.t3.micro"
backup_retention_period = local.backup_retention_days
}
This demonstrates the main purpose of locals: moving repeated logic and naming rules into one understandable section.
Avoiding Overuse of Locals
Locals are helpful, but too many locals can make Terraform harder to read.
Bad pattern: creating locals for trivial values
locals {
bucket_acl = "private"
}
If a value is only used once and adds no clarity, keeping it inline may be simpler.
Bad pattern: turning every resource attribute into a local
locals {
s3_bucket_id = aws_s3_bucket.logs.id
}
If you use that ID once, just reference aws_s3_bucket.logs.id directly.
Bad pattern: hiding module inputs behind unnecessary locals
locals {
region = var.region
}
This adds one more name without adding meaning.
Good rule of thumb
Use locals when they do at least one of these things:
- remove repetition
- clarify intent
- group related logic
- centralize naming or tagging rules
- simplify complex expressions
If a local does none of those things, it may not be worth creating.
Best Practices for Terraform Locals
- Use clear names.
name_prefix,common_tags, andbackup_retention_daysare much better thanxorconfig1. - Group related locals together. Keep naming, tagging, and environment-specific settings organized.
- Prefer locals for internal logic. Do not expose implementation details as variables unless callers truly need to control them.
- Do not over-abstract. Terraform is configuration, not a general-purpose programming language.
- Keep module interfaces small. Use locals to compute internal values from a smaller set of inputs.
Final Thoughts
Local values are one of the easiest ways to make Terraform code more readable. They help you avoid copy-paste, keep naming conventions consistent, and express environment-specific logic once instead of in ten different places.
A useful mental model is:
- Variables are inputs chosen by the caller
- Locals are internal calculations chosen by the module author
- Data sources are external facts retrieved from the provider
If you apply that rule consistently, your Terraform code will be easier to understand and easier to maintain as it grows.
Knowledge Check
Question 1: Local Syntax
How do you reference a Terraform local value named name_prefix?
Question 2: Variables vs Locals
Which value is usually the best candidate for a local instead of an input variable?
Question 3: Overusing Locals
Which example best shows unnecessary overuse of Terraform locals?