Initializing Enclave...

How to Fix Invalid Template String Interpolation with ${var} in Terraform 0.12

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5 mins

TL;DR

  • What broke: Terraform 0.12 introduced a native type system and first-class expressions. Legacy "${var.foo}" wrapping in non-string contexts and ${...} inside templatefile() heredocs now throws a parser error or produces unexpected type coercion.
  • How to fix it: Remove redundant "${...}" wrappers where a direct reference suffices; replace inline template_file data sources with templatefile() built-in function.
  • Use the Client-Side Sandbox above to auto-refactor your HCL — paste your failing config and get corrected output instantly.

The Incident (What Does the Error Mean?)

Raw error from terraform plan:

Error: Invalid template interpolation value

  on main.tf line 12, in resource "aws_instance" "web":
  12:   ami = "${var.ami_id}"

The template interpolation on line 12 is not valid. A single template
directive with no surrounding text is redundant — use the expression
directly, e.g. var.ami_id instead of "${var.ami_id}".

Or the harder-to-debug variant when mixing types:

Error: Incorrect attribute value type

Expected a string, got a number wrapped in an interpolation.

Immediate consequence: terraform plan hard-fails. No diff is produced. CI pipeline blocks. If this is gating a deployment, your infra change is dead in the water until this is resolved.


The Attack Vector / Blast Radius

This is not just a cosmetic lint warning. The blast radius is operational:

  1. Type coercion silently breaks logic. In 0.11, everything was a string. "${var.instance_count}" coerced a number to string transparently. In 0.12, that same pattern forces a string type where a number is expected — causing downstream count or for_each expressions to throw type mismatch errors that are non-obvious to trace.

  2. Module consumers break on upgrade. If a shared module uses legacy interpolation and a consuming team upgrades their Terraform CLI to 0.12+, every caller of that module fails simultaneously. One unfixed module = org-wide blast.

  3. template_file data source is deprecated and broken. Code still using data "template_file" with ${var.x} inside the template argument will silently produce wrong output or fail outright when the variable is a complex type (list, map).


How to Fix It (The Solution)

Basic Fix — Remove Redundant Interpolation Wrappers

When the entire string value is a single variable or expression reference, drop the quotes and the ${} entirely.

# main.tf
 resource "aws_instance" "web" {
-  ami           = "${var.ami_id}"
-  instance_type = "${var.instance_type}"
-  count         = "${var.instance_count}"
+  ami           = var.ami_id
+  instance_type = var.instance_type
+  count         = var.instance_count
 }

Interpolation syntax is still valid and required when embedding a variable inside a larger string:

# Still correct — interpolation inside a mixed string
-  tags = { Name = var.env }
+  tags = { Name = "${var.env}-web-server" }

Enterprise Best Practice — Migrate template_file to templatefile()

The data "template_file" pattern is the most common source of 0.12 interpolation failures in large codebases. Replace it entirely.

# BEFORE — legacy 0.11 pattern
-data "template_file" "user_data" {
-  template = "${file("${path.module}/templates/userdata.sh.tpl")}"
-  vars = {
-    environment = "${var.environment}"
-    db_host     = "${var.db_host}"
-  }
-}
-
-resource "aws_instance" "app" {
-  user_data = "${data.template_file.user_data.rendered}"
-}

# AFTER — native 0.12+ pattern
+resource "aws_instance" "app" {
+  user_data = templatefile("${path.module}/templates/userdata.sh.tpl", {
+    environment = var.environment
+    db_host     = var.db_host
+  })
+}

Inside the .tpl file itself, variable references use ${var_name} (no var. prefix — these are template-local variables, not Terraform variables):

# templates/userdata.sh.tpl
-#!/bin/bash
-echo "Env: ${var.environment}"
-echo "DB: ${var.db_host}"

+#!/bin/bash
+echo "Env: ${environment}"
+echo "DB: ${db_host}"

💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.


Prevention in CI/CD

Don't let this reach code review. Enforce it at commit time.

1. terraform validate in every PR pipeline

# .github/workflows/terraform.yml
- name: Terraform Validate
  run: |
    terraform init -backend=false
    terraform validate

This catches type mismatch interpolation errors before plan.

2. tflint with the AWS ruleset

tflint --enable-rule=terraform_deprecated_interpolation

The terraform_deprecated_interpolation rule flags every "${single_expr}" wrapper. Zero config required.

# .tflint.hcl
rule "terraform_deprecated_interpolation" {
  enabled = true
}

3. Checkov for policy-as-code gates

checkov -d . --framework terraform

Checkov's static analysis parses the AST and will surface template interpolation anti-patterns alongside security misconfigs in one pass.

4. terraform 0.12upgrade CLI command (one-time migration)

If you're migrating an existing 0.11 codebase, run the official upgrade tool first — it auto-rewrites the most common interpolation patterns:

terraform 0.12upgrade

Review the diff it produces. It handles ~80% of cases automatically. The remaining 20% — especially template_filetemplatefile() migrations — require manual intervention as shown above.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →