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

  instance_type = "t2.micro"

}

Поскольку параметру count присвоено статическое значение, этот код заработает без проблем: когда вы выполните команду apply, он создаст три сервера EC2. Но если вам захотелось развернуть по одному серверу в каждой зоне доступности (Availability Zone или AZ) в рамках текущего региона AWS? Вы можете сделать так, чтобы ваш код загрузил список зон из источника данных aws_availability_zones и затем «циклически» прошелся по каждой из них и создал в ней сервер EC2, используя параметр count и доступ к массиву по индексу:

resource "aws_instance" "example_2" {

  count             = length(data.aws_availability_zones.all.names)

  availability_zone = data.aws_availability_zones.all.names[count.index]

  ami               = "ami-0c55b159cbfafe1f0"

  instance_type     = "t2.micro"

}

data "aws_availability_zones" "all" {}

Этот код тоже будет прекрасно работать, поскольку параметр count может без проблем ссылаться на источники данных. Но что произойдет, если количество серверов, которые вам нужно создать, зависит от вывода какого-то ресурса? Чтобы это продемонстрировать, проще всего взять ресурс random_integer, который, как можно догадаться по названию, возвращает случайное целое число:

resource "random_integer" "num_instances" {

  min = 1

  max = 3

}

Этот код генерирует случайное число от 1 до 3. Посмотрим, что случится, если мы попытаемся использовать вывод result этого ресурса в параметре count ресурса aws_instance:

resource "aws_instance" "example_3" {

  count         = random_integer.num_instances.result

  ami           = "ami-0c55b159cbfafe1f0"

  instance_type = "t2.micro"

}

Если выполнить для этого кода terraformplan, получится следующая ошибка:

Error: Invalid count argument

  on main.tf line 30, in resource "aws_instance" "example_3":

  30: count = random_integer.num_instances.result

The "count" value depends on resource attributes that cannot be determined

until apply, so Terraform cannot predict how many instances will be created.

To work around this, use the -target argument to first apply only the

resources that the count depends on.

Terraform требует, чтобы count и for_each вычислялись на этапе планирования, до создания или изменения каких-либо ресурсов. Это означает, что count и for_each могут ссылаться на литералы, переменные, источники данных и даже списки ресурсов (при условии, что их длину можно определить во время планирования), но не на вычисляемые выходные переменные ресурса.


count и for_each нельзя использовать в конфигурации модуля

Когда-нибудь у вас может появиться соблазн добавить параметр count в конфигурации модуля:

module "count_example" {

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

  count = 3

  cluster_name  = "terraform-up-and-running-example"

  server_port   = 8080

  instance_type = "t2.micro"

}

Этот код пытается использовать count внутри модуля, чтобы создать три копии ресурса webserver-cluster. Или, возможно, вам захочется сделать подключение модуля опциональным в зависимости от какого-нибудь булева условия, присвоив его параметру count значение 0. Такой код будет выглядеть вполне разумно, однако в результате выполнения terraformplan вы получите такую ошибку:

Error: Reserved argument name in module block

  on main.tf line 13, in module "count_example":

  13: count = 3

The name "count" is reserved for use in a future version of Terraform.

