Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Terraform By James Joyner IV · · 9 min read

Terraform Error Guide: 'Invalid template interpolation value' (interpolating non-string types into strings)

Fix Terraform's 'Invalid template interpolation value' by converting lists, maps, and objects to strings with jsonencode, join, lookup, or tostring.

  • #terraform
  • #troubleshooting
  • #errors
  • #expressions

Exact Error Message

When Terraform refuses to embed a complex value inside a string template, you will see an error shaped like this:

Error: Invalid template interpolation value

  on main.tf line 20, in resource "aws_instance" "web":
  20:   user_data = "subnets: ${var.subnet_ids}"
    |----------------
    | var.subnet_ids is list of string with 3 elements

Cannot include the given value in a string template: string required.

The diagnostic line under the | is the most useful part. It tells you the type of the offending value, for example list of string with 3 elements, map of string, object, or null. That type description is the whole problem: a string template can only hold a string.

What the Error Means

A string template is any string containing a ${ ... } interpolation, such as "subnets: ${var.subnet_ids}". When Terraform renders the template, it evaluates the expression inside the braces and concatenates the result into the surrounding text. To do that, the result must be representable as a string.

Primitive types convert cleanly: a string stays a string, a number becomes its decimal form, and a bool becomes true or false. But Terraform deliberately refuses to guess how a list, map, object, set, or null should look as text. There is no single obvious string form for ["a", "b", "c"] — it could be comma-separated, JSON, newline-delimited, or something else entirely. Rather than pick for you, Terraform raises Invalid template interpolation value and asks you to convert the value explicitly.

The fix is always to turn the complex value into a string before it enters the template: pick a single element, join a list, look up a map key, or serialize the whole structure with jsonencode().

Common Causes

  • Interpolating a whole list where a single value is expected, e.g. "${var.subnet_ids}" when subnet_ids is list(string).
  • Interpolating a map or object directly, e.g. "${var.tags}" or "${aws_instance.web}", instead of one of its attributes.
  • Referencing a resource block instead of an attribute, e.g. "${aws_s3_bucket.data}" rather than "${aws_s3_bucket.data.id}".
  • A null value flowing into a template when a variable has no default and no value was supplied.
  • A splat expression like aws_instance.web[*].id, which produces a list, dropped straight into a string.
  • A for expression that builds a list or map being interpolated without aggregation.
  • A module output that returns a complex type used where a scalar is expected.

How to Reproduce the Error

Save the following to main.tf to trigger the error deterministically:

variable "subnet_ids" {
  type    = list(string)
  default = ["subnet-aaa", "subnet-bbb", "subnet-ccc"]
}

variable "tags" {
  type = map(string)
  default = {
    Environment = "prod"
    Team        = "platform"
  }
}

resource "aws_instance" "web" {
  ami           = "ami-0123456789abcdef0"
  instance_type = "t3.micro"

  # Both of these lines are invalid: a list and a map in a string template
  user_data = "subnets: ${var.subnet_ids}"
  # user_data = "tags: ${var.tags}"
}

Running terraform validate immediately reports Invalid template interpolation value, pointing at the user_data line and naming the type as list of string with 3 elements.

Diagnostic Commands

Everything here is read-only. Nothing changes your infrastructure or state.

Validate the configuration to surface the exact line and type:

terraform validate

Use terraform console to inspect the type of any value before you interpolate it. This is the fastest way to confirm what you are dealing with:

terraform console
> type(var.subnet_ids)
list(string)

> type(var.tags)
map(string)

> type(var.tags["Environment"])
string

> length(var.subnet_ids)
3

The type() function tells you whether you have a primitive (safe to interpolate) or a complex type (must be converted). You can also preview the converted forms right in the console:

> join(", ", var.subnet_ids)
"subnet-aaa, subnet-bbb, subnet-ccc"

> jsonencode(var.tags)
"{\"Environment\":\"prod\",\"Team\":\"platform\"}"

> var.subnet_ids[0]
"subnet-aaa"

Grep the codebase to find every interpolation so you can audit which ones touch complex values:

grep -rn '\${' --include='*.tf' .

To inspect a value computed during planning, add a temporary output block and run a plan — this evaluates and prints the expression without applying anything:

output "debug_subnets_type" {
  value = type(var.subnet_ids)
}
terraform plan

Step-by-Step Resolution

