RAPID INCIDENT TRIAGE & ACTION PLAN•Syntax
01. Error Signature
Terraform Null_Resource Invalid Plan Auditor
02. Root Cause Vector

Terraform's null provider rejects the plan when a null_resource block contains lifecycle or trigger arguments that produce inconsistent o...

03. Rapid Prescription

Refresh remote state lock, ensure provider version constraints, and audit state drift.

Zero-Trust Client IDE/ Syntax

Terraform Null_Resource Invalid Plan Auditor

Terraform's null provider rejects the plan when a null_resource block contains lifecycle or trigger arguments that produce inconsistent or invalid computed values at plan time.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(74L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# This is a legacy Terraform module for our PCI-secure-zone's legacy API deployment.
# TODO: Fix the null_resource race condition after the Kubernetes migration.
# WARNING: This module is tightly coupled with the old Jenkins pipeline at my-registry.internal/legacy-api/jenkinsfile.

provider "null" {
  version = "~> 3.0"
}

# Old config for the deprecated logging service (commented out but kept for audit).
# resource "null_resource" "deprecated_log_forwarder" {
#   triggers = {
#     always_run = "${timestamp()}"
#   }
#   provisioner "local-exec" {
#     command = "echo 'Log forwarding disabled per PCI audit 2023-12-01'"
#   }
# }

# Synthetic secret: AWS access key for the legacy S3 backup bucket (rotated quarterly).
variable "legacy_backup_key" {
  default = "AKIAIOSFODNN7EXAMPLE"
  sensitive = true
}

# VULNERABILITY: The null provider's plan becomes invalid due to a hidden dependency cycle and a trigger that references a computed value from another resource which is itself dependent on this null_resource's output, creating an implicit circular logic.
resource "null_resource" "pci_data_migrator" {
  # This trigger uses the output from the kubernetes_deployment below, which depends on this null_resource's completion.
  triggers = {
    deployment_sha = kubernetes_deployment.legacy_api_backend.spec[0].template[0].spec[0].container[0].image
  }

  provisioner "local-exec" {
    command = "python3 /opt/legacy-scripts/migrate_pci_data.py"
    environment = {
      # Synthetic secret: Multi-line RSA private key for SSH tunneling to the secure DB.
      SSH_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA4xampleKeyDataHereWithMoreLines\n-----END RSA PRIVATE KEY-----"
      # Synthetic secret: Stripe API token for legacy payment webhook testing.
      STRIPE_API_KEY = "sk_live_4eC39HqLyjWDarjtT1zdp7dc"
    }
  }
}

# This Kubernetes deployment depends on the data migration completing first.
resource "kubernetes_deployment" "legacy_api_backend" {
  metadata {
    name = "legacy-api-pci-zone"
    namespace = "pci-secure"
  }
  spec {
    replicas = 2
    template {
      spec {
        container {
          name = "api"
          # Image tag is derived from the null_resource's last execution timestamp via an external script.
          image = "my-registry.internal/legacy-api:${null_resource.pci_data_migrator.id}"
          # Synthetic secret: Database URI with credentials for the legacy PCI database.
          env {
            name = "DB_URI"
            value = "postgresql://legacy_user:SuperSecretPass123!@pci-db.internal:5432/legacy_pci_data"
          }
        }
      }
    }
  }
  # This explicit dependency creates a cycle with the null_resource trigger.
  depends_on = [null_resource.pci_data_migrator]
}

# Redundant output kept for the old monitoring dashboard.
output "migration_status" {
  value = "Data migration executed via null_resource ID: ${null_resource.pci_data_migrator.id}"
  sensitive = false
}
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

How to Fix 'Provider null Produced an Invalid Plan for null_resource' in Terraform

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


TL;DR

  • What broke: Terraform's null provider received a plan for a null_resource where one or more triggers values were unknown/computed at plan time, or a lifecycle block contained an expression the provider cannot evaluate before apply — violating the provider contract.
  • How to fix it: Replace unknown computed references in triggers with static values, use depends_on to sequence the dependency, or migrate to the native terraform_data resource (Terraform ≥ 1.4) which handles this correctly.
  • Fast path: Use our Client-Side Sandbox above to auto-refactor this — paste your null_resource block and get a corrected diff 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?)

Raw error output from terraform plan or terraform apply:

