Использование for_each в terraform с данными, у которых нет уникального ключа

При использовании for_each terraform вы должны указать уникальный идентификатор, который будет использоваться как способ связать сгенерированный ресурс с его исходным определением.

Я хотел бы использовать для этого естественный индекс, а не произвольное уникальное значение. В этом случае я работаю с DNS, поэтому естественным индексом будет имя записи DNS (FQDN)... Только это не всегда уникально; то есть у вас может быть несколько записей A для example.com, чтобы обеспечить балансировку нагрузки, или у вас может быть несколько записей TXT для проверки нескольких поставщиков.

Есть ли способ объединить натуральный индекс с вычисленным значением, чтобы получить уникальное значение; например Итак, у нас есть естественный индекс, за которым следует 1, если это значение встречается впервые, 2 для первого дубликата и т. д.?

Конкретное требование/контекст

Я работаю над переносом наших DNS-записей для управления через IaC с помощью Terraform/Terragrunt (это для сценариев, в которых записи управляются вручную, а не для тех, где связанная служба также находится в IaC). Я надеюсь хранить данные записи в файлах CSV (или подобных), чтобы те, кто управляет записями изо дня в день, не требовали знакомства с TF/TG; вместо этого позволяя им просто обновлять данные, а конвейер позаботится обо всем остальном.

Формат CSV будет примерно таким:

мой ID RecordName Тип Ценить 1 А 1.2.3.4 2 А 2.3.4.5 3 тест А 3.4.5.6 4 тест А 4.5.6.7 5 www cname пример.com

Примечание. Я считаю, что каждая зона DNS будет иметь папку со своим именем и CSV-файл, отформатированный, как указано выше, который содержит записи для этой зоны; так что приведенное выше будет в папке /example.com/, и, таким образом, у нас будет 2 записи A для example.com, 2 для test.example.com и одна CName для www.example.com, которая указывает на example.com.

locals {
  instances = csvdecode(file("myDnsRecords.csv"))
}

resource aws_route53_zone zone {
  name = var.domainname
  provider = aws
}

resource aws_route53_record route53_entry {
  for_each = {for inst in local.instances : inst.myId => inst}
  name = "${each.value.RecordName}${each.value.RecordName == "" ? "" : "."}${var.domainname}"
  type = each.value.Type
  zone_id = aws_route53_zone.zone.zone_id
  ttl = 3600
  records = [each.value.Value]
}

Однако мне не нужна колонка myId; поскольку это не добавляет ценности / не имеет реального отношения к записям; поэтому, если бы мы удалили/вставили запись в начале CSV и перенумеровали следующие записи, это привело бы к необходимости внесения ряда изменений в записи, которые на самом деле не изменились, только потому, что их связанный «индекс» изменился.

Я также не хочу, чтобы тем, кто работает с этими файлами CSV, приходилось вручную управлять такими полями; т. е. я мог бы предоставить еще один столбец и попросить заполнить его, как показано ниже... но это требует человеческой ошибки и добавляет сложности:

мой ID RecordName Тип Ценить 1 А 1.2.3.4 2 А 2.3.4.5 тест1 тест А 3.4.5.6 тест2 тест А 4.5.6.7 www1 www cname пример.com

Вопрос

Есть ли способ использовать цикл for_each с данными CSV, как показано ниже, при работе с уникальным ограничением?

RecordName Тип Ценить А 1.2.3.4 А 2.3.4.5 тест А 3.4.5.6 тест А 4.5.6.7 www cname пример.com
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
75
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вы можете добавить уникальные ключи в структуру данных:

locals {
  instances = csvdecode(file("myDnsRecords.csv"))
  instance_map = zipmap(range(0,length(local.instances)), local.instances)
}

 resource "..." "..." {
   for_each = local.instance_map
   ...
 }

Хороший - так что, по сути, создайте карту между «номером строки» (индекс записи, начиная с 0) и самими данными строки. Однако один вопрос: если бы я создал 5 записей с этим, затем удалил первую и повторно запустил ее, будет ли она отображаться как 1 удаление и 4 нетронутых записи, или это будет 4 изменения и 1 удаление (т.е. потому что значение, связанное с каждый индекс теперь изменился, хотя теперь они просто смещены)?

JohnLBevan 26.01.2023 15:32

@JohnLBevan Хороший вопрос .. Я только что попробовал, и похоже, что терраформ достаточно умен, так что это будет 1 удаление и 4 нетронутых записи.

Paolo 26.01.2023 15:43

Это фантастическая новость - спасибо, Пауло (и извините, что не проверил себя; все еще на ранних стадиях, поэтому пока не на чем протестировать)

JohnLBevan 26.01.2023 15:50

Получает ли это то, что вы ищете?

Набор данных

RecordName,Type,Value
,A,1.2.3.4
,A,2.3.4.5
test,A,3.4.5.6
test,A,4.5.6.7
www,cname,example.com

main.tf

locals {
  records = [for pref in {for _, key in distinct([for i, v in csvdecode(file("myDnsRecords.csv")): v.RecordName]): key => [for r in csvdecode(file("myDnsRecords.csv")): r if key == r.RecordName]}: {for i, r in pref: ("${r.RecordName}_${i}") => r}]
}

output "test" {
  value = local.records
}

Выход

Changes to Outputs:
  + test = [
      + {
          + _0 = {
              + RecordName = ""
              + Type       = "A"
              + Value      = "1.2.3.4"
            }
          + _1 = {
              + RecordName = ""
              + Type       = "A"
              + Value      = "2.3.4.5"
            }
        },
      + {
          + test_0 = {
              + RecordName = "test"
              + Type       = "A"
              + Value      = "3.4.5.6"
            }
          + test_1 = {
              + RecordName = "test"
              + Type       = "A"
              + Value      = "4.5.6.7"
            }
        },
      + {
          + www_0 = {
              + RecordName = "www"
              + Type       = "cname"
              + Value      = "example.com"
            }
        },
    ]

