Terraform Local Values
Introduction
When writing Terraform configurations, you often need to transform or combine values before using them in your resources. Terraform's local values feature (commonly referred to as "locals") provides a way to assign a name to an expression, allowing you to use that name multiple times within a module instead of repeating the expression.
Local values are similar to variables, but they can use complex expressions that reference other values in your configuration, making them more powerful for intermediate calculations and value transformations.
What Are Local Values?
Local values are named values that you can reference within your Terraform configuration. Unlike input variables which are meant to be set from outside the module, local values are calculated within the module itself.
Local values help you:
- Avoid Repetition: Define a complex expression once and reuse it
- Improve Readability: Name expressions to make their purpose clear
- Simplify Complex Expressions: Break down complex expressions into smaller, named parts
- Avoid Recalculation: Compute a value once and reuse it multiple places
Basic Syntax
Local values are defined using the locals
block:
locals {
service_name = "api-gateway"
environment = "production"
# Combining values
name_prefix = "${local.service_name}-${local.environment}"
}
And referenced in your configuration using the local
keyword:
resource "aws_instance" "server" {
tags = {
Name = "${local.name_prefix}-server"
}
}
Local Values vs Variables
Before we dive deeper, let's understand how locals differ from variables:
Feature | Local Values | Variables |
---|---|---|
Purpose | Internal calculations | External inputs |
Declaration | locals block | variable block |
Assignment | Direct in configuration | Via command line, files, etc. |
Reference | local.name | var.name |
Default value | Always required | Optional |
Validation | Not directly | Built-in validation |
Complex expressions | Yes | Limited (only in defaults) |
Common Use Cases
1. Simplifying Tag Management
locals {
common_tags = {
Project = "MyProject"
Environment = "Dev"
Owner = "DevOps Team"
ManagedBy = "Terraform"
}
service_tags = merge(local.common_tags, {
Service = "API"
})
}
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = local.service_tags
}
resource "aws_s3_bucket" "example" {
bucket = "my-tf-test-bucket"
tags = local.common_tags
}
2. Conditional Resource Configuration
locals {
is_production = var.environment == "prod"
instance_type = local.is_production ? "m5.large" : "t2.micro"
instance_count = local.is_production ? 3 : 1
}
resource "aws_instance" "server" {
count = local.instance_count
ami = "ami-0c55b159cbfafe1f0"
instance_type = local.instance_type
}
3. Data Transformation
variable "user_information" {
type = list(object({
name = string
email = string
role = string
}))
}
locals {
# Transform list to map with email as key
users_by_email = {
for user in var.user_information :
user.email => user
}
# Filter only admin users
admin_users = [
for user in var.user_information :
user
if user.role == "admin"
]
# Count users by role
users_by_role = {
for role in distinct([for user in var.user_information : user.role]) :
role => length([for user in var.user_information : user if user.role == role])
}
}
4. String Manipulation
locals {
domain_name = "example.com"
service_names = ["web", "api", "auth"]
# Create a list of fully qualified domain names
fqdns = [for service in local.service_names : "${service}.${local.domain_name}"]
# Join list elements with commas
services_csv = join(", ", local.service_names)
}
5. Working with Maps and Lists
locals {
environments = {
dev = {
instance_type = "t2.micro"
instance_count = 1
multi_az = false
}
staging = {
instance_type = "t2.medium"
instance_count = 2
multi_az = true
}
prod = {
instance_type = "m5.large"
instance_count = 3
multi_az = true
}
}
# Select configuration based on current environment
env_config = local.environments[var.environment]
}
resource "aws_instance" "app" {
count = local.env_config.instance_count
instance_type = local.env_config.instance_type
# Other configuration...
}
Advanced Techniques
Combining Multiple Local Blocks
You can use multiple locals
blocks in your configuration, and they will be merged:
locals {
project = "myproject"
environment = "production"
}
# Later in the configuration
locals {
resource_prefix = "${local.project}-${local.environment}"
}
Using Functions with Local Values
Terraform's functions can be used to manipulate values:
locals {
uppercase_project = upper(var.project_name)
project_name_slug = replace(lower(var.project_name), " ", "-")
instance_types = {
small = "t2.micro"
medium = "t2.medium"
large = "m5.large"
}
chosen_instance = lookup(local.instance_types, var.size, local.instance_types.small)
}
Local Values with for
Expressions
variable "users" {
type = list(string)
default = ["john", "mary", "peter"]
}
locals {
# Create user resource names
user_resource_names = [for name in var.users : "user-${name}"]
# Create map of user IDs
user_ids = {
for idx, name in var.users :
name => idx + 100
}
}
Using Local Values with dynamic
Blocks
locals {
ingress_rules = [
{
port = 80
protocol = "tcp"
description = "HTTP"
},
{
port = 443
protocol = "tcp"
description = "HTTPS"
}
]
}
resource "aws_security_group" "example" {
name = "example"
dynamic "ingress" {
for_each = local.ingress_rules
content {
from_port = ingress.value.port
to_port = ingress.value.port
protocol = ingress.value.protocol
description = ingress.value.description
cidr_blocks = ["0.0.0.0/0"]
}
}
}
Best Practices
-
Use Descriptive Names: Choose names that clearly indicate the purpose of the local value.
-
Group Related Values: Keep related local values together in a single
locals
block. -
Document Complex Expressions: Add comments explaining complex calculations.
-
Avoid Overuse: Don't create locals for simple values that are only used once.
-
Use for DRY Configurations: If you're repeating the same expression multiple times, it's a good candidate for a local value.
-
Keep Locals Focused: Each local should serve a specific, clear purpose.
locals {
# Good: Descriptive name, clear purpose
standard_tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "Terraform"
}
# Avoid: Too generic, unclear purpose
stuff = "${var.environment}-data"
}
Complete Example
Let's look at a complete example that combines several techniques:
variable "project" {
type = string
default = "my-awesome-app"
}
variable "environment" {
type = string
default = "dev"
}
variable "region" {
type = string
default = "us-west-2"
}
variable "instance_count" {
type = number
default = 2
}
# Local values for the module
locals {
# Basic name formatting
name_prefix = "${var.project}-${var.environment}"
# Environment-specific configurations
environment_configs = {
dev = {
instance_type = "t2.micro"
monitoring = false
disk_size = 20
}
staging = {
instance_type = "t2.medium"
monitoring = true
disk_size = 30
}
prod = {
instance_type = "m5.large"
monitoring = true
disk_size = 50
}
}
# Select the appropriate config based on environment
config = local.environment_configs[var.environment]
# Common tags for all resources
common_tags = {
Project = var.project
Environment = var.environment
Region = var.region
ManagedBy = "Terraform"
}
# Generate list of instance names
instance_names = [
for i in range(1, var.instance_count + 1) :
"${local.name_prefix}-instance-${format("%02d", i)}"
]
}
output "configuration" {
value = local.config
}
output "instance_names" {
value = local.instance_names
}
output "tags" {
value = local.common_tags
}
Example output:
Outputs:
configuration = {
"disk_size" = 20
"instance_type" = "t2.micro"
"monitoring" = false
}
instance_names = [
"my-awesome-app-dev-instance-01",
"my-awesome-app-dev-instance-02",
]
tags = {
"Environment" = "dev"
"ManagedBy" = "Terraform"
"Project" = "my-awesome-app"
"Region" = "us-west-2"
}
Summary
Local values are a powerful feature in Terraform that help you:
- Create intermediate values for reuse throughout your configuration
- Transform and combine values with complex expressions
- Make your Terraform code more readable and maintainable
- Implement DRY (Don't Repeat Yourself) principles in your infrastructure code
- Simplify complex expressions by breaking them down into named components
By using local values effectively, you can write cleaner, more maintainable Terraform code that's easier to understand and modify.
Additional Resources
- Terraform Documentation: Local Values
- Terraform Functions - Useful for manipulating values in locals
- Terraform Expressions - Learn more about expressions you can use in locals
Exercises
-
Create a locals block that generates different resource naming conventions based on an environment variable.
-
Use local values to implement a tagging strategy for AWS resources that includes required tags like owner, project, and cost center.
-
Transform a list of user objects into a map where the keys are user IDs and the values are the user objects.
-
Create a local value that generates different VPC CIDR blocks for different environments, ensuring they don't overlap.
-
Use locals with conditional expressions to configure different capacity settings for resources based on whether the environment is production or non-production.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)