Tutorial 17: Local Values and Computed Values
Learning Objectives
By the end of this tutorial, you will be able to:
- Create and use local values to reduce repetition
- Implement computed values and expressions
- Use locals for data transformation and manipulation
- Apply conditional logic in local values
- Optimize configurations with strategic local value placement
Prerequisites
- Completed Tutorial 16: Advanced Variable Types and Validation
- Understanding of Terraform variables and expressions
- Basic knowledge of HCL functions
Introduction
Local values in Terraform allow you to define reusable expressions within a module. They help reduce repetition, improve readability, and make configurations more maintainable by centralizing complex calculations and transformations.
Understanding Local Values
Basic Local Values
Local values are defined in a locals
block and can reference variables, resources, and other locals.
# main.tf
locals {
# Simple string concatenation
environment_prefix = "${var.project_name}-${var.environment}"
# Current timestamp
timestamp = formatdate("YYYY-MM-DD-hhmm", timestamp())
# Common tags used across resources
common_tags = {
Project = var.project_name
Environment = var.environment
Owner = var.owner
ManagedBy = "terraform"
CreatedAt = local.timestamp
}
}
# Using locals in resources
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(local.common_tags, {
Name = "${local.environment_prefix}-vpc"
Type = "networking"
})
}
Computed Values from Resources
# variables.tf
variable "instance_count" {
description = "Number of instances to create"
type = number
default = 3
}
variable "base_cidr" {
description = "Base CIDR block"
type = string
default = "10.0.0.0/16"
}
# main.tf
locals {
# Calculate subnet CIDRs automatically
subnet_cidrs = [
for i in range(var.instance_count) :
cidrsubnet(var.base_cidr, 8, i + 1)
]
# Create availability zone mapping
availability_zones = slice(data.aws_availability_zones.available.names, 0, var.instance_count)
# Calculate total instances across all environments
total_instances = sum([
for env in keys(var.environments) :
var.environments[env].instance_count
])
}
data "aws_availability_zones" "available" {
state = "available"
}
# Create subnets using computed values
resource "aws_subnet" "public" {
count = var.instance_count
vpc_id = aws_vpc.main.id
cidr_block = local.subnet_cidrs[count.index]
availability_zone = local.availability_zones[count.index]
map_public_ip_on_launch = true
tags = merge(local.common_tags, {
Name = "${local.environment_prefix}-public-subnet-${count.index + 1}"
Type = "public-subnet"
AZ = local.availability_zones[count.index]
})
}
Advanced Local Value Patterns
Conditional Logic in Locals
# variables.tf
variable "environment" {
description = "Environment name"
type = string
}
variable "enable_monitoring" {
description = "Enable detailed monitoring"
type = bool
default = null
}
variable "backup_retention" {
description = "Backup retention period"
type = number
default = null
}
# main.tf
locals {
# Environment-specific configurations
is_production = var.environment == "prod"
is_development = var.environment == "dev"
# Conditional instance sizing
instance_type = local.is_production ? "t3.large" : "t3.micro"
# Conditional monitoring (enable by default in prod)
monitoring_enabled = var.enable_monitoring != null ? var.enable_monitoring : local.is_production
# Conditional backup retention
backup_retention_days = var.backup_retention != null ? var.backup_retention : (
local.is_production ? 30 : 7
)
# Conditional security settings
security_config = {
deletion_protection = local.is_production
backup_enabled = local.is_production || var.environment == "staging"
encryption_enabled = local.is_production
multi_az = local.is_production
}
# Environment-specific resource counts
instance_config = {
min_size = local.is_production ? 2 : 1
max_size = local.is_production ? 10 : 3
desired_capacity = local.is_production ? 3 : 1
}
}
Data Transformation with Locals
# variables.tf
variable "users" {
description = "List of users with their configurations"
type = list(object({
username = string
role = string
groups = list(string)
active = bool
}))
}
variable "applications" {
description = "Application configurations"
type = map(object({
port = number
protocol = string
health_check = string
replicas = number
}))
}
# main.tf
locals {
# Filter active users
active_users = [
for user in var.users : user
if user.active
]
# Group users by role
users_by_role = {
for role in distinct([for user in local.active_users : user.role]) :
role => [
for user in local.active_users : user
if user.role == role
]
}
# Create user-role mappings
user_roles = {
for user in local.active_users :
user.username => user.role
}
# Flatten user groups
user_group_memberships = flatten([
for user in local.active_users : [
for group in user.groups : {
username = user.username
group = group
}
]
])
# Transform applications for load balancer
lb_targets = {
for name, config in var.applications :
name => {
port = config.port
protocol = upper(config.protocol)
health_check_path = config.health_check
target_type = "instance"
deregistration_delay = 300
}
}
# Calculate resource requirements
total_cpu_requests = sum([
for app, config in var.applications :
config.replicas * 100 # 100m CPU per replica
])
total_memory_requests = sum([
for app, config in var.applications :
config.replicas * 128 # 128Mi memory per replica
])
}
Complex Calculations and Mappings
# variables.tf
variable "regions" {
description = "Regions with their configurations"
type = map(object({
primary = bool
instance_types = list(string)
availability_zones = list(string)
cidr_block = string
}))
}
variable "workloads" {
description = "Workload definitions"
type = map(object({
cpu_request = string
memory_request = string
replicas = number
regions = list(string)
}))
}
# main.tf
locals {
# Find primary region
primary_region = [
for region, config in var.regions :
region if config.primary
][0]
# Calculate multi-region deployment matrix
workload_deployments = flatten([
for workload_name, workload in var.workloads : [
for region in workload.regions : {
workload = workload_name
region = region
key = "${workload_name}-${region}"
config = workload
region_config = var.regions[region]
}
]
])
# Group deployments by region
deployments_by_region = {
for region in keys(var.regions) :
region => [
for deployment in local.workload_deployments :
deployment if deployment.region == region
]
}
# Calculate network configurations
network_configs = {
for region, config in var.regions :
region => {
vpc_cidr = config.cidr_block
subnet_cidrs = [
for i, az in config.availability_zones :
cidrsubnet(config.cidr_block, 8, i + 1)
]
az_subnet_map = {
for i, az in config.availability_zones :
az => cidrsubnet(config.cidr_block, 8, i + 1)
}
}
}
# Calculate total resource requirements per region
region_resources = {
for region in keys(var.regions) :
region => {
total_cpu = sum([
for deployment in local.deployments_by_region[region] :
parseint(regex("([0-9]+)", deployment.config.cpu_request)[0], 10) * deployment.config.replicas
])
total_memory = sum([
for deployment in local.deployments_by_region[region] :
parseint(regex("([0-9]+)", deployment.config.memory_request)[0], 10) * deployment.config.replicas
])
workload_count = length(local.deployments_by_region[region])
}
}
}
Practical Example: Multi-Environment Infrastructure
# variables.tf
variable "project_name" {
description = "Name of the project"
type = string
}
variable "environments" {
description = "Environment configurations"
type = map(object({
cidr_block = string
instance_type = string
min_instances = number
max_instances = number
enable_monitoring = bool
backup_retention = number
allowed_ports = list(number)
}))
}
variable "global_tags" {
description = "Global tags applied to all resources"
type = map(string)
default = {}
}
# main.tf
locals {
# Environment processing
environment_configs = {
for env_name, env_config in var.environments :
env_name => merge(env_config, {
# Add computed values to each environment
name_prefix = "${var.project_name}-${env_name}"
is_production = env_name == "prod"
availability_zones = slice(data.aws_availability_zones.available.names, 0, 3)
# Calculate subnet CIDRs for each AZ
subnet_cidrs = [
for i in range(3) :
cidrsubnet(env_config.cidr_block, 8, i + 1)
]
# Security group rules
ingress_rules = [
for port in env_config.allowed_ports : {
from_port = port
to_port = port
protocol = "tcp"
cidr_blocks = env_name == "prod" ? ["10.0.0.0/8"] : ["0.0.0.0/0"]
}
]
})
}
# Global configurations
timestamp = formatdate("YYYY-MM-DD-hhmm", timestamp())
# Base tags for all resources
base_tags = merge(var.global_tags, {
Project = var.project_name
ManagedBy = "terraform"
CreatedAt = local.timestamp
})
# Environment-specific tag functions
env_tags = {
for env_name, env_config in local.environment_configs :
env_name => merge(local.base_tags, {
Environment = env_name
Production = env_config.is_production ? "true" : "false"
})
}
# Flatten all subnets for easy iteration
all_subnets = flatten([
for env_name, env_config in local.environment_configs : [
for i, cidr in env_config.subnet_cidrs : {
environment = env_name
index = i
cidr_block = cidr
az = env_config.availability_zones[i]
key = "${env_name}-subnet-${i + 1}"
name = "${env_config.name_prefix}-subnet-${i + 1}"
tags = local.env_tags[env_name]
}
]
])
# Create subnet lookup map
subnets_by_env = {
for env_name, env_config in local.environment_configs :
env_name => [
for subnet in local.all_subnets :
subnet if subnet.environment == env_name
]
}
# Calculate monitoring configurations
monitoring_configs = {
for env_name, env_config in local.environment_configs :
env_name => {
detailed_monitoring = env_config.enable_monitoring
log_retention_days = env_config.is_production ? 90 : 30
alarm_threshold = env_config.is_production ? 80 : 90
notification_topic = env_config.is_production ? "prod-alerts" : "dev-alerts"
}
}
}
data "aws_availability_zones" "available" {
state = "available"
}
# Create VPCs for each environment
resource "aws_vpc" "environment" {
for_each = local.environment_configs
cidr_block = each.value.cidr_block
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(local.env_tags[each.key], {
Name = "${each.value.name_prefix}-vpc"
Type = "networking"
})
}
# Create subnets using flattened configuration
resource "aws_subnet" "environment" {
for_each = {
for subnet in local.all_subnets :
subnet.key => subnet
}
vpc_id = aws_vpc.environment[each.value.environment].id
cidr_block = each.value.cidr_block
availability_zone = each.value.az
map_public_ip_on_launch = true
tags = merge(each.value.tags, {
Name = each.value.name
Type = "public-subnet"
AZ = each.value.az
})
}
# Create security groups with computed rules
resource "aws_security_group" "environment" {
for_each = local.environment_configs
name_prefix = "${each.value.name_prefix}-sg"
vpc_id = aws_vpc.environment[each.key].id
description = "Security group for ${each.key} environment"
dynamic "ingress" {
for_each = each.value.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.env_tags[each.key], {
Name = "${each.value.name_prefix}-security-group"
Type = "security"
})
}
# Output computed configurations for verification
output "environment_summary" {
description = "Summary of computed environment configurations"
value = {
for env_name, env_config in local.environment_configs :
env_name => {
name_prefix = env_config.name_prefix
is_production = env_config.is_production
vpc_cidr = env_config.cidr_block
subnet_count = length(env_config.subnet_cidrs)
monitoring = local.monitoring_configs[env_name]
total_ports = length(env_config.allowed_ports)
}
}
}
Local Values for Data Processing
Working with External Data
# External data source
data "external" "instance_metadata" {
program = ["python3", "${path.module}/scripts/get_instance_metadata.py"]
query = {
region = var.aws_region
environment = var.environment
}
}
# Data processing with locals
locals {
# Parse external data
instance_metadata = jsondecode(data.external.instance_metadata.result.metadata)
# Process and transform the data
recommended_instances = {
for family, sizes in local.instance_metadata.families :
family => [
for size in sizes :
size if size.cpu >= var.min_cpu_requirements
]
}
# Calculate optimal instance selection
optimal_instance = {
for env in ["dev", "staging", "prod"] :
env => {
family = env == "prod" ? "c5" : "t3"
size = env == "prod" ? "large" : "micro"
full_type = env == "prod" ? "c5.large" : "t3.micro"
}
}
}
API Integration and Data Manipulation
# HTTP data source for API integration
data "http" "service_catalog" {
url = "https://api.example.com/services"
request_headers = {
Accept = "application/json"
Authorization = "Bearer ${var.api_token}"
}
}
locals {
# Parse API response
services = jsondecode(data.http.service_catalog.response_body)
# Filter and transform services
active_services = [
for service in local.services.items :
service if service.status == "active"
]
# Group services by category
services_by_category = {
for category in distinct([for service in local.active_services : service.category]) :
category => [
for service in local.active_services :
service if service.category == category
]
}
# Create deployment configurations
service_deployments = {
for service in local.active_services :
service.name => {
image = service.docker_image
port = service.port
replicas = service.recommended_replicas
resources = {
cpu = "${service.cpu_request}m"
memory = "${service.memory_request}Mi"
}
environment_vars = {
for var in service.environment_variables :
var.name => var.value
}
}
}
}
Best Practices for Local Values
1. Naming Conventions
locals {
# Use descriptive names
common_tags = { ... } # Good
tags = { ... } # Less clear
# Group related values
network_config = { ... }
security_config = { ... }
monitoring_config = { ... }
# Use prefixes for computed values
computed_subnet_cidrs = [...]
calculated_instance_sizes = [...]
derived_security_rules = [...]
}
2. Organization and Structure
# Organize locals logically
locals {
# Basic values first
environment_name = var.environment
project_prefix = "${var.project_name}-${local.environment_name}"
# Then computed values
is_production = local.environment_name == "prod"
instance_type = local.is_production ? "t3.large" : "t3.micro"
# Complex transformations last
network_configuration = {
# ... complex object
}
}
3. Performance Considerations
locals {
# Avoid expensive operations in frequently used locals
# Good: Calculate once
availability_zones = data.aws_availability_zones.available.names
# Bad: Multiple data source calls
# Don't do this in locals that are used many times
expensive_calculation = [
for i in range(100) :
data.external.some_call[i].result
]
}
Common Pitfalls and Solutions
1. Circular Dependencies
# Bad: Circular reference
locals {
a = local.b + 1
b = local.a + 1
}
# Good: Linear dependencies
locals {
base_value = 10
derived_value = local.base_value + 1
final_value = local.derived_value * 2
}
2. Over-Complex Expressions
# Bad: Too complex
locals {
complex_config = {
for k, v in var.configs :
k => {
for attr, val in v :
attr => attr == "special" ? (
val.type == "A" ? "value_a" : (
val.type == "B" ? "value_b" : "default"
)
) : val
}
}
}
# Good: Break into smaller pieces
locals {
# First transformation
normalized_configs = {
for k, v in var.configs :
k => {
for attr, val in v :
attr => attr == "special" ? local.special_values[val.type] : val
}
}
# Helper values
special_values = {
A = "value_a"
B = "value_b"
default = "default"
}
}
3. Type Conversion Issues
# Handle type conversions carefully
locals {
# Ensure consistent types
instance_counts = {
for env, config in var.environments :
env => tonumber(config.instance_count) # Explicit conversion
}
# Validate data before processing
valid_ports = [
for port in var.allowed_ports :
port if port >= 1 && port <= 65535
]
}
Key Takeaways
- Reduce Repetition: Use locals to eliminate duplicate expressions
- Improve Readability: Name complex calculations for better understanding
- Centralize Logic: Keep related computations together
- Type Safety: Be explicit about type conversions
- Performance: Avoid expensive operations in frequently-used locals
- Organization: Structure locals logically and use clear naming
Next Steps
- Tutorial 18: Learn about output values and dependencies
- Practice creating complex local value expressions
- Experiment with data transformation patterns
- Review the Terraform Local Values Documentation