Search code examples
terraformterraform-provider-awsokta

Terraform - How to increment a variable under for_each loop?


I have a requirement to increment the priority in the below resource block for each of the data items present. eg: priority =1, priority =2 and so on.

resource "okta_app_group_assignments" "idc_app_assignments" {
    app_id   = okta_app_saml.aws_identity_center.id
    for_each = data.okta_groups.all_groups
    group {
       id       = each.value.groups.0.id
       priority = 1
    }
}

I could not use index function because data.okta_groups.all_groups is an object and it is not supported. Also, tried using a local variable and use that under priority, but it does not seem to work. Please let me know, if there is any way to achieve it.

    data "okta_groups" "all_groups" {
       for_each = {
          for okta_account in local.aws_okta_accounts : 
 "${okta_account.name}.${okta_account.role}.${okta_account.env_name}" => okta_account}
         q = "${each.value.name}-${each.value.env_name}#AWS#OKTA#${each.value.role}#${each.value.account_id}"
#example value for q - abc-dev#AWS#OKTA#dev#12345
}

example for data.okta_groups.all_groups: 
+ "abc.dev.qa"                           = {
      + groups = [
          + {
              + custom_profile_attributes = jsonencode({})
              + description               = ""
              + id                        = "00xxxxxxxxxxxxxb416"
              + name                      = "abc-qa#AWS#OKTA#dev#12345"
              + type                      = "OKTA_GROUP"
            },
        ]
      + id     = "987654"
      + q      = "abc-qa#AWS#OKTA#dev#12345"
      + search = null
      + type   = null
    }

Solution

  • A Terraform map is not an ordered data structure, so there isn't any meaningful answer for "the index of a map element".

    For collections that have a meaningful order, a list is the more appropriate data structure because it preserves the order in which the elements are defined. Therefore I would suggest changing your approach to use a list instead of a map.

    I can't show a full example that works with your existing configuration because you've only included a small snippet of what you have, but here's a more contrived example showing the principle which hopefully you can adapt to suit your requirements:

    variable "groups" {
      type = list(object({
        id = string
        # (and any other attributes you need)
      }))
    }
    
    resource "okta_app_group_assignments" "idc_app_assignments" {
      app_id   = okta_app_saml.aws_identity_center.id
    
      dynamic "group" {
        for_each = var.groups
        content {
          id       = group.value.id
          priority = group.key + 1
        }
      }
    }
    

    It seems like your situation needs a single resource with multiple group blocks rather than a separate resource for each group, so here I've used a dynamic block to achieve that.

    dynamic blocks allow collections of any type in their for_each because, unlike with resource-level for_each, Terraform does not need to individually track each block For nested blocks, that's the provider's responsibility, and providers can decide whether to treat a sequences of blocks as an ordered list or as some other kind of data structure.

    Inside the content block of dynamic "group", group.key represents the key of the current element, and since var.groups is a list the "keys" in this case are the list element indices. group.key will therefore be integers counting up from zero, and so I added one to instead produce integers counting up from one.

    group.value is the current element value, similar to each.value for resource-level for_each, and so group.value.id is the id attribute from the current element.


    If you did need to do this with resource-level for_each instead of a dynamic block then that's possible too, but it requires an extra step: Terraform cannot use a list for resource-level for_each because that doesn't give Terraform enough information to decide a tracking key for each instance of the resource.

    In that case then, the typical answer is to use a for expression to project the list into a map just before assigning it to for_each, like this:

    resource "example" "example" {
      for_each = tomap({
        for idx, group in var.groups : group.id => merge(
          group,
          { priority = idx + 1 },
        )
      })
    
      # ...
    }
    

    This for expression produces a map that has one element for each element of the given list, and switches it to identify each element by its id attribute instead of by its index.

    But the index is still needed to decide the priority, so the merge call adds an extra priority attribute to each object which would then be available as each.value.priority for other arguments in the resource block.