How to Fix Terraform 'Error: Invalid for_each Set Value' Caused by Null in a Set
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 5–15 mins
TL;DR
- What broke: Terraform's
for_eachreceived asetcontaining at least onenullelement — this is a hard runtime error; Terraform refuses to proceed and the entireapplyorplanaborts. - How to fix it: Filter nulls out of the collection before it reaches
for_eachusingcompact(), aforexpression with anifguard, ortoset([ for v in var.items : v if v != null ]). - Fast path: Use our Client-Side Sandbox above to auto-refactor this — paste your failing
.tfblock and get corrected HCL without sending your state or secrets anywhere.
▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]
The Incident (What Does the Error Mean?)
You hit this during terraform plan or terraform apply:
Error: Invalid for_each set value
on main.tf line 12, in resource "aws_iam_role_policy_attachment" "this":
12: for_each = toset(var.policy_arns)
Invalid single element for a set: element index 2 is null.
Only non-null values may be used as set element keys.Immediate consequence: The entire Terraform operation halts. No resources in the run are created, updated, or destroyed — including unrelated resources later in the graph. In a pipeline, this breaks the deployment stage completely. If this is a partial re-apply after a failed run, your state may already have diverged.
▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]
The Attack Vector / Blast Radius
This is not just a syntax annoyance. The blast radius is:
- Pipeline hard-stop. Every downstream job depending on Terraform outputs (
terraform output) gets null or stale values. Services expecting freshly provisioned ARNs, IPs, or IDs break silently or loudly depending on how your pipeline handles exit codes. - State drift. If this apply was mid-flight (e.g., some resources already created in a prior partial run), you now have real infrastructure with no corresponding state lock. Drift accumulates.
- Root cause is almost always dynamic input. The null sneaks in from: an optional variable with no default, a
lookup()that missed a key, adatasource attribute that returned null because the resource doesn't exist yet, or a list built fromjsondecode()on a sparse JSON blob. The set type in Terraform does not tolerate nulls — ever. toset()does not sanitize. Many engineers assumetoset()will coerce or drop nulls. It does not. It hard-errors.
How to Fix It (The Solution)
Basic Fix — Filter Nulls Before `toset()`
The minimal surgical fix: use a for expression with a null guard.
resource "aws_iam_role_policy_attachment" "this" {
- for_each = toset(var.policy_arns)
+ for_each = toset([for arn in var.policy_arns : arn if arn != null])
role = aws_iam_role.this.name
policy_arn = each.value
}For list(string) inputs that may contain empty strings as well as nulls, use compact() (drops both nulls and empty strings):
resource "aws_iam_role_policy_attachment" "this" {
- for_each = toset(var.policy_arns)
+ for_each = toset(compact(var.policy_arns))
role = aws_iam_role.this.name
policy_arn = each.value
}⚠️
compact()only works onlist(string). If your set contains objects or complex types, use theforexpression withif v != null.
Enterprise Best Practice — Validate at the Variable Boundary
Don't let nulls propagate into your module internals. Enforce non-null at the variable declaration using validation blocks, and use a nullable-safe default.
variable "policy_arns" {
- type = list(string)
- default = []
+ type = list(string)
+ default = []
+
+ validation {
+ condition = alltrue([for arn in var.policy_arns : arn != null && arn != ""])
+ error_message = "policy_arns must not contain null or empty string values."
+ }
}For module authors publishing reusable modules, always defensively sanitize inside the module even if you validate at the boundary — consumers will pass bad data:
locals {
- policy_set = toset(var.policy_arns)
+ policy_set = toset([for arn in var.policy_arns : arn if arn != null && arn != ""])
}
resource "aws_iam_role_policy_attachment" "this" {
for_each = local.policy_set
role = aws_iam_role.this.name
policy_arn = each.value
}💡 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
Stop this class of error before it ever reaches plan.
1. terraform validate in pre-commit
Add terraform validate as a pre-commit hook. It catches type mismatches early, though it won't catch runtime-null issues from dynamic data sources.
2. Checkov — static analysis for Terraform Checkov won't catch this specific null issue natively, but custom checks can be written:
checkov -d . --check CKV_TF_FOR_EACH_NULLWrite a custom Checkov Python check that parses HCL AST and flags any toset(var.*) call without a null filter.
3. OPA/Conftest policy
Enforce in your pipeline with a Rego policy that rejects plans where for_each expressions reference raw variable sets without a compact or null-filter expression:
terraform show -json tfplan.binary | conftest test - --policy ./policies/foreach_null.rego4. tflint
Use tflint with the terraform ruleset. Add a custom rule to flag toset(var.*) patterns:
tflint --enable-rule=terraform_required_version5. Strongly-typed variable defaults
Never declare list(string) variables without a non-null default and a validation block. Make nulls impossible at the input layer, not just at the resource layer.
variable "policy_arns" {
type = list(string)
default = []
nullable = false # Terraform 1.1+ — prevents null being passed as the entire variable
}nullable = false (Terraform ≥ 1.1) prevents a caller from passing null as the variable value itself — it falls back to the default. This does not prevent null elements inside the list, which is why the for expression guard is still required.
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.