Terraform: инфраструктура на уровне кода — страница 29 из 65

}

Представьте, что вы убрали из этого списка "trinity". Что произойдет при выполнении terraformplan?

$ terraform plan

(...)

Terraform will perform the following actions:

  # aws_iam_user.example[1] will be updated in-place

  ~ resource "aws_iam_user" "example" {

        id            = "trinity"

      ~ name          = "trinity" -> "morpheus"

    }

  # aws_iam_user.example[2] will be destroyed

  - resource "aws_iam_user" "example" {

      - id            = "morpheus" -> null

      - name          = "morpheus" -> null

    }

Plan: 0 to add, 1 to change, 1 to destroy.

Постойте, это не совсем то, чего мы ожидали! Вместо простого удаления пользователя IAM "trinity" вывод plan говорит о том, что Terraform собирается переименовать его в "morpheus" и затем удалить оригинального пользователя с этим именем. Что происходит?

Ресурс, в котором указан параметр count, превращается в список или массив ресурсов. К сожалению, Terraform определяет каждый элемент массива по его позиции (индексу). То есть после первого выполнения apply с именами трех пользователей внутреннее представление массива в Terraform выглядит примерно так:

aws_iam_user.example[0]: neo

aws_iam_user.example[1]: trinity

aws_iam_user.example[2]: morpheus

Если удалить элемент посреди массива, все остальные элементы, которые шли за ним, смещаются назад на позицию. Поэтому после выполнения plan с именами двух пользователей внутреннее представление будет таким:

aws_iam_user.example[0]: neo

aws_iam_user.example[1]: morpheus

Обратите внимание на то, что у имени "morpheus" теперь индекс 1, а не 2. Terraform воспринимает индекс в качестве идентификатора ресурса, поэтому данное изменение можно перефразировать так: «переименовать элемент с индексом 1 в morpheus и удалить элемент с индексом 2». Иными словами, каждый раз, когда вы удаляете находящийся внутри списка ресурс, Terraform удаляет все ресурсы, которые следуют за ним, и воссоздает их заново, с нуля. Ох. Конечно, итоговым результатом будет именно то, о чем вы просили (то есть два пользователя IAM с именами "morpheus" и "neo"), но вряд ли вам хотелось бы достичь этого за счет удаления и изменения ресурсов.

Чтобы вы могли обойти эти два ограничения, в Terraform 0.12 появились выражения for_each.


Циклы с выражениями for_each

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

resource "_" "" {

  for_each =

   [CONFIG ...]

}

PROVIDER — это имя провайдера (например, aws), TYPE — тип ресурса, который нужно создать в этом провайдере (скажем, instance), NAME — идентификатор, с помощью которого можно ссылаться на ресурс на разных участках кода Terraform (вроде my_instance), COLLECTION — множество или ассоциативный массив, который нужно перебрать в цикле (при использовании for_each в сочетании с ресурсом списки не поддерживаются), а CONFIG состоит из одного или нескольких аргументов, предназначенных специально для этого ресурса. Внутри CONFIG можно применять ссылки each.key и each.value для доступа к ключу и значению текущего элемента COLLECTION.

Например, так можно создать тех же трех пользователей IAM с помощью for_each:

resource "aws_iam_user" "example" {

  for_each = toset(var.user_names)

  name     = each.value

}

Обратите внимание на функцию toset, которая превращает список var.user_names во множество. Дело в том, что выражение for_each поддерживает множества и ассоциативные массивы только для ресурсов. При переборе этого множества оно предоставляет имя каждого пользователя в виде each.value. То же самое значение будет доступно и в each.key, хотя эта ссылка обычно используется только в ассоциативных массивах с ключами и значениями.

Ресурс, к которому применяется for_each, становится ассоциативным массивом ресурсов (а не обычным массивом, как в случае с count). Чтобы это продемонстрировать, заменим оригинальные выходные переменные all_arns и neo_arn новой, all_users:

output "all_users" {

  value = aws_iam_user.example

}

Вот что произойдет, если выполнить terraformapply:

$ terraform apply

(...)

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Outputs:

all_users = {

  "morpheus" = {

    "arn" = "arn:aws:iam::123456789012:user/morpheus"

    "force_destroy" = false

    "id" = "morpheus"

    "name" = "morpheus"

    "path" = "/"

    "tags" = {}

  }

  "neo" = {

    "arn" = "arn:aws:iam::123456789012:user/neo"

    "force_destroy" = false

    "id" = "neo"

    "name" = "neo"

    "path" = "/"

    "tags" = {}

  }

  "trinity" = {

    "arn" = "arn:aws:iam::123456789012:user/trinity"

    "force_destroy" = false

    "id" = "trinity"

    "name" = "trinity"

    "path" = "/"

    "tags" = {}

  }

}

