Підводне каміння Terraform

Підводне каміння Terraform
Виділимо кілька підводних каменів, включаючи ті, що пов'язані з циклами, виразами 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: інфраструктура на рівні коду".

Джерело: habr.com

Додати коментар або відгук