How to Fix Terraform 'Invalid Index on List' When Using count.index with for_each
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 5–15 mins
TL;DR
- What broke: You used
count.indexto index a list inside a resource that also declaresfor_each, or vice versa — Terraform's meta-argument system is mutually exclusive and panics on invalid list access. - How to fix it: Pick one iteration strategy. Use
for_eachwitheach.key/each.value, OR usecountwithvar.list[count.index]. Never mix them on the same resource block. - Use our Client-Side Sandbox above to paste your failing
.tfblock and auto-refactor it instantly without exposing your config to external servers.
The Incident (What Does the Error Mean?)
Raw error output from terraform plan or terraform apply:
Error: Invalid index
on main.tf line 14, in resource "aws_instance" "servers":
14: ami = var.ami_list[count.index]
The given key does not identify an element in this collection value.
Or the variant when for_each is active:
Error: Invalid use of count.index in for_each block
on main.tf line 9, in resource "aws_subnet" "this":
9: cidr_block = var.cidrs[count.index]
count.index is only valid within a resource that uses the count meta-argument.
Immediate consequence: Terraform exits non-zero. No resources are created, modified, or destroyed. If this is mid-apply in a partial state, you may have orphaned resources with no corresponding state entry — a manual remediation nightmare.
The Blast Radius
This is not a soft warning. Terraform treats this as a hard panic:
countandfor_eachare mutually exclusive on any single resource or module block. Declaring both, or referencingcount.indexinside afor_eachresource, is a compile-time type error in the Terraform evaluator.- In a monorepo or shared module, one bad resource block cascades: the entire root module fails to plan. Every downstream dependent workspace is blocked.
- In CI/CD pipelines (Atlantis, Terraform Cloud, GitHub Actions), this causes a hard pipeline failure. If your merge queue depends on speculative plans, all open PRs are gated until fixed.
- Partial applies that hit this mid-run can leave your state file referencing resources that were created before the error, with no
for_eachkey orcountindex to track them — leading toterraform state rmsurgery.
How to Fix It
Root Cause
You have one of three bad patterns:
- A resource declares
for_eachbut referencescount.indexinternally. - A resource declares
countbut the list length doesn't match whatcount.indexexpects at evaluation time (e.g., a dynamic list with unknown length at plan time). - A module call passes both
countandfor_eachsimultaneously.
Basic Fix — Choose Your Iteration Primitive and Commit
Pattern A: You want to iterate over a list → use count
resource "aws_instance" "servers" {
- for_each = toset(var.instance_names)
- ami = var.ami_list[count.index] # WRONG: count.index undefined here
+ count = length(var.ami_list)
+ ami = var.ami_list[count.index] # CORRECT
instance_type = "t3.micro"
}
Pattern B: You want key-based tracking (safer for state) → use for_each
resource "aws_instance" "servers" {
- count = length(var.ami_list)
- ami = var.ami_list[count.index]
+ for_each = { for idx, ami in var.ami_list : idx => ami }
+ ami = each.value
instance_type = "t3.micro"
}
Enterprise Best Practice — for_each with a Map Input
Stop passing raw lists to modules or resources. Lists are index-fragile — inserting an element at position 0 forces Terraform to destroy and recreate everything. Use maps.
# variables.tf
-variable "ami_list" {
- type = list(string)
-}
+variable "instances" {
+ type = map(object({
+ ami = string
+ instance_type = string
+ }))
+}
# main.tf
resource "aws_instance" "servers" {
- count = length(var.ami_list)
- ami = var.ami_list[count.index]
- instance_type = "t3.micro"
+ for_each = var.instances
+ ami = each.value.ami
+ instance_type = each.value.instance_type
+
+ tags = {
+ Name = each.key
+ }
}
With this pattern, adding or removing a single instance only affects that one state key. No collateral destroy/recreate.
💡 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
1. terraform validate as a Pre-Commit Gate
This error is caught at validate time, before any API calls. Add it to your pre-commit hooks:
# .pre-commit-config.yaml
- repo: https://github.com/antonbabenko/pre-commit-terraform
hooks:
- id: terraform_validate
- id: terraform_tflint
2. TFLint Rule — terraform_required_version + Iterator Checks
# .tflint.hcl
plugin "terraform" {
enabled = true
preset = "recommended" # catches mixed count/for_each patterns
}
3. Checkov Policy — Enforce Map Inputs on Shared Modules
Write a custom Checkov check that flags any variable of type = list(string) used directly in a resource iteration without conversion to a map.
4. OPA/Conftest Policy in Atlantis
# policy/no_count_index_in_foreach.rego
deny[msg] {
resource := input.resource_changes[_]
resource.change.actions[_] == "create"
# Flag resources where plan shows both for_each keys and count-style numeric indexes
is_number(resource.index)
msg := sprintf("Resource %v uses numeric index — migrate to for_each with map input.", [resource.address])
}
5. Terraform Cloud Sentinel (Hard Fail)
# sentinel policy
import "tfplan/v2" as tfplan
main = rule {
all tfplan.resource_changes as _, rc {
rc.index is not number # enforce string keys (for_each) over numeric (count)
}
}
Enforce this as a hard-mandatory policy in your TFC organization. Numeric count indexes in production state are a liability — one list reorder triggers mass resource replacement.