Initializing Enclave...

How to Fix 'Invalid Character { in Template String' in Terraform HCL

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


TL;DR

  • What broke: A raw { character inside a Terraform template string is illegal — HCL's template engine tries to parse it as an interpolation sequence (${...}) or directive (%{...}) and immediately aborts with a parse error, blocking every terraform plan and terraform apply.
  • How to fix it: Escape every literal brace that is NOT part of an interpolation: use $${ to render a literal ${, or %%{ to render a literal %{. If the brace IS part of interpolation, ensure the full ${expression} syntax is well-formed.
  • Use our Client-Side Sandbox below to auto-refactor this — paste your failing .tf file and get corrected HCL output without leaking your state or secrets.

The Incident — What Does This Error Mean?

Raw error output from terraform plan:

Error: Invalid character '{' in template string

  on main.tf line 14, in resource "aws_instance" "web":
  14:   user_data = "#!/bin/bash\necho {HOSTNAME} > /etc/hostname"

The character '{' is not valid at this position in a template string.
Use ${ to begin a template interpolation sequence.

Terraform's HCL2 template engine is a strict state machine. The moment it encounters a bare { that is not preceded by $ or %, it throws a hard parse error and refuses to produce a plan. This is not a warning. Your entire pipeline is dead until this is resolved. No resources will be created, modified, or destroyed — the binary exits non-zero before evaluation even begins.


The Attack Vector / Blast Radius

This error is deceptively common in three high-blast-radius scenarios:

1. Bash / cloud-init user_data scripts embedded as inline strings. Shell scripts are full of {} — brace expansion, ${VAR}, for f in {1..10}. Every single one is a landmine inside a Terraform string literal.

2. JSON policy documents written as raw HCL strings instead of jsonencode(). IAM policies, S3 bucket policies, and KMS key policies are JSON objects — they are entirely curly braces. Embedding them as raw strings without escaping is a guaranteed parse failure.

3. Heredoc templates with shell or Jinja2 content. Ansible playbooks, Helm values, or Dockerfiles injected via templatefile() that contain {{ (Jinja2) or ${ (shell) will conflict with Terraform's own interpolation syntax.

Cascading failure risk: In a CI/CD pipeline, this error causes terraform plan to exit code 1, which blocks the entire PR gate. If your pipeline lacks proper exit-code handling, a downstream terraform apply -auto-approve step may still execute against a stale, pre-error plan artifact — a silent infrastructure drift scenario.


How to Fix It

Basic Fix — Escape Literal Braces

The rule is simple:

  • A literal ${ that should NOT be interpolated → escape as $${
  • A literal %{ that should NOT be a directive → escape as %%{
  • A lone { with no preceding $ or % → it only needs escaping if HCL misreads it; in practice, wrap the whole value in a heredoc or use $${ for the ${ case.
# BROKEN — bare {HOSTNAME} triggers parse error
- user_data = "#!/bin/bash\necho {HOSTNAME} > /etc/hostname"

# FIXED — either use proper shell variable syntax escaped for HCL,
# or use a heredoc to avoid double-escaping hell
+ user_data = <<-EOF
+   #!/bin/bash
+   echo $${HOSTNAME} > /etc/hostname
+ EOF
# BROKEN — raw JSON IAM policy as a string literal
- assume_role_policy = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\"}]}"

# FIXED — use jsonencode(); no escaping required, HCL handles serialization
+ assume_role_policy = jsonencode({
+   Version = "2012-10-17"
+   Statement = [{
+     Effect = "Allow"
+     Principal = { Service = "ec2.amazonaws.com" }
+     Action = "sts:AssumeRole"
+   }]
+ })

Enterprise Best Practice — templatefile() + External Script Files

For any user_data, cloud-init, or policy content longer than two lines, never embed it as an inline string. Store it as a file and use templatefile().

# BROKEN — inline heredoc with shell brace expansion
- user_data = <<-EOF
-   #!/bin/bash
-   for port in {8080..8090}; do
-     firewall-cmd --add-port=$${port}/tcp
-   done
- EOF

# FIXED — external file, Terraform variables injected cleanly
+ user_data = templatefile("${path.module}/scripts/init.sh.tpl", {
+   start_port = 8080
+   end_port   = 8090
+ })

scripts/init.sh.tpl:

#!/bin/bash
for port in $(seq ${start_port} ${end_port}); do
  firewall-cmd --add-port=$port/tcp
done

With templatefile(), Terraform only interpolates ${start_port} and ${end_port} — the rest of the shell syntax is passed through verbatim. No escaping required in the template file itself.


💡 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

1. terraform validate as a pre-commit hook. This catches parse errors before they ever reach your pipeline.

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.83.5
    hooks:
      - id: terraform_validate
      - id: terraform_fmt

2. TFLint with the AWS ruleset. TFLint catches semantic issues that validate misses, including malformed user_data patterns.

tflint --init
tflint --recursive

3. Checkov static analysis in your CI pipeline. Checkov's CKV_TF_* rules flag raw JSON policy strings and insecure user_data patterns.

# GitHub Actions step
- name: Run Checkov
  uses: bridgecrewio/checkov-action@master
  with:
    directory: ./terraform
    framework: terraform
    soft_fail: false

4. OPA / Conftest policy to ban inline JSON strings.

# policy/no_inline_json_policy.rego
package terraform

deny[msg] {
  resource := input.resource.aws_iam_role[name]
  policy   := resource.assume_role_policy
  startswith(policy, "{")
  msg := sprintf(
    "Resource aws_iam_role.%v: use jsonencode() instead of a raw JSON string for assume_role_policy",
    [name]
  )
}
terraform show -json tfplan.binary | conftest test -p policy/ -

5. IDE enforcement. Configure the HashiCorp Terraform VS Code extension — it surfaces template string parse errors inline, before you ever run a CLI command.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →