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, which is 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.