Ви думаєте, що «нормально» пишете Terraform? Або п’ять best-practices, як робити НЕ треба
Усі статті, обговорення, новини про DevOps — в одному місці. Підписуйтеся на DOU | DevOps!
«Ніколи такого не було, і ось знову...» — саме така думка в мене виникає кожного разу, коли я бачу помилки в Terraform плані на зразок «Index out of range».
«Ну... певно, це дуже старий Terraform-код!» — плекаю надію я. Потім йду в репозиторій, знаходжу необхідний ресурс, що містить славнозвісний «count», і бачу поряд з ним напис в IDLE: «This code was created by mr. X 8 months ago». Що? Ви серйозно? 8 місяців тому? count? Я вважаю, що таких людей потрібно автоматично позбавляти лички «senior», якщо вона в них звісно є!
Нормальна така заява від якогось там мідл-сопляка з 3 роками досвіду, погодьтесь.... Не зрозумійте мене неправильно, але якщо ви за 5 років існування «for_each» в Terraform, не зрозуміли його переваги і не навчились його використовувати замість «count» — в мене для вас погані новини (дуже символічно, до речі, що цю статтю я пишу на
В мене є товариш, якому купили права на водіння автомобілем. І він вже близько 10 років їздить на машині. Треба віддати йому належне — водить він добре, питань нема. Починаючи з відчуття габаритів машини та «інтуїтивного» розуміння ситуації на дорозі, і закінчуючи ідеальними навичками паралельного паркування, про нього можна сказати: «він наче був народжений з кермом в руках».... Але!
Їхали ми якось на його машині, і в один прекрасний момент він повертає направо під всесвітньо відомий «знак цеглини», після чого ми «успішно» вискакуємо на односторонній зустрічний рух і вдаривши по гальмам зупиняємось за півметра від ошалівшого водія Gelandewagen. Я кажу йому: «Ти ненормальний? Нащо ти це зробив? Там же ж знак був!» На що отримую відповідь: «Який знак? Мені права купили! Ти думаєш, я знаю, що він означає? Я знаю лише одне правило: красний- стій, зелений- їдь!»
А тепер питання до вас: чи можна вважати його «нормальним» водієм і дозволити мати права? Відповідь: звісно ні!!! Яким би він «крутим» водієм не був, по-хорошому в нього треба забрати права, відправити навчатись, і лише після здачі екзамену дозволити знову сідати за кермо!
Так само потрібно вчиняти і з сіньйорами, які продовжують в 2024 році використовувати «count» в Terraform коді! Адже «Index out of range» так само як і штраф за їзду в полосі громадського транспорту — це найменші із зол, які можуть бути, коли ти не знаєш як користуватись «for_each», або не знаєш правил дорожнього руху.
Тож я хочу поділитися з вами своїми
Що не так з «count»
Використання «count» здається дуже зручним на перший погляд: визначив кількість ресурсів, яку хочеш створити, додав кондішн якщо необхідно — вуаля, ресурси створюються при виконанні цього кондішину. Але це лише на перший погляд....
Річ у тім, що кожен з цих ресурсів отримує свій ідентифікаційний номер в списку(починаючи з 0), і зберігається в Terraform стейті як ресурс із цим порядковим номером. Відповідно, якщо ви створювали 10 ресурсів за допомогою «count» півроку назад і вони успішно «живуть» у вас на продакшені, а зараз по якійсь із причин ви вирішили змінити їх кількість (або видалити якийсь конкретний із них), то видаливши цей ресурс «із середини» списку, ви отримуєте 100% ризик видалення і перестворення всіх ресурсів, які знаходяться далі в списку (мають більший порядковий номер), так як їхній ідентифікатор тепер теж змінюється.
І ваше щастя, якщо у вас налаштована CI/CD «Continuous delivery» стратегія — ви це помітите перед ‘terraform apply’.
А що ж на це каже офіційна документація Terraform
— «Using for_each gives the same flexibility without the extra churn.» Що в перекладі на українську звучить як: «Ну так, ми колись трохи налажали з ‘count’, але все виправили за допомогою ‘for_each’. І оскільки ми не хочемо всім ламати існуючі інфраструктури, ми ‘count’ теж будемо підтримувати, але дуже радимо вам використовувати ‘for_each’, адже в нього є весь функціонал як в ‘count’ і навіть більше, але немає таких ризиків отримати неочікуваний результат».
А ось вам приклад як створювати ресурси в залежності від значення змінної за допомогою «for_each» :
А ви досі використовуєте «locals» всередині Terraform темплейтів?
Особисто я проти «locals» нічого не маю — куди ж без них! Але питання в тому, де вони використовуються (в якому місці лежать). Що мається на увазі.
Всі знають, що повторне використання коду — це святе святих правило всієї розробки ПЗ. Девелопери для цього навіть ООП собі придумали наряду з іншими парадигмами програмування. Девопс інженери вирішили не відставати і придумали таку штуку як «module» в Terraform (насправді то скоріш за все придумали теж девелопери, які працювали над Terraform, але то таке).
Ідея полягає в чому: ми описуємо «темплейт» для якихось ресурсів інфраструктури, і замість конкретних значень для атрибутів/пропертів цих ресурсів вказуємо плейсхолдери, які отримають свої фактичні значення вже протягом деплою. Такий «module» ми називаємо «child» модулем, і тепер можемо «викликати» його з багатьох інших «root» модулів, які потребують створення ресурсів, описаних в «child» (той що наш «темплейт»). І як ви всі напевно вже знаєте, ці «child» модулі/темплейти мають приймати в себе якісь значення (назву енву, кількість інстансів, неймінг мікросервісу, назву сабскіпшину і т.д), на основі яких генеруватимуться вже «фактичні» значення для атрибутів ресурсів. Для цієї генерації ми й використовуємо «locals».
А тепер уявімо собі таку ситуацію: ми маємо 5 «root» модулів в різних репозиторіях для деплою різних частин інфраструктури, але всі ці 5 модулів «викликають» один і той же «child» модуль/темплейт для створення одного й того ж набору ресурсів, проте з різними значеннями. І от уявімо, що ми передаємо в «child» модуль/темплейт якісь значення (vars наприклад), і генеруємо на їх основі фактичні значення для атрибутів ресурсів всередині (за допомогою «locals» звісно ж). Потім ресурси деплояться і всі щасливі....
Але проходить деякий час, і в нас виникає така ситуація, коли один з 5 «root» модулів потребує «специфічної» конфігурації/неймінгу в порівнянні з іншими
Проте тепер інші 4 «root» модулі, передаючи свої значення, можуть мати конфлікти з нашим новим кодом, адже не завжди неймінг та структура об’єктів, що використовується для створення ресурсів одного «root» модуля підходить і співпадає з неймінгом та структурою об’єктів для іншого. І гратися з написанням «locals», які б одночасно підійшли для всіх
А що з цим робити? А все дуже просто: генеруйте всі свої фактичні значення прямо в «root» модулях, і нехай ваш «child» модуль/темплейт лише отримує вже готові значення чи об’єкти, не займаючись їхньою генерацією. Іншими словами — не пхайте «locals» в «child» модуль/темплейт, якщо це не «конче необхідно»!
Розбивай свій Terraform код на частинки за «логічним» принципом
Знову ж таки, повертаючись до девелоперів, які, на відміну від відносно молодого «Девопс племені», взрощують свої «best practices» вже понад 50 років.... Вони придумали для зручності розбивати код на логічні частини по репозиторіям, пакетам, папкам, файлам....
А з Terraform що? Все точнісінько так само! Якщо ви вже помітили, то в скриншоті з прикладом про «for_each» вище все розбито по окремим файлам: «locals» знаходяться в locals.tf, провайдери в providers.tf, змінні в variables.tf, і далі по списку. Думаю, бенефіти від такого «групування» очевидні, тому не бачу необхідності затримуватись на цій темі. Хочу лише поділитись прикладом як робити не треба:
Погодьтесь, виходить якась «каша», в якій розібратись, де, і що лежить, доволі складно (особливо, якщо ми в одному файлі описуємо безліч ресурсів разом з компонентами для них).
Будьте незалежні від «depends_on»
А що ж не так з «depends_on» ? Найкраще на це питання відповідає офіційна документація: «You should use depends_on as a last resort because it can cause Terraform to create more conservative plans that replace more resources than necessary. For example, Terraform may treat more values as unknown „(known after apply)“ because it is uncertain what changes will occur on the upstream object.»
Переклад: «Не треба використовувати „depends_on“ в усіх ресурсах, де тільки можна! Terraform і без вас розуміє (в більшості випадків), що, і в якому порядку треба деплоїти.»
Тож просте правило: якщо можна обійтись без «depends_on» — обходимось без «depends_on». Особливо остерігайтесь його використання з модулями !!! Перше, що вас повинно насторожувати, це думка про те, що в «module» зазвичай лежить більше одного ресурсу («depends_on» на більшу кількість ресурсів — більше потенційних проблем, більше «known after apply» в вашому Terraform плані).
А друге.... Розкажу вам що буває, коли використовуєш «depends_on» разом з Terraform «data». Уявімо такий кейс: ви описуєте відразу декілька Terraform модулів. Скажімо, це модуль для створення Kubernetes кластеру, і модуль для створення нетворку до нього.
Все, що нам відомо перед деплоєм цих ресурсів — ім’я Vnet та subnet в яких будуть працювати наші K8’s ресурси. Цей subnet ми будемо «тягнути» за допомогою Terraform «data» і передавати його значення в Kubernetes кластер. Наш код виглядає приблизно ось так:
А знаєте, що відбудеться коли ми задеплоїмо ці ресурси, а через деякий час зробимо будь-які зміни в модулі «aks_network» ? Terraform буде перестворювати K8’s кластер !!! Відбуватиметься це тому, що виконання Terraform «data» буде завжди чекати допоки не відбудеться «terraform apply» на ресурси модулю «aks_network» (через «depends_on»).
І як наслідок ми матимемо в Terraform плані для K8’s кластеру:
“vnet_subnet_id = "/subscriptions/..." -> (known after apply) # forces replacement”
А що можна зробити щоб цього не сталось? Ну навіть не знаю..., як мінімум можна перенести ресурс «data.azurerm_subnet» з «child» модуля «aks_cluster» в окремий data.tf файл всередину «root» модуля (пам’ятаєте попередні 2 «best-practices»? забув сказати, що з «data» теж так працює) — передавайте в «child» модуль ВЖЕ ГОТОВІ ЗНАЧЕННЯ, і буде вам щастя!
Або, як радить сам Hashicorp support в своєму «Best Practice: depends_on Meta-Argument» — використовуйте «прямі» посилання на «output» модулів замість використання «depends_on» на ці модулі.
«local-exec» працює всупереч концепції Terraform
А чого це він «працює всупереч концепції», запитаєте ви? Давайте згадаємо історію еволюції підходів деплою інфраструктури....
«Спочатку у нас все мале, ну там раковини, спірохети разні.... Потом знов все велике, динозаври. бронтозаври ....» Ой, вибачте, не той текст.... Так от: було значить напочатках все «встановиРукамиОпс» — були адміністратори, були дискети з Віндовсом на них, були компакт диски з Лінуксом.... І те все встановлювалось на залізяки в серверних руцями, і руцями так само і конфігурилось.
Потім придумали писати автоматизацію, скріптіки на Python, bash, віртуальні машинки в клаудах.... І вигадали для того всього назву — DevOps методологія. Те все потроху розвивалось, йшло в масси....
Після чого якийсь розумник взяв та й придумав декларативний підхід (або він сам собі утворився, хто ж його знає). З’явилась така штука як K8’s, яка сама себе менеджить і хілить (подавай тільки .yaml файли), і з’явився наш славнозвісний Terraform, про який ми сьогодні балакаємо — теж яскравий представник декларативного підходу.
І все наче зрозуміло: описав бажане за дійсне — сиди, чекай, поки за тебе все зробиться! Але ж ні... давайте в декларативному Terraform використовувати імперативні команди! Ну, якщо пацани з Hashicorp контори створили ті Provisioners — значить так треба, значить є специфічна необхідність, яку вони покривають. Але наша з вами задача яка? Правильно! Бути в курсі, що Provisioners — це «the last resort. There are better alternatives for most situations».
І в принципі можна було б пошерити лінку на поради щодо використання, та альтернативи Provisioners, та й на тому все. Але я так зробити не можу, тому поділюсь з вами кейсом, який звісно ж трапився не зі мною (мені товариш розказував), і трапився він (як же ж по-іншому) не на роботі, а вдома на якомусь пет-проєкті.
Якось існував собі «local-exec» з дуже сенсітів значенням всередині. Щось на кшталт цього:
«І ніс він свою службу справно... та от, зістарився....» — взяв та й впав якось цей «local-exec». А коли Terraform падає — він справно виводить помилки в аутпут, навіть з «quiet» аргументом («Note that the output of the command will still be printed in any case»)
До речі, може й Дію так само взломали в 2022 ?)
P.S: наперед прошу вибачення у всіх сеньйорів, в яких можуть забрати личку сеньйора через мене. Сподіваюсь, мій досвід буде для когось корисним, і, можливо, вбереже від фатальних помилок.
Якщо ви з чимось не згодні чи, навпаки, згодні, або просто хочете поділитись своїми думками — велком в коментарі або в мій LinkedIn, я завжди відкритий до спілкування.
24 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів