Object attribute to String conversion (also allowing string function on each.value/key)

We defined below data structure (variable) to manage Route 53 hosted zone delegation. We use subdomain hosted zone delegated to an account (hsdevel,hsstage) with base domain hosted zone in another account (shared), e.g. as depicted here: https://serverless-stack.com/chapters/share-route-53-domains-across-aws-accounts.html.

For this delegation to work the host zones must be created on env one accounts and records created in the shared account pointing to Name Servers of defined subdomain host zones.

Data Structure variable

  hosted_zones_for_delegation = {
    "domain1.net" = {
      hsdevel = {
        "hz1" = {
          "hsdevel1.domain1.net" = {
            comment     = "..."
            tags        = {
              "Environment" = "hsdevel"
            }
          }
        }
        "hz2" = {
          "hsdevel2.domain1.net" = {
            comment     = "..."
            tags        = {
              "Environment" = "hsdevel"
            }
          }
        }
      }
      hsstage = {
        "hz1" = {
          "hsstage.domain1.net" = {
            comment     = "..."
            tags        = {
              "Environment" = "hsstage"
            }
          }
        }
      }
    }
    "domain2.net" = {
      hsdevel = {
        "hz1" = {
          "hsdevel.domain2.net" = {
            comment     = "..."
            tags        = {
              "Environment" = "hsdevel"
            }
          }
        }
      }
      hsstage = {
        "hz1" = {
          "hsstage.domain2.net" = {
            comment     = "..."
            tags        = {
              "Environment" = "hsstage"
            }
          }
        }
      }
    }
  }

As every subdomain hosted zone (in account hsstage and hsdevel) will have own Name Servers list (dynamically assigned by aws) that must be referenced on Route 53 records in the base hosted zone accuont (shared) we need to create a single hosted zone struct like below to loop on to create hosted zones (in hsdevel, hsstage accounts) and related records (in shared account), to do so we use this flattening code:

  hosted_zones_for_delegation_with_parents_list = flatten([
    for hzparentname, hzcfg in local.hosted_zones_for_delegation  : [
      for accountname, accounthzcfg in hzcfg  : [
        for hzmap, hzmapelement in accounthzcfg  : [
          for hzname, hzcfg in hzmapelement  : {
            "account"                = accountname
            "deleg_zone"             = hzmapelement
            "deleg_zone_name"        = hzname
            "deleg_zone_parent_name" = hzparentname
          }
        ]
      ]
    ]
  ])
  hosted_zones_for_delegation_with_parents = { for hz in local.hosted_zones_for_delegation_with_parents_list : "${hz.account}_${hz.deleg_zone_name}" => hz }

We get variable like this for new flattened data:

debug1 = {
  "hsdevel_hsdevel.domain2.net" = {
    "account" = "hsdevel"
    "deleg_zone" = {
      "hsdevel.domain2.net" = {
        "comment" = "..."
        "tags" = {
          "Environment" = "hsdevel"
        }
      }
    }
    "deleg_zone_name" = "hsdevel.domain2.net"
    "deleg_zone_parent_name" = "domain2.net"
  }
  "hsdevel_hsdevel1.domain1.net" = {
    "account" = "hsdevel"
    "deleg_zone" = {
      "hsdevel1.domain1.net" = {
        "comment" = "..."
        "tags" = {
          "Environment" = "hsdevel"
        }
      }
    }
    "deleg_zone_name" = "hsdevel1.domain1.net"
    "deleg_zone_parent_name" = "domain1.net"
  }
  
 ...
 
}

We use for_each on “hosted_zones_for_delegation_with_parents” to create hosted zone in, e.g. in hsdevel account we’ll have (module.route53_hosted_zones_hsdevel), obtaining this data structure:

