Тераформирани капани

Тераформирани капани
Нека подчертаем няколко клопки, включително тези, свързани с цикли, оператори 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"
}

Нека ги разгледаме един по един.

Тъй като параметърът за броене е зададен на статична стойност, този код ще работи без проблеми: когато изпълните командата apply, тя ще създаде три EC2 сървъра. Но какво ще стане, ако искате да разположите по един сървър във всяка зона на достъпност (AZ) в рамките на текущия ви AWS регион? Можете да накарате вашия код да зареди списък със зони от източника на данни aws_availability_zones и след това да преминете през всяка от тях и да създадете EC2 сървър в него, като използвате параметъра за броене и достъп до индекса на масива:

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. Нека да видим какво ще се случи, ако се опитаме да използваме изхода на този ресурс в параметъра за броене на ресурса aws_instance:

resource "aws_instance" "example_3" {
   count             = random_integer.num_instances.result
   ami                = "ami-0c55b159cbfafe1f0"
   instance_type = "t2.micro"
}

Ако изпълните тераформен план на този код, ще получите следната грешка:

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 не могат да се използват в конфигурацията на модула

Някой ден може да се изкушите да добавите параметър за броене към конфигурацията на вашия модул:

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 вътре в модул, за да създаде три копия на ресурса на клъстера на уеб сървъра. Или може да искате да направите свързването на модул незадължително в зависимост от някакво булево условие, като зададете параметъра му за броене на 0. Това може да изглежда като разумен код, но ще получите тази грешка при изпълнение на тераформен план:

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 в ресурс на модул не се поддържа. Според бележките за изданието на Terraform 0.12 (http://bit.ly/3257bv4), HashiCorp планира да добави тази възможност в бъдеще, така че в зависимост от това кога четете тази книга, тя може вече да е налична. За да разберете със сигурност, прочетете регистъра на промените на Terraform тук.

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

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

Например, модулът уебсървър-клъстер съдържа двойка ресурси aws_autoscaling_schedule, което в 9 сутринта увеличава броя на сървърите в клъстера от два на десет. Ако внедрите в, да речем, 11 сутринта, новият ASG ще стартира само с два сървъра, а не с десет и ще остане така до 9 сутринта на следващия ден.

Това ограничение може да бъде заобиколено по няколко начина.

  • Променете параметъра за повторение в aws_autoscaling_schedule от 0 9 * * * („работи в 9 сутринта“) на нещо като 0-59 9-17 * * * („изпълнявай всяка минута от 9 сутринта до 5 следобед“). Ако ASG вече има десет сървъра, повторното изпълнение на това правило за автоматично мащабиране няма да промени нищо, което искаме. Но ако ASG е бил внедрен едва наскоро, това правило ще гарантира, че след максимум минута броят на неговите сървъри ще достигне десет. Това не е съвсем елегантен подход и големите скокове от десет на два сървъра и обратно също могат да причинят проблеми на потребителите.
  • Създайте персонализиран скрипт, който използва API на AWS, за да определи броя на активните сървъри в ASG, извикайте го с помощта на външен източник на данни (вижте „Външен източник на данни“ на страница 249) и задайте параметъра desire_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, но и на почти всеки ресурс. Възможно е някой да е създал този ресурс ръчно или чрез командния ред, но и в двата случая съвпадението на ID води до конфликти. Има много варианти на тази грешка, които често изненадват новодошлите в Terraform.

Ключовият момент е, че командата за план на terraform взема предвид само онези ресурси, които са посочени във файла за състояние на Terraform. Ако ресурсите са създадени по някакъв друг начин (например ръчно чрез щракване в конзолата на AWS), те няма да попаднат в държавния файл и следователно Terraform няма да ги вземе предвид при изпълнение на командата за планиране. В резултат на това план, който на пръв поглед изглежда правилен, ще се окаже неуспешен.

От това могат да се извлекат два урока.

  • Ако вече сте започнали да работите с Terraform, не използвайте нищо друго. Ако част от вашата инфраструктура се управлява с помощта на Terraform, вече не можете да я променяте ръчно. В противен случай не само рискувате странни грешки на Terraform, но и отричате много от предимствата на IaC, тъй като кодът вече няма да бъде точно представяне на вашата инфраструктура.
  • Ако вече имате някаква инфраструктура, използвайте командата import. Ако започвате да използвате Terraform със съществуваща инфраструктура, можете да я добавите към държавния файл с помощта на командата terraform import. По този начин Terraform ще знае каква инфраструктура трябва да се управлява. Командата за импортиране приема два аргумента. Първият е адресът на ресурса във вашите конфигурационни файлове. Синтаксисът тук е същият като при връзките към ресурси: _. (като aws_iam_user.existing_user). Вторият аргумент е идентификаторът на ресурса, който ще се импортира. Да кажем, че идентификаторът на ресурс aws_iam_user е потребителското име (например yevgeniy.brikman), а идентификаторът на ресурс aws_instance е идентификаторът на сървъра EC2 (като i-190e22e5). Как да импортирате ресурс обикновено е посочено в документацията в долната част на страницата му.

    По-долу е дадена команда за импортиране, която синхронизира ресурса 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. Отсега нататък, когато изпълните командата за план, Terraform ще знае, че IAM потребителят вече съществува и няма да се опитва да го създаде отново.

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

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

    Рефакторинг е обичайна практика в програмирането, при която променяте вътрешната структура на кода, като оставяте външното поведение непроменено. Това прави кода по-ясен, спретнат и лесен за поддръжка. Рефакторингът е незаменима техника, която трябва да се използва редовно. Но когато става въпрос за Terraform или друг IaC инструмент, трябва да сте изключително внимателни какво имате предвид под „външното поведение“ на част от кода, в противен случай ще възникнат неочаквани проблеми.

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

    Например, модулът на клъстера на уеб сървъра има входна променлива cluster_name:

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

    Представете си, че сте започнали да използвате този модул, за да внедрите микроуслуга, наречена foo. По-късно искате да преименувате услугата си на бар. Тази промяна може да изглежда тривиална, но в действителност може да причини прекъсване на услугата.

    Факт е, че модулът на уеб сървър-клъстер използва променливата cluster_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]
    }

    Ако промените параметъра за име на ресурс, Terraform ще изтрие старата версия на този ресурс и ще създаде нова на нейно място. Но ако този ресурс е ALB, между изтриването му и изтеглянето на нова версия, няма да имате механизъм за пренасочване на трафика към вашия уеб сървър. По същия начин, ако група за сигурност бъде изтрита, вашите сървъри ще започнат да отхвърлят всеки мрежов трафик, докато не бъде създадена нова група.

    Друг тип рефакторинг, от който може да се интересувате, е промяната на Terraform ID. Нека вземем за пример ресурса aws_security_group в модула на клъстера на уеб сървъра:

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

    Идентификаторът на този ресурс се нарича инстанция. Представете си, че по време на рефакторинг сте решили да го промените на по-разбираемо (според вас) име cluster_instance:

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

    Какво ще стане накрая? Точно така: смущение.

    Terraform свързва всеки идентификатор на ресурс с идентификатора на облачен доставчик. Например iam_user е свързан с потребителския идентификатор на AWS IAM, а aws_instance е свързан с идентификатора на сървъра AWS EC2. Ако промените ID на ресурса (да речем от 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: инфраструктура на ниво код“.

Източник: www.habr.com

Добавяне на нов коментар