Terraform Local Variables
Brendan Thompson • 27 September 2022 • 8 min read
What are local variables?#
Local variables are blocks of code within Terraform that allow us to store static pieces of data that we might want to refer to at a later date, or deeply dynamic pieces of data that can be manipulated by the state of our resources, data sources, and information provided by our input variables or even other local variables. They are incredibly powerful! In my opinion, however, they are far too commonly used to store large amounts of static configuration that should be passed into the code through the use of input variables.
Below is an example of a local variable block:
locals {
cat_sound = "meow"
}
As can be seen above we have declared a variable called cat_sound
and assigned it the value meow
we would access this value by calling the variable like so local.cat_sound
. One thing that makes
local variables different from input variables is the fact that their types are implicit rather than
explicit.
Static Local Variables#
Static local variables are very much so like the above where the local block we are declaring contains the value alongside the variable name, this is supremely useful when you're storing properties that don't change. I like to think of locals when used in this manner as constants, this means we should not mutate the variable at all and it should be very rarely updated or not at all.
If you find yourself storing your configuration - think the explicit configuration of a virtual machine - in local variables you should be rethinking your configuration ingestion strategy. (I would suggest having a read of Terraform Configuration Ingestion)
Let's have a look at a proper example and consumption of local variables used in this manner.
locals {
location = "australiaeast"
storage_account = {
tier = "Standard"
replication_type = "ZRS"
tls = "1.2"
}
}
resource "azurerm_resource_group" "this" {
name = "rg-mom-spells"
location = local.location
}
resource "azurerm_storage_account" "this" {
name = "samomspells"
resource_group_name = azurerm_resource_group.this.name
location = azurerm_resource_group.this.location
account_tier = local.storage_account.tier
account_replication_type = local.storage_account.replication_type
min_tls_version = local.storage_account.tls
}
In the above example, we are creating an Azure resource group and a storage account for the Ministry of Magic to store its spells reference. The cloud engineers there have some requirements around what tier, replication type and tls version can be used when provisioning the storage account, as well as the location where these resources can be provisioned. Having these properties statically set in the local variables block ensures that they cannot be changed without going through a Pull Request. If this was baked into a module then any consumer of the module would have no ability to change these static properties. These act as constants for our code.
Dynamic Local Variables#
Dynamic local variables are where things become more fun, and useful. If you were passing in
some configuration through input variables and you needed to mutate them based on some context
within your code then using locals
to do that mutation is a wise decision.
In the following example, we are going to be using input variables to lookup our true values which
are defined within a locals
block.
variable "location" {
type = string
description = <<DESC
(Required) The location in which the resources will be provisioned.
DESC
validation {
condition = contains(["syd"], var.location)
error_message = "Err: The location must be 'syd'."
}
}
variable "cloud" {
type = string
description = <<DESC
(Required) The cloud that the resources will be deployed to.
DESC
validation {
condition = contains(["azure", "aws", "gcp"], var.cloud)
error_message = "Err: valid cloud options are: 'azure', 'aws', 'gcp'."
}
}
locals {
location = {
syd = {
azure = "australiasoutheast"
aws = "ap-southeast-2"
gcp = "australia-southeast1"
}
}
}
resource "scratch_string" "this" {
in = format("Cloud: %s, Location: %s",
var.cloud,
local.location[var.location][var.cloud]
)
}
In the above example, we have a single local block called location
(in a real-world scenario this
wouldn't be a single value, it would likely be several options), with this, we will be able to
find the cloud-specific region name based on a common name. On the highlighted line we are using
[]
to get the location with an input variable for location
and cloud
. If we were to write
it out fully without using variables it would look like this:
// azure syd
local.location[var.cloud][var.location]
// Returns: 'australiaeast'
The above allows us to retrieve simple data based on one or more pieces of context, in this
instance location
and cloud
. Below is the same logic we are just looking up the values
a little differently. Both ways work perfectly well the below is just a little clearer in my opinion.
resource "scratch_string" "this" {
in = format("Cloud: %s, Location: %s",
var.cloud,
lookup(
lookup(local.location, var.location, ""),
var.cloud, ""
)
)
}
Other ways that we can use locals
are as follows:
Mutate data/information/configuration from input variables, resources or data sources
data "azurerm_resource_group" "this" { name = "rg-mom" } locals { uppercase_location = upper(data.azurerm_resource_group.this.location) } output "location" { value = local.uppercase_location }
The above example is a very very basic example of how we can mutate data with
locals
. In this example we are taking the location from a resource group and uppercasing it.Concatenate information from input variables, resources or data sources
variable "location" { type = string description = <<DESC (Required) The location in which the resources will be provisioned. DESC validation { condition = contains(["syd", "mel", "lon"], var.location) error_message = "Err: The location must be 'syd'." } } variable "cloud" { type = string description = <<DESC (Required) The cloud that the resources will be deployed to. DESC validation { condition = contains(["azure", "aws", "gcp"], var.cloud) error_message = "Err: valid cloud options are: 'azure', 'aws', 'gcp'." } } variable "environment" { type = string description = <<DESC (Required) The environment the resources will exist within. DESC validation { condition = contains(["dev", "uat", "stg", "prg"], var.environment) error_message = "Err: environment must be valid." } } variable "project" { type = string description = <<DESC (Required) The three-letter acronym for the project. DESC } locals { name_prefix = format("%s-%s-%s-%s", var.cloud, var.location, var.environment, var.project) } resource "scratch_string" "this" { in = format("%s-string", local.name_prefix) }
The above example shows how we can concatenate the four inputs (location, cloud, environment, project) together to give us a prefix that we can use for naming our resources. As an example, if we were to pass in;
mel
forlocation
,azure
forcloud
,dev
forenvironment
andblt
for project thenlocal.name_prefix
would return usazure-mel-dev-blt
. Compiling all of those variables together in a local means we now have this new piece of data that we can use all over our code and it will change dynamically with the variables we pass in. This ensures consistency and reduces errors if properties were to change.Ingest configuration from a file (e.g.
yaml
orjson
), which you can read about in my previous post Terraform Configuration IngestionLookup data/information/configuration from ingested configuration
All of these can be extremely powerful, and useful when we are writing our Terraform code. However as
I have said many times in other posts great power comes with great responsibility. The more locals
we use the more complex our code gets to read and comprehend. If you are using a lot of dynamic local
variables as we have discussed here then you should document what they are for so that future
engineers understand what is going on.
Closing Out#
Local variables are a critical tool to have in your Terraform toolbox and something that you should have a solid grasp on. They can be used to store static pieces of information like a constant in a programming language, or they can be used to mutate data and/or concatenate pieces of data. Both are equally useful.
Local variables can become dangerous when there are too many and the reasons for why aren't well documented, or when you're using them to store all the configurations that Terraform is used to provision resources. If you notice you are storing a lot of configuration in local variables it might be a good opportunity to assess the use of a module or ingesting configuration via another method.
When declaring local variables you should:
- Keep them close to the call site if they are only called a single time
- If they are called multiple times keep them at the top of the Terraform file that uses them
- If they are used by all the Terraform code (you might say global) then put them in a dedicated file for local variables
Hopefully, this has given some good insights into local variables!