AskLearn
Loading...
← Back to Terraform Course
AdvancedConfiguration

Local Values

Computed and local values

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

  1. Reduce Repetition: Use locals to eliminate duplicate expressions
  2. Improve Readability: Name complex calculations for better understanding
  3. Centralize Logic: Keep related computations together
  4. Type Safety: Be explicit about type conversions
  5. Performance: Avoid expensive operations in frequently-used locals
  6. 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

Additional Resources