Mastering Terraform Local Variables

In today’s blog post, we will learn terraform local variables better known as local values. While input variables are the standard way to define variables in terraform, you can use local variable to temporarily perform some data manipulation and store the outcome.

This detailed guide will walk you through everything you need to know about Terraform Local Values. We will explore their purpose, common use cases, how they differ from input/output variables, and provide practical examples that will help you understand and utilize local variables in your terraform code.

What are Terraform Local Values?

Terraform Local Values (often informally called “local variables”) are named expressions that you define and use within a single Terraform module. Think of them as temporary, computed values or constants that help avoid repeating complex expressions or values multiple times throughout your configuration.

They act like internal variables, simplifying complex logic and making your configuration more readable and DRY (Don’t Repeat Yourself).

Why Use Local Values?

  1. Reduce Repetition (DRY Principle): Avoid writing the same complex expression or hardcoded value repeatedly.
  2. Improve Readability: Assign meaningful names to complex expressions, making your code easier to understand.
  3. Simplify Logic: Break down complex calculations or data transformations into smaller, manageable pieces.
  4. Enhance Maintainability: Changes to a common value or expression only need to be made in one place.
  5. Enable Dynamic Configurations: Compute values based on inputs or other resources, driving dynamic resource creation or configuration.

How to Define Local Values

You define local values within a locals block. This block can appear in any .tf file within your root modules or sub module, though it is common practice to put them in a dedicated locals.tf file for organization.

# locals.tf

locals {
  # Example 1: Simple string concatenation
  resource_name_prefix = "my-app-${var.environment}"

  # Example 2: List transformation
  private_subnet_cidrs = [for i, vpc_cidr in var.vpc_cidr_blocks : cidrsubnet(vpc_cidr, 8, i + 1)]

  # Example 3: Map of tags (often used for consistent tagging)
  common_tags = {
    Environment = var.environment
    Project     = var.project_name
    ManagedBy   = "Terraform"
  }

  # Example 4: Conditional logic
  instance_type_based_on_env = var.environment == "prod" ? "m5.large" : "t3.medium"
}

Explanation:

  • locals { ... }: This block defines all your local values.
  • resource_name_prefix = "my-app-${var.environment}": Each line defines a local value. The name (e.g., resource_name_prefix) is on the left, and the expression that computes its value is on the right.
  • Local values can reference input variables (var.environment), other local values, or even attributes of resources.

Referencing Local Values

Once defined, you reference local values using the local. prefix, similar to how you use var. for input variables.

# main.tf

resource "aws_instance" "web_server" {
  ami           = data.aws_ami.ubuntu.id # Assuming you have an AMI data source
  instance_type = local.instance_type_based_on_env
  count         = var.instance_count # Input variable

  tags = local.common_tags # Using a map local value

  user_data = <<-EOF
              #!/bin/bash
              echo "Hello from ${local.resource_name_prefix}!" > /var/www/html/index.html
              EOF
}

resource "aws_s3_bucket" "app_data" {
  bucket = "${local.resource_name_prefix}-data-bucket"
  acl    = "private"

  tags = local.common_tags
}

Terraform Locals vs. Variables vs. Outputs

Let us explore the difference between terraform Local Values vs. Input Variables vs. Output Values

FeatureInput Variables (variable)Local Values (locals)Output Values (output)
PurposeCustomize a module from the outside. User input.Compute and store intermediate values inside a module.Expose data from a module to the outside.
ScopeModule-wide. Values provided at runtime.Module-wide. Only accessible within the module they’re defined.Module-wide. Exposed after terraform apply.
Source of ValueUser input (CLI, .tfvars, environment variables, default).Expressions based on inputs, other locals, resource attributes.Resource attributes, local values, input variables.
MutabilityImmutable once set for a terraform run.Computed once per terraform run based on their expression.Immutable once resources are provisioned.
VisibilityVisible to the user running Terraform.Internal to the module. Not directly exposed outside.Visible to the user, or other configurations/modules.
Prefixvar.local.output. (when referenced from another module)

When to choose which:

  • Use Input Variables when a value needs to be passed into your module from an external source (e.g., environment name, instance count, specific configuration parameters).
  • Use Local Values when you need to perform calculations, transformations, or consolidate common expressions within your module for internal use (e.g., creating resource names, complex conditional logic, derived values).
  • Use Output Values when you want to output a specific piece of information from your module to the outside world, so other Terraform configurations or external tools can consume it (e.g., VPC ID, load balancer endpoint, public IP).

Common Use Cases for Local Values

Consistent Naming Conventions

This is perhaps the most frequent use case. By defining a local value for a common name prefix, you ensure consistency across all your resources.

