For_each vs. count.index: How to do simple arithmetic?

Hi there,
What’s the equivalent of calculation with count.index (e.g. count.index+1) when using for_each?
I have a aws_network_acl_rule resource, where I calculate the rule_number based on the index (e.g. 400+10*(count.index+1)) and that seems not possible using for_each. So basically , need to rewrite this using for_each:

resource "aws_network_acl_rule" "ig_ssh" {
  count          = length(local.v_cidrs)
  egress         = false
  protocol       = "tcp"
  to_port        = 22
  from_port      = 22
  rule_number    = 400 + 10 * (count.index+1)
  rule_action    = "allow"
  cidr_block     = local.v_cidrs[count.index]
  network_acl_id = aws_network_acl.nacls.id
}

What’s the recommended way of doing this?

-S

Hi @dsantanu,

The usual reason to use for_each rather than count is to avoid the ordering dependency on the input elements. In this case the ordering seems to be significant, so it may be best to keep using count for this one.

If you want to use for_each anyway for other reasons, then the requirement would be for you write an expression to explicitly assign an index to each of the elements in some way, because maps and sets are not naturally ordered and so there is no automatic index to populate count.index with. For example:

resource "aws_network_acl_rule" "ig_ssh" {
  for_each = {
    for i, cidr_block in local.v_cidrs : cidr_block => {
      cidr_block  = cidr_block
      rule_number = 400 + 10 * (i + 1)
    }
  }

  rule_number = each.value.rule_number
  cidr_block  = each.value.cidr_block
  # (and then all of the other arguments, unchanged)
}

In the above example, Terraform would track each of the rules by its CIDR block, but would still populate the rule number from the position in the list.

thanks @apparentlymart for the code and that works functionally perfectly okay. But it seems brings back the same issue as the count.index.

The main reason for moving away from count was so that it doesn’t destroy and recreate every single NACL rule the moment new CIDR is added to the list (local.v_cidrs) or removed. With explicit indexing using for_each, it also trashing and recreating the the resources, every time that list changes.

Is that expected? Am I experiencing the correct thing?

Hi @dsantanu,

Indeed, any time you make use of the indexes of elements in a list you’ll see changes when those indexes change. for_each aims to address that by taking the indexes out of the picture altogether, but the code example I shared in my previous comment re-introduces them because your framing of the question seemed to require considering the ordering in the list to be significant.

The only way I can think of to represent ACL rules without being sensitive to index positions is to explicitly write hard-coded rule_number values into your collection, so that they are no longer derived from the element indexes:

locals {
  v_cidrs = {
    "10.1.2.0/24" = 400
    "10.1.3.0/24" = 410
  }
}

resource "aws_network_acl_rule" "ig_ssh" {
  for_each = local.v_cidrs

  rule_number = each.value
  cidr_block  = each.key
}

Given that the underlying data model in AWS is order-sensitive here, I don’t think we can do any better than this within the constraints of how this resource type is designed.

Looking at the underlying API operations CreateNetworkAclEntry, DeleteNetworkAclEntry, and ReplaceNetworkAclEntry I don’t see any way around the thrashing when the order changes; it seems to just be a requirement of the underlying API.

Remember also that Network ACL is not intended as the primary method for restricting network traffic, the primary being Security Groups.

yeah, 100%! not trying to use it as one.

-S