skip to Main Content

I want to create Hosted zones and Route53 records in it in AWS.
For that reason I am creating such a variable:

variable "hosted_zones_domains" {
  description = "Map of hosted zones to their domains and corresponding IPs"
  type = map(map(string))
  default = {
    "first-example.com" = {
       "beta.first-example.com"  = "192.168.0.1",
       "stage.first-example.com" = "192.168.0.2",
       "prod.first-example.com" = "192.168.0.3"
    },
    "second-example.com" = {
       "beta.second-example.com"  = "192.168.1.1",
       "stage.second-example.com" = "192.168.1.2",
       "prod.second-example.com" = "192.168.1.3"
    }
  }
}

I am able to create Hosted zones

resource "aws_route53_zone" "subdomain_zone" {
  for_each = var.hosted_zones_domains
  name     = each.key
    
  tags = {
    Environment = var.environment
  }
}

But I have no idea how to iterate through the inner map to get records names and values and use it in aws_route53_record resource.

2

Answers


  1. Chosen as BEST ANSWER

    OK, so this is how I make that to work.

    in my root directory I've added locals

    locals {
      name             = "eks"
      eks_cluster_name = "${local.name}-${var.environment}"
      hosted_zones_domains = {
        "first-example.com" = toset([
          "app1",
          "app2",
          "app3"
        ]),
        "second-example.com" = toset([
          "app1",
          "app2",
          "app3"
        ])
        }
      hosted_zones_domains_list = { for zone, domains in local.hosted_zones_domains : zone => tolist(domains) }
      zone_domain_pairs = flatten([
        for zone, domains in local.hosted_zones_domains_list : [
          for domain in domains : {
            zone   = zone
            domain = domain
          }
        ]
      ])
    }
    

    Then passed that to submodule zone_domain_pairs = local.zone_domain_pairs

    And then in submodule something like that

    resource "aws_route53_zone" "subdomain_zone" {
      for_each = toset([for pair in var.zone_domain_pairs : pair.zone])
    
      name     = each.key
    
      tags = {
        Environment = var.environment
      }
    }
    resource "aws_route53_record" "zone_records" {
      for_each = { for pair in var.zone_domain_pairs : "${pair.zone}.${pair.domain}" => pair }
    
      zone_id = aws_route53_zone.subdomain_zone[each.value.zone].id
      name    = each.value.domain
      type    = "CNAME"
      ttl     = 300
      records = [aws_lb.eks_alb.dns_name]
    }
    

    Thanks guys for your help!


  2. The main rule for for_each is that you need a map with one element per instance of the resource you want to declare.

    In this case, your for_each meets that requirement for the zones — each element of the top-level map represents one zone — but it’s not yet suitable for declaring the records themselves. You’ll need to derive a new map that has one element per record across all of the zones.

    You can derive a suitable flattened map with one element per recordset (which Route53 confusingly calls a "record") like this:

    locals {
      all_records = tomap(merge([
        for zone_name, records in var.hosted_zones_domains : {
          for hostname, ip_addr in records : hostname => {
            zone_name = zone_name
            ip_addr   = ip_addr
          }
        }
      ]...))
    }
    

    With the default value you showed for the variable, the value of local.all_records would be the following:

    tomap({
      "beta.first-example.com" = {
        zone_name = "first-example.com"
        ip_addr   = "192.168.0.1"
      }
      "stage.first-example.com" = {
        zone_name = "first-example.com"
        ip_addr   = "192.168.0.2"
      }
      "prod.first-example.com" = {
        zone_name = "first-example.com"
        ip_addr   = "192.168.0.3"
      }
      "beta.second-example.com" = {
        zone_name = "second-example.com"
        ip_addr   = "192.168.1.1"
      }
      "stage.second-example.com" = {
        zone_name = "second-example.com"
        ip_addr   = "192.168.1.2"
      }
      "prod.second-example.com" = {
        zone_name = "second-example.com"
        ip_addr   = "192.168.1.3"
      }
    })
    

    Notice that the zone names are now part of the values of the map, and that the records themselves are toplevel.

    Your record names already include the name of the zone they belong to, so the record name alone is a sufficient unique key for tracking each element, which avoids the usual need to concatenate multiple keys together when doing flattening in this way. If you’d like to see an example which does need key concatenation (In case you need to solve a similar problem in future where the inner keys are not sufficiently unique, refer to Flattening nested structures for for_each for another example which achieves a similar effect in a different way.)

    You can then use local.all_records as the for_each for your Route53 zone resource:

    resource "aws_route53_record" "example" {
      for_each = local.all_records
    
      zone_id = aws_route53_zone.subdomain_zone[each.value.zone_name].id
      name    = each.key
      type    = "A"
      records = [each.value.ip_addr]
    }
    

    Notice that each.value.zone_name will always match one of the keys from the original var.hosted_zones_domains value, and so will also match one of the instance keys of aws_route53_zone.subdomain_zone. Therefore you can use this connection to correlate the records with the zones so that each record ends up attached to the zone it ought to be attached to.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search