Terraform Functions and Expressions: Transforming Configuration Values

Terraform variables transformed into consistent resource names, tags, and environment settings through connected data shapes.

What You’ll Learn

Terraform functions and expressions let you transform input values into consistent configuration without duplicating values throughout your infrastructure code. In this lesson, you will use string functions, map functions, list comprehensions, and conditional expressions to generate reusable resource settings.

  • Normalize names with functions such as trimspace, lower, and replace.
  • Transform maps and lists with for expressions.
  • Combine configuration maps with merge.
  • Build predictable settings for multiple environments and services.

The Concept

A Terraform function accepts values and returns a transformed value. For example, lower("Production") returns "production", while join("-", ["payments", "api"]) returns "payments-api".

An expression is a Terraform statement that produces a value. Function calls are expressions, but so are references such as var.project_name, conditional expressions, and for expressions.

These features are useful when resource settings need to follow a consistent convention. Instead of manually creating a different name and tag map for every environment, you can transform one set of variables into a complete configuration map.

Commonly useful functions include:

  • trimspace(string) removes whitespace at the beginning and end of a string.
  • lower(string) converts a string to lowercase.
  • replace(string, old, new) replaces matching text.
  • join(separator, list) combines list elements into one string.
  • merge(map1, map2) combines maps, with later maps overriding duplicate keys.
  • distinct(list) removes duplicate list elements.

A for expression transforms every item in a collection. For example, [for region in var.regions : lower(region)] returns a new list containing a lowercase version of every region.

Basic Example

This configuration receives a project name, common tags, a list of regions, and settings for several environments. It then generates normalized names and complete environment settings.

variable "project_name" {
  type    = string
  default = "  Payments API "
}

variable "common_tags" {
  type = map(string)

  default = {
    ManagedBy = "Terraform"
    Team      = "platform"
  }
}

variable "regions" {
  type    = list(string)
  default = ["US-EAST-1", "EU-WEST-1"]
}

variable "environments" {
  type = map(object({
    instance_type = string
    replicas      = number
  }))

  default = {
    development = {
      instance_type = "t3.small"
      replicas      = 1
    }

    production = {
      instance_type = "t3.medium"
      replicas      = 3
    }
  }
}

locals {
  normalized_name = lower(replace(trimspace(var.project_name), " ", "-"))

  normalized_regions = [
    for region in var.regions : lower(region)
  ]

  environment_settings = {
    for environment, settings in var.environments :
    environment => merge(
      settings,
      {
        name = join("-", [local.normalized_name, environment])

        tags = merge(
          var.common_tags,
          {
            Environment = environment
            Service     = local.normalized_name
          }
        )
      }
    )
  }
}

output "normalized_name" {
  value = local.normalized_name
}

output "normalized_regions" {
  value = local.normalized_regions
}

output "environment_settings" {
  value = local.environment_settings
}

Expected Output

After running terraform apply, the outputs contain normalized names and generated settings similar to these values:

normalized_name = "payments-api"
normalized_regions = [
  "us-east-1",
  "eu-west-1",
]
environment_settings = {
  development = {
    instance_type = "t3.small"
    name          = "payments-api-development"
    replicas      = 1
    tags = {
      Environment = "development"
      ManagedBy   = "Terraform"
      Service     = "payments-api"
      Team        = "platform"
    }
  }
  production = {
    instance_type = "t3.medium"
    name          = "payments-api-production"
    replicas      = 3
    tags = {
      Environment = "production"
      ManagedBy   = "Terraform"
      Service     = "payments-api"
      Team        = "platform"
    }
  }
}

How the Code Works

A top-to-bottom Terraform data flow showing input variables transformed into normalized names and regions, then iterated and merged into conditional environment or workload settings for reusable resource configuration.
Terraform functions and expressions normalize inputs, transform collections, merge tags and settings, and produce reusable resource configuration.

trimspace(var.project_name) removes the extra spaces from the input. The result is passed to replace, which changes the remaining space between the words to a hyphen. Finally, lower makes the name lowercase:

" Payments API " becomes "payments-api".

The normalized_regions local uses a list for expression. Terraform evaluates lower(region) once for each item in var.regions and returns a new list.

The map for expression iterates over both the environment name and its settings:

  • environment becomes the key in the generated map.
  • settings contains values such as instance_type and replicas.
  • merge preserves those settings and adds a generated name and tags map.

The nested merge combines shared tags with environment-specific tags. If both maps contain the same key, the later map wins. Therefore, the generated Environment or Service value would override a value with the same key in var.common_tags.

These locals can be passed into resource arguments, modules, or other locals. Keeping the transformation in one place makes resource configuration easier to review and reduces naming inconsistencies.

Another Example

