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}"whensubnet_idsislist(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
nullvalue 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
forexpression 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)ortype = 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. Largeuser_dataor config files belong in a separate template file where you pass complex values as named variables and convert them withjsonencodeorjoininside the template. - Default your maps and lists. A variable with no default can resolve to
null, which also fails interpolation. Providedefault = {}ordefault = []where sensible. - Test in
terraform console. Before wiring a new expression into config, confirm its type and string form interactively. - Run
terraform validatein 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.
Related Errors
- Invalid function argument — appears when you pass the wrong type into
join,lookup, orjsonencodewhile fixing this error; for example callingjoinon 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.
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.