# locals.tf
locals {
  project_name       = "ecommerce"
  environment_suffix = var.environment == "prod" ? "" : "-${var.environment}" # Add suffix for non-prod
  base_name          = "${local.project_name}${local.environment_suffix}"
}

# main.tf
resource "aws_s3_bucket" "app_logs" {
  bucket = "${local.base_name}-logs"
  # ...
}

resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr
  tags = {
    Name = "${local.base_name}-vpc"
  }
  # ...
}

Standardized Tagging

Applying a consistent set of tags across all resources is a best practice for cost allocation, organization, and automation. Locals make this effortless.

# locals.tf
locals {
  common_tags = {
    ManagedBy   = "Terraform"
    Environment = var.environment
    Owner       = var.owner_email
    DateCreated = formatdate("YYYY-MM-DD", timestamp())
  }
}

# main.tf
resource "aws_ec2_instance" "web" {
  # ...
  tags = local.common_tags
}

resource "aws_rds_instance" "db" {
  # ...
  tags = merge(local.common_tags, {
    DatabaseType = "PostgreSQL"
  }) # You can merge locals with additional tags
}

Data Transformations and Mappings

When you need to transform input data or create mappings, locals are ideal.

# variables.tf
variable "instance_sizes" {
  type = map(string)
  default = {
    "dev"     = "t3.small"
    "staging" = "t3.medium"
    "prod"    = "m5.large"
  }
}

# locals.tf
locals {
  # Get the instance type for the current environment
  current_instance_type = lookup(var.instance_sizes, var.environment, "t3.nano") # default to t3.nano if not found

  # Create a map for conditional resource creation using `for` loop
  instance_config = {
    for env, instance_type in var.instance_sizes : env => {
      type       = instance_type
      disk_size  = env == "prod" ? 100 : 50
      public_ip  = env == "dev" ? true : false
    }
  }
}

# main.tf
resource "aws_instance" "env_instance" {
  for_each      = local.instance_config
  ami           = data.aws_ami.ubuntu.id
  instance_type = each.value.type
  associate_public_ip_address = each.value.public_ip
  tags = merge(local.common_tags, {
    Name = "${local.base_name}-${each.key}-instance"
  })
}

Simplifying Conditional Logic

Complex condition ? true_val : false_val expressions can be encapsulated in locals for better readability.

# locals.tf
locals {
  # Determine if a public IP should be associated based on environment
  should_associate_public_ip = var.environment == "dev" || var.environment == "staging" ? true : false

  # Choose the subnet ID based on a boolean flag
  subnet_id = var.use_private_subnet ? var.private_subnet_id : var.public_subnet_id
}

# main.tf
resource "aws_instance" "app" {
  # ...
  associate_public_ip_address = local.should_associate_public_ip
  subnet_id                   = local.subnet_id
}

Derived Values from Resource Attributes

Locals can compute values based on attributes of resources that have already been defined or will be provisioned.

# locals.tf
locals {
  # Concatenate IDs of all created subnets (requires a resource defined elsewhere)
  all_subnet_ids_string = join(",", aws_subnet.app_subnets.*.id)
}

# main.tf
output "combined_subnet_ids" {
  description = "A comma-separated string of all application subnet IDs."
  value       = local.all_subnet_ids_string
}

Best Practices for Local Values

  • Organize in locals.tf: While not strictly required, placing all your locals blocks in a dedicated locals.tf file (or grouping them logically) enhances readability.
  • Descriptive Naming: Use clear, self-explanatory names for your local values (e.g., resource_name_prefix, common_tags, db_endpoint).
  • Avoid Circular Dependencies: A local value cannot reference another local value that directly or indirectly references itself. Terraform will throw an error (Circular dependency detected!).
  • Do not Overuse: Locals are for internal computation and readability. Do not use them to replace input variables if a value truly needs to be passed into the module. Similarly, do not use them if the value’s primary purpose is to be exposed to other modules; that is what outputs are for.
  • Keep Expressions Focused: If a local value’s expression becomes excessively complex, consider breaking it down into multiple, simpler local values.
  • Test Your Locals: Before applying, terraform plan will evaluate your local values, allowing you to catch errors or unexpected results early.

Conclusion

Terraform Local Values is an important feature for writing clean, efficient, and maintainable Infrastructure as Code. By wisely applying them for consistent naming, standardized tagging, data transformations, and simplifying complex logic, you can significantly enhance your Terraform configurations. Use local values today and see their power!

Author

Debjeet Bhowmik

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins. In my free time, I write blogs on ckdbtech.com

Leave a Comment