A common variation is generating settings for several services from a list of service definitions. This example removes duplicate team members, creates a stable service name, and uses a conditional expression to select the number of replicas.

variable "application_name" {
  type    = string
  default = "Customer Portal"
}

variable "services" {
  type = list(object({
    name    = string
    tier    = string
    owners  = list(string)
    enabled = bool
  }))

  default = [
    {
      name    = "Web API"
      tier    = "production"
      owners  = ["platform", "platform", "payments"]
      enabled = true
    },
    {
      name    = "Worker"
      tier    = "staging"
      owners  = ["operations", "platform"]
      enabled = true
    }
  ]
}

locals {
  application_slug = lower(replace(trimspace(var.application_name), " ", "-"))

  service_settings = {
    for service in var.services :
    lower(replace(trimspace(service.name), " ", "-")) => {
      enabled = service.enabled
      name    = join("-", [
        local.application_slug,
        lower(replace(trimspace(service.name), " ", "-"))
      ])
      owners   = distinct(service.owners)
      replicas = service.tier == "production" ? 3 : 1
      tier     = service.tier
    }
  }
}

output "service_settings" {
  value = local.service_settings
}

Here, distinct(service.owners) changes the repeated "platform" entry into one entry. The conditional expression sets three replicas for production services and one replica for other tiers.

The map key is generated from the service name. This is convenient for lookups, but each normalized service name must be unique. Two names that normalize to the same value could overwrite one another in the generated map.

Common Mistakes

  • Forgetting to normalize before constructing names: Inputs such as "Customer Portal" and " customer portal " can produce inconsistent resource names unless you trim, replace, and lowercase them.
  • Assuming merge performs a deep merge: Terraform’s merge function is shallow. If two maps contain a nested map under the same key, the later nested map replaces the earlier one rather than merging each nested key.
  • Creating duplicate map keys: A for expression that generates a map requires unique keys. Normalize names carefully and check whether two input names can become identical.
  • Confusing lists and sets: A list preserves order and can contain duplicates. A set is intended for unique unordered values. Use distinct when you need to remove duplicates from a list while retaining list output.
  • Using a function with the wrong type: String functions require strings, while functions such as distinct require a collection. Variable type constraints help Terraform detect invalid inputs early.

Try It Yourself

Modify the basic example so that it also creates a resource_prefix local. The prefix should combine the normalized project name and the first normalized region using join. For the default values, the result should be "payments-api-us-east-1".

Then add the prefix to each environment’s generated settings by including it in the map passed to merge.

Challenge

Create a complete Terraform configuration that generates consistent settings for several workloads.

  • Normalize application_name by trimming whitespace, replacing spaces with hyphens, and converting it to lowercase.
  • Generate a settings map keyed by each workload name.
  • Build each workload’s resource name from the normalized application name and workload name.
  • Convert every region to lowercase with a list for expression.
  • Set replicas to 3 for production workloads and 1 for all other tiers.
  • Combine common tags with an owner and tier tag.

Solution

variable "application_name" {
  type    = string
  default = " Inventory Portal "
}

variable "regions" {
  type    = list(string)
  default = ["US-EAST-1", "EU-WEST-1"]
}

variable "workloads" {
  type = map(object({
    tier  = string
    owner = string
  }))

  default = {
    frontend = {
      tier  = "production"
      owner = "web-team"
    }

    reporting = {
      tier  = "staging"
      owner = "data-team"
    }
  }
}

variable "common_tags" {
  type = map(string)

  default = {
    ManagedBy = "Terraform"
    Project   = "shared-platform"
  }
}

locals {
  application_slug = lower(
    replace(trimspace(var.application_name), " ", "-")
  )

  workload_settings = {
    for workload, configuration in var.workloads :
    workload => {
      name = join("-", [
        local.application_slug,
        lower(workload)
      ])

      regions = [
        for region in var.regions : lower(region)
      ]

      replicas = configuration.tier == "production" ? 3 : 1

      tags = merge(
        var.common_tags,
        {
          Owner = configuration.owner
          Tier  = configuration.tier
        }
      )
    }
  }
}

output "workload_settings" {
  value = local.workload_settings
}

The solution centralizes the application naming rule in application_slug, then uses a map for expression to create one settings object per workload. The conditional expression handles replica counts, while merge combines shared and workload-specific tags.

Key Takeaways

  • Terraform functions transform individual values, while expressions combine references, functions, comprehensions, and conditions into configuration values.
  • Normalize user-provided names before using them in resource names or map keys.
  • Use list and map for expressions to generate consistent settings from collections.
  • merge combines maps shallowly, and later maps override duplicate keys.
  • Centralizing transformations in local values makes resource configuration reusable and predictable.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top