Skip to content
ExponaBLOG
TERRAFORM, IAC & CLOUD AUTOMATION · DAY 008 / 100

Locals in Terraform in Terraform, IaC & Cloud Automation

Name repeated expressions.

Expona Admin8 min read

Locals in Terraform in Terraform, IaC & Cloud Automation

When you're managing infrastructure across multiple environments and teams, you quickly discover that repeating the same expressions throughout your Terraform code leads to maintenance headaches and inconsistency. Locals solve this by letting you define and name complex values once, then reference them throughout your configuration. This article walks you through how to use locals effectively without crossing the line into over-abstraction.

What Are Locals and Why They Matter

Locals are named values computed within your Terraform module. Unlike variables, locals are not inputs—they're derived values you calculate based on other locals, variables, data sources, or static values. They live in your current module scope and aren't passed between modules.

Think of locals as your module's internal constants and computed values. They're especially valuable when you find yourself writing the same expression multiple times, or when you want to make a complex expression readable by giving it a meaningful name.

Basic Syntax: locals { name = ... }

A locals block declares one or more local values. Here's the simplest form:

hcl
locals {
  environment = var.env
  region      = var.aws_region
}

resource "aws_s3_bucket" "app_data" {
  bucket = "app-data-${local.environment}-${local.region}"
}

resource "aws_s3_bucket" "logs" {
  bucket = "app-logs-${local.environment}-${local.region}"
}

You reference a local value with local.name (singular). You can declare multiple locals blocks in a module, but most teams keep them in one place for readability. Order matters only if one local depends on another, so arrange them logically.

Locals can reference variables, data sources, resource attributes, and other locals:

hcl
locals {
  app_name = var.application_name
  env      = var.environment
  region   = data.aws_region.current.name
  
  # Depends on other locals
  common_tags = {
    Application = local.app_name
    Environment = local.env
    Region      = local.region
    ManagedBy   = "Terraform"
  }
  
  # String interpolation
  bucket_prefix = "${local.app_name}-${local.env}"
}

Build Tags and Names Once

One of the most practical uses for locals is centralizing how you build naming conventions and tags. In real-world infrastructure, you'll apply the same set of tags to dozens of resources and use consistent naming across buckets, instances, queues, and databases. A locals block becomes your source of truth for this.

Let's look at a typical e-commerce platform scenario where we manage multiple environments:

hcl
locals {
  app_name    = "checkout-service"
  environment = var.environment
  region      = var.aws_region
  cost_center = var.cost_center
  owner_email = var.owner_email
  
  # Computed naming convention
  resource_name_prefix = "${local.app_name}-${local.environment}"
  
  # Universal tags applied to all resources
  common_tags = {
    Application = local.app_name
    Environment = local.environment
    Region      = local.region
    CostCenter  = local.cost_center
    Owner       = local.owner_email
    CreatedBy   = "Terraform"
    CreatedAt   = timestamp()
  }
  
  # Service-specific naming
  service_tags = merge(local.common_tags, {
    Component = "api-gateway"
  })
}

resource "aws_s3_bucket" "checkout_logs" {
  bucket = "${local.resource_name_prefix}-logs"
  tags   = local.common_tags
}

resource "aws_sqs_queue" "checkout_events" {
  name                       = "${local.resource_name_prefix}-events"
  visibility_timeout_seconds = 300
  
  tags = merge(local.common_tags, {
    Component = "event-queue"
  })
}

resource "aws_lambda_function" "checkout_handler" {
  function_name = "${local.resource_name_prefix}-handler"
  
  tags = local.service_tags
}

By defining common_tags once, you guarantee consistency across all resources. When the finance team asks you to add a new cost center tag or when you move to a new region, you update one place. All resources automatically inherit the change. This also makes auditing and compliance reporting significantly easier.

Keep Expressions Readable

Locals aren't just about reducing repetition—they make complex logic easier to understand. When you have intricate conditionals, data source queries, or computed values, naming them clearly improves code maintainability for your entire team.

Consider a scenario where you're conditioning resource creation and configuration based on deployment tier:

hcl
locals {
  is_production = var.environment == "prod"
  is_staging    = var.environment == "staging"
  is_dev        = var.environment == "dev"
  
  # Derived capacity decisions
  instance_count = local.is_production ? 3 : (local.is_staging ? 2 : 1)
  instance_type  = local.is_production ? "t3.large" : "t3.micro"
  
  # Database settings
  db_multi_az           = local.is_production
  db_backup_retention   = local.is_production ? 30 : 7
  db_deletion_protected = local.is_production
  
  # Logging and monitoring
  enable_detailed_monitoring = local.is_production || local.is_staging
  log_retention_days        = local.is_production ? 90 : 30
}