Error: Provider "registry.terraform.io/hashicorp/null" produced an invalid plan
  for null_resource.example,
  provider "registry.terraform.io/hashicorp/null" planned an invalid value
  for null_resource.example.triggers["instance_id"]: planned value
  cty.StringVal("") does not match config value
  cty.UnknownVal(cty.String)

Immediate consequence: The entire Terraform plan is aborted. No resources in the current run are created, updated, or destroyed. If this null_resource is gating a provisioner (e.g., a remote-exec or local-exec running a migration script), that downstream automation is completely blocked. In a pipeline, this surfaces as a hard exit 1, failing the stage and potentially leaving infrastructure in a partially-converged state if this was a terraform apply -auto-approve mid-run.

The null provider is strict: it requires that every key in the triggers map resolves to a known string value at plan time. If any trigger value is (known after apply) — meaning it references an attribute of a resource not yet created — the provider cannot produce a valid plan and throws this error.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

The Attack Vector / Blast Radius

This is not a security vulnerability, but the blast radius in a production pipeline is severe:

  1. Cascading pipeline failure. null_resource blocks are almost always used as orchestration glue — running DB migrations, invoking Ansible, or signaling external systems. A broken plan here means none of that runs. A zero-downtime deployment that depends on a migration step will hang or deploy broken application code against an un-migrated schema.

  2. State corruption risk on partial applies. If this error surfaces mid-apply (not mid-plan), resources created before the null_resource in the dependency graph are now live in your infrastructure but not consistently reflected in what downstream modules expect. Rolling back requires manual terraform state rm surgery.

  3. The silent trigger trap. The most dangerous variant: a developer uses triggers = { always_run = timestamp() } to force re-execution, but then adds a second trigger referencing a computed value. The timestamp() trigger works fine in isolation, masking the issue until the computed resource is introduced — often in a PR that looks unrelated.

  4. Terraform Cloud / Enterprise run failures. In TFC, a failed plan locks the workspace run queue. Depending on your workspace settings, this can block ALL other runs against that workspace, causing a full infrastructure freeze for the team.

Root cause taxonomy:

Cause Frequency Severity
Trigger references (known after apply) attribute Very High Blocks plan
Trigger value type is not string (e.g., list, map) High Blocks plan
lifecycle.replace_triggered_by referencing unknown resource Medium Blocks plan
Using deprecated null_resource where terraform_data is required Low (TF 1.4+) Blocks plan

How to Fix It (The Solution)

Basic Fix — Defer the Unknown Value with `depends_on`

The broken pattern: a trigger directly references an attribute that won't exist until apply.

 resource "aws_instance" "app" {
   ami           = "ami-0abcdef1234567890"
   instance_type = "t3.micro"
 }
 
 resource "null_resource" "run_migration" {
   triggers = {
-    instance_id = aws_instance.app.id
+    # aws_instance.app.id is (known after apply) — cannot be used as a trigger directly
+    # Use a static sentinel or the instance's stable config attribute instead
+    instance_ami = aws_instance.app.ami
   }
 
+  depends_on = [aws_instance.app]
+
   provisioner "local-exec" {
     command = "./run_migration.sh"
   }
 }

Why this works: aws_instance.app.ami is a known value at plan time (you provided it in config). aws_instance.app.id is assigned by AWS and is unknown until the resource is created. The depends_on ensures ordering without requiring the unknown value in the trigger map.


Enterprise Best Practice — Migrate to `terraform_data` (Terraform ≥ 1.4)

terraform_data is the official successor to null_resource. It is a built-in resource (no provider required) and handles unknown values in triggers_replace gracefully by deferring evaluation to apply time.

-resource "null_resource" "run_migration" {
-  triggers = {
-    instance_id = aws_instance.app.id
-  }
-
-  provisioner "local-exec" {
-    command = "./run_migration.sh ${aws_instance.app.private_ip}"
-  }
-}
+resource "terraform_data" "run_migration" {
+  # triggers_replace accepts any type and handles (known after apply) values correctly
+  triggers_replace = [
+    aws_instance.app.id,
+    aws_instance.app.private_ip
+  ]
+
+  provisioner "local-exec" {
+    command = "./run_migration.sh ${aws_instance.app.private_ip}"
+  }
+}

Remove the null provider block if null_resource was your only usage:

 terraform {
   required_providers {
-    null = {
-      source  = "hashicorp/null"
-      version = "~> 3.0"
-    }
   }
 }

This reduces your provider lock surface and eliminates the entire class of "invalid plan" errors from the null provider.


