Terraform for_each vs count

05/06/2020

Если в 2-х словах, то count - это итерация по листу, причем всегда содержащему только целочисленные элементы, например [0, 1, 2, 3, n], а for_each - это итерация по корневым (top level) ключам словаря, которые в свою очередь уже могут содержать данные любого типа. Пока, может, не очень понятно, но давайте разбираться на примерах.

Terraform count

Возмем, к примеру, создание EC2 инстанса в AWS:

data "aws_ami" "debian_buster" {
  filter {
    name = "name"
    values = [ "debian-10-amd64-*" ]
  }
  most_recent = true
  owners = [
    "136693071363"
  ] // https://wiki.debian.org/Cloud/AmazonEC2Image
}
 
resource "aws_instance" "web" {
  instance_type = "t2.micro"
  ami = data.aws_ami.debian_buster.id
  tags = {
    Name = "WebServer"
  }
}

Данный кусок кода поднимает EC2 инстанс t2.micro в AWS с Debian Buster внутри. Теперь скажем, мы вообще-то хотим 3 инстанса и будем гонять там Kafka. Тут на помощь приходит terraform count. Просто добавим его в начало декларации ресурса:

resource "aws_instance" "web" {
  count = 3
 
  instance_type = "t2.micro"
  ami           = data.aws_ami.debian_buster.id
  tags = {
    Name = "WebServer-${count.index + 1}"
  }
}

Такой модуль поднимет уже 3 абсолютно одинаковых EC2 инстанса, изменив лишь имя. Обратите внимание, в конце имени мы добавили count.index - специальную переменную, которая активируется, когда вы декларируете count, и обозначает текущую итерацию начиная с нуля. Собственно поэтому мы и добавили "+ 1", чтобы избежать появление инстанса с именем, содержащим 0.

Обращаться к ресурсам, имеющим count внутри, нужно по их индексу. Например, если мы хотим вывести ID интсанца 0, достаточно просто добавить индекс к имени ресурса:

output "instance_0_id" {
 value = aws_spot_instance_request.web[0]["spot_instance_id"]
}

Можно также обратиться ко всем инстансам сразу, используя звездочку:

output "instance_ids" {
  value = aws_spot_instance_request.web.*.spot_instance_id

}

Неплохо, да? Да, но в чем же тут проблема?

Terraform count идеально работает, когда вам нужно поднять несколько экземпляров одного и того же ресурса с одинаковыми параметрами. Проблемы начинаются, когда вам нужно поменять что-то еще, нежели просто добавить 1, 2 или 3 к имени. Предположим, я хочу не просто поднять несколько инстансов, но еще и дать им разный тип и, возможно, аллоцировать публичный IP адрес только для одного инстанса.

Вы скажете, просто скопируй и вставь тот кусок кода, что уже есть, и поменяй нужные параметры. Действительно, так можно сделать, и пользователи terraform 0.11 так и делают. Но это увеличивает количество дублирующегося кода и может негативно влиять на читаемость, если проект действительно большой. Terraform версии 0.12 предлагает использовать for_each - оператор, который позволяет итерироваться по словарю и, соответственно, регулировать параметры на более глубоком уровне.

Terraform for_each

Давайте изменим наш образец кода, заменив count на for_each:

resource "aws_instance" "server" {
  for_each = {
    web = { type = "t2.micro", public_ip = true },
    db  = { type = "m5.large", public_ip = false }
  }
 
  instance_type = each.value["type"]
  ami           = data.aws_ami.debian_buster.id
  associate_public_ip_address = each.value["public_ip"]
  tags = {
    Name = "each.key"
  }
}

Как видите, мы уже не просто итерируемся от 0 до n, мы держим на бэкенде целый набор переменных. Terraform применяет имя ключа словаря к переменной each.key, а значение ключа к переменной each.value. Соответственно, если в each.value вы держите дополнительный набор ключей, то получить к ним доступ можно просто указав его имя в квадратных скобках each.value["KEY_NAME"]

На этом возможности terraform for_each не исчерпываются. Пример выше - это пример со статическим словарем. Terraform поддерживает динамические словари. Как это работает?

Хорошим примером является создание подсетей. Мы определились, что в нашей инфраструктуре будет 2 сервера: веб-сервер и сервер баз данных. Логично предоставить доступ в интернет для первой машины, но не для второй, так как она может содержать приватные данные. В AWS это достигается путем линковки маршрута по-умолчанию на Интернет Шлюз (Intenet Gateway). Соответственно, одна подсеть будет иметь маршрут по-умолчанию, а другая нет.

Задекларируем переменную:

variable "subnets" {
  description = ""
  type = list(object(
    {
      name        = string
      ig_attached = bool
    }
  ))
  default = [
    {
      name        = "public"
      ig_attached = true
    },
    {
      name        = "private"
      ig_attached = false
    }
  ]
}

И посмотрим, как мы может ее использовать в terraform for_each:

resource "aws_vpc" "vpc" {
  cidr_block = "10.0.0.0/16"
  assign_generated_ipv6_cidr_block = false
  enable_dns_hostnames = true
}
 
resource "aws_route_table" "table" {
  for_each = {
    for subnet in var.subnets: subnet.name => subnet
  }
 
  vpc_id = aws_vpc.vpc.id
  tags = {
    "Name" = each.key
  }
}
 
resource "aws_route" "public_defaut_gateway_ipv4" {
  for_each = {
    for subnet in var.subnets:
      subnet.name => subnet if lookup(subnet, "ig_attached", false)
  }
 
  route_table_id = aws_route_table.table[each.key]["id"]
  destination_cidr_block = "0.0.0.0/0"
  gateway_id = aws_internet_gateway.gateway.id
}

В данном примере, словарь для terraform for_each создается "на лету" на основе переменной var.subnets. Ключи словаря принимают значение имен подсетей, тело словаря берет все остальное. Особенно интересен должен быть ресурс "aws_route", так как он не просто использует динамический словать, но еще и приправляет его условной конструкцией - сеть попадает в итоговый словарь, только если "ig_attached" установлен в значение "true".

Осталось поговорить о том, как обращаться к ресурсам с for_each. Точно так же, как и к ресурсам с count, только вместо индекса необходимо указывать имя - то, что ассайнится в each.key

output "route_table_ids" {
  description = "Produces a map { 'subnet_name': 'route_table_id'] }"
  value = {
    for subnet in var.subnets :
    subnet["name"] => aws_route_table.table[subnet["name"]]["id"]
  }
}

Темы:

Добавить комментарий