debug2 = {
  "hsdevel_hsdevel.domain2.net" = {
    "this_route53_zone_name_servers" = {
      "hsdevel.domain2.net" = [
        "ns-.....org",
        "ns-.....com",
        "ns-.....co.uk",
        "ns-....net",
      ]
    }
    "this_route53_zone_zone_id" = {
      "hsdevel.domain2.net" = "Z0... K0"
    }
  }
  "hsdevel_hsdevel1.domain1.net" = {
    "this_route53_zone_name_servers" = {
      "hsdevel1.domain1.net" = [
        "ns-.....org",
        "ns-.....com",
        "ns-.....co.uk",
        "ns-....net",
      ]
    }
    "this_route53_zone_zone_id" = {
      "hsdevel1.domain1.net" = "Z0...2U"
    }
  }
  "hsdevel_hsdevel2.domain1.net" = {
    "this_route53_zone_name_servers" = {
      "hsdevel2.domain1.net" = [
        "ns-.....org",
        "ns-.....com",
        "ns-.....co.uk",
        "ns-....net",      ]
    }
    "this_route53_zone_zone_id" = {
      "hsdevel2.domain1.net" = "Z0...TJ"
    }
  }

On record creation in shared account (see code below):

### NOTE: provider block does not support interpolation so we need to manage each account separately instead of using plain for_each for all accounts

module "route53_records_for_delegation_hsdevel" {
  source = "..."
  
  for_each =  {
                for hz, hzinfo in local.hosted_zones_for_delegation_with_parents :
                  hz => hzinfo
                  if length(regexall("hsdevel",hz)) > 0
              }

  providers = {
    aws = aws.shared
  }

  zone_name  = each.value.deleg_zone_parent_name

  records = [
    {
      name    = "hsdevel"
      type    = "NS"
      ttl     = 300
      records = module.route53_hosted_zones_hsdevel[each.key].this_route53_zone_name_servers[each.value.deleg_zone]
    },
  ]

  depends_on = [module.route53_hosted_zones_hsdevel]
}

We get error:

Error: Invalid index

  on account_shared.tf line 273, in module "route53_records_for_delegation_hsdevel":
 273:       records = module.route53_hosted_zones_hsdevel[each.key].this_route53_zone_name_servers[each.value.deleg_zone]
    |----------------
    | each.key is "hsdevel_hsdevel2.domain1.net"
    | each.value.deleg_zone is object with 1 attribute "hsdevel2.domain1.net"
    | module.route53_hosted_zones_hsdevel is object with 3 attributes

The given key does not identify an element in this collection value: string
required.
  1. It is possible to convert object attribute in a string to avoid this error and potentially allowing manipulation with string function (extract domani, …, e.g instead of statically define name = "hsdevel" extract hsdevel synamically fromm data strucrture) ? (this simplify a lot also data structure variable definition)

  2. It is possible to print resulting struct create in for_each to be sure of the data on which for_rach works (e.g. as output value) ?

 for_each =  {
                for hz, hzinfo in local.hosted_zones_for_delegation_with_parents :
                  hz => hzinfo
                  if length(regexall("devel",hz)) > 0  # to select only spec accout hosted zones
  }
  1. There is a chance to have nested for_each support in terraform ? It’ll simplify a lot infra configuration with complex data structure allowing infra description more close to objects hierarchy with related properties

  2. There is a chance for provider block interpolation support ?

WORKAROUND
As a workaround for (1.) we just added:

“deleg_zone_name” = hzname

to the flattened data, to have a plain string (adding complexity). This works.

SOLUTION ??
We also probably found a best solution for (1.), using function keys and selecting first element:

keys(each.value.deleg_zone)[0]:

records = module.route53_hosted_zones_hsdevel[each.key].this_route53_zone_name_servers[keys(each.value.deleg_zone)[0]]

It will be appreciated if someone can explain the internal behavior for this.

(Update)

name = “hsdevel” (in module “route53_records_for_delegation_hsdevel”) is wrong, correct with:

name = keys(each.value.deleg_zone)[0]

to allow unique record identification and correctly assign its name servers to delegation zone.

...
records = [
    {
      name = keys(each.value.deleg_zone)[0]
      type    = "NS"
      ttl     = 300
      records = ...
    },
  ]
...