When conditional count = 0, apply fails with empty tuple error

when conditional count evaluates to a whole number other than 0, code runs fine. but, when the count evaluates to 0, apply fails.

I see there was discussion of workarounds for this a few years ago by @apparentlymart , @jbardin , and @brikis98, ( "Referencing Attributes from Resources with count = 0" change in Terraform 0.11 should be a warning! · Issue #16726 · hashicorp/terraform · GitHub ) but now it seems that the workarounds are deprecated too. How is this supposed to be handled now?

THE ERRORS:

╷
│ Error: Invalid index
│
│   on network.tf line 72, in resource "aws_nat_gateway" "NAT":
│   72:   allocation_id = aws_eip.NATEIP[count.index].id
│     ├────────────────
│     │ aws_eip.NATEIP is empty tuple
│     │ count.index is 1
│
│ The given key does not identify an element in this collection value.
╵
╷
│ Error: Invalid index
│
│   on network.tf line 72, in resource "aws_nat_gateway" "NAT":
│   72:   allocation_id = aws_eip.NATEIP[count.index].id
│     ├────────────────
│     │ aws_eip.NATEIP is empty tuple
│     │ count.index is 0
│
│ The given key does not identify an element in this collection value: the collection has no elements.
╵
╷
│ Error: Invalid index
│
│   on network.tf line 79, in output "NatEip1":
│   79:   value = aws_eip.NATEIP[0].public_ip
│     ├────────────────
│     │ aws_eip.NATEIP is empty tuple
│
│ The given key does not identify an element in this collection value: the collection has no elements.

THE CODE:

resource "aws_eip" "NATEIP" {
  provider      = aws.region-main
  count = var.RdsCreate ? 2 : 0
  vpc   = true
  depends_on    = [aws_internet_gateway.IGW]
 }

resource "aws_nat_gateway" "NAT" {
  provider      = aws.region-main
  allocation_id = aws_eip.NATEIP[count.index].id
  count         = length(aws_subnet.RdsSubnets)
  subnet_id     = aws_subnet.RdsSubnets[count.index].id  #aws_subnet.Public.id
  depends_on    = [aws_internet_gateway.IGW, aws_eip.NATEIP]
}

output "NatEip1" {
  value = aws_eip.NATEIP[0].public_ip
  #value                = "${element(concat(aws_eip.NATEIP.*.public_ip, tolist("")), 0)}"
}
output "NatEip2" {
  value = aws_eip.NATEIP[1].public_ip
  #value         = "${element(concat(aws_eip.NATEIP.*.public_ip, tolist("")), 0)}"
}

Hi @sara.hann,

That GitHub issue you found is essentially obsolete at this point, because it’s describing behavior from the old version of the Terraform language prior to Terraform v0.12. Terraform v0.12 included a total rewrite of the Terraform language to address various bugs and quirks like the one described by that issue, although I agree that the problem you’ve encountered here does have some terminology in common with that discussion and so it’s understandable why you thought they might be connected.

As a general rule, anything that’s about Terraform v0.11 or earlier is probably not relevant if you are using Terraform v0.12 or later.


The main requirement when you create a situation like this where there can potentially be zero instances of a particular resource is to add logic to explain to Terraform what value it should use instead for any situation where there is no real object to refer to.

For example, in the case of your aws_nat_gateway resource I believe the allocation_id argument is required and so I expect you’d need to arrange for there to be no instances of that resource in any case where there are no instances of aws_eip.NATEIP. Without seeing the bigger picture I can’t be sure about what would make sense here but perhaps your intent was that length(aws_subnet.RdsSubnets) is always 2 and therefore it’s valid to correlate NAT gateway instances with EIP instances in the case where var.RdsCreate is true. So perhaps then the idea would be to also declare zero NAT gateways if there are zero elastic IPs:

resource "aws_nat_gateway" "NAT" {
  provider = aws.region-main
  count    = length(aws_eip.NATEIP) != 0 ? length(aws_subnet.RdsSubnets) : 0

  allocation_id = aws_eip.NATEIP[count.index].id
  subnet_id     = aws_subnet.RdsSubnets[count.index].id  #aws_subnet.Public.id
  depends_on    = [aws_internet_gateway.IGW, aws_eip.NATEIP]
}

For your output values I can at guess by the commented out older expressions that your intent was to return an empty string in the case where there is no EIP to return the IP address of; the element and concat trick you were using there ought to still work in modern Terraform after translation to the newer syntax, but perhaps a more straightforward version would be to use the modern Terraform try function, which arrived in a later Terraform version:

output "NatEip1" {
  value = try(aws_eip.NATEIP[0].public_ip, "")
}
output "NatEip2" {
  value = try(aws_eip.NATEIP[1].public_ip, "")
}

Another option would be to combine these two output values into a single one that is a set of strings type, and then you could return both of the IP addresses together and have the list be empty if there aren’t any EIPs:

output "NatEips" {
  value = toset(aws_eip.NATEIP[*].public_ip)
}

Or perhaps it’d be helpful for the caller to be able to see which AZ each IP address belongs to, by returning a map from AZ name to IP address derived using a for expression:

output "NatEips" {
  value = tomap({
    for i, eip in aws_eip.NATEIP :
    aws_subnet.RdsSubnets[i].availability_zone => eip.public_ip
  })
}

In this case, you’d be returning a map whose keys are the AZ names and values are IP addresses, and again it would be an empty map in the case where there are no elastic IP addresses.

There are many other possible variations here. The general idea is to use whatever Terraform language features make sense to describe to Terraform how you intend to handle all of the possible situations.

thank you @apparentlymart ! I’m fairly new to terraform, so I tend to google when I get stuck. My biggest challenge is determining what is still current and what is obsolete.

I wasn’t aware of the try function, thanks for that. It seems from your example that it works like:
Try to resolve aws_eip.NATEIP[1].public_ip, and if value not found, return “”, right?

I’ve been unclear on the use of “=>”. I keep seeing it mentioned, so I’ll do some googling to get familiar. Thank you for all the super helpful suggestions. your response was very educational!

Before I saw your response, I had continued to tinker, and ended up with the following, which works too. My RdsCreate is just a variable with a value of true or false depending on whether I want to spin up an AWS RDS, or install a local database on the VM.

I ended up doing this:

resource “aws_eip” “NATEIP” {
provider = aws.region-main
count = var.RdsCreate ? 2 : 0
vpc = true
depends_on = [aws_internet_gateway.IGW]
}

resource “aws_nat_gateway” “NAT” {
provider = aws.region-main
allocation_id = aws_eip.NATEIP[count.index].id
count = length(aws_eip.NATEIP) > 0 ? length(var.RdsSubnets) : 0 #length(aws_subnet.RdsSubnets)
subnet_id = aws_subnet.RdsSubnets[count.index].id #aws_subnet.Public.id
depends_on = [aws_internet_gateway.IGW, aws_eip.NATEIP]
}