Initializing Enclave...

How to Fix Terraform 'Invalid Combination of count and for_each' Error (With Refactored Examples)

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

TL;DR

  • What broke: A Terraform resource or module block declares both count and for_each simultaneously — Terraform's meta-argument system treats these as mutually exclusive and hard-fails at plan time, blocking every resource in the dependency graph downstream.
  • How to fix it: Remove one of the two meta-arguments. Use for_each when iterating over a map or set of distinct keys; use count only for simple numeric repetition or boolean toggles. Never mix them in the same block.
  • Shortcut: Use our Client-Side Sandbox above to auto-refactor this — paste your failing .tf block and get corrected HCL instantly without sending your config to a third-party server.

The Incident (What Does the Error Mean?)

Raw error output from terraform plan or terraform apply:

Error: Invalid combination of count and for_each

  on main.tf line 12, in resource "aws_instance" "web":
  12: resource "aws_instance" "web" {

The "count" and "for_each" meta-arguments are mutually exclusive.
A resource may use either count or for_each, but not both simultaneously.

Terraform evaluates meta-arguments (count, for_each, depends_on, lifecycle) before any provider logic runs. The moment it detects both count and for_each in the same block, it raises this error and exits immediately — no partial plan is generated, no state is written. Every resource that depends on this block is also blocked. In a large root module, this can freeze an entire environment promotion pipeline.


The Attack Vector / Blast Radius

This is not a runtime error — it is a compile-time plan failure. The blast radius is determined entirely by your module dependency graph:

  • Single resource block: Only that resource fails to plan. Low blast radius in isolation.
  • Root module resource: All resources with depends_on or implicit references to this resource's outputs are also blocked. terraform plan exits non-zero, failing your CI pipeline gate.
  • Reusable module: If this block lives inside a shared module called by 12 other root modules across environments, every environment that calls this module is broken simultaneously. This is the production-outage scenario.
  • State drift risk: If this error surfaces mid-refactor after a partial terraform apply already ran, you may have orphaned real infrastructure with no corresponding state entry, requiring manual terraform import surgery.

The secondary risk is engineer time: developers unfamiliar with Terraform's meta-argument model will attempt to combine both thinking they are additive. The error message is clear, but the correct replacement pattern (especially for_each with maps vs. count with conditionals) is not obvious to mid-level engineers.


How to Fix It (The Solution)

Basic Fix — Remove the Conflicting Meta-Argument

The most common bad pattern is a developer adding count for a feature-flag toggle on top of an existing for_each loop.

 resource "aws_security_group_rule" "allow_ingress" {
-  count     = var.enable_rule ? 1 : 0
-  for_each  = var.ingress_ports
+  for_each  = var.enable_rule ? var.ingress_ports : {}
 
   type        = "ingress"
   from_port   = each.value.from_port
   to_port     = each.value.to_port
   protocol    = each.value.protocol
   cidr_blocks = each.value.cidr_blocks
   security_group_id = aws_security_group.main.id
 }

Key insight: The boolean toggle count = var.enable ? 1 : 0 pattern is unnecessary when using for_each. Pass an empty map {} when the feature is disabled. for_each on an empty map creates zero instances — identical behavior, no conflict.


Enterprise Best Practice — Typed Input Variables with Validation

The root cause is often an untyped or loosely typed variable that allows callers to pass unexpected structures. Lock it down at the module interface.

 # variables.tf
 variable "ingress_ports" {
-  type    = any
-  default = {}
+  type = map(object({
+    from_port   = number
+    to_port     = number
+    protocol    = string
+    cidr_blocks = list(string)
+  }))
+  default = {}
+
+  validation {
+    condition     = length(var.ingress_ports) >= 0
+    error_message = "ingress_ports must be a valid map of port objects or an empty map."
+  }
 }
 
 variable "enable_ingress_rules" {
-  type = any
+  type    = bool
+  default = true
 }
 
 # main.tf
 resource "aws_security_group_rule" "allow_ingress" {
-  count    = var.enable_ingress_rules ? 1 : 0
-  for_each = var.ingress_ports
+  for_each = var.enable_ingress_rules ? var.ingress_ports : {}
 
   type              = "ingress"
   from_port         = each.value.from_port
   to_port           = each.value.to_port
   protocol          = each.value.protocol
   cidr_blocks       = each.value.cidr_blocks
   security_group_id = aws_security_group.main.id
 }

For cases where count is genuinely the right tool (simple numeric repetition, no key-based addressing needed):

 resource "aws_instance" "worker" {
-  count    = var.worker_count
-  for_each = toset(var.worker_names)
+  count = var.worker_count
 
-  ami           = each.value.ami
+  ami           = var.worker_ami
   instance_type = "t3.medium"
 }

💡 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

This error should never reach a human code review. Gate it automatically:

1. terraform validate in pre-commit

This catches the conflict before any network call is made:

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

2. TFLint with the AWS ruleset

tflint --init && tflint --recursive will flag meta-argument conflicts and type mismatches before plan.

3. Checkov static analysis in CI

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

Checkov won't catch this specific meta-argument conflict (it's a Terraform core validation), but terraform validate run as a prior CI step will. Make terraform validate a required status check on your main branch — a non-zero exit blocks the merge. This error has a 0-second escape rate from a properly gated pipeline.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →