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 everyterraform planandterraform 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
.tffile 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.