DEV Community

Cover image for How to Manage Terraform Locals
env0 Team for env0

Posted on • Originally published at env0.com

How to Manage Terraform Locals

What are Terraform Locals

All programming languages have a way to express and store values within the context of a code block. In the case of Terraform configurations, that functionality is delivered through Terraform local values. These allow you to define temporary values and then reference them elsewhere in the configuration.

Local values – often called “locals” or “Terraform local variables” – can be used to store an expression that will be referenced multiple times, perform data transformation from other sources, or store static values to be used in the configuration. 

Locals are one of three varieties of Terraform variables that can be used to request or public values, with the other two being “input variables” and “output values.” In this post, we'll examine how to define local values, how they differ from input variables, and common uses for locals.

How to Implement Terraform Locals

Defining local values in Terraform code is done using a locals block, with each local assigned a name and value in the format of a key-value pair. The following code creates a local value called environment and assigns it the string value development.

locals {
  environment = "development"
}
Enter fullscreen mode Exit fullscreen mode

Locals can be assigned any valid Terraform data type, such as a string, list, map, or object. The actual values can come from input variables, resource attributes, or other local values defined in the configuration.

locals {
  environment = "development"
  server_list = ["web", "app", "db"]
  subnet_map  = {
    web = var.web_subnet
    app = var.app_subnet
    db  = var.db_subnet
  }
} 
Enter fullscreen mode Exit fullscreen mode

A locals block is also useful for composing expressions for values that will be used elsewhere in the configuration, including in both ternary and for expressions. The following example sets the DNS value to the load balancer fqdn if a load balancer is being created, and to the fqdn of a virtual machine otherwise.

locals {
  dns_entry = var.create_load_balancer ? 
                azurerm_public_ip.lb.fqdn : azurerm_public_ip.vm.fqdn
}
Enter fullscreen mode Exit fullscreen mode

A for expression could be used to create a local value for naming resources. The code below would add a naming prefix input variable to the beginning of each server in the list.

locals {
  server_list = ["web", "app", "db"]
  server_names = [ for svr in local.server_list : "${var.prefix}-${svr}" ]
}
Enter fullscreen mode Exit fullscreen mode

The locals block can appear multiple times in a Terraform configuration, so long as each local value has a unique name within the configuration. 

Some organizations prefer to define all their locals in a single locals block stored in a dedicated locals.tf file, while others will use multiple locals blocks, placing each close to the resources and data sources referencing it.

The value stored in a local is referenced with the local keyword followed by the name of the local. The following code makes use of the server_names and environment locals in an Azure VM.

resource "azurerm_linux_virtual_machine" "web" {
  name = local.server_names[0]
  #...
  tags = {
    environment = local.environment
  }
}
Enter fullscreen mode Exit fullscreen mode

Locals defined in a Terraform module are only available within the scope of that module. For instance, the local value named environment that is defined in the root module cannot be referenced directly by a child module. 

Additionally, locals defined in the child module are not directly available to the parent module. Locals follow the same scoping principles as input variables, resources, and data sources.

Terraform Locals vs. Input Variables

While Terraform input variables and local values might sound very similar, there are some key differences to keep in mind when selecting which construct to use.

Input Variables

  • Dynamic values defined at run time
  • Values cannot come from other configuration sources
  • Default value can be overridden

Local Values

  • Defined by an internal expression
  • Can be assigned any valid Terraform expression
  • Can be assigned static values

In most other programming languages, input variables would instead be called “parameters” and local values would simply be called “variables.”

If you are coming from another programming language, that may be a helpful mental framework to apply when thinking about whether to use an input variable or local value.

Generally, if you need to be able to change the value of a placeholder at runtime, you should use an input variable. If you simply need a reusable expression or need to perform data transformation, you should use a local value.

Examples of Terraform Locals

Let's walk through a few common examples of how and when Terraform locals might be used.

1. Reusing a Value

Locals allow you to define a value once and reuse it throughout the configuration. For instance, we can use a local value to define a set of common tags for your resources based on input variables.

locals {
  common_tags = {
    environment = var.environment
    project     = var.project
    billing     = var.billingcode
  }
}

resource "azurerm_resource_group" "main" {
  ...
  tags = local.common_tags
}
Enter fullscreen mode Exit fullscreen mode

By defining the common tags in a locals block, it is simple to add a new tag to all resources with having to update every tags argument in every resource block.

Another common use case is establishing a standard naming convention for all resources in a configuration. The Cloud Posse terraform-null-label module makes extensive use of locals for exactly that purpose.

2. Data Transformation

Locals can also be used to transform data before it is passed on in the configuration. For instance, you could use a local variable and a for expression to update the values in a list.

locals {
  env_config_list = [ for item in var.config_list : "${local.environment}-${item}"
}
Enter fullscreen mode Exit fullscreen mode

Even if the local value isn't going to be used multiple times, performing the data transformation in a locals block can help make other configuration blocks easier to read. Locals can also be used as an intermediary step in data transformation, to simplify code updates for future maintainers.

3. Constant Values

Unlike input variables, locals don't accept input values, so they can be used to set static constants in the configuration that can only be updated by altering the code itself. For instance, the code below establishes default ports for a web application.

locals {
  web_app_ports = ["8080", "8443"]
}
Enter fullscreen mode Exit fullscreen mode

If someone wishes to alter the port list, they will need to go through the code change process, rather than changing the input variable values submitted during a Terraform run. Using locals helps code maintainers view all the configuration values in a single place, while not allowing consumers of the configuration to change values easily.

Conclusion

Locals in Terraform configuration files construct reusable values to be referenced throughout a configuration. Values can be assigned to locals from input variables, resources, data sources, and other related local values.

Local values are a core component of Terraform and are also supported on the new open-source project OpenTofu. OpenTofu is intended to be a drop-in replacement for existing Terraform deployments, and its support of locals is one example of its interoperability. You can join OpenTofu’s Slack community and check out how to contribute.

Top comments (1)

Collapse
 
whimsicalbison profile image
Jack

Thank you for the article; I enjoyed it. I generally prefer to place local blocks close to the resources that are using them if the number of variables is limited. However, when local variables start to be used across multiple files or there are too many of them, I move them to their own file.

One situation where it may be necessary to use local variables is to avoid a circular dependency. For example, the following code would cause a circular dependency because resources a and b reference each other:

resource "something" "a" {
  name      = "something-awesome"
  reference = something-else.b.attribute
}

resource "something-else" "b" {
  reference = something.a.name
}
Enter fullscreen mode Exit fullscreen mode

Using a local variable can break this dependency:

locals {
  a_name = "something-awesome"
}

resource "something" "a" {
  name      = local.a_name
  reference = something-else.b.attribute
}

resource "something-else" "b" {
  reference = local.a_name
}
Enter fullscreen mode Exit fullscreen mode