Fix for the `always_run` Anti-Pattern

If you're using timestamp() to force re-runs, this is a footgun:

 resource "null_resource" "always_run" {
   triggers = {
-    always_run  = timestamp()
-    instance_id = aws_instance.app.id   # This kills the plan
+    always_run = timestamp()
+    # Do NOT mix timestamp() with (known after apply) values in the same triggers map
+    # Use depends_on for ordering instead
   }
+
+  depends_on = [aws_instance.app]
 }

💡 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

1. `terraform validate` as a Pre-Commit Hook

# .git/hooks/pre-commit
terraform validate -no-color
if [ $? -ne 0 ]; then
  echo "[BLOCKED] terraform validate failed. Fix HCL before committing."
  exit 1
fi

terraform validate catches type mismatches in trigger values before a plan is ever attempted.

2. Checkov Policy — Flag `null_resource` Trigger Anti-Patterns

Add a custom Checkov check or use tflint with the hashicorp/null ruleset:

# In your CI pipeline (GitHub Actions, GitLab CI, etc.)
tflint --enable-rule=terraform_required_version
checkov -d . --framework terraform --check CKV_TF_1

For teams on Terraform 1.4+, enforce terraform_data over null_resource via an OPA policy:

# policy/deny_null_resource.rego
package terraform.deny_null_resource

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "null_resource"
  msg := sprintf(
    "[POLICY VIOLATION] null_resource '%s' is deprecated. Migrate to terraform_data (TF >= 1.4).",
    [resource.address]
  )
}

Integrate with Terraform Cloud via Sentinel or OPA in your Atlantis atlantis.yaml:

# atlantis.yaml
projects:
  - name: production
    dir: ./infra
    workflow: default
    apply_requirements:
      - approved
      - mergeable
    pre_workflow_hooks:
      - run: checkov -d . --framework terraform --soft-fail-on LOW

3. Renovate / Dependabot for Provider Pinning

Keep hashicorp/null pinned. Breaking behavior between null provider minor versions has historically caused plan failures:

# versions.tf
terraform {
  required_providers {
    null = {
      source  = "hashicorp/null"
      version = "= 3.2.2"  # Pin exact, not ~>
    }
  }
}

Upgrade path: Audit all null_resource usages with:

grep -r 'null_resource' . --include='*.tf' -l

For each file found, evaluate whether terraform_data is a drop-in replacement. In 95% of cases, it is.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Why does Terraform say 'provider null produced an invalid plan' even though my HCL syntax looks correct?
Syntactically valid HCL is not the same as a plannable configuration. The null provider enforces a strict contract: every value in the `triggers` map must be a known string at plan time. If you reference any attribute marked `(known after apply)` — such as a resource ID assigned by a cloud provider — the null provider cannot compute a deterministic plan and throws this error. The fix is to either use only statically-known values in triggers, or migrate to `terraform_data` which defers trigger evaluation to apply time.
Q2.Should I use null_resource or terraform_data in 2024?
Use `terraform_data` for all new code if you are on Terraform 1.4 or later. It is a built-in resource requiring no provider, supports unknown values in `triggers_replace`, and is the officially supported replacement for `null_resource`. The `null_resource` is not deprecated yet, but HashiCorp has signaled `terraform_data` as the forward path. However, manually migrating existing `null_resource` blocks is highly error-prone and risky. A single misconfigured trigger can introduce a security vulnerability by creating an unintended dependency or state leak, expanding the blast radius of a failure. Do not attempt this manually. Drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix.
Q3.How do I force a null_resource to re-run on every apply without triggering the invalid plan error?
Use `triggers = { always_run = timestamp() }` — but ONLY if that is the sole key in the triggers map. The moment you add a second trigger that references a computed value (like a resource ID), the plan will fail. The safe pattern for 'always re-run plus ordering dependency' is: use `triggers = { always_run = timestamp() }` with `depends_on = [your_resource.name]` to enforce sequencing without requiring the computed attribute in the trigger map. On Terraform 1.4+, `terraform_data` with `triggers_replace = [your_resource.id]` handles this cleanly without any workaround. However, implementing and auditing this pattern across large mono-repos or at scale is a compliance nightmare, making drift and policy violations inevitable. For reliable, automated pull request generation with Maker-Checker review, upgrade to the StackEngine CI/CD Automator (Team Edition) for on-prem analysis and bulk fixes.