Terraform Strings Part 2: Advanced Templating and Built-in Functions

Master Terraform strings: learn advanced templating, heredocs, and built-in functions for cleaner, reusable HCL code in minutes.

Welcome back to our series on mastering Terraform strings! In Part 1, we covered the essentials of defining strings using quoted literals and heredocs, and how to make them dynamic with string interpolation (${...}).

Now, it's time to level up! In this second installment, we'll dive into more advanced techniques:

  • Using template directives (%{...}) to embed conditional logic and loops directly within your strings.
  • Exploring Terraform's powerful built-in string functions to manipulate and transform your string data in sophisticated ways.

Let's get started!

Beyond Basic Interpolation: Template Directives (%{...})

While ${...} interpolation is great for inserting values, sometimes you need more control flow within your string generation. This is where template directives come in. They use the %{} syntax and are most readable and effective when used inside heredoc strings.

1. Conditional Logic: %{if <BOOL>}%{else}%{endif}

The if directive lets you include parts of a string template based on a boolean condition.

Syntax:

%{if <CONDITION_BOOL>}
  // Content if true
%{else}
  // Content if false (optional)
%{endif}
  • <CONDITION_BOOL> is any Terraform expression that evaluates to true or false.
  • The %{else} block is optional. If it's omitted and the condition is false, nothing from that directive segment is rendered.

Example: Customizing a deployment message based on the environment.

variable "environment" {
  type    = string
  default = "staging"
}

resource "local_file" "deployment_notes" {
  filename = "${path.module}/deployment_notes.txt"
  content  = <<-EOT
    Deployment Details:
    Target Environment: ${var.environment}

    %{if var.environment == "production"}
    IMPORTANT: This is a PRODUCTION deployment.
    Please exercise extreme caution and ensure all pre-flight checks are complete.
    Double-check monitoring dashboards post-deployment.
    %{else if var.environment == "staging"}
    This is a STAGING deployment.
    Ideal for final testing and validation before production.
    %{else}
    This is a DEVELOPMENT or QA deployment.
    Feel free to experiment. Data may be wiped.
    %{endif}

    Deployment initiated at: ${timestamp()}
  EOT
}

This creates a deployment_notes.txt file with a message tailored to the var.environment value.

2. Iteration and Loops: %{for <ITEM> in <COLLECTION>}%{endfor}

The for directive iterates over elements in a collection (list, set, or map) and renders a block of template text for each item, concatenating the results.

Syntax for Lists/Sets:

%{for <ITEM_VAR> in <COLLECTION>}
  // Content for each item, using ${ITEM_VAR}
%{endfor}

You can also get the index:

%{for <INDEX_VAR>, <ITEM_VAR> in <COLLECTION>}
  // Content using ${INDEX_VAR} and ${ITEM_VAR}
%{endfor}

Syntax for Maps:

%{for <KEY_VAR>, <VALUE_VAR> in <COLLECTION_MAP>}
  // Content using ${KEY_VAR} and ${VALUE_VAR}
%{endfor}

Example (List): Generating a list of user access rules.

variable "readonly_users" {
  type    = list(string)
  default = ["auditor", "guest_viewer", "support_tier1"]
}

output "user_access_policy_snippet" {
  value = <<-POLICY
    # Read-Only User Access
    %{for user_name in var.readonly_users}
    define_access {
      user   = "${user_name}"
      role   = "read_only"
      status = "active"
    }
    %{endfor}
  POLICY
}

Example (Map): Creating firewall rule configurations.

variable "service_ports" {
  type = map(number)
  default = {
    "http"  = 80
    "https" = 443
    "ssh"   = 22
  }
}

output "firewall_config_block" {
  value = <<-RULES
    # Service Port Configuration
    %{for service, port in var.service_ports}
    firewall_rule {
      name        = "allow_${service}"
      port        = ${port}
      protocol    = "tcp"
      action      = "accept"
      description = "Allow incoming ${upper(service)} traffic on port ${port}"
    }
    %{endfor}
  RULES
}

Managing Whitespace in Directives: The Tilde (~) Modifier

A common challenge with directives in heredocs is unwanted whitespace. Indentation for HCL readability can creep into your generated string, potentially breaking its syntax (e.g., in YAML or shell scripts).

Terraform's tilde (~) modifier helps control this:

  • Place ~ after the opening characters of a directive (e.g., %{for item in list ~}) to remove whitespace (including newlines) following it on that line.
  • Place ~ before the closing characters (e.g., ~%{endfor}) to remove whitespace (including newlines) preceding it on that line.

Example: Generating a clean list.

