Подводните камъни на 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

Стойността "count" зависи от атрибути на ресурси, които не могат да бъдат определени до прилагане, така че Terraform не може да предвиди колко инстанции ще бъдат създадени. За да се справите с това, използвайте аргумента -target, за да приложите първо само ресурсите, от които count зависи.

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

Името "count" е резервирано за използване в бъдеща версия на 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:00 увеличават броя на сървърите в кластера от два на десет. Ако завършите разгръщане, да предположим, в 11:00, новата група ASG ще се зареди не с десет, а само с два сървъра и ще остане в това състояние до 9:00 на следващия ден.

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

  • Променете параметъра recurrence в aws_autoscaling_schedule от 0 9 * * * ("стартиране в 9:00") на нещо като 0-59 9-17 * * * ("стартиране всяка минута от 9:00 до 17:00"). Ако в ASG вече има десет сървъра, повторното изпълнение на това правило за автоматично мащабиране няма да донесе промяна, което е точно това, от което се нуждаем. Но ако групата ASG е разгръщена съвсем наскоро, това правило гарантира, че максимум след минута броят на сървърите й ще достигне десет. Това не е съвсем елегантен подход, а големите скокове от десет до два сървъра и обратно също могат да предизвикат проблеми у потребителите.
  • Създайте потребителски скрипт, който използва AWS API за определяне на броя на активните сървъри в 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 = "Името, което да се използва за всички ресурси на клъстера"
       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

      ОРИГИНАЛНА_РЕФЕРЕНЦИЯ е израз, който се отнася до ресурса в текущия му вид, а НОВА_РЕФЕРЕНЦИЯ е местоположението, към което искате да го преместите. Например, при преименуването на групата 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:
    Subnet ID 'subnet-xxxxxxx' не съществува

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

    Този откъс е представен от книгата на Евгений Брикман «Terraform: инфраструктура като код».

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

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster