linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
Intro
Lessons
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Knowledge base
  • Cheat sheet
  • Capstone
  • Interview prep
home/terraform/kb/Terraform basics/tf-interpolation

kb/core ── Terraform basics ── beginner

${...}: substituting values into strings

${expression} inside a string substitutes the value of that expression. ${var.env}, ${aws_s3_bucket.demo.id}. When the expression is the entire value of an argument (no surrounding string), modern HCL lets you write it without ${...}.

view as markdown

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.

hcl
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 ${...}:

hcl
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:

hcl
count = "${var.instance_count}"           # old style
ami   = "${data.aws_ami.ubuntu.id}"        # old style

In modern HCL, ${...} is needed only when the value is part of a larger string:

hcl
# part of a string → ${...} is required
bucket = "myapp-${var.env}-logs"
# whole string = one expression → ${...} is NOT needed and not recommended
count  = var.instance_count
ami    = data.aws_ami.ubuntu.id
region = "us-east-1"   # a literal: quotes are needed, ${...} is not

terraform 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:

hcl
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:

hcl
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:

hcl
# ${{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):

hcl
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. Prefer count = 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 change var.env, Terraform sees that bucket must now be different and proposes a change.

  • A list/map variable in interpolation is an error. bucket = "myapp-${var.tags}" where tags is 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.

§ команды

bash
terraform fmt -recursive

Removes the deprecated ${var.x} wrappers around single expressions automatically.

bash
terraform console

You can type a string with interpolation and see the result. Handy for debugging complex templates.

§ см. также

  • hcl-syntaxHCL: the language you write Terraform inHCL (HashiCorp Configuration Language) is the language you use to describe the desired state of your infrastructure. It looks like JSON, but it is easier to read: you can write comments, variables, and loops.
  • tf-referencesReferences in HCL: how to read aws_s3_bucket.demo.bucketAny value in HCL is reachable through an address: var.x, local.x, aws_s3_bucket.demo.arn, module.net.vpc_id, data.aws_region.current.name. Knowing this syntax is half of your productivity in Terraform.
  • tf-conditional-expressionConditionals and null safety: ?:, try, can, coalesceThe ternary operator a ? b : c is a plain if/else. try(expr, fallback) evaluates an expression or substitutes a backup. can(expr) returns true/false. coalesce(...) returns the first non-null value. They all keep your config from crashing.
Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies