skip to content

In Terraform HCL, when is the ${ ... } interpolation syntax actually required, and why does writing "${var.name}" as an entire argument value produce a deprecation warning?

level: juniorimportance: must knowfreq 70%

answer

  1. two grammars: expression and template
  2. first-class since 0.12
  3. only needed inside literal text
  4. doubling escapes the sigil
  5. backend block evaluates too early

basics

~20 s

Since Terraform 0.12, expressions are first-class, so you write var.name directly. The ${ } syntax is only for embedding an expression inside a larger string, such as "app-${var.env}". Quoting a whole expression adds nothing and Terraform warns that it is deprecated.

solid answer

~40 s

HCL has two layers: an expression language and a string-template language. In an argument position you are already in expression context, so `name = var.bucket_name` is the idiomatic form. `${ ... }` is a *template* construct — it only exists to splice an expression into surrounding literal text, like `name = "${var.env}-logs"` or a heredoc. Wrapping the whole value, `name = "${var.bucket_name}"`, converts the value to a string and back for nothing, and Terraform emits an "interpolation-only expression" deprecation warning, a leftover habit from 0.11 where interpolation was the only way to reference anything. Templates also support directives — `%{ if ... }` / `%{ for ... }` — and you escape a literal by doubling the sigil: `$${` and `%%{`. And note that `${ }` outside quotes is not valid syntax at all.

code

hcl · 16 lines
hcl
variable "env" { type = string }
variable "app" { type = string }

locals {
  # idiomatic: expression context, no interpolation needed
  good_name = var.app

  # legacy: interpolation-only expression, warned as deprecated
  noisy_name = "${var.app}"

  # required: expression spliced into literal text
  bucket = "${var.env}-${var.app}-logs"

  # escaped: renders the literal text ${HOME}
  script = "echo $${HOME}"
}

go deeper

for a junior

Be able to write both forms and say which is idiomatic: bare var.name in an argument, and quotes with ${ } only when text surrounds the value. Know that the fully wrapped form triggers a deprecation warning.

for a middle

Explain the two grammars — expression versus string template — and why wrapping a non-string value in a template round-trips it through string conversion and can lose its type. Know $${ escaping and the indented heredoc.

for a senior

Show judgment about generated text: use jsonencode/yamlencode instead of hand-built documents, keep whitespace-trimming markers in mind for shell and YAML output, and recognise the escaping bug behind a mysterious rendering error in user_data.

for a principal

Own the convention across a repo: where free-form templating is allowed at all, when a rendered file should become a real artifact instead, and how to keep interpolation out of places that force literals, such as backend configuration, without hand-editing per environment.

## Two languages in one file Every `.tf` file mixes two grammars. The outer one is the **expression language**: the right-hand side of `argument = ...` is parsed as an expression, so a reference, a function call, an arithmetic operator or a literal all work there directly. The inner one is the **string template language**, which is what lives *inside* a quoted string or a heredoc. `${ ... }` belongs exclusively to the second grammar. It means "stop treating this as literal text, evaluate the expression inside, convert the result to a string, and splice it in". That is the whole rule. If you are already in expression context, you do not need it: ```hcl bucket = var.bucket_name # idiomatic instance_type = var.big ? "m5.large" : "t3.micro" tags = merge(var.common_tags, { Name = var.name }) ``` If you need the value embedded in surrounding text, you do: ```hcl bucket = "${var.env}-${var.app}-logs" ``` ## Why the whole-value form is deprecated In Terraform 0.11 and earlier the language had no first-class expressions — every non-constant value had to be smuggled through a string, so `"${var.name}"` and even `"${count.index}"` were the only way to write anything. Terraform 0.12 rewrote the language on HCL2 and made expressions first-class. The old form still parses, but the config loader emits a warning along the lines of "Interpolation-only expressions are deprecated", because it is pure noise: the value is converted to a string and then converted back to whatever type the argument wants. That round trip can actually change behaviour when the value is not a string — a number, a bool, a list or an object forced through a string template loses its type, and for complex types Terraform will refuse or produce something unusable. A related trap: `${ }` **outside** quotes is a syntax error. `name = ${var.x}` does not parse. The sigil only has meaning inside a template. ## Directives and escaping Templates support more than substitution. Two directives exist, both written with `%{ }`: ```hcl user_data = <<-EOT #!/bin/bash %{ for pkg in var.packages ~} yum install -y ${pkg} %{ endfor ~} %{ if var.debug }set -x%{ endif } EOT ``` The `~` marker trims the whitespace and newline adjacent to it, which is how you keep generated files from filling with blank lines. `<<-EOT` is the *indented* heredoc: the closing marker's indentation is stripped from every line, so the block can sit at the code's indentation level without pushing spaces into the rendered output. When you want a literal dollar-brace or percent-brace in the output — shell parameter expansion, a Jenkins or Prometheus template, a `${}` in a JSON policy consumed by something else — double the sigil: ```hcl script = "echo $${HOME}" # renders: echo ${HOME} fmt = "%%{ literal }" # renders: %{ literal } ``` Forgetting this is one of the most common causes of a confusing "Invalid template interpolation value" or "Unknown variable" error in a shell script rendered by Terraform. ## Where interpolation is not allowed Not every part of the config is an expression context. The `terraform` block — including `backend` and `required_providers` — is evaluated before variables exist, so `${var.bucket}` there is rejected; backend settings are supplied with partial configuration on the command line or a backend config file instead. Similarly you cannot interpolate a resource *type* or a block label: those are part of the language's structure, not values. Candidates who believe "anything can be interpolated" walk straight into this in an interview. ## What to actually say Write the bare expression in argument position; reach for `"..."` and `${ }` only when there is literal text around the value. Prefer `jsonencode()` and `yamlencode()` over hand-built JSON or YAML strings — they handle quoting and escaping for you and cannot produce invalid output. Keep templates for genuinely free-form text such as scripts and config files, and reach for `templatefile()` once the text outgrows a heredoc.

  • What does the minus in a heredoc marker like <<-EOT change?
    It makes the heredoc *indented*: Terraform strips the closing marker's leading whitespace from every line of the body. That lets the block sit at the surrounding code's indentation without injecting those spaces into the rendered file — important for anything whitespace-sensitive such as YAML or a shell script.
  • Why can't you use a variable inside the backend block?
    The `terraform` block, backend included, is read before Terraform evaluates variables, locals or providers — it has to know where state lives before it can do anything else. So it accepts only literal values. The usual answer is partial configuration: omit the settings and pass them with `-backend-config` or a backend config file at `terraform init` time.
  • You need to emit a JSON document from Terraform. Heredoc template or jsonencode?
    `jsonencode()`. It takes a Terraform value and guarantees syntactically valid JSON with correct quoting and escaping, and it keeps types honest — numbers stay numbers, bools stay bools. A heredoc forces you to hand-manage commas, quotes and escaping, and a single interpolated value containing a quote silently produces a broken document.

saying these in an interview costs you the question

  • Says every variable reference needs ${ } around it
  • Writes ${var.x} outside quotes and expects it to parse
  • Thinks %{ } and ${ } are interchangeable
  • Claims you can interpolate variables into the backend block
  • Builds JSON by hand in a heredoc instead of jsonencode

context