Guide to Terraform Local Values
Understand Terraform local values: when to use them, how to define and reference locals, and best practices that keep your HCL clean, DRY, and maintainable.
Terraform has become a cornerstone of Infrastructure as Code (IaC), allowing teams to define and manage infrastructure with unparalleled precision. Within the HashiCorp Configuration Language (HCL), local values (or "locals") are a seemingly simple feature that can dramatically improve the clarity, maintainability, and efficiency of your configurations. Let's dive into what they are, why you should use them, and how they fit into a well-architected IaC strategy.
1. Understanding Local Values: The What and Why
Terraform local values allow you to assign a name to an expression. Think of them as temporary, named constants or variables within your Terraform module. Their primary purpose is to make your configurations:
- DRY (Don't Repeat Yourself): Define a value or expression once and reuse it. If it needs to change, you only update it in one place.
- More Readable: Assign meaningful names to complex expressions or "magic strings/numbers," making your code easier to understand at a glance.
Locals are evaluated during Terraform's planning phase and their values are fixed for that run. They are scoped strictly to the module in which they are defined, acting as internal helpers rather than part of a module's external interface.
2. Syntax and Usage: Getting Started with Locals
Local values are declared within a locals
block (note the plural locals
). You can have multiple locals
blocks in a module, and Terraform will merge them.
locals {
project_prefix = "mycorp"
environment = "dev"
region = "us-east-1"
// A local that references other locals
common_name_prefix = "${local.project_prefix}-${local.environment}"
common_tags = {
Project = local.project_prefix
Environment = local.environment
ManagedBy = "Terraform"
}
}
To use a local value elsewhere in your module, you reference it using the local.
syntax (note the singular local
):
resource "aws_instance" "web_server" {
ami = "ami-0c55b31ad20599c04" // Example AMI
instance_type = "t2.micro"
// Referencing locals
tags = merge(
local.common_tags,
{
Name = "${local.common_name_prefix}-web-server"
}
)
}
In this example, local.common_tags
and local.common_name_prefix
simplify the resource definition and ensure consistency.
3. Locals vs. Input Variables vs. Output Values: A Clear Distinction
It's crucial to understand how locals differ from input variables (variable
) and output values (output
):
Feature | Local Value ( | Input Variable ( | Output Value ( |
---|---|---|---|
Purpose | Internal naming, reduce repetition, improve readability | Parameterize modules, accept external configuration | Expose module results, link modules |
Scope | Internal to the module | Defines module's input API; values passed in | Exports values from a module for external use |
Assignment | Defined via expression within the module | Set via CLI, env vars, | Value is an expression, often a resource attribute or local |
User Input | No direct user input; derived internally | Primary mechanism for user/caller input | Not for input; displays/returns data |
Analogy | Function's temporary local variables | Function arguments | Function return values |
Referencing |
|
|
|
Essentially:
- Input Variables: How you pass data into a module.
- Local Values: How you manipulate and organize data within a module.
- Output Values: How you pass data out of a module.
4. Practical Power: Common Use Cases for Locals
Locals shine in several common scenarios:
Consistent Naming Conventions
Enforce standardized names for your resources.
locals {
app_name = "inventory"
environment = "prod"
base_name = "${local.app_name}-${local.environment}"
}
resource "aws_s3_bucket" "app_data" {
bucket = "${local.base_name}-data-storage"
// ...
}
resource "aws_db_instance" "app_db" {
identifier = "${local.base_name}-rds"
// ...
}
Standardized Tagging
Ensure all resources have a consistent set of tags for cost allocation, organization, and automation.
locals {
default_tags = {
CostCenter = "IT-DEPT-123"
Application = "CustomerPortal"
Environment = var.env // Assuming var.env is an input variable
Terraformed = "true"
}
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = local.default_tags
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
tags = merge(local.default_tags, {
Tier = "Public"
})
}
Dynamic Default Values
Terraform input variables can have static default
values. If you need a default that's computed based on other variables or logic, locals are the way to go.
variable "is_production" {
type = bool
default = false
}
locals {
instance_type = var.is_production ? "m5.large" : "t3.micro"
// If var.is_production is true, instance_type is "m5.large", else "t3.micro"
}
resource "aws_instance" "compute" {
ami = "ami-0abcdef1234567890" // Example AMI
instance_type = local.instance_type // Uses the dynamically determined type
// ...
}
5. Advanced Techniques: Transformations and Conditional Logic
Locals can leverage Terraform's full expression syntax, including for
expressions and functions, for powerful data transformations:
variable "user_names" {
type = list(string)
default = ["alice", "bob", "charlie"]
}
locals {
// Create a map of user objects from a list of names
user_objects = {
for user in var.user_names : user => {
description = "User account for ${title(user)}"
group = "standard_users"
}
}
// user_objects will be:
// {
// "alice" = { description = "User account for Alice", group = "standard_users" },
// "bob" = { description = "User account for Bob", group = "standard_users" },
// ...
// }
// Flatten a list of lists
security_group_rules = flatten([
var.common_rules, // e.g., [ { port = 22, proto = "tcp" } ]
var.app_specific_rules // e.g., [ { port = 80, proto = "tcp" }, { port = 443, proto = "tcp" } ]
])
}
// Example usage for user_objects (conceptual)
resource "some_user_resource" "users" {
for_each = local.user_objects
name = each.key
details = each.value
}
This allows you to prepare complex data structures needed for for_each
loops or dynamic blocks, keeping your resource configurations clean.
6. Best Practices for Effective Local Value Management
To make the most of locals:
- Naming: Use clear, descriptive, lowercase, underscore-separated names (e.g.,
vpc_cidr_block
). - File Organization: For locals used across multiple files in a module, define them in a dedicated
locals.tf
file. For file-specific locals, define them at the top of that file. - Moderation: Don't over-abstract. Use locals for values/expressions that are repeated, complex, or likely to change. Too many layers of indirection can reduce readability.
- Comments: For complex local definitions, add comments explaining their purpose or derivation.
- Lifecycle Awareness: Be mindful that changing a local used in a resource's name or
for_each
key can cause Terraform to see it as a new resource, potentially leading to recreation.
While these practices help manage complexity within your HCL, scaling IaC across larger teams and multiple environments introduces broader governance challenges. This is where platforms like Scalr can provide significant value. Scalr helps enforce standards, manage state, control costs, and provide visibility across all your Terraform workspaces, complementing the in-code organization that locals provide by offering a higher-level management and policy enforcement layer. Effective use of locals makes your code more amenable to such overarching governance.
7. Avoiding Common Pitfalls
- Circular Dependencies: Don't define locals that refer to each other in a loop (e.g.,
local.a = local.b
andlocal.b = local.a
). Terraform will detect and error on this. - Over-Abstraction: Creating locals for trivial, non-repeated values can add unnecessary complexity.
- Using Locals for User Inputs: If a value should be configurable by the module user, use an input variable, not a local.
8. Conclusion: Locals as a Building Block for Scalable IaC
Terraform local values are a fundamental tool for writing clean, maintainable, and understandable Infrastructure as Code. By reducing repetition, naming complex expressions, and enabling sophisticated data transformations, they empower you to build more robust and manageable configurations.
As your infrastructure grows in complexity and scale, the good habits formed by effectively using locals—such as clear naming, modularity, and adherence to the DRY principle—become even more critical. These foundational practices, when combined with robust IaC management platforms like Scalr, enable organizations to confidently scale their cloud operations, ensuring consistency, compliance, and collaboration across teams. Mastering locals is a key step on the journey to truly effective and scalable IaC.