Падводныя камяні 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-й вечара»). Калі ў 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

Дадаць каментар