How to Fix 'Error: invalid JSON' in Terraform Provisioner Blocks
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 10 mins
TL;DR
- What broke: A
provisionerblock in your Terraform resource is passing a JSON payload to a shell command or API call. The JSON is malformed — typically due to unescaped double-quotes inside HCL string interpolation, a trailing comma, or a raw newline inside a single-line string. - How to fix it: Escape inner quotes with
\", usejsonencode()to construct JSON programmatically, or use aheredocwith properEOTfencing. Never hand-craft JSON inside an HCL string literal. - Fast path: Use our Client-Side Sandbox above to auto-refactor this — paste your failing provisioner block and get corrected HCL instantly without sending your config to any external server.
▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]
The Incident (What Does the Error Mean?)
You ran terraform apply and it died with:
Error: invalid JSON
on main.tf line 34, in resource "aws_instance" "app":
34: inline = ["curl -X POST https://internal-api/config -d '{\"key\": \"value\"}'"]
Cannot parse JSON: invalid character '\'' looking for beginning of valueOr a variant:
Error: invalid JSON
json: cannot unmarshal string into Go value of type map[string]interface {}Immediate consequence: terraform apply halts. If this provisioner was on a resource that was already created (e.g., the EC2 instance is UP), Terraform marks it as tainted. Next apply will destroy and recreate that resource. In production, that means downtime.
▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]
The Attack Vector / Blast Radius
This isn't just a cosmetic syntax error. The blast radius is real:
Tainted resource cascade. Terraform taints the resource on provisioner failure by default. A tainted
aws_db_instanceoraws_elasticache_clustergetting destroyed on next apply is a data-loss event, not a deployment inconvenience.Partial state corruption. If the provisioner fires mid-
applyafter dependent resources are already created (security groups, IAM roles, ENIs), you now have orphaned cloud resources that Terraform's state no longer tracks cleanly.Silent misconfiguration. The most dangerous variant: the JSON is structurally valid but semantically wrong because HCL interpolation silently inserted a null or wrong value. The API accepts it, returns 200, and your app runs with a broken config for hours before anyone notices.
CI/CD pipeline blast. In a GitOps pipeline, this error fails the apply stage, but the plan stage likely passed — meaning your PR checks gave a false green. Every engineer on the team assumes the config is valid.
How to Fix It (The Solution)
Root Cause Taxonomy
| Cause | Symptom |
|---|---|
Unescaped " inside HCL string |
invalid character error |
| Trailing comma in last JSON key | invalid character ',' |
| Raw newline in single-line string | invalid character at newline |
${var.x} resolving to null or object |
Type mismatch at parse time |
Single quotes used instead of \" in shell |
Shell strips the JSON before the API sees it |
Basic Fix — Escape Quotes Correctly
resource "aws_instance" "app" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.medium"
provisioner "remote-exec" {
inline = [
- "curl -X POST https://internal-api/config -d '{"key": "value"}'",
+ "curl -X POST https://internal-api/config -d '{\"key\": \"value\"}'",
]
}
}Why: HCL uses " to delimit strings. Any " inside the string must be escaped as \". Single quotes ' are not JSON — they work in bash to wrap the payload but break if the JSON itself contains apostrophes or if you're on a system where the remote shell interprets them differently.
Enterprise Best Practice — Use `jsonencode()` and Never Hand-Craft JSON in Strings
This is the only production-safe pattern. jsonencode() is a Terraform built-in that guarantees valid JSON output regardless of what your variables contain.
locals {
- config_payload = "{\"environment\": \"${var.env}\", \"region\": \"${var.region}\"}"
+ config_payload = jsonencode({
+ environment = var.env
+ region = var.region
+ })
}
resource "aws_instance" "app" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.medium"
provisioner "remote-exec" {
inline = [
- "curl -X POST https://internal-api/config -d '${local.config_payload}'",
+ "curl -X POST https://internal-api/config -H 'Content-Type: application/json' -d '${local.config_payload}'",
]
}
}Why this is the only correct pattern at scale:
jsonencode()handles nested objects, lists, null values, and special characters automatically.- If
var.envcontains a"or a backslash (e.g., a Windows path), hand-crafted interpolation breaks silently.jsonencode()escapes it correctly. - Reviewers can read the HCL object natively — no mental JSON-in-string parsing required.
Heredoc Pattern for Multi-Line Payloads
If your JSON is large, use a templatefile() instead of inline strings entirely:
- provisioner "local-exec" {
- command = "curl -X POST https://api/endpoint -d '{\"key1\": \"val1\", \"key2\": \"val2\", \"key3\": \"val3\"}'"
- }
+ provisioner "local-exec" {
+ command = "curl -X POST https://api/endpoint -d @/tmp/payload.json"
+ }
+
+ resource "local_file" "api_payload" {
+ content = jsonencode({
+ key1 = var.val1
+ key2 = var.val2
+ key3 = var.val3
+ })
+ filename = "/tmp/payload.json"
+ }💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.
(For CI/CD pipeline automation, install via
npm i -g stackengine).
▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]
Prevention in CI/CD
Fix this class of error permanently in your pipeline. One-time setup, zero recurring incidents.
1. `terraform validate` in Pre-Commit
# .pre-commit-config.yaml
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.83.0
hooks:
- id: terraform_validate
- id: terraform_fmtterraform validate catches most interpolation-induced JSON errors before they hit your pipeline.
2. TFLint with Deep Variable Checking
tflint --enable-plugin=aws
tflint --var-file=terraform.tfvarsTFLint resolves variables during linting and catches cases where a null variable would produce invalid JSON at apply time.
3. Checkov Policy — Ban Raw JSON Strings in Provisioners
# checkov custom check: no raw JSON strings in provisioner inline commands
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
class NoRawJsonInProvisioner(BaseResourceCheck):
def __init__(self):
name = "Ensure provisioner inline commands do not contain raw JSON strings"
id = "CKV_CUSTOM_TF_001"
supported_resources = ["*"]
super().__init__(name=name, id=id, categories=[], supported_resources=supported_resources)
def scan_resource_conf(self, conf):
provisioners = conf.get("provisioner", [])
for p in provisioners:
inline = p.get("remote-exec", [{}])[0].get("inline", [])
for cmd in inline:
if isinstance(cmd, list):
cmd = cmd[0]
if '{"' in str(cmd) or "{'" in str(cmd):
return CheckResult.FAILED
return CheckResult.PASSED4. OPA/Conftest Policy — Enforce `jsonencode()` Usage
# policy/provisioner_json.rego
package terraform.provisioner
deny[msg] {
resource := input.resource_changes[_]
provisioner := resource.change.after.provisioner[_]
inline_cmd := provisioner["remote-exec"].inline[_]
contains(inline_cmd, "-d '{") # raw JSON in curl -d flag
msg := sprintf(
"Resource '%s' uses raw JSON in provisioner. Use jsonencode() and a local_file resource instead.",
[resource.address]
)
}Run in CI:
terraform show -json plan.tfplan | conftest test -p policy/ -5. Pipeline Gate Summary
# .github/workflows/terraform.yml (relevant steps)
- name: Terraform Format Check
run: terraform fmt -check -recursive
- name: Terraform Validate
run: terraform validate
- name: TFLint
run: tflint --var-file=terraform.tfvars
- name: Checkov Scan
run: checkov -d . --framework terraform --check CKV_CUSTOM_TF_001
- name: Conftest OPA Policy
run: terraform show -json tfplan.binary | conftest test -p policy/ -The rule: if jsonencode() isn't used for any dynamic JSON construction in a provisioner, the pipeline fails. No exceptions.
Official Grounding & Normative Specifications
The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:
Core concepts governing declarative infrastructure lifecycle, state lock contention, and provider graph synthesis.
Federal security guidelines for secret exfiltration prevention, automated integrity checks, and peer sign-offs.
Core memory subsystem controls, pressure stall information (PSI), and kernel OOM invocation mechanics.
Industry-standard specification for container execution boundaries and POSIX namespace isolation.