При использовании for_each terraform вы должны указать уникальный идентификатор, который будет использоваться как способ связать сгенерированный ресурс с его исходным определением.
Я хотел бы использовать для этого естественный индекс, а не произвольное уникальное значение. В этом случае я работаю с DNS, поэтому естественным индексом будет имя записи DNS (FQDN)... Только это не всегда уникально; то есть у вас может быть несколько записей A для example.com, чтобы обеспечить балансировку нагрузки, или у вас может быть несколько записей TXT для проверки нескольких поставщиков.
Есть ли способ объединить натуральный индекс с вычисленным значением, чтобы получить уникальное значение; например Итак, у нас есть естественный индекс, за которым следует 1, если это значение встречается впервые, 2 для первого дубликата и т. д.?
Я работаю над переносом наших DNS-записей для управления через IaC с помощью Terraform/Terragrunt (это для сценариев, в которых записи управляются вручную, а не для тех, где связанная служба также находится в IaC). Я надеюсь хранить данные записи в файлах CSV (или подобных), чтобы те, кто управляет записями изо дня в день, не требовали знакомства с TF/TG; вместо этого позволяя им просто обновлять данные, а конвейер позаботится обо всем остальном.
Формат CSV будет примерно таким:
Примечание. Я считаю, что каждая зона 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, приходилось вручную управлять такими полями; т. е. я мог бы предоставить еще один столбец и попросить заполнить его, как показано ниже... но это требует человеческой ошибки и добавляет сложности:
Есть ли способ использовать цикл for_each с данными CSV, как показано ниже, при работе с уникальным ограничением?





Вы можете добавить уникальные ключи в структуру данных:
locals {
instances = csvdecode(file("myDnsRecords.csv"))
instance_map = zipmap(range(0,length(local.instances)), local.instances)
}
resource "..." "..." {
for_each = local.instance_map
...
}
@JohnLBevan Хороший вопрос .. Я только что попробовал, и похоже, что терраформ достаточно умен, так что это будет 1 удаление и 4 нетронутых записи.
Это фантастическая новость - спасибо, Пауло (и извините, что не проверил себя; все еще на ранних стадиях, поэтому пока не на чем протестировать)
Получает ли это то, что вы ищете?
Набор данных
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 Не беспокойтесь, все дело в том, что ваша проблема решена. Мне пришлось иметь дело с похожим случаем, когда мне пришлось расширить список IP-адресов для создания правил NACL, поэтому вам не нужно добавлять каждое правило для каждого IP-адреса в коде.
Выражения 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.
Это потрясающе - спасибо; хорошее замечание по части рекорда; это кажется наиболее подходящим решением для этого сценария. Спасибо
Хороший - так что, по сути, создайте карту между «номером строки» (индекс записи, начиная с 0) и самими данными строки. Однако один вопрос: если бы я создал 5 записей с этим, затем удалил первую и повторно запустил ее, будет ли она отображаться как 1 удаление и 4 нетронутых записи, или это будет 4 изменения и 1 удаление (т.е. потому что значение, связанное с каждый индекс теперь изменился, хотя теперь они просто смещены)?