如何在 Terraform 资源循环中使用非唯一键创建唯一资源

How to Use a Non-unique Key in Terraform Resource Loop to Create Unique Resources

我想使用通用模块为域创建每种类型的 DNS 记录,这样就可以用类似的方式调用它:

module "example_com_dns" {
  source = "[PATH_TO_MODULES]/modules/dns"

  domain  = "example.com"

  a_records = {
    "@"    = [SOME IP]
    "www"  = [SOME IP]
    "home" = [SOME IP]
  }

  txt_records = {
    "@"                    = "txt-foobar1"
    "@"                    = "txt-foobar2"
    "mail._domainkey.self" = "foobar"
  }

  mx_entries = {
     "10" = "mail.someprovider.com"
     "20" = "mail2.someprovider.com"
  }

  cname_records {
    "cname-foo" = "cname-bar
  }
}

我有一些东西可以很好地用于 A、CNAME 和 MX 记录,但是 TXT 有一个边缘情况,我需要解决这个问题。我的模块具有用于每种记录类型的资源块,运行 通过循环。我就直接贴TXT了,不过都是一样的:

resource "digitalocean_record" "this_txt_record" {
  for_each = var.txt_records

  domain = var.domain
  type   = "TXT"
  name   = each.key
  value  = each.value
}

一切正常,除了,因为有 2 条记录的键为“@”,结果只创建了最后一条(在我的上面的例子,这是“txt-foobar2”):


...

  # module.example_com.digitalocean_record.this_txt_record["@"] will be created
  + resource "digitalocean_record" "this_txt_record" {
      + domain = "example.com"
      + fqdn   = (known after apply)
      + id     = (known after apply)
      + name   = "@"
      + ttl    = (known after apply)
      + type   = "TXT"
      + value  = "txt-foobar2"
    }

我希望它同时创建“txt-foobar1”和“txt-foobar2”,即使在地图中给定非唯一键也是如此。

也许这是错误的方法,我只需要找出一个聪明的循环来代替解析这个结构?:

  txt_records = [
    { "@" = "foo" },
    { "@" = "bar"},
    { "mail._domainkey.self" = "foobar"}
  ]

如果是这样,我目前也失败了:)

无法通过 for_each 列表来创建资源,因为必须有一个唯一键,该键将成为 terraform 资源名称的一部分。列表索引不能成为可靠​​的键,因为如果您重新排序列表中的项目,您的 TF 计划将一团糟。

另一方面,根据定义,地图确实具有唯一键。

不过您可以从列表中生成地图!我发现了这个小技巧here。请注意,您还需要手动计算唯一映射键(下例中的 ${txt_record[0]}=${txt_record[1]})。

您的更新资源:

module "example_com_dns" {
  ...

  txt_records = [
    ["@",                     "txt-foobar1"],
    ["@",                     "txt-foobar2"],
    ["mail._domainkey.self",  "foobar"],
  ]
}
resource "digitalocean_record" "this_txt_record" {
  for_each = {for txt_record in var.txt_records: "${txt_record[0]}=${txt_record[1]}" => txt_record}

  domain = var.domain
  type   = "TXT"
  name   = each.value[0]
  value  = each.value[1]
}

或者如果您愿意,可以稍微详细一点:

module "example_com_dns" {
  ...

  txt_records = [
    {name: "@", value: "txt-foobar1"},
    {name: "@", value: "txt-foobar2"},
    {name: "mail._domainkey.self", value: "foobar"},
  ]
}
resource "digitalocean_record" "this_txt_record" {
  for_each = {for txt_record in var.txt_records: "${txt_record.name}=${txt_record.value}" => txt_record}

  domain = var.domain
  type   = "TXT"
  name   = each.value.name
  value  = each.value.value
}

已经给出的另一种方法是使用以下方法:

 variable "txt_records" {
  default = {
       "@" = ["foo", "bar"],
       "mail._domainkey.self" = ["foobar"]
       }
 }

然后您可以使用以下方法展平 txt_records

locals {

  txt_records_flat = merge([
      for key, values in var.txt_records:
        {for value in values: 
           "${key}-${value}" => {"record_name" = key, "record_value" = value}
        }
  ]...)
}

这导致 local.txt_records_flat 个:

{
  "@-bar" = {
    "record_name" = "@"
    "record_value" = "bar"
  }
  "@-foo" = {
    "record_name" = "@"
    "record_value" = "foo"
  }
  "mail._domainkey.self-foobar" = {
    "record_name" = "mail._domainkey.self"
    "record_value" = "foobar"
  }
}

那你就用它:

resource "digitalocean_record" "this_txt_record" {
  for_each = local.txt_records_flat

  domain = var.domain
  type   = "TXT"
  name   = each.value.record_name
  value  = each.value.record_value
}