TL;DR
You're treating a count resource like a single object. It's a list. Add an index: aws_instance.web[0] for the first instance, aws_instance.web[count.index] inside another counted resource, or aws_instance.web[*].id to collect all IDs at once.
What triggers this error
The moment you add count to a resource block, Terraform converts it into an ordered list โ even when count = 1. Every reference to that resource must specify which instance you mean. Skip the index and you get:
Error: Missing resource instance key: Because aws_instance.web has "count" set, its attributes must be accessed on specific instances.
Here's the setup that triggers it:
resource "aws_instance" "web" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
output "web_ip" {
value = aws_instance.web.private_ip # โ ERROR: missing index
}
Fix approaches
1. Reference a specific index
Know exactly which instance you want? Pick it by zero-based index:
output "first_web_ip" {
value = aws_instance.web[0].private_ip
}
Indices start at 0 โ the second instance is [1], the third is [2]. Simplest fix for most cases.
2. Use a splat expression to get all values
Need the same attribute from every instance? The splat operator [*] returns a list in one shot:
output "all_web_ips" {
value = aws_instance.web[*].private_ip
}
Works in outputs, locals, and anywhere a list is valid. One caveat: don't use it where the argument expects a single string โ you'll get a type mismatch error instead.
3. Inside another resource โ use count.index
Both resources using count? Map them 1-to-1 with count.index:
resource "aws_eip" "web" {
count = 3
instance = aws_instance.web[count.index].id
}
Each EIP gets paired with the web instance at the same position โ EIP 0 โ web[0], EIP 1 โ web[1], and so on.
4. Switch from count to for_each (recommended for anything non-trivial)
Index juggling getting messy? That's a signal to switch to for_each. String keys replace numeric indices โ cleaner to read, and safe when items are added or removed mid-list:
variable "web_names" {
default = ["web-a", "web-b", "web-c"]
}
resource "aws_instance" "web" {
for_each = toset(var.web_names)
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = each.key
}
}
output "web_ips" {
value = { for k, v in aws_instance.web : k => v.private_ip }
}
With plain count, deleting "web-b" shifts every index above it โ Terraform sees "web-c" as changed and plans a destroy/recreate. With for_each, each instance has a stable string key, so removing "web-b" only touches "web-b".
Edge case: count = 1 still needs an index
This one catches people off guard. Even count = 1 turns the resource into a list:
resource "aws_instance" "bastion" {
count = 1
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
# Still errors:
resource "aws_eip" "bastion" {
instance = aws_instance.bastion.id # ERROR
}
# Correct:
resource "aws_eip" "bastion" {
instance = aws_instance.bastion[0].id
}
Common places this shows up
- Outputs โ referencing
resource.name.attributedirectly instead of indexing - Dependent resource arguments โ passing an ID or ARN to a resource that depends on a counted one
- Locals โ computing a derived value from a counted resource attribute
- Module inputs โ passing a single-value variable that expects a string, not a list
Verify the fix
Two commands to confirm everything is wired up correctly. First, validate the syntax:
terraform validate
Success! The configuration is valid.
Then run the plan. The output block or dependent resource should show an actual value โ an IP address, an ID, an ARN โ with no errors:
terraform plan
If anything is unexpectedly marked for replacement, double-check which index you referenced.
Quick reference cheatsheet
resource.name[0]โ first instanceresource.name[count.index]โ current index, inside another counted resourceresource.name[*].attrโ all instances, returns a listresource.name[length(resource.name) - 1]โ last instance{ for k, v in resource.name : k => v.attr }โ map over afor_eachresource