К сожалению, на момент выхода Terraform 0.12.6 использование count или for_each в ресурсе module не поддерживается. Согласно заметкам о выпуске Terraform 0.12 (http://bit.ly/3257bv4) компания HashiCorp планирует добавить эту возможность в будущем, поэтому, в зависимости от того, когда вы читаете эту книгу, она уже может быть доступна. Чтобы узнать наверняка, почитайте журнал изменений Terraform по адресу https://github.com/hashicorp/terraform/blob/master/CHANGELOG.md.


Ограничения развертываний с нулевым временем простоя

Использование блока create_before_destroy в сочетании с ASG является отличным решением для организации развертываний с нулевым временем простоя, если не считать один нюанс: правила автомасштабирования при этом не поддерживаются. Или, если быть более точным, это сбрасывает размер ASG обратно к min_size при каждом развертывании, что может стать проблемой, если вы использовали правила автомасштабирования для увеличения количества запущенных серверов.

Например, модуль webserver-cluster содержит пару ресурсов aws_autoscaling_schedule, которые в 9 утра увеличивают количество серверов в кластере с двух до десяти. Если выполнить развертывание, скажем, в 11 утра, новая группа ASG загрузится не с десятью, а всего с двумя серверами и будет оставаться в таком состоянии до 9 утра следующего дня.

Это ограничение можно обойти несколькими путями.

• Поменять параметр recurrence в aws_autoscaling_schedule с 09*** («запускать в 9 утра») на что-то вроде 0-599-17*** («запускать каждую минуту с 9 утра до 5 вечера»). Если в ASG уже есть десять серверов, повторное выполнение этого правила автомасштабирования ничего не изменит, что нам и нужно. Но если группа ASG развернута совсем недавно, это правило гарантирует, что максимум через минуту количество ее серверов достигнет десяти. Это не совсем элегантный подход, и большие скачки с десяти до двух серверов и обратно тоже могут вызвать проблемы у пользователей.

• Создать пользовательский скрипт, который применяет API AWS для определения количества активных серверов в ASG, вызвать его с помощью внешнего источника данных (см. пункт «Внешний источник данных» на с. 249) и присвоить параметру desired_capacity группы ASG значение, возвращенное этим скриптом. Таким образом, каждый новый экземпляр ASG всегда будет запускаться с той же емкостью, что и старый. Недостаток в том, что применение пользовательских скриптов ухудшает переносимость вашего кода Terraform и усложняет его обслуживание.

Конечно, в идеале в Terraform должна быть встроенная поддержка развертываний с нулевым временем простоя, но по состоянию на май 2019 года команда HashiCorp не планировала добавлять эту функциональность (подробности — по адресу git­hub.com/hashicorp/terraform/issues/1552).


Корректный план может быть неудачно реализован

Иногда при выполнении команды plan получается вполне корректный план развертывания, однако команда apply возвращает ошибку. Попробуйте, к примеру, добавить ресурс aws_iam_user с тем же именем, которое вы использовали для пользователя IAM, созданного вами ранее в главе 2:

resource "aws_iam_user" "existing_user" {

  # Подставьте сюда имя уже существующего пользователя IAM,

  # чтобы попрактиковаться в использовании команды terraform import

  name = "yevgeniy.brikman"

}

Теперь, если выполнить команду plan, Terraform выведет на первый взгляд вполне разумный план развертывания:

Terraform will perform the following actions:

  # aws_iam_user.existing_user will be created

  + resource "aws_iam_user" "existing_user" {

      + arn           = (known after apply)

      + force_destroy = false

      + id            = (known after apply)

      + name          = "yevgeniy.brikman"

      + path          = "/"

      + unique_id     = (known after apply)

    }

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

Если выполнить команду apply, получится следующая ошибка:

Error: Error creating IAM User yevgeniy.brikman: EntityAlreadyExists:

User with name yevgeniy.brikman already exists.

  on main.tf line 10, in resource "aws_iam_user" "existing_user":

  10: resource "aws_iam_user" "existing_user" {

Проблема, конечно, в том, что пользователь IAM с таким именем уже существует. И это может случиться не только с пользователями IAM, но и почти с любым ресурсом. Возможно, кто-то создал этот ресурс вручную или с помощью командной строки, но, как бы то ни было, совпадение идентификаторов приводит к конфликтам. У этой ошибки существует множество разновидностей, которые часто застают врасплох новичков в Terraform.

Ключевым моментом является то, что команда terraformplan учитывает только те ресурсы, которые указаны в файле состояния Terraform. Если ресурсы созданы каким-то другим способом (например, вручную, щелчком кнопкой мыши на консоли AWS), они не попадут в файл состояния и, следовательно, Terraform не будет их учитывать при выполнении команды plan. В итоге корректный на первый взгляд план окажется неудачным.

Из этого можно извлечь два урока.

Если вы уже начали работать с Terraform, не используйте ничего другого. Если часть вашей инфраструктуры управляется с помощью Terraform, больше нельзя изменять ее вручную. В противном случае вы не только рискуете получить странные ошибки Terraform, но также сводите на нет многие преимущества IaC, так как код больше не будет точным представлением вашей инфраструктуры.

• Если у вас уже есть какая-то инфраструктура, используйте команду import. Если вы начинаете использовать Terraform с уже существующей инфраструктурой, ее можно добавить в файл состояния с помощью команды terraformimport. Так Terraform будет знать, какой инфраструктурой нужно управлять. Команда import принимает два аргумента. Первым служит адрес ресурса в ваших конфигурационных файлах. Здесь тот же синтаксис, что и в ссылках на ресурсы: _. (вроде aws_iam_user.existing_user). Второй аргумент — это идентификатор ресурса, который нужно импортировать. Скажем, в качестве ID ресурса aws_iam_user выступает имя пользователя (например, yevgeniy.brikman), а ID ресурса aws_instance будет идентификатор сервера EC2 (вроде i-190e22e5). То, как импортировать ресурс, обычно указывается в документации внизу его страницы.