What ${...} does
Inside a string, ${...} is a template. The curly braces can hold any HCL expression: a variable, a reference to a resource, a function, a condition.
bucket = "myapp-${var.env}"# = "myapp-dev" if var.env = "dev"
tags = { Name = "${local.prefix}-bucket-${count.index}"# = "myapp-dev-bucket-0", "myapp-dev-bucket-1", ...
}
user_data = <<EOF
#!/bin/bash
echo "Environment: ${var.env}"echo "Region: ${data.aws_region.current.name}"EOF
If a string has unusual requirements, you can call functions inside ${...}:
bucket = "${var.app}-${upper(var.env)}-${formatdate("YYYY", timestamp())}"When ${...} is required and when it is not
In old HCL (Terraform before 0.12), ${...} was needed everywhere:
count = "${var.instance_count}" # old styleami = "${data.aws_ami.ubuntu.id}" # old styleIn modern HCL, ${...} is needed only when the value is part of a larger string:
# part of a string → ${...} is requiredbucket = "myapp-${var.env}-logs"# whole string = one expression → ${...} is NOT needed and not recommendedcount = var.instance_count
ami = data.aws_ami.ubuntu.id
region = "us-east-1" # a literal: quotes are needed, ${...} is notterraform fmt removes redundant ${...} automatically. And terraform validate raises a warning on the old style.
Heredoc for multiline strings
When you need a multiline string with substitutions:
user_data = <<EOT
#!/bin/bash
set -e
echo "Hello from ${var.env}!"apt-get update
apt-get install -y nginx
EOT
<<EOT ... EOT (any marker can stand in for EOT) keeps everything between them as a string. ${...} works inside.
A heredoc with a hyphen trims leading whitespace:
config = <<-EOT
key = "value"
another = "thing"
EOT
# = "key = \"value\"\nanother = \"thing\"\n", with no indentation
Without the hyphen, the indentation stays as is. With the hyphen, Terraform strips the smallest common indent.
Special characters inside ${...}
When you need a literal curly brace or dollar sign inside ${...}, escape it:
# ${{var}}. Terraform reads ${{var}} as "start of interpolation, then the literal {var}"# That is an error. The correct way is to escape:
literal = "$${not_interpolation}"▸"$ {not_interpolation}", in the output string with no substitution
literal_dollar = "%%{also_not}"▸"%{also_not}". A lone % is not interpolation, but a %{ would start a template block
$$ and %% are the escapes for interpolation and for a template block, respectively. In everyday work you rarely need them.
Template blocks %{...}
Do not confuse ${...} (a value) with %{...} (control structures):
user_data = <<-EOT
#!/bin/bash
%{ if var.env == "prod" }echo "Production setup"
systemctl enable monitoring
%{ else }echo "Non-prod setup"
%{ endif }hostnames:
%{ for name in var.hostnames }- ${name}%{ endfor }EOT
You need this when a string needs logic. It is used rarely, but it is handy for cloud-init and user_data scripts.
Pitfalls
-
${...}without quotes is invalid.${var.x}with no surrounding string is an error. If you want an expression without interpolation, do not wrap it in a string:count = var.x. -
The old style
"${var.x}"for a single value is deprecated. It works, but you get a warning. Prefercount = var.x. -
You cannot go multiline inside
${...}. It is a single-line expression. For multiline constructs, use a heredoc. -
${...}does not interpolate in the state. The state stores values that are already substituted. If you changevar.env, Terraform sees thatbucketmust now be different and proposes a change. -
A list/map variable in interpolation is an error.
bucket = "myapp-${var.tags}"wheretagsis a map fails. Only primitives go inside strings. For everything else, use separate assignments. -
Heredoc backticks behave differently on Windows vs Unix. If the HCL is generated cross-platform, be careful with line endings inside a heredoc.