Продовжуємо розбиратись з можливостями Terraform.
В попредньому пості познайомились з типами даних – Terraform: знайомство з типами даних – primitives та complex. Тепер подивимось, як ці типи можна використовувати в циклах.
Terraform підтримує три типи циклів:
count
: самий простий, використовується з заданим числом або з фукнцієюlength()
; використовує індексиlist
абоmap
для ітерації- підходить для створення однакових ресурсів, які не будуть змінюватись
for_each
: має більше можливостей, використовується зmap
абоset
, використовує іммена ключів послідовності для ітерації- підходить для створення однотипних ресурсів, але з можливістю задати різні параметри
for
: використовується для фільтрації та трансмормації об’єктів зlists
,sets
,tuples
абоmaps
; може бути використано разом з такими функціями, якif
,join
,replace
,lower
абоupper
Зміст
Terraform count
Отже, count
самий базовий і перший метод для виконання задач в циклі.
Аргументом приймає або number
, або list
чи map
, виконує ітерацію, і кожному об’єкту задає індекс відповідвідно до його позиції в послідовності.
Наприклад, ми можемо створити три корзини так:
resource "aws_s3_bucket" "bucket" { count = 3 bucket = "bucket-${count.index}" }
В результаті Terraform створить масив (array) з трох корзин з іменами bucket-0, bucket-1 та bucket-2.
Ми також можемо передати список і використати функцію length()
, щоб отримати кількість елементів в цьому списку, і потім пройтись по кожному з них, використовуючи їхні індекси:
variable "projects" { type = list(string) default = ["test-project-1", "test-project-2", "test-project-3"] } resource "aws_s3_bucket" "bucket" { count = length(var.projects) bucket = "bucket-${var.projects[count.index]}" }
В такому випадку будуть створені три корзини з іменами “bucket-test-project-1“, “bucket-test-project-2” та “bucket-test-project-3“.
Щоб отримати значеня імен корзин, які створювались таким чином, можемо використати “*
” для вибору всіх індекісів з масиву aws_s3_bucket.bucket
:
... output "bucket_names" { value = aws_s3_bucket.bucket[*].id }
Але у count
є один важливий нюанс: саме через прив’язку елементів до індексів, ви може отримати несподіваний результат.
Наприклад, якщо створити ці три корзини, а потім додати новий проект на початку або всередені списку, то Terraform видалить коризини для проектів після доданого, бо в списку зміняться індекси об’єктів.
Тобто:
variable "projects" { type = list(string) default = ["test-project-1", "another-test-project", "test-project-2", "test-project-3"] }
Приведе до:
$ terraform apply ... # aws_s3_bucket.bucket[1] must be replaced -/+ resource "aws_s3_bucket" "bucket" { ... ~ bucket = "bucket-test-project-2" -> "bucket-another-test-project" # forces replacement ... # aws_s3_bucket.bucket[2] must be replaced -/+ resource "aws_s3_bucket" "bucket" { ... ~ bucket = "bucket-test-project-3" -> "bucket-test-project-2" # forces replacement ... # aws_s3_bucket.bucket[3] will be created + resource "aws_s3_bucket" "bucket" { ... + bucket = "bucket-test-project-3" ... Plan: 3 to add, 0 to change, 2 to destroy.
І якщо в корзинах є дані, то деплой зупиниться з помилкою BucketNotEmpty
, бо Terraform буде намагатись видалити бакети.
Проте count
чудово підійде, якщо вам треба перевірити умову на кшталт “створювати ресурс чи ні”. Це можна зробити таким чином:
variable "enabled" { type = bool default = true } resource "aws_s3_bucket" "bucket" { count = var.enabled ? 1 : 0 bucket = "bucket-test" }
Тобто якщо enabled = true
, то створюємо 1 корзину, якщо false
– то 0.
Terraform for_each
for_each
довзляє виконувати ітерації більш гнучко.
Він приймає map
або set
, і для ітерації замість індексів використовує кожен key
та value
з послідовності. В такому випадку саме кількість key
буде визначати кількість ресурсів, котрі будуть створені.
Завдяки тому, що кожен key
являється унікальним, зміна значень в set
/map
не впливає на те, як ресурси будуть створені.
Крім set
та map
ви можете використати тип list
, але його треба буде “загорнути” у фунцію toset()
, щоб перетворити на set
, з якого for_each
зможе отримати пару key:value
– в такому випадку значення key
буде == значенню value
.
for_each
з set
та list
Отже, якщо взяти той же ресурс aws_s3_bucket
, то з for_each
ми можемо створити корзини так:
variable "projects" { type = set(string) default = ["test-project-1", "test-project-2", "test-project-3"] } resource "aws_s3_bucket" "bucket" { for_each = var.projects bucket = "bucket-${each.value}" }
Або з variable
з типом list
і toset()
для for_each
:
variable "projects" { type = list(string) default = ["test-project-1", "test-project-2", "test-project-3"] } resource "aws_s3_bucket" "bucket" { for_each = toset(var.projects) bucket = "bucket-${each.value}" }
Але так як в результаті ми отримаємо не масив даних, а map
з окремими об’єктами:
... # aws_s3_bucket.bucket["test-project-1"] will be created ...
І тоді в outputs
просто викликати aws_s3_bucket.bucket[*].id
ни вийде.
Натомість, ми можемо використати функцію values()
щоб отримати всі значення ресурсів aws_s3_bucket.bucket
:
... output "bucket_names" { value = values(aws_s3_bucket.bucket)[*].id }
for_each
з map
Або приклад з map
для створення тегу Name
:
variable "projects" { type = map(string) default = { "test-project-1" = "Test Project 1", "test-project-2" = "Test Project 2", "test-project-3" = "Test Project 3", } } resource "aws_s3_bucket" "bucket" { for_each = var.projects bucket = "bucket-${each.key}" tags = { "Name" = each.value } }
Або з використанням merge()
, щоб додавати загальні теги + тег Name
(див. також default_tags
):
variable "projects" { type = map(string) default = { "test-project-1" = "Test Project 1", "test-project-2" = "Test Project 2", "test-project-3" = "Test Project 3", } } variable "common_tags" { type = map(string) default = { "Team" = "devops", "CreatedBy" = "terraform" } } resource "aws_s3_bucket" "bucket" { for_each = var.projects bucket = "bucket-${each.key}" tags = merge(var.common_tags, {Name = each.value}) }
В результаті отримаємо три теги:
... ~ resource "aws_s3_bucket" "bucket" { id = "bucket-test-project-1" ~ tags = { + "CreatedBy" = "terraform" + "Name" = "Test Project 1" + "Team" = "devops" } ...
for_each
з map of maps та атрибутами
Або можна використати навіть map of maps, і для кожної корзини передавати набір параметрів, і потім звертатись до параметра через each.value.PARAM_NAME
.
Наприклад, в одному параметрі задамо тег Name
, а в іншому – object_lock_enabled
:
variable "projects" { type = map(map(string)) default = { "test-project-1" = { tag_name = "Test Project 1", object_lock_enabled = true }, "test-project-2" = { tag_name = "Test Project 2", object_lock_enabled = false }, "test-project-3" = { tag_name = "Test Project 3", object_lock_enabled = false } } } variable "common_tags" { type = map(string) default = { "Team" = "devops", "CreatedBy" = "terraform" } } resource "aws_s3_bucket" "bucket" { for_each = var.projects bucket = "bucket-${each.key}" object_lock_enabled = each.value.object_lock_enabled tags = merge(var.common_tags, {Name = each.value.tag_name}) }
Результат:
Terraform for
На відміну від count
та for_each
, метод for
використовується не для створення ресурсів, а для операцій фільтрування та трансформації над значеннями змінних.
Ситнаксис для for
виглядає так:
[for <ITEM> in <LIST> : <OUTPUT>]
Тут ITEM – ім’я локальної до циклу змінної, LIST – список, в якому буде виконуватись ітерація, а OUTPUT – результат трансформації.
Наприклад, можемо вивести імена бакетів як UPPERCASE таким чином:
... output "bucket_names" { value = [for a in values(aws_s3_bucket.bucket)[*].id : upper(a)] }
for
та conditionals expressions
Також перед OUTPUT можемо додати фільтр, тобто виконати дію тільки над деякими об’єктами зі списку, наприклад:
output "bucket_names" { value = [for a in values(aws_s3_bucket.bucket)[*].id : upper(a) if can(regex(".*-1", a))] }
Тут ми за допомогою функцій can()
та regex()
перевіряємо значення змінної a
, і якщо вона закінчується на “-1”, то виконуємо upper(a)
:
... bucket_names = [ "BUCKET-TEST-PROJECT-1", ]
for
та ітерація по map
Можно виконати ітерацію над key:value з map
variable:
variable "common_tags" { type = map(string) default = { "Team" = "devops", "CreatedBy" = "terraform" } } output "common_tags" { value = [for a, b in var.common_tags : "Key: ${a} value: ${b}" ] }
В результаті отримаємо об’єкт типу list
зі значеннями:
... common_tags = [ "Key: CreatedBy; Value: terraform;", "Key: Team; Value: devops;", ]
А за допомогою =>
можемо перетворити list
на map
. Крім того, для map замість []
цикл записуємо в {}
:
output "common_tags" { value = { for a, b in var.common_tags : upper(a) => b } }
Отримуємо:
... common_tags = { "CREATEDBY" = "terraform" "TEAM" = "devops" }
for
та for_each
для ітерації над complex objects
Можна зробити єдину змінну, яка буде мати різні типи даних для різних значень, а потім виконати ітерацію з for_each
та for
разом.
Наприклад, створимо variable з типом list
, в якому будуть значення типу object
, а в object
будуть два поля типу string
, та одне для списку тегів з типом list
:
variable "projects" { type = list(object({ name = string object_lock_enabled = string tags = map(string) })) default = [ { name = "test-project-1" object_lock_enabled = "true" tags = { "Name" = "Test Project 1" "Team" = "devops" "CreatedBy" = "terraform" } }, { name = "test-project-2", object_lock_enabled = true, tags = { "Name" = "Test Project 2", "Team" = "devops", "CreatedBy" = "terraform" } }, { name = "test-project-3", object_lock_enabled = true, tags = { "Name" = "Test Project 3", "Team" = "devops", "CreatedBy" = "terraform" } } ] } resource "aws_s3_bucket" "bucket" { for_each = { for a in var.projects : a.name => a } bucket = "bucket-${each.key}" object_lock_enabled = each.value.object_lock_enabled tags = { for key,value in each.value.tags : key => value } }
Потім в ресурсі aws_s3_bucket
в цикл for_each
передаємо значення var.projects.name
, а для тегів робимо цикл по кожному ресурсу з list
, і в кожному ресурсі створюємо key:value з each.value.tags
.
Nested for
loops для map of lists
Для роботи з багаторівневими об’єктами в одному циклі for
можна визивати інший.
Наприклад, маємо список проектів, для кожного є один чи кілька “dev/prod” оточень:
variable "projects" { description = "project names list to be used in S3 and DynamoDB names" type = map(list(string)) default = { atlas-tf-backends-test = [ "prod" ] atlas-eks-test = [ "dev", "prod" ] } }
Щоб побудувати list
з елементами, які будуть містити ім’я проекту + ім’я оточення – використовуємо два for
:
locals { table_names = [ for project, envs in var.projects : [ for env in envs : "${project}-${env}" ] ] } output "dynamodb_table_names" { value = local.table_names }
В результаті отримаємо:
Changes to Outputs: + dynamodb_table_names = [ + [ + "atlas-eks-test-dev", + "atlas-eks-test-prod", ], + [ + "atlas-tf-backends-test-prod", ], ]
А щоб створити єдиний list
замість list[list, list]
– можна використати фунцію flatten
:
locals { table_names = flatten([ for project, envs in var.projects : [ for env in envs : "${project}-${env}" ] ]) }
В результаті отримаємо:
Changes to Outputs: + dynamodb_table_names = [ + "atlas-eks-test-dev", + "atlas-eks-test-prod", + "atlas-tf-backends-test-prod", ]
А щоб побудувати map
, де ключами будуть ім’я проекту + ім’я, а в значенні інший map
– можно використати функцію merge
та оператор “...
“, як наведено в цьому коментарі на GitHub:
locals { table_names_map = merge([ for project, envs in var.projects : { for env in envs : "${project}-${env}" => { "project" = project "env" = env } } ]...) } output "dynamodb_table_names" { value = local.table_names_map }
Результат:
Changes to Outputs: + dynamodb_table_names = { + atlas-eks-test-dev = { + env = "dev" + project = "atlas-eks-test" } + atlas-eks-test-prod = { + env = "prod" + project = "atlas-eks-test" } + atlas-tf-backends-test-prod = { + env = "prod" + project = "atlas-tf-backends-test" } }
for
та String Templates
Документація – Strings and Templates.
Синтаксис для ітерації по map
буде таким:
%{ for <KEY>, <VALE> in <COLLECTION> }<RESULTED_TEXT>%{ endfor }
Тобто, можемо створити текствий файл зі змістом значень змінної:
resource "local_file" "foo" { content = "%{ for a, b in var.common_tags }Key: ${a}\nValue: ${b}\n%{ endfor }" filename = "foo.txt" }
Результат:
$ cat foo.txt Key: CreatedBy Value: terraform Key: Team Value: devops
Готово.
Посилання по темі
- Terraform For Loop
- Terraform tips & tricks: loops, if-statements, and gotchas
- Terraform: Mastering Foreach with List of Maps within a List of Maps
- Choosing Between Count and For-Each
- Terraform For Each Examples – How to use for_each
- How to Use Terraform’s ‘for_each’, with Examples
- Using Loops in Terraform Code
- Terraform Count vs. For Each Meta-Argument – When to Use It
- Terraform Dynamic Blocks
- Terraform For Loop – Expression Overview with Examples