Продовжуємо розбиратись з можливостями 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
