Even though both variables and locals allow you to define values in Terraform code, they serve specific purposes, have different scopes, mutability and of course use cases. When organization and infrastructure are growing it is very important to manage those properly so that the code is clean, maintainable and reusable.
This blog post will help you navigate challenges of writing clean code at scale.
Variables:
- Definition: named entities that allow you to parametrize your TF configurations
- Purpose: make TF code flexible, reusable across different environments
Locals:
- Definition: named values that allow you to define single value or complex structure to be used within a module
- Purpose: simplify complex expressions and make TF code more readable
Key Differences:
- Scope: locals are limited to the module where they are defined, whereas variables can be defined at the module level and are passed between modules.
- Mutability: locals are immutable once defined, whereas variables can have default value but also be overridden during runtime
- Use case: use locals within single module to avoid repetitive code and use variables whenever you expect input to module such as different EC2 configuration per environment
Example:
Lets take example of AWS Lambda function and since in our project we will need many, it is of course best practice to put Lambda related resources such as Lambda itself, IAM Roles & Policies, Event Source, CloudWatch Group etc in Terraform module so that it can be reused across project. As you can imagine this module will have a lot of variables because we want to pass different values for each Lambda module call. These are some of the variables your module could have:
variable "lambda_name" {
type = string
description = "AWS Lambda name"
}
variable "lambda_iam_role_arn" {
type = string
description = "The IAM role ARN for lambda"
}
variable "lambda_memory_size" {
type = number
description = "Lambda memory size"
default = 128
}
variable "lambda_environment_variables" {
type = map(string)
description = "Map of lambda environment variables"
}
These variables provide configuration flexibility and parametrization. This allows you to customize each Lambda function's settings. Additionally, defining values as variables clarifies their purpose and promotes encapsulation. Note that lambda_memory_size has default value meaning when calling Lambda module you do not need to provide input for it unless you want to override the value.
Module call example:
module "my_lambda_function" {
source = "./lambda_module" # Path to the Lambda module
lambda_name = "my_lambda_function" # Name of the Lambda function
lambda_iam_role_arn = aws_iam_role.lambda_exec.arn # ARN of the IAM role for the Lambda
lambda_memory_size = 256 # Memory size for the Lambda function
lambda_environment_variables = {
LOG_LEVEL = "INFO" # Environment variable for logging level
KINESIS_DATA_STREAM = aws_kinesis_stream.data_stream.name # Example Kinesis stream name
}
}
This module call shows the power of flexibility and readability if variables are properly used. However, as you might notice it would be better to provide Lambda specific env variables using locals than directly forwarding the values.
module "my_lambda_function" {
source = "./lambda_module" # Path to the Lambda module
lambda_name = "my_lambda_function" # Name of the Lambda function
lambda_iam_role_arn = aws_iam_role.lambda_exec.arn # ARN of the IAM role for the Lambda
lambda_memory_size = 256 # Memory size for the Lambda function
lambda_environment_variables = local.lambda_env_variables
}
Using locals to pass environment variables to a module allows you centralized management of values so that you can update them in one place instead of searching for module calls.
Also locals make the configuration more readable. For example, if the same local value is needed for different Lambda (module call) you can just pass this:
locals {
lambda_env_variables = {
LOG_LEVEL = "INFO"
KINESIS_DATA_STREAM = aws_kinesis_stream.data_stream.name
...
}
}
This example further explains why we did not use variables instead of locals for LOG_LEVEL and KINESIS_DATA_STREAM. These values are Lambda specific and we should parametrize them since we do not need to pass them to other modules. Note that we are using type map for this variable since we expect to pass multiple values through locals where we make module call.
There is high probability that you will want different value for lambda_memory_size depending on whether you are deploying Lambda on DEV or PROD environment. That is the case when you pass another variable to it instead of local even though it is Lambda specific.
This means our module call would instead look like:
module "my_lambda_function" {
source = "./lambda_module" # Path to the Lambda module
lambda_name = "my_lambda_function" # Name of the Lambda function
lambda_iam_role_arn = aws_iam_role.lambda_exec.arn # ARN of the IAM role for the Lambda
lambda_memory_size = var.lambda_memory_size # Memory size for the Lambda function
lambda_environment_variables = local.lambda_env_variables
}
variable "lambda_memory_size" {
type = number
description = "Lambda memory size for lambda"
default = 256
}
This is very handy in cases where you are using multiple environments, and it will let you set the desired value depending on the environment:
lambda_memory_size = 512
This means that for dev, the default value and when deployed on prod, the value would be overridden to 512. Using a local here would make it harder to override, reducing the flexibility for further changes across different environments.
Conclusion
Understanding the difference between locals and variables in Terraform is crucial for building clean, reusable, and scalable infrastructure code. While variables provide better flexibility and enable easy customization across different environments, locals can be used to simplify complex logic within a module.
Top comments (0)