12 April

Подводные камни Terraform

Издательский дом «Питер» corporate blogDevOps
image

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

  • параметры count и for_each имеют ограничения;
  • ограничения развертываний с нулевым временем простоя;
  • даже хороший план может оказаться неудачным;
  • рефакторинг может иметь свои подвохи;
  • отложенная согласованность согласуется… с отлагательством.

Параметры count и for_each имеют ограничения


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

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

В count и for_each нельзя ссылаться ни на какие выходные переменные ресурса


Представьте, что нужно развернуть несколько серверов EC2 и по какой-то причине вы не хотите использовать ASG. Ваш код может быть таким:

resource "aws_instance" "example_1" {
   count             = 3
   ami                = "ami-0c55b159cbfafe1f0"
   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"
}

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

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. Такой код будет выглядеть вполне разумно, однако в результате выполнения terraform plan вы получите такую ошибку:

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 здесь.

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


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

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

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

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

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

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


Иногда при выполнении команды 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.

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

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

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

    Ниже показана команда import, позволяющая синхронизировать ресурс aws_iam_user, который вы добавили в свою конфигурацию Terraform вместе с пользователем IAM в главе 2 (естественно, вместо yevgeniy.brikman нужно подставить ваше имя):

    $ terraform import aws_iam_user.existing_user yevgeniy.brikman

    Terraform обратится к API AWS, чтобы найти вашего пользователя IAM и создать в файле состояния связь между ним и ресурсом aws_iam_user.existing_user в вашей конфигурации Terraform. С этого момента при выполнении команды plan Terraform будет знать, что пользователь IAM уже существует, и не станет пытаться создать его еще раз.

    Следует отметить, что, если у вас уже есть много ресурсов, которые вы хотите импортировать в Terraform, ручное написание кода и импорт каждого из них по очереди может оказаться хлопотным занятием. Поэтому стоит обратить внимание на такой инструмент, как Terraforming (http://terraforming.dtan4.net/), который может автоматически импортировать из учетной записи AWS код и состояние.

    Рефакторинг может иметь свои подвохи


    Рефакторинг — распространенная практика в программировании, когда вы меняете внутреннюю структуру кода, оставляя внешнее поведение без изменения. Это нужно, чтобы сделать код более понятным, опрятным и простым в обслуживании. Рефакторинг — это незаменимая методика, которую следует регулярно применять. Но, когда речь идет о Terraform или любом другом средстве IaC, следует крайне осторожно относиться к тому, что имеется в виду под «внешним поведением» участка кода, иначе возникнут непредвиденные проблемы.

    Например, распространенный вид рефакторинга — замена названий переменных или функций более понятными. Многие IDE имеют встроенную поддержку рефакторинга и могут автоматически переименовать переменные и функции в пределах всего проекта. В языках программирования общего назначения это тривиальная процедура, о которой можно не задумываться, однако в Terraform с этим следует быть крайне осторожными, иначе можно столкнуться с перебоями в работе.

    К примеру, у модуля webserver-cluster есть входная переменная cluster_name:

    variable "cluster_name" {
       description = "The name to use for all the cluster resources"
       type          = string
    }

    Представьте, что вы начали использовать этот модуль для развертывания микросервиса с названием foo. Позже вам захотелось переименовать свой сервис в bar. Это изменение может показаться тривиальным, но в реальности из-за него могут возникнуть перебои в работе.

    Дело в том, что модуль webserver-cluster использует переменную cluster_name в целом ряде ресурсов, включая параметр name двух групп безопасности и ALB:

    resource "aws_lb" "example" {
       name                    = var.cluster_name
       load_balancer_type = "application"
       subnets = data.aws_subnet_ids.default.ids
       security_groups      = [aws_security_group.alb.id]
    }

    Если поменять параметр name в каком-то ресурсе, Terraform удалит старую версию этого ресурса и создаст вместо него новую. Но если таким ресурсом является ALB, в период между его удалением и загрузкой новой версии у вас не будет механизма для перенаправления трафика к вашему веб-серверу. Точно так же, если удаляется группа безопасности, ваши серверы начнут отклонять любой сетевой трафик, пока не будет создана новая группа.

    Еще одним видом рефакторинга, который вас может заинтересовать, является изменение идентификатора Terraform. Возьмем в качестве примера ресурс aws_security_group в модуле webserver-cluster:

    resource "aws_security_group" "instance" {
      # (...)
    }

    Идентификатор этого ресурса называется instance. Представьте, что во время рефакторинга вы решили поменять его на более понятное (по вашему мнению) имя cluster_instance:

    resource "aws_security_group" "cluster_instance" {
       # (...)
    }

    Что в итоге случится? Правильно: перебой в работе.

    Terraform связывает ID каждого ресурса с идентификатором облачного провайдера. Например, iam_user привязывается к идентификатору пользователя IAM в AWS, а aws_instance — к ID сервера AWS EC2. Если изменить идентификатор ресурса (скажем, с instance на cluster_instance, как в случае с aws_security_group), для Terraform это будет выглядеть так, будто вы удалили старый ресурс и добавили новый. Если применить эти изменения, Terraform удалит старую группу безопасности и создаст другую, а между тем ваши серверы начнут отклонять любой сетевой трафик.

    Вот четыре основных урока, которые вы должны извлечь из этого обсуждения.

    • Всегда используйте команду plan. Ею можно выявить все эти загвоздки. Тщательно просматривайте ее вывод и обращайте внимание на ситуации, когда Terraform планирует удалить ресурсы, которые, скорее всего, удалять не стоит.
    • Создавайте, прежде чем удалять. Если вы хотите заменить ресурс, хорошенько подумайте, нужно ли создавать замену до удаления оригинала. Если ответ положительный, в этом может помочь create_before_destroy. Того же результата можно добиться вручную, выполнив два шага: сначала добавить в конфигурацию новый ресурс и запустить команду apply, а затем удалить из конфигурации старый ресурс и воспользоваться командой apply еще раз.
    • Изменение идентификаторов требует изменения состояния. Если вы хотите поменять идентификатор, связанный с ресурсом (например, переименовать aws_security_group с instance на cluster_instance), избегая при этом удаления ресурса и создания его новой версии, необходимо соответствующим образом обновить файл состояния Terraform. Никогда не делайте этого вручную — используйте вместо этого команду terraform state. При переименовании идентификаторов следует выполнить команду terraform state mv, которая имеет следующий синтаксис:

      terraform state mv <ORIGINAL_REFERENCE> <NEW_REFERENCE>

      ORIGINAL_REFERENCE — это выражение, ссылающееся на ресурс в его текущем виде, а NEW_REFERENCE — то место, куда вы хотите его переместить. Например, при переименовании группы aws_security_group с instance на cluster_instance нужно выполнить следующую команду:

      $ terraform state mv \
         aws_security_group.instance \
         aws_security_group.cluster_instance

      Так вы сообщите Terraform, что состояние, которое ранее относилось к aws_security_group.instance, теперь должно быть связано с aws_security_group.cluster_instance. Если после переименования и запуска этой команды terraform plan не покажет никаких изменений, значит, вы все сделали правильно.

    • Некоторые параметры нельзя изменять. Параметры многих ресурсов неизменяемые. Если попытаться их изменить, Terraform удалит старый ресурс и создаст вместо него новый. На странице каждого ресурса обычно указывается, что происходит при изменении того или иного параметра, поэтому не забывайте сверяться с документацией. Всегда используйте команду plan и рассматривайте целесообразность применения стратегии create_before_destroy.

    Отложенная согласованность согласуется… с отлагательством


    API некоторых облачных провайдеров, таких как AWS, асинхронные и имеют отложенную согласованность. Асинхронность означает, что интерфейс может сразу же вернуть ответ, не дожидаясь завершения запрошенного действия. Отложенная согласованность значит, что для распространения изменений по всей системе может понадобиться время; пока это происходит, ваши ответы могут быть несогласованными и зависеть от того, какая реплика источника данных отвечает на ваши API-вызовы.

    Представьте, к примеру, что вы делаете API-вызов к AWS с просьбой создать сервер EC2. API вернет «успешный» ответ (201 Created) практически мгновенно, не дожидаясь создания самого сервера. Если вы сразу же попытаетесь к нему подключиться, почти наверняка ничего не получится, поскольку в этот момент AWS все еще инициализирует ресурсы или, как вариант, сервер еще не загрузился. Более того, если вы сделаете еще один вызов, чтобы получить информацию об этом сервере, может прийти ошибка (404 Not Found). Дело в том, что сведения об этом сервере EC2 все еще могут распространяться по AWS, чтобы они стали доступными везде, придется подождать несколько секунд.

    При каждом использовании асинхронного API с отложенной согласованностью вы должны периодически повторять свой запрос, пока действие не завершится и не распространится по системе. К сожалению, AWS SDK не предоставляет для этого никаких хороших инструментов, и проект Terraform раньше страдал от множества ошибок вроде 6813 (https://github.com/hashicorp/terraform/issues/6813):

    $ terraform apply
    aws_subnet.private-persistence.2: InvalidSubnetID.NotFound:
    The subnet ID 'subnet-xxxxxxx' does not exist

    Иными словами, вы создаете ресурс (например, подсеть) и затем пытаетесь получить о нем какие-то сведения (вроде ID только что созданной подсети), а Terraform не может их найти. Большинство из таких ошибок (включая 6813) уже исправлены, но время от времени они все еще проявляются, особенно когда в Terraform добавляют поддержку нового типа ресурсов. Это раздражает, но в большинстве случаев не несет никакого вреда. При повторном выполнении terraform apply все должно заработать, поскольку к этому моменту информация уже распространится по системе.

    Данный отрывок представлен из книги Евгения Брикмана «Terraform: инфраструктура на уровне кода».
Tags:книга
Hubs: Издательский дом «Питер» corporate blog DevOps
+6
4.1k 23
Comments 10
Top of the last 24 hours
Information
Founded

5 October 1991

Location

Россия

Website

piter.com

Employees

201–500 employees

Registered

15 August 2012