Terraform locals are a way to define reusable values within a single Terraform module, but most people use them as a crutch for avoiding repetitive code that should really be in a variable.

Let’s see locals in action. Imagine you have a Terraform configuration that deploys a few AWS S3 buckets, and you want to ensure they all follow a consistent naming convention with a specific environment prefix.

# main.tf

variable "environment" {
  description = "The deployment environment (e.g., dev, staging, prod)."
  type        = string
  default     = "dev"
}

locals {
  bucket_prefix = "${var.environment}-"
  common_tags = {
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}

resource "aws_s3_bucket" "data" {
  bucket = "${local.bucket_prefix}my-data-bucket"
  tags   = local.common_tags
}

resource "aws_s3_bucket" "logs" {
  bucket = "${local.bucket_prefix}my-logs-bucket"
  tags   = local.common_tags
}

resource "aws_s3_bucket" "app" {
  bucket = "${local.bucket_prefix}my-app-bucket"
  tags   = local.common_tags
}

Here, variable "environment" defines an input that can be passed into the module. locals then defines two values: bucket_prefix and common_tags. These locals are then used within the aws_s3_bucket resources. When you run terraform apply -var="environment=prod", the S3 buckets will be created with names like prod-my-data-bucket and tagged accordingly. This makes it easy to change the prefix or tags across multiple resources by modifying them in just one place.

The core problem Terraform locals solve is reducing repetition and improving readability within a module. Instead of writing "${var.environment}-my-data-bucket", "${var.environment}-my-logs-bucket", and "${var.environment}-my-app-bucket", you define the common prefix once in a local and reuse it. This is particularly useful for complex strings, calculations, or map merges that are used in multiple places. You can also use locals to conditionally set values or to create more complex data structures from simpler inputs.

The mental model for locals is that they are module-scoped constants. They are computed once when Terraform reads your configuration and then used as many times as you reference them within that module. They cannot be changed from outside the module; their values are determined entirely by the module’s input variables and its internal logic. This is a key distinction from input variables, which are designed to be passed in from the calling configuration. Outputs, on the other hand, are used to expose values from a module to the calling configuration.

You can chain locals together. For example, if you had multiple environment-specific configurations that shared some base settings but differed in others, you could have a local that builds upon another local.

locals {
  base_config = {
    instance_type = "t3.micro"
    ami_id        = "ami-0abcdef1234567890" # Example AMI ID
  }

  dev_config = merge(local.base_config, {
    instance_type = "t3.nano" # Override for dev
    tags = {
      Environment = "dev"
    }
  })

  prod_config = merge(local.base_config, {
    instance_type = "t3.medium" # Override for prod
    tags = {
      Environment = "prod"
    }
  })
}

resource "aws_instance" "example" {
  count         = var.is_production ? 1 : 1 # Simplified for example
  ami           = var.is_production ? local.prod_config.ami_id : local.dev_config.ami_id
  instance_type = var.is_production ? local.prod_config.instance_type : local.dev_config.instance_type

  tags = merge(
    {
      Name = var.is_production ? "prod-server" : "dev-server"
    },
    var.is_production ? local.prod_config.tags : local.dev_config.tags
  )
}

In this example, local.base_config sets common defaults. Then, local.dev_config and local.prod_config are created by merging local.base_config with environment-specific overrides. This pattern is incredibly powerful for managing complex configurations where many resources share common settings but have subtle variations.

The real power of locals lies in their ability to abstract complex logic or data transformations within a module. For instance, you might have a local that dynamically constructs a list of security group IDs based on a set of input variables and other module resources, or a local that merges multiple tag maps from different sources into a single, cohesive map for a resource. This keeps your resource blocks cleaner and the module’s intent clearer. A common pitfall is overusing locals when an input variable would be more appropriate, leading to modules that are harder to configure from the outside.

The next concept to explore is how to use for_each and for expressions within locals to dynamically generate maps and lists that can then be used to create multiple resources.

Want structured learning?

Take the full Terraform course →