variable "features_enabled" {
  type    = list(string)
  default = ["feature_x", "feature_y", "feature_z"]
}

# Without tilde - notice the extra newlines
output "feature_list_unstripped" {
  value = <<-LIST
    Enabled Features:
    %{ for feature in var.features_enabled }
    - ${feature}
    %{ endfor }
  LIST
}
# Output (approximate):
# Enabled Features:
#
# - feature_x
#
# - feature_y
#
# - feature_z
#

# With tilde - much cleaner!
output "feature_list_stripped" {
  value = <<-LIST
    Enabled Features:
    %{ for feature in var.features_enabled ~}
    - ${feature}
    %{ endfor ~}
  LIST
}
# Output (approximate):
# Enabled Features:
# - feature_x
# - feature_y
# - feature_z

The ~ is powerful but requires care. Test your outputs!

Unleash the Power: Built-in String Functions

Terraform comes packed with built-in functions for all sorts of string manipulations. You can use these inside interpolations (${...}), local value assignments, output values, and more. The terraform console is your best friend for experimenting with these.

Here are some of the most commonly used and important ones:

1. format(spec, values...)

  • Purpose: Creates a string by formatting values according to a spec string (like printf).

Example:

> format("Server: %s, IP: %s, Cores: %d", "web01", "10.0.1.5", 4)
"Server: web01, IP: 10.0.1.5, Cores: 4"

2. join(separator, list)

  • Purpose: Combines elements of a list of strings into a single string, with a separator.

Example:

> join("-", ["app", "prod", "web", "01"])
"app-prod-web-01"

3. split(separator, string)

  • Purpose: Divides a string into a list of substrings based on a delimiter.

Example:

> split(".", "www.example.com")
["www", "example", "com"]

4. replace(string, search, replacement)

  • Purpose: Replaces occurrences of a substring (or regex) with another.

Example:

> replace("my_app_v1.0", "_", "-")
"my-app-v1.0"

> replace("user_id_123", "/_(\\d+)$/", "-num-$1") // Regex replace
"user-num-123"

5. lower(string) and upper(string)

  • Purpose: Convert string to all lowercase or all uppercase. Essential for resources with case-sensitive naming rules (e.g., S3 buckets).

Example:

> lower("MyAwesomeBucket")
"myawesomebucket"
> upper("dev-instance")
"DEV-INSTANCE"

6. substr(string, offset, length)

  • Purpose: Extracts a substring. offset is 0-indexed; length is character count.

Example:

> substr("abcdefgh", 2, 3)
"cde"
> substr("HelloTerraform", 0, 5)
"Hello"

7. Trimming Functions:

chomp(string): Removes trailing newline characters (\n, \r\n).

> chomp("some text with a newline\n")
"some text with a newline"

trimsuffix(string, suffix): Removes a specific suffix.

> trimsuffix("backup_final.zip", ".zip")
"backup_final"

trimprefix(string, prefix): Removes a specific prefix.

> trimprefix("project-app-server", "project-")
"app-server"

trimspace(string): Removes leading/trailing Unicode whitespace.

> trimspace("   hello world   \n")
"hello world"

8. Positional Checks (return booleans):

  • startswith(string, prefix)
  • endswith(string, suffix)
  • strcontains(string, substring)

Example:

> startswith("http://example.com", "http://")
true
> endswith("main.tfvars", ".tfvars")
true
> strcontains("production-database-primary", "database")
true

9. Regular Expression Functions (regex, regexall)

  • Terraform uses RE2 syntax.
  • regex(pattern, string): Returns the first match (or captures). Errors if no match.
  • regexall(pattern, string): Returns a list of all non-overlapping matches.

Example:

> regex("v([0-9]+\\.[0-9]+)", "product-version v1.23-beta")
["1.23"] // Returns the captured group

> regexall("\\b[a-z]{3}\\b", "cat sat on the mat") // Find 3-letter words
["cat", "sat", "mat"]

This is just a selection! Check the official Terraform Functions documentation for the full list and more details.

Tip: Remember Terraform handles strings as Unicode characters, not bytes. This is great for internationalization but be mindful if you're used to byte-oriented string ops. length("你好") is 2, not 6.

What's Next?

You're now equipped with advanced templating directives and a powerful arsenal of string functions! These tools allow you to create highly dynamic and sophisticated configurations.

In our final installment, Part 3, we'll focus on:

  • Best practices for writing clean, maintainable, and robust string manipulations.
  • Common pitfalls to watch out for and how to avoid them.
  • Tips to truly master your Terraform string fu.

Stay tuned, and keep experimenting with these powerful features!