resource "aws_rds_cluster" "main" {
  cluster_identifier     = "app-cluster"
  multi_az               = local.db_multi_az
  backup_retention_days  = local.db_backup_retention
  deletion_protection    = local.db_deletion_protected
}

resource "aws_cloudwatch_log_group" "app_logs" {
  name              = "/aws/app/${local.environment}"
  retention_in_days = local.log_retention_days
}

By naming is_production, instance_count, and log_retention_days, anyone reading your code immediately understands the intent. The conditionals are applied in one place, making future changes predictable and testable.

You can also use locals to make data transformations explicit:

hcl
locals {
  # Map input list into a usable structure
  availability_zones = data.aws_availability_zones.available.names
  
  # Create a map for easy lookup
  subnet_configs = {
    for idx, az in local.availability_zones : az => {
      cidr_block = "10.0.${idx * 16}.0/20"
      az         = az
    }
  }
  
  # Filter and transform
  public_subnets = {
    for az, config in local.subnet_configs :
    az => config if can(regex("^us-east-1[ac]$", az))
  }
}

Do Not Overuse — Inputs Belong in Variables

The biggest mistake teams make is blurring the line between locals and variables. A good rule: if a value should be configurable by the person or system calling your module, it belongs in a variable, not a local.

Variables are the module's public interface. Locals are internal implementation details. When you hardcode business logic into locals, you remove flexibility from your infrastructure code:

hcl
// WRONG: Instance type should be configurable per environment
locals {
  instance_type = "t3.xlarge"  // Hard-coded, not flexible
}

// RIGHT: Let the caller decide, use locals for derived values
variable "instance_type" {
  type        = string
  description = "EC2 instance type"
}

variable "environment" {
  type        = string
  description = "Deployment environment"
}

locals {
  # This is fine: derived from inputs
  env_tags = {
    Environment = var.environment
    InstanceType = var.instance_type
  }
}

When you're building a module that other teams will use, expose the knobs they need to turn. Reserve locals for computed values, defaults derived from inputs, and internal shortcuts that improve readability without reducing flexibility.

Another trap: using locals to hide complexity that should live in separate modules or be made explicit through variables:

hcl
// RISKY: Complex logic buried in locals
locals {
  # What happens if we have 200+ lines here? Hard to test or understand
  vpc_config = merge(
    var.vpc_base_config,
    var.environment == "prod" ? var.prod_vpc_overrides : {},
    var.environment == "staging" ? var.staging_vpc_overrides : {},
    # ... dozens more conditions
  )
}

// BETTER: Make this a separate module or expose it clearly
module "vpc_configuration" {
  source = "./modules/vpc-config"
  
  base_config       = var.vpc_base_config
  environment       = var.environment
  environment_rules = var.vpc_environment_rules
}

Locals are best kept simple and focused. If your locals block spans more than 20-30 lines or contains deeply nested logic, consider whether you're solving a problem that belongs elsewhere.

Common Mistakes

  • Treating locals as variables: Locals can't be overridden at apply time or passed between modules. If you need external control, use a variable instead.
  • Over-nesting and over-abbreviating: Names like local.ct or local.pt_env_tags save typing but cost readability. Spend the extra characters for clarity.
  • Creating circular dependencies: While Terraform catches this, avoid letting one local depend on another in convoluted ways. Keep the dependency graph simple and top-down.
  • Putting all configuration into locals: Your module's public interface (variables) and internal logic (locals) should be clearly separated. Don't hide important decisions in locals.
  • Using locals for sensitive values: Locals are not encrypted in state. For secrets, use sensitive = true on variables and reference them directly, or use a secrets manager.

Key Takeaways

  • Use locals {} to define named values computed within your module; reference them with local.name.
  • Centralize naming conventions and tags in locals to ensure consistency and reduce maintenance burden across dozens of resources.
  • Name complex expressions explicitly to improve readability and make future changes more predictable.
  • Keep locals simple and focused on internal implementation; expose configurable values as variables.
  • Reserve locals for derived values, computed tags, and internal shortcuts—not for business logic that should be flexible or tested independently.
  • A well-designed locals block is usually 10-30 lines; if it grows beyond that, reconsider whether some logic belongs in a separate module.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles