Terraform Local Values

Learn how Terraform local values work, when to use locals instead of variables or data sources, and how to simplify repetitive expressions in your infrastructure code.

What Are Local Values in Terraform?

Terraform local values, usually just called locals, are named expressions defined inside a locals block. They let you store a calculated value once and then reuse it throughout your configuration.

If input variables are the values that come into your Terraform code, local values are the values you compute and organize inside it.

A basic example looks like this:

variable "environment" {
  type = string
}

variable "project_name" {
  type = string
}

locals {
  name_prefix = "${var.project_name}-${var.environment}"
}

resource "aws_s3_bucket" "logs" {
  bucket = "${local.name_prefix}-logs"
}

Here, local.name_prefix saves you from repeating the same string construction everywhere.

Locals do not accept input from the user. They are not part of the public interface of a module. They are strictly an internal convenience that improves readability and removes duplication.

Why Local Values Matter

Beginners sometimes skip locals and write everything inline. Terraform allows that, but the configuration becomes noisy very quickly.

For example:

resource "aws_cloudwatch_log_group" "app" {
  name = "/aws/${var.project_name}/${var.environment}/app"
}

resource "aws_s3_bucket" "logs" {
  bucket = "${var.project_name}-${var.environment}-logs"
}

resource "aws_iam_role" "app" {
  name = "${var.project_name}-${var.environment}-app-role"
}

The repeated project_name + environment pattern appears everywhere. If your naming convention changes, you have to update many places.

With locals:

locals {
  name_prefix = "${var.project_name}-${var.environment}"
}

resource "aws_cloudwatch_log_group" "app" {
  name = "/aws/${local.name_prefix}/app"
}

resource "aws_s3_bucket" "logs" {
  bucket = "${local.name_prefix}-logs"
}

resource "aws_iam_role" "app" {
  name = "${local.name_prefix}-app-role"
}

This is easier to read and easier to change.

Defining Locals with the locals Block

A locals block can define one or many local values:

locals {
  environment_upper = upper(var.environment)
  common_tags = {
    Environment = var.environment
    Project     = var.project_name
    ManagedBy   = "Terraform"
  }
  enable_backups = var.environment == "prod"
}

You reference them with local.<name>:

tags = local.common_tags

Note the syntax carefully:

  • var.name for input variables
  • local.name for local values
  • module.name.output_name for module outputs

When to Use Locals vs Variables vs Data Sources

This is one of the most important conceptual distinctions in Terraform.

Use a variable when the value should come from outside

Variables are for values that should be supplied by:

  • the person running Terraform
  • a parent module
  • a .tfvars file
  • a CI/CD pipeline

Example:

variable "region" {
  type = string
}

The region is a decision the caller should make, so it belongs in a variable.

Use a local when the value is internal logic

Locals are for values derived from variables, resources, or expressions inside the module.

Example:

locals {
  bucket_name = "${var.project_name}-${var.environment}-logs"
}

This does not need to be exposed to the caller. It is implementation detail.

Use a data source when Terraform must query an external system

Data sources fetch existing information from a provider.

Example:

data "aws_availability_zones" "available" {}

This value comes from AWS, not from the user and not from pure local computation.

Quick decision guide

Ask this question:

  • Should a caller choose this value? Use a variable.
  • Can Terraform calculate this value from what it already knows? Use a local.
  • Must Terraform ask a provider for this value? Use a data source.

Understanding this boundary keeps your module interfaces clean.

Using Expressions in Locals

Local values can contain almost any Terraform expression. This makes them very powerful.

String expressions

locals {
  name_prefix = "${var.project_name}-${var.environment}"
  log_group   = "/aws/${var.project_name}/${var.environment}/app"
}

Arithmetic expressions

locals {
  desired_capacity = var.environment == "prod" ? 4 : 2
  total_disk_size  = var.instance_count * var.disk_size_gb
}

Map and list expressions

locals {
  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = "Terraform"
  }

  private_subnet_names = [
    for index, az in var.availability_zones :
    "${var.project_name}-${var.environment}-private-${index + 1}"
  ]
}

Referencing resources

Locals can use resource attributes too:

locals {
  alb_dns_name = aws_lb.app.dns_name
}

This is valid, but use it thoughtfully. If the value is only used once, creating a local may add unnecessary indirection.

Using Functions in Locals

Terraform includes many built-in functions, and locals are a great place to use them.

merge()

A classic pattern is combining default tags with user-supplied tags.

variable "tags" {
  type    = map(string)
  default = {}
}

locals {
  common_tags = merge(
    {
      Project     = var.project_name
      Environment = var.environment
      ManagedBy   = "Terraform"
    },
    var.tags
  )
}

Now every resource can reuse local.common_tags.

lower(), upper(), replace()

Useful for naming standards:

locals {
  normalized_project = replace(lower(var.project_name), "_", "-")
  name_prefix        = "${local.normalized_project}-${var.environment}"
}

concat() and distinct()

Useful for combining lists:

locals {
  all_cidrs = distinct(concat(var.office_cidrs, var.vpn_cidrs))
}

lookup()

A common way to choose environment-specific values:

locals {
  instance_size = lookup(
    {
      dev     = "t3.micro"
      staging = "t3.small"
      prod    = "t3.medium"
    },
    var.environment,
    "t3.micro"
  )
}

jsonencode()