1. Read the type from the error. The |---- annotation names the offending type. list, set, map, object, or null all need conversion.

2. Decide what the string should actually contain. Do you want one element, a delimited list, or a serialized blob? The answer drives which function you use.

3. Pick a single element instead of the whole collection. If you only needed one value, index into it:

# Instead of "${var.subnet_ids}"
subnet_id = var.subnet_ids[0]

# Or, more safely, with element() which wraps the index
subnet_id = element(var.subnet_ids, 0)

4. Join a list into a delimited string. When you genuinely want all the values inline:

user_data = "subnets: ${join(", ", var.subnet_ids)}"

5. Reference a single attribute, not the whole object or resource. This is the most common real-world cause:

# Wrong: interpolates the entire resource object
bucket_ref = "arn: ${aws_s3_bucket.data}"

# Right: reference one string attribute
bucket_ref = "arn: ${aws_s3_bucket.data.arn}"

6. Look up a map by key. Maps need a key to yield a string:

environment = "env: ${var.tags["Environment"]}"

# Or with a safe default when the key may be absent
environment = "env: ${lookup(var.tags, "Environment", "unknown")}"

7. Serialize a whole structure with jsonencode(). When you truly need the entire map or object as text — common for user_data, tags rendered to JSON, or templated config:

user_data = "config: ${jsonencode(var.tags)}"

8. Convert primitives explicitly with tostring(). Rarely needed for numbers or bools, but useful when a value is typed loosely or may be null. Pair it with a fallback for null safety:

label = "count: ${tostring(coalesce(var.maybe_number, 0))}"

9. Re-validate. Run terraform validate again. The error should be gone, and terraform plan will show the rendered string.

Prevention and Best Practices

  • Type your variables. Declaring type = list(string) or type = map(string) makes mismatches obvious at the call site rather than deep inside a template.
  • Reference attributes, not blocks. Get into the habit of always appending an attribute (.id, .arn, .name) when interpolating a resource.
  • Reach for templatefile() for anything multi-line. Large user_data or config files belong in a separate template file where you pass complex values as named variables and convert them with jsonencode or join inside the template.
  • Default your maps and lists. A variable with no default can resolve to null, which also fails interpolation. Provide default = {} or default = [] where sensible.
  • Test in terraform console. Before wiring a new expression into config, confirm its type and string form interactively.
  • Run terraform validate in CI. This catches interpolation type errors before they reach a plan or a teammate. For teams that want automated triage of recurring Terraform failures, the incident-response workflow can route these errors to the right owner with suggested fixes.
  • Invalid function argument — appears when you pass the wrong type into join, lookup, or jsonencode while fixing this error; for example calling join on a map instead of a list.
  • Invalid expression — raised when the syntax inside ${ ... } is malformed, which can surface as you rewrite a broken interpolation.
  • Unsuitable value type — a closely related message that fires when a complex value is assigned to an argument expecting a string, even outside a template.

For more Terraform troubleshooting guides, browse the Terraform category.

Frequently Asked Questions

Why does Terraform allow numbers and bools in templates but not lists?

Numbers and booleans have a single, unambiguous string representation (42, true). A list or map has many possible textual forms, so Terraform refuses to choose one for you and asks you to convert explicitly with join, jsonencode, or an index.

What is the difference between join and jsonencode for a list?

join(", ", list) produces a flat delimited string like a, b, c, which is human-readable but loses structure. jsonencode(list) produces valid JSON like ["a","b","c"], which preserves structure and is the right choice when something downstream needs to parse it.

My value is null and I get this error. How do I fix it?

Interpolating null fails the same way. Wrap the value with coalesce(var.thing, "default") or tostring(coalesce(...)) so a concrete string is always produced, and give the underlying variable a sensible default.

Can I interpolate a whole object, like a resource, into a string?

No. You must reference one of its attributes, such as ${aws_s3_bucket.data.arn}. If you genuinely need the entire object as text, serialize it with ${jsonencode(aws_s3_bucket.data)}, though this is uncommon and usually a sign you want a specific attribute instead.

Should I use templatefile() instead of inline interpolation?

For anything beyond a short single-line string — especially user_data, cloud-init, or config files — yes. templatefile() keeps the template readable, lets you pass complex values as named variables, and makes conversions like jsonencode explicit and easy to review.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.