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

    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: инфраструктура на уровне кода».
    Издательский дом «Питер»
    Компания

    Похожие публикации

    Комментарии 10

      0
      Для тех, кто не знает, что такое Terraform было бы неплохо небольшое введение в статье, например, такое:

      Terraform: новый подход к Infrastructure as code
      habr.com/en/company/piter/blog/351878

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

      Terraform и инфраструктура на уровне кода

      Terraform – это инструмент от компании Hashicorp, помогающий декларативно управлять инфраструктрой. В данном случае не приходится вручную создавать инстансы, сети и т.д. в консоли вашего облачного провайдера; достаточно написать конфигурацию, в которой будет изложено, как вы видите вашу будущую инфраструктуру. Такая конфигурация создается в человеко-читаемом текстовом формате. Если вы хотите изменить вашу инфраструктуру, то редактируете конфигурацию и запускаете terraform apply. Terraform направит вызовы API к вашему облачному провайдеру, чтобы привести инфраструктуру в соответствие с конфигурацией, указанной в этом файле.
        0
        И фразу про «книгу Евгения Брикмана» тоже неплохо было бы в начале разместить. Повествование начинается так «с места в карьер» и непонятно то ли это часть серии статей или что.
        0

        Вначале все выглядит красиво, на уровне Hello world. Далее идет усложнение, декларативность языка больше мешает, чем помогает.


        Знакомые выкинули все это и учправляют через API из нормального скриптового языка

          0
          и управляют через API из нормального скриптового языка
          а что есть нормальный скриптовый язык?
            0
            У них это… ммм… вы наверное удивитесь… PowerShell.
              0
              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]
              }
              

              vs

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

              как по мне, то второй способ записи намного легче для восприятия
                0
                 Кстати, кому не нравится декларативщина, придумали уже Pulumi.
                  0

                  Pulumi тоже декларативный, но в нем можно писать процедурный код.

                0

                А state они как администрируют?

                  0
                  Тоже интересно. Возможно, они сначала опрашивают текущую инфраструктуру? По идее это самый лучший путь (но и самый сложный). Ну или просто права у всех отобрали, чтоб руками некому было зайти и сломать :)

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое