Explain string templates: heredocs, `templatefile()`, and the `%{if}`/`%{for}` template directives.

5 minintermediateterraformtemplateshcl

Quick Answer

A heredoc (<<-EOT ... EOT) lets you write a multi-line string inline with ${...} interpolation for variable substitution. templatefile("path.tpl", { var1 = ..., var2 = ... }) renders an external template file with a given set of variables, commonly used for cloud-init/user-data scripts kept in their own .tpl file rather than embedded inline. Inside any template, %{if condition}...%{else}...%{endif} and %{for item in list}...%{endfor} are template directives: control-flow constructs usable only within string templates, letting you conditionally include text or repeat a block per list item without leaving the string context, which is how templated config files handle optional sections or per-item repetition.

Detailed Answer

Terraform's string-handling goes beyond simple "${var.x}" interpolation. Heredocs, templatefile(), and template directives together form a small templating language of their own, most commonly reached for when generating cloud-init/user-data scripts or config files.

Heredoc strings

locals {
  motd = <<-EOT
    Welcome to ${var.environment}.
    This instance is managed by Terraform.
  EOT
}

<<-EOT ... EOT defines a multi-line string inline, with the leading - allowing the closing marker (and content) to be indented to match surrounding code, and ${...} performing ordinary interpolation exactly as in a single-line string.

templatefile() — rendering an external file

resource "aws_instance" "web" {
  user_data = templatefile("${path.module}/user-data.sh.tpl", {
    environment = var.environment
    app_port    = var.app_port
  })
}
# user-data.sh.tpl
#!/bin/bash
echo "Starting app in ${environment} on port ${app_port}"

Keeping the template as its own file (rather than an inline heredoc) is preferred once it grows beyond a few lines. It's easier to read, lint, and edit as an actual shell/config file, and templatefile() renders it with the given variable map substituted in, just like an inline string would be.

Template directives — control flow inside a string

# user-data.sh.tpl
#!/bin/bash
%{ if enable_monitoring }
systemctl enable monitoring-agent
%{ endif }

%{ for pkg in packages }
apt-get install -y ${pkg}
%{ endfor }

%{if condition} ... %{else} ... %{endif} and %{for item in list} ... %{endfor} are directives, distinct from ${...} interpolation. They exist only inside string templates and provide the conditional/repetition logic that plain interpolation can't — interpolation can only substitute a value, not decide whether a whole block of text should appear at all, or repeat a block per list element.

Why this matters

Together these let a single template file express real logic — "include this section only if a flag is set," "repeat this line for every package in a list" — entirely within a string. That's exactly the shape needed for generating bootstrap scripts or config files whose content genuinely varies based on module inputs, without resorting to string concatenation gymnastics in the surrounding HCL.