Great for IAM policies or ECS task definitions:

locals {
  bucket_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["s3:GetObject"]
        Resource = "${aws_s3_bucket.assets.arn}/*"
      }
    ]
  })
}

This keeps complex JSON generation readable.

Using Conditionals in Locals

Locals often centralize environment-specific decisions.

locals {
  instance_type = var.environment == "prod" ? "t3.medium" : "t3.micro"
  backup_days   = var.environment == "prod" ? 30 : 7
  multi_az      = var.environment == "prod" ? true : false
}

Now you express the rule once and reuse it everywhere.

You can also make conditionals more expressive by grouping them in locals rather than scattering ternary operators across resource blocks.

Common Patterns for Local Values

1. Common tags

This is probably the most common real-world use of locals.

locals {
  common_tags = merge(
    {
      Project     = var.project_name
      Environment = var.environment
      ManagedBy   = "Terraform"
      Owner       = "platform-team"
    },
    var.extra_tags
  )
}

Then:

resource "aws_instance" "web" {
  ami           = "ami-0abcdef1234567890"
  instance_type = var.instance_type
  tags          = local.common_tags
}

This keeps tags consistent across your infrastructure.

2. Name prefixes

locals {
  name_prefix = "${var.project_name}-${var.environment}"
}

Then:

resource "aws_security_group" "app" {
  name = "${local.name_prefix}-sg"
}

resource "aws_s3_bucket" "artifacts" {
  bucket = "${local.name_prefix}-artifacts"
}

If the naming convention changes later, you update one local value instead of many resources.

3. Environment-specific values

locals {
  desired_count = {
    dev     = 1
    staging = 2
    prod    = 4
  }[var.environment]
}

This is cleaner than repeating the same environment branching logic in multiple places.

4. Standardized subnet names

locals {
  public_subnet_names = [
    for index, az in var.availability_zones :
    "${var.project_name}-${var.environment}-public-${index + 1}"
  ]
}

5. Feature flags derived from environment

locals {
  enable_waf        = var.environment == "prod"
  enable_monitoring = contains(["staging", "prod"], var.environment)
}

This keeps business rules in one place.

A Complete Example

variable "project_name" {
  type = string
}

variable "environment" {
  type = string
}

variable "availability_zones" {
  type = list(string)
}

variable "extra_tags" {
  type    = map(string)
  default = {}
}

locals {
  normalized_project = replace(lower(var.project_name), "_", "-")
  name_prefix        = "${local.normalized_project}-${var.environment}"

  common_tags = merge(
    {
      Project     = var.project_name
      Environment = var.environment
      ManagedBy   = "Terraform"
    },
    var.extra_tags
  )

  instance_type = {
    dev     = "t3.micro"
    staging = "t3.small"
    prod    = "t3.medium"
  }[var.environment]

  backup_retention_days = var.environment == "prod" ? 30 : 7
}

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

resource "aws_db_instance" "app" {
  allocated_storage      = 20
  engine                 = "postgres"
  instance_class         = local.instance_type == "t3.medium" ? "db.t3.medium" : "db.t3.micro"
  backup_retention_period = local.backup_retention_days
}

This demonstrates the main purpose of locals: moving repeated logic and naming rules into one understandable section.

Avoiding Overuse of Locals

Locals are helpful, but too many locals can make Terraform harder to read.

Bad pattern: creating locals for trivial values

locals {
  bucket_acl = "private"
}

If a value is only used once and adds no clarity, keeping it inline may be simpler.

Bad pattern: turning every resource attribute into a local

locals {
  s3_bucket_id = aws_s3_bucket.logs.id
}

If you use that ID once, just reference aws_s3_bucket.logs.id directly.

Bad pattern: hiding module inputs behind unnecessary locals

locals {
  region = var.region
}

This adds one more name without adding meaning.

Good rule of thumb

Use locals when they do at least one of these things:

  • remove repetition
  • clarify intent
  • group related logic
  • centralize naming or tagging rules
  • simplify complex expressions

If a local does none of those things, it may not be worth creating.

Best Practices for Terraform Locals

  1. Use clear names. name_prefix, common_tags, and backup_retention_days are much better than x or config1.
  2. Group related locals together. Keep naming, tagging, and environment-specific settings organized.
  3. Prefer locals for internal logic. Do not expose implementation details as variables unless callers truly need to control them.
  4. Do not over-abstract. Terraform is configuration, not a general-purpose programming language.
  5. Keep module interfaces small. Use locals to compute internal values from a smaller set of inputs.

Final Thoughts

Local values are one of the easiest ways to make Terraform code more readable. They help you avoid copy-paste, keep naming conventions consistent, and express environment-specific logic once instead of in ten different places.

A useful mental model is:

  • Variables are inputs chosen by the caller
  • Locals are internal calculations chosen by the module author
  • Data sources are external facts retrieved from the provider

If you apply that rule consistently, your Terraform code will be easier to understand and easier to maintain as it grows.


Knowledge Check

Exercise

Question 1: Local Syntax

How do you reference a Terraform local value named name_prefix?

Exercise

Question 2: Variables vs Locals

Which value is usually the best candidate for a local instead of an input variable?

Exercise

Question 3: Overusing Locals

Which example best shows unnecessary overuse of Terraform locals?

Continue Learning

Explore Related Topics

Try the Tool

Related Resources