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
countandfor_eachsimultaneously — 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_eachwhen iterating over a map or set of distinct keys; usecountonly 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
.tfblock 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_onor implicit references to this resource's outputs are also blocked.terraform planexits 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 applyalready ran, you may have orphaned real infrastructure with no corresponding state entry, requiring manualterraform importsurgery.
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.