You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.

Хороший - да, это то, что мне нужно; Спасибо. Я оставил ответ Паоло в качестве принятого ответа, поскольку он решает проблему с меньшей сложностью, чем то, что я предложил/вы создали; но когда-нибудь возникнет проблема с индексом, о которой я беспокоился, ваш будет идеальным. Еще раз спасибо.

JohnLBevan 26.01.2023 15:56

@JohnLBevan Не беспокойтесь, все дело в том, что ваша проблема решена. Мне пришлось иметь дело с похожим случаем, когда мне пришлось расширить список IP-адресов для создания правил NACL, поэтому вам не нужно добавлять каждое правило для каждого IP-адреса в коде.

Prav 26.01.2023 15:59
Ответ принят как подходящий

Выражения Terraform for при построении сопоставления имеют «режим группировки», в котором он позволяет дублировать ключи в обмен на значения карты, все из которых представляют собой списки потенциально множественных значений, которые все имеют один и тот же ключ.

Поэтому я бы начал с использования этого для проецирования данных CSV в значение map(list(map(string))), где ключи строятся из имени и типа записи, например:

locals {
  records_raw = csvdecode(file("${path.module}/myDnsRecords.csv"))

  records_grouped = tomap({
    for row in local.records_raw :
    "${row.RecordName} ${row.RecordType}" => row...
  })
}

Результирующая структура данных будет иметь следующую форму:

records_grouped = tomap({
  " A" = tolist([
    { RecordName = "", Type = "A", Value = "1.2.3.4" },
    { RecordName = "", Type = "A", Value = "2.3.4.5" },
  ])
  "test A" = tolist([
    { RecordName = "test", Type = "A", Value = "3.4.5.6" },
    { RecordName = "test", Type = "A", Value = "4.5.6.7" },
  ])
  "www CNAME" = tolist([
    { RecordName = "www", Type = "CNAME", Value = "example.com" },
  ])
})

Сбор записей с общими ключами в списки означает, что теперь у нас есть индекс списка для каждой из них, уникальный только в пределах записей с общим ключом.

Итак, теперь мы можем спроецировать это еще раз на плоскую карту карт (map(map(string))), включив эти индексы списка в ключи карты:

locals {
  records = tomap(merge([
    for group_key, group in local.records_grouped : {
      for idx, record in group :
      "${group_key} ${idx}" => group
    } 
  ]...))
}

Это должно создать структуру данных, подобную следующей:

records = tomap({
  " A 0"        = { RecordName = "", Type = "A", Value = "1.2.3.4" }
  " A 1"        = { RecordName = "", Type = "A", Value = "2.3.4.5" }
  "test A 0"    = { RecordName = "test", Type = "A", Value = "3.4.5.6" }
  "test A 1"    = { RecordName = "test", Type = "A", Value = "4.5.6.7" }
  "www CNAME 0" = { RecordName = "www", Type = "CNAME", Value = "example.com" }
})

Эта структура данных имеет подходящую форму для выражения for_each, поэтому, наконец:

resource "aws_route53_record" "example" {
  for_each = local.records

  name    = "${each.value.RecordName}${each.value.RecordName == "" ? "" : "."}${var.domainname}"
  type    = each.value.Type
  zone_id = aws_route53_zone.zone.zone_id
  ttl     = 3600
  records = [each.value.Value]
}

Это создаст уникальные ключи экземпляра для каждой записи в исходном CSV-файле, сохраняя при этом все отдельные пары (имя, тип) разделенными, чтобы вы могли добавлять новые, не нарушая существующие записи:

  • aws_route53_record.example[" A 0"]
  • aws_route53_record.example[" A 1"]
  • aws_route53_record.example["test A 0"]
  • ...и т. д

Вы упомянули о необходимости отдельного экземпляра для каждой строки в вашем CSV-файле, но я также хотел отметить, что тип ресурса aws_route53_record уже предназначен для управления несколькими записями с одинаковым именем и типом вместе, и поэтому я думаю, что на самом деле было бы неплохо оставить записи сгруппированы вместе. (Название `aws_route53_record немного неправильное, поскольку каждый экземпляр этого типа ресурса управляет набором записей, а не одной записью.)

Вот вариант, который работает таким образом:

locals {
  records_raw = csvdecode(file("${path.module}/myDnsRecords.csv"))

  record_groups = tomap({
    for row in local.records_raw :
    "${row.RecordName} ${row.RecordType}" => row...
  })
  recordsets = tomap({
    for group_key, group in local.record_groups : group_key => {
      name   = group[0].Name
      type   = group[0].Type
      values = group[*].Value
    } 
  })
}

resource "aws_route53_record" "example" {
  for_each = local.recordsets

  name    = "${each.value.name}${each.value.name == "" ? "" : "."}${var.domainname}"
  type    = each.value.type
  zone_id = aws_route53_zone.zone.zone_id
  ttl     = 3600
  records = each.value.values
}

На этот раз окончательная карта имеет один элемент на набор записей вместо одного элемента на запись после группировки всех отдельных записей вместе с использованием их имен и типов. Теперь вам вообще не нужны синтетические индексы, потому что пара имени и типа является естественным уникальным идентификатором для набора записей Route53.

Это потрясающе - спасибо; хорошее замечание по части рекорда; это кажется наиболее подходящим решением для этого сценария. Спасибо

JohnLBevan 27.01.2023 09:59

Другие вопросы по теме