This article covers Terraform local values, often just called locals. It explains what they are, how they differ from variables, and when reaching for one actually pays off, with a few worked examples.
A local lets you give a name to the result of an expression and then reuse that result wherever you need it. You set the value once and reference it by name, instead of writing out the same expression in several places.
There's a cost to that, though. Locals are easy to overuse, and when you do, the configuration gets harder to follow. The reader no longer sees the actual value or expression at the point of use. It's 'hidden' behind the name, and anyone working on the code later has to chase it back to the locals block. Use them in moderation.
Local values are not set by user input or values in .tfvars files; instead, they are set 'locally' to the configuration (hence the name). Instead of hardcoding values, local values can produce a more meaningful or readable result. The ability to change values that are likely to change in the future is the key benefit of using Terraform locals. Unlike variable values, local values can use dynamic expressions and resource arguments.
These values live alongside the configuration, and in a well-organized module a contributor should be able to find them quickly. In my experience, if a module uses local values, it's worth putting them in a separate file called locals.tf so they're easy to reference. That's a convention, not a rule. Locals can go anywhere in your Terraform configuration files.
Local values do not change between Terraform runs or stages in the lifecycle, such as plan, apply or destroy.
Naming resources is a good place to start. Say you want every resource to follow one naming convention with a fixed prefix.
In the example below, we define name_prefix in the locals block. It combines "JacksDemo-" with the contents of the environment variable, giving something like "JacksDemo-Prod". We then use that prefix to name an application gateway, together with var.application_gateway_name.
locals {
name_prefix = "JacksDemo-${var.environment}"
}
module "appgateway" {
application_gateway_name = "${local.name_prefix}-${var.application_gateway_name}"
...
}Say you have a set of tags that must go on every resource, plus a few tags that only apply to one resource. With locals, you define the mandatory tags once and then merge them with the resource-specific ones to get the final set. The user can still add their own tags, but the required set always comes along.
In the below example, mandatory tags include the cost_center and environment which will then be merged with those tags defined in the resource_tags variable.
You can use the merge function to combine local values with variable values.
locals {
mandatory_tags = {
cost_center = var.cost_center,
environment = var.environment
}
}
resource "aws_instance" "example" {
# ... other configuration ...
tags = merge(var.resource_tags, local.mandatory_tags)
}In this example, we will use locals to configure an Azure Application Gateway.
locals {
gateway_ip_configuration = [
{
name = var.gateway_ip_configuration_name
subnet_id = module.vnet.subnet01_id
}
]
ssl_certificate = [
{
name = var.ssl_certificate_name
data = data.azurerm_key_vault_secret.certificate.value
password = var.ssl_certificate_password
}
]
authentication_certificate = [
{
name = var.authentication_certificate_name
data = data.azurerm_key_vault_secret.authentication_certificate.value
}
]
mandatory_tags = {
cost_center = var.cost_center,
environment = var.environment
}
name_prefix = "JacksDemo-${var.environment}"
}
###### Current Config ######
data "azurerm_client_config" "current" {}
###### Key Vault Certificate ######
data "azurerm_key_vault_secret" "certificate" {
name = var.ssl_certificate_name
key_vault_id = module.kv.kv_id
depends_on = [module.kv]
}
###### Key Vault authentication_certificate - For HTTPS HTTP Settings ######
data "azurerm_key_vault_secret" "authentication_certificate" {
name = var.authentication_certificate_name
key_vault_id = module.kv.kv_id
depends_on = [module.kv]
}
### Application Gateway ###
module "appgateway" {
source = "./../appgwv2"
application_gateway_name = "${local.name_prefix}-${var.application_gateway_name}"
resource_group_name = var.rg_name
location = var.rg_location
frontend_port = var.frontend_port
gateway_ip_configuration = local.gateway_ip_configuration
autoscale_configuration = var.autoscale_configuration
sku = var.agw_sku
ssl_certificate = local.ssl_certificate
authentication_certificate = local.authentication_certificate
subnetId = module.vnet.subnet01_id
frontendPrivateIpConfigName = var.frontendPrivateIpConfigName
agw_private_ip_addr = var.agw_private_ip_addr
backend_address_pool = var.backend_address_pool
backend_http_settings = var.backend_http_settings
http_listener = var.http_listener
probe = var.probe
url_path_map = var.url_path_map
rewrite_rule_set = var.rewrite_rule_set
request_routing_rule = var.request_routing_rule
redirect_configuration = var.redirect_configuration
tags = merge(var.resource_tags, local.mandatory_tags)
depends_on = [module.vnet, module.kv]
}First, we define our locals block, containing gateway_ip_configuration, ssl_certificate and authentication_certificate.
Note these take a combination of values from variables, data sources, and modules, not something we would be able to define in a single variable or hardcode.
The relevant data source blocks are shown for completeness, they are basically pulling the certificate from an Azure Key Vault and applying it to the Application Gateway (you will also need a key vault access policy and managed identity to set this up in practice).
The local values we defined can then be passed into our appgateway module, on lines 59, 62, and 63. Note we are also using the naming prefix and mandatory_tags from the previous examples on lines 55 and 75.
Local values pull their weight in a Terraform configuration when you use them with some restraint. The clearest case for a local is a value or expression that shows up in several places, so naming it once keeps the code consistent and saves you from repeating yourself.
Locals keep values consistent within a single configuration, but enforcing standards like mandatory tags and naming conventions across many teams and workspaces is a governance problem better solved with policy-as-code on a dedicated Terraform management platform.
The following samples were added by Scalr, not the original author.
Here are some straightforward examples of Terraform locals:
Basic string concatenation in Terraform is the process of combining multiple strings together to form a single string. This is a fundamental technique that helps you create dynamic resource names, consistent naming patterns, and organized infrastructure.
String concatenation in Terraform is typically done using the string interpolation syntax with the ${} notation inside a string.
locals {
# Simple name prefix combining project and environment
name_prefix = "${var.project}-${var.environment}"
}
resource "aws_s3_bucket" "example" {
bucket = "${local.name_prefix}-bucket"
}A "Simple Map for Tags" in Terraform refers to using a local map variable to define common resource tags in one place, making them easy to apply consistently across multiple resources.
locals {
# Common tags in one place
tags = {
Project = var.project
Environment = var.environment
Owner = "DevOps"
}
}
resource "aws_instance" "example" {
ami = "ami-123456"
instance_type = "t2.micro"
tags = local.tags
}Basic conditional logic in Terraform locals involves using "if-then-else" style expressions to make decisions about what values to use based on certain conditions.
Terraform's conditional expressions use the syntax condition ? true_value : false_value, which means:
This is also known as a "ternary operator" and lets you set different values based on conditions like environment type, region, or any other variable.
locals {
# Simple boolean check
is_production = var.environment == "prod"
# Set value based on environment
instance_size = local.is_production ? "t2.medium" : "t2.micro"
}
resource "aws_instance" "server" {
instance_type = local.instance_size
ami = "ami-123456"
}Simple list creation in Terraform locals involves defining and manipulating collections of values that can be used consistently throughout your configuration.
Lists in Terraform are ordered collections of values, similar to arrays in other programming languages. When used with locals, they allow you to:
locals {
# Create a list of availability zones
azs = ["us-west-2a", "us-west-2b", "us-west-2c"]
# Get first two zones
selected_azs = slice(local.azs, 0, 2)
}
resource "aws_subnet" "example" {
count = length(local.selected_azs)
vpc_id = aws_vpc.main.id
availability_zone = local.selected_azs[count.index]
cidr_block = "10.0.${count.index}.0/24"
}Simple map transformation is the process of creating a new map by modifying an existing map in straightforward ways. This usually involves iterating over the original map's elements (key-value pairs) and applying some logic to change the keys, the values, or both, or to filter out certain pairs.
locals {
# Original map
services = {
web = { port = 80 }
api = { port = 8080 }
}
# Add prefix to each key
prefixed_services = {
for name, config in local.services :
"${var.project}-${name}" => config
}
}Basic string manipulation in Terraform involves using built-in functions to modify and transform string values within your configuration files. These manipulations are essential for constructing dynamic resource names, generating specific output formats, and managing data effectively.
locals {
# Convert to lowercase
environment = lower(var.environment)
# Join strings with separator
tags_csv = join(",", ["env=${var.environment}", "project=${var.project}"])
}locals {
# Use provided region or default to us-east-1
region = var.region != "" ? var.region : "us-east-1"
}
provider "aws" {
region = local.region
}Simple numeric calculations involve using operators and functions to perform these basic mathematical tasks directly within your code or configuration.
locals {
# Calculate number of instances
instance_count = var.environment == "prod" ? 3 : 1
# Calculate disk size
disk_size_gb = var.base_disk_size + (var.environment == "prod" ? 50 : 0)
}Locals organize expressions inside a module. Values that differ per environment still come from variables and tfvars files, which our tfvars guide covers. Once those values span dozens of workspaces, centrally scoped variables in a platform like Scalr (free up to 50 runs per month) replace copy-pasted tfvars files.