Terraform Error Guide: 'Invalid index' (the given key does not identify an element)
Fix Terraform's 'Invalid index' error: match count.index vs each.key, guard missing keys with try() and lookup(), and stop indexing empty or computed collections.
- #terraform
- #troubleshooting
- #errors
- #expressions
Exact Error Message
Error: Invalid index
on main.tf line 22, in resource "aws_route53_record" "app":
22: records = [aws_instance.web["primary"].private_ip]
The given key does not identify an element in this collection value.
A list-indexing variant looks like this:
Error: Invalid index
on outputs.tf line 4, in output "first_subnet":
4: value = aws_subnet.app[0].id
The given key does not identify an element in this collection value.
What the Error Means
Invalid index means you indexed a collection — a list, map, or a count/for_each resource — with a key that does not exist in it. Terraform evaluated the expression, found the collection, and could not find the element at the key or position you asked for.
The two most common shapes are a position that is out of range (an empty list indexed at [0], an off-by-one [length(x)]) and a key that is not present (a map missing the key, or — very commonly — indexing a for_each resource with a numeric [0] when its instances are keyed by string, or indexing a count resource with a string key ["name"] when its instances are keyed by integer).
Common Causes
- count vs for_each indexing mismatch — referencing
aws_instance.web[0]when the resource usesfor_each(string keys), oraws_instance.web["primary"]when it usescount(integer keys). - Map missing the key —
var.settings["region"]whenregionis not in the map. - Empty list indexed —
data.x.items[0]when a filter returned nothing. - Off-by-one —
local.zones[length(local.zones)]instead oflength - 1. - Indexing before the element is known — a list whose length depends on a resource not yet created.
- Wrong collection entirely — indexing a single object as if it were a list.
How to Reproduce the Error
Mix for_each keys with positional indexing:
variable "instances" {
default = { primary = "t3.small", replica = "t3.micro" }
}
resource "aws_instance" "web" {
for_each = var.instances
ami = "ami-0abcd1234ef567890"
instance_type = each.value
}
output "first_ip" {
value = aws_instance.web[0].private_ip # for_each => string keys, not [0]
}
terraform plan
Error: Invalid index
on main.tf line 14, in output "first_ip":
14: value = aws_instance.web[0].private_ip
The given key does not identify an element in this collection value.
Diagnostic Commands
Validate to surface the file and line:
terraform validate
Inspect the actual keys of the collection in the console — this immediately reveals whether they are integers or strings:
terraform console
> keys(aws_instance.web)
[
"primary",
"replica",
]
Check whether a resource uses count or for_each, which determines the index type:
grep -rn 'count\|for_each' main.tf
Confirm a list is non-empty before indexing it:
terraform console
> length(data.aws_subnets.app.ids)
Step-by-Step Resolution
1. Find out how the resource is indexed. A count resource is a list (integer keys 0,1,2). A for_each resource is a map (the keys are whatever you passed to for_each).
terraform console
> keys(aws_instance.web) # for_each => ["primary","replica"]
2. Use the matching index form.
# for_each resource — index by key
output "primary_ip" {
value = aws_instance.web["primary"].private_ip
}
# count resource — index by integer
output "first_ip" {
value = aws_instance.web[0].private_ip
}
3. For maps that may be missing a key, use lookup() with a default instead of bracket indexing:
locals {
region = lookup(var.settings, "region", "us-east-1")
}
4. For expressions that may not resolve, wrap with try() so a missing element falls back instead of erroring:
locals {
first_id = try(aws_subnet.app[0].id, null)
}
5. Guard before indexing with length(), contains(), or can():
locals {
has_subnets = length(data.aws_subnets.app.ids) > 0
subnet_id = local.has_subnets ? tolist(data.aws_subnets.app.ids)[0] : ""
}
6. Fix off-by-one ranges by indexing to length - 1:
locals {
last_zone = local.zones[length(local.zones) - 1]
}
7. Re-run terraform plan to confirm the index now resolves.
Prevention and Best Practices
- Decide on
countorfor_eachper resource and reference it consistently — never index afor_eachresource positionally. - Prefer
for_eachwith stable string keys; positionalcountindexes shift when items are inserted or removed, which silently re-creates the wrong resources. - Use
lookup(map, key, default)for any map access that might miss, andtry(expr, fallback)for chained references that might not exist yet. - Validate collection length with
length() > 0before indexing data-source results, which are often empty when filters match nothing. - Add
contains(keys(var.m), "k")checks inpreconditionblocks to fail with a clear message instead ofInvalid index. - Run
terraform validatein CI so index errors are caught before apply.
Related Errors
Invalid for_each argument—for_eachkeys cannot be determined at plan time (a related but distinct expansion problem).Call to function "element" failed—element()on an empty list; the wrap-around still needs at least one element.Unsupported attribute— you indexed correctly but then read an attribute that the element does not have.Error: Invalid value for input variable— avalidationblock rejected the collection before indexing.
Frequently Asked Questions
Why does resource.name[0] fail when I clearly have instances? Because the resource almost certainly uses for_each, so its instances are keyed by strings, not integers. Run keys(resource.name) in terraform console and index by the actual string key.
How do I safely read a map value that might be absent? Use lookup(var.map, "key", default) rather than var.map["key"]. lookup returns the default when the key is missing; bracket indexing raises Invalid index.
What is the difference between try() and lookup() here? lookup() is specifically for map key access with a default. try() is general — it evaluates a sequence of expressions and returns the first that succeeds, so it handles deep chains like try(x[0].y.z, null) where any link might not exist.
My index works on apply but fails on the first plan. Why? The element you index depends on a resource not yet created, so the collection is empty or unknown at plan time. Guard with try() or split the apply so the producing resource exists first.
Can I default a whole missing list to empty? Yes — try(data.x.items, []) or coalescelist(var.items, []) give you an empty list to index safely, and length() checks then short-circuit cleanly.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.