Как видите, Terraform создает трех пользователей IAM, а выходная переменная all_users содержит ассоциативный массив, ключи которого (в данном случае имена пользователей) используются в for_each, а значения служат выходными переменными этого ресурса. Если хотите вернуть выходную переменную all_arns, нужно приложить дополнительные усилия, чтобы извлечь соответствующие ARN, добавив встроенную функцию values (которая возвращает только значения ассоциативного массива) и символ *:

output "all_arns" {

  value = values(aws_iam_user.example)[*].arn

}

Мы получим нужный нам вывод:

$ terraform apply

(...)

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

all_arns = [

  "arn:aws:iam::123456789012:user/morpheus",

  "arn:aws:iam::123456789012:user/neo",

  "arn:aws:iam::123456789012:user/trinity",

]

Важно, что у вас теперь есть ассоциативный массив ресурсов с for_each вместо обычного массива ресурсов с count, так как теперь вы можете безопасно удалять его элементы. Например, если опять удалить "trinity" из списка var.user_names и выполнить terraformplan, результат будет следующим:

$ terraform plan

Terraform will perform the following actions:

  # aws_iam_user.example["trinity"] will be destroyed

  - resource "aws_iam_user" "example" {

      - arn           = "arn:aws:iam::123456789012:user/trinity" -> null

      - name          = "trinity" -> null

    }

Plan: 0 to add, 0 to change, 1 to destroy.

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

Рассмотрим еще одно преимущество выражения for_each: его способность создавать множественные вложенные блоки внутри ресурса. Например, с его помощью можно динамически сгенерировать вложенные блоки tag для ASG в модуле webserver-cluster. Чтобы пользователь мог указывать собственные теги, добавим в файл modules/services/webserver-cluster/variables.tf новую входную переменную с ассоциативным массивом под названием custom_tags:

variable "custom_tags" {

  description = "Custom tags to set on the Instances in the ASG"

  type        = map(string)

  default     = {}

}

Далее установим некоторые пользовательские теги в промышленной среде в файле live/prod/services/webserver-cluster/main.tf:

module "webserver_cluster" {

  source = "../../../../modules/services/webserver-cluster"

  cluster_name           = "webservers-prod"

  db_remote_state_bucket = "(YOUR_BUCKET_NAME)"

  db_remote_state_key    = "prod/data-stores/mysql/terraform.tfstate"

  instance_type      = "m4.large"

  min_size           = 2

  max_size           = 10

  custom_tags = {

    Owner      = "team-foo"

    DeployedBy = "terraform"

  }

}

В этом листинге устанавливается несколько полезных тегов: Owner определяет, какой команде принадлежит ASG, а DeployedBy сигнализирует, что данная инфраструктура была развернута с помощью Terraform (это говорит о том, что ее не следует редактировать вручную; см. подраздел «Даже хороший план может оказаться неудачным» на с. 207). Обычно имеет смысл выработать систему тегов внутри своей команды и воплотить ее в виде кода с помощью модулей Terraform.

Итак, вы указали свои теги. Но как назначить их ресурсу aws_autoscaling_group? Для этого нужно циклически перебрать var.custom_tags. Пример этого показан в следующем псевдокоде:

resource "aws_autoscaling_group" "example" {

  launch_configuration = aws_launch_configuration.example.name

  vpc_zone_identifier  = data.aws_subnet_ids.default.ids

  target_group_arns    = [aws_lb_target_group.asg.arn]

  health_check_type    = "ELB"

  min_size = var.min_size

  max_size = var.max_size

  tag {

    key                 = "Name"

    value               = var.cluster_name

    propagate_at_launch = true

  }

  # Это просто псевдокод. Он не будет работать в Terraform.

  for (tag in var.custom_tags) {

    tag {

      key                 = tag.key

      value               = tag.value

      propagate_at_launch = true

    }

  }

}

Псевдокод, приведенный выше, работать не будет. Вместо него мы воспользуемся выражением for_each. Так выглядит синтаксис динамической генерации вложенных блоков:

dynamic "" {

  for_each =

  content {

     [CONFIG...]

  }

}

VAR_NAME — имя переменной, которая будет хранить значение каждой итерации (вместо each), COLLECTION — список или ассоциативный массив, который нужно перебрать, а блок content — это то, что генерируется при каждом проходе. Внутри блока content можно использовать ссылки