Explain string templates: heredocs, `templatefile()`, and the `%{if}`/`%{for}` template directives.
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.