Основи Ansible, без яких ваші плейбуки - грудка макаронів, що злиплися.

Я роблю багато реву для чужого коду на Ансібл і багато пишу сам. У ході аналізу помилок (як чужих, так і своїх), а також деякої кількості співбесід, я зрозумів основну помилку, яку припускаються користувачі Ансибла — вони лізуть у складне, не освоївши базового.

Для виправлення цієї вселенської несправедливості я вирішив написати вступ до Ансиблу для тих, хто його вже знає. Попереджаю, це не переказ манів, це лонгрід, в якому багато літер і немає картинок.

Очікуваний рівень читача вже написано кілька тисяч рядків ямла, вже щось у продакшені, але "якось все криво".

назви

Головна помилка користувача Ansible - це не знати як називається. Якщо ви не знаєте назви, ви не можете розуміти те, що написано в документації. Живий приклад: на співбесіді, людина, яка начебто заявляла, що вона багато писала на Ансиблі, не змогла відповісти на питання "з яких елементів складається playbook'а?". А коли я підказав, що "очікувалася відповідь, що playbook складається з play", то був убивчий коментар "ми цього не використовуємо". Люди пишуть на Ансіблі за гроші та не використовують play. Насправді використовують, але не знають, що таке.

Тож почнемо із простого: як що називається. Можливо, ви це знаєте, а може й ні, бо не звернули уваги, коли читали документацію.

ansible-playbook виконує playbook. Playbook - це файл з розширенням yml/yaml, всередині якого щось таке:

---
- hosts: group1
  roles:
    - role1

- hosts: group2,group3
  tasks:
    - debug:

Ми вже зрозуміли, що весь цей файл – плейбук. Ми можемо показати, де тут ролі (roles), де таски (tasks). Але де тут грати? І чим відрізняється play від role чи playbook?

У документації це все є. І це пропускають. Початківці тому, що там занадто багато і все відразу не запам'ятаєш. Досвідчені – тому що "тривіальні речі". Якщо ви досвідчений - перечитуйте ці сторінки хоча б раз на півроку, і ваш код стане класом кращим.

Отже, запам'ятовуйте: Playbook - це список, що складається з play і import_playbook.
Ось це одна play:

- hosts: group1
  roles:
    - role1

і ось це теж ще одна play:

- hosts: group2,group3
  tasks:
    - debug:

Що таке play? Для чого вона?

Play - це ключовий елемент для playbook, тому що play і тільки play пов'язує список ролей та/або тасок зі списком хостів, на яких їх потрібно виконувати. У глибоких надрах документації можна знайти згадку про delegate_to, локальні lookup-плагіни, network-cli-специфічні налаштування, jump-хости і т.д. Вони дозволяють трохи змінити місце виконання тасок. Але забудьте про це. Кожна з цих хитрих опцій має дуже спеціальні застосування, і вони точно не є універсальними. А ми говоримо про базові речі, які мають знати та використати всі.

Якщо ви хочете "щось" виконати "десь" - ви пишете play. Чи не роль. Не роль із модулями та делегейтами. Ви берете та пишете play. У якій, у полі hosts ви перераховуєте де виконувати, а roles/tasks - що виконувати.

Просто ж так? А як може бути інакше?

Одним із характерних моментів, коли у людей виникає бажання зробити це не через play, це "роль, яка все налаштовує". Хочеться мати роль, яка налаштовує сервера першого типу, і сервера другого типу.

Архетиповим прикладом є моніторинг. Хочеться мати роль monitoring, яка налаштує моніторинг. Роль monitoring призначається на хости моніторингу (соотв. play). Але з'ясовується, що для моніторингу нам треба поставити пакети на хости, які ми моніторимо. Чому б не використати delegate? А ще треба налаштувати iptables. delegate? А ще треба написати/поправити конфіг для СУБД, щоб моніторинг пускала. delegate! А якщо креатив попер, то можна зробити делегацію include_role у вкладеному циклі по хитрому фільтру на список груп, а всередині include_role можна ще робити delegate_to знову. І помчало…

Добре побажання - мати одну-єдину роль monitoring, яка "все робить" - веде нас непрозоре пекло з якого найчастіше один вихід: все переписати з нуля.

Де тут трапилася помилка? У той момент, коли ви виявили, що для виконання завдання "x" на хості X вам треба піти на хост Y і зробити там "y", ви повинні були виконати просту вправу: піти та написати play, яка на хості Y робить y. Не дописувати щось у "x", а написати з нуля. Нехай навіть із захардшкіреними змінними.

Начебто в абзацах вище все сказано правильно. Але ж це не ваш випадок! Тому що ви хочете написати код, що перевикористовується, DRY і схожий на бібліотеку, і потрібно шукати метод як це зробити.

Ось тут причаїлася ще одна груба помилка. Помилка, яка перетворила безліч проектів з терпимо написаних (можна краще, але все працює і легко дописати) на досконалий жах, в якому навіть автор не може розібратися. Воно працює, але боронь боже щось змінити.

Ця помилка звучить так: роль — бібліотечна функція. Ця аналогія занапастила стільки хороших починань, що просто сумно дивитися. Роль – не бібліотечна функція. Вона не може робити обчислення і вона не може ухвалити рішення рівня play. Нагадайте мені, які рішення приймає play?

Дякую, ви маєте рацію. Play приймає рішення (точніше, містить у собі інформацію) про те, які таски та ролі на яких хостах виконувати.

Якщо ви делегуєте це рішення на роль, та ще й з обчисленнями, ви прирікаєте себе (і того, хто ваш код намагатиметься розібрати) на жалюгідне існування. Роль не вирішує, де їй виконуватися. Це рішення приймає play. Роль робить те, що їй сказали там, де їй сказали.

Чому займатися програмуванням на Ансіблі небезпечно і чим COBOL краще Ансибла ми поговоримо на чолі про змінні та jinja. Поки що скажемо одне — кожне ваше обчислення залишає за собою нестертий слід із зміни глобальних змінних, і ви нічого з цим не можете вдіяти. Як тільки два "сліди" перетнулися - все пропало.

Зауваження для в'їдливих: роль, безумовно, може проводити control flow. Є delegate_to і має розумні застосування. Є meta: end host/play. Але! Пам'ятаєте, ми вчимо основи? Забули про delegate_to. Ми говоримо про найпростіший і красивий код на Ансібл. Який легко читати, легко писати, легко налагоджувати, легко тестувати та легко дописувати. Отже, ще раз:

play та тільки play вирішує на яких хостах що виконується.

У цьому розділі ми розібралися з протистоянням play та role. Тепер поговоримо про стосунки tasks vs role.

Таски та Ролі

Розглянемо play:

- hosts: somegroup
  pre_tasks:
    - some_tasks1:
  roles:
     - role1
     - role2
  post_tasks:
     - some_task2:
     - some_task3:

Допустимо, вам треба зробити foo. І виглядає це як foo: name=foobar state=present. Куди це писати? в pre? post? Створювати role?

… І куди поділися tasks?

Ми знову починаємо з азів – пристрій play. Якщо ви плаваєте в цьому питанні, ви не можете використовувати play як основу для решти, і ваш результат виходить "хитким".

Пристрій play: директива hosts, налаштування самої play та секції pre_tasks, tasks, roles, post_tasks. Інші параметри для play нам зараз не важливі.

Порядок їх секцій з тасками та ролями: pre_tasks, roles, tasks, post_tasks. Оскільки семантично порядок виконання між tasks и roles не зрозумілий, то best practices каже, що ми додаємо секцію tasksтільки якщо ні roles. Якщо є roles, то всі таски, що додаються, поміщаються в секції pre_tasks/post_tasks.

Залишається лише те, що семантично все зрозуміло: спочатку pre_tasks, потім roles, потім post_tasks.

Але ми ще не відповіли на запитання: а куди виклик модуля foo писати? Чи потрібно під кожний модуль писати цілу роль? Або краще мати товсту роль під усе? А якщо не роль, то куди писати — в pre чи post?

Якщо на ці питання немає аргументованої відповіді, то це ознака відсутності інтуїції, тобто ті самі "хиткі основи". Давайте розумітися. Спочатку контрольне питання: Якщо у play є pre_tasks и post_tasks (і немає ні tasks, ні roles), то чи може щось зламатися, якщо я першу тягу з post_tasks перенесу в кінець pre_tasks?

Зрозуміло, що формулювання питання натякає, що зламається. Але що?

… Хендлери. Читання основ відкриває важливий факт: всі хендлери flush'атся автоматично після кожної секції. Тобто. виконуються всі таски з pre_tasksпотім всі хендлери, які були нефти. Потім виконуються всі ролі та всі хендлери, які були нефти у ролях. Потім post_tasks та їх хендлери.

Таким чином, якщо ви тягу перетягнете з post_tasks в pre_tasks, то, потенційно, ви виконаєте її до виконання handler'а. наприклад, якщо в pre_tasks встановлюється та конфігурується веб-сервер, а в post_tasks у нього щось засилається, то перенесення цієї таски до секції pre_tasks призведе до того, що в момент "засилання" сервер буде ще не запущено і все зламається.

А тепер давайте ще раз подумаємо, а навіщо нам pre_tasks и post_tasks? Наприклад, щоб виконати все необхідне (включаючи хендлери) до виконання ролі. А post_tasks дозволить нам працювати з результатами виконання ролей (включаючи хендлери).

В'їдливий знавець Ansible скаже нам, що є meta: flush_handlersале навіщо нам flush_handlers, якщо ми можемо покластися на порядок виконання секцій у play? Більше того, використання meta: flush_handlers може нам доставити несподіваного з хендлерами, що повторюються, зробити нам дивні варнінги у разі використання when у block і т.д. Чим краще ви знаєте ансибл, тим більше нюансів ви можете назвати для "хитрого" рішення. А просте рішення – використання натурального поділу між pre/roles/post – не викликає нюансів.

І, повертаємось, до нашого 'foo'. Куди його помістити? У pre, post або roles? Очевидно, це залежить від того, чи нам потрібні результати роботи хендлера для foo. Якщо їх немає, то foo не потрібно класти ні в pre, ні в post ці секції мають спеціальний сенс виконання тасок до і після основного масиву коду.

Тепер відповідь на питання "роль чи тяга" зводиться до того, що вже є в play - якщо там є tasks, то треба дописати до tasks. Якщо є roles - треба виконувати роль (нехай і з однієї task). Нагадую, tasks та roles одночасно не використовуються.

Розуміння основ Ансибла дає обґрунтовані відповіді на, начебто, питання смаківщини.

Таски та ролі (частина друга)

Тепер обговоримо ситуацію, коли ви починаєте писати плейбуку. Вам потрібно зробити foo, bar та baz. Це три таски, одна роль чи три ролі? Узагальнюючи питання: коли треба починати писати ролі? У чому сенс писати ролі, коли можна писати таски? А що таке роль?

Одна з найгрубіших помилок (я про це вже говорив) вважати, що роль це як функція в бібліотеці у програми. Як виглядає узагальнений опис функції? Вона приймає аргументи на вхід, взаємодіє з side causas, робить side effects, повертає значення.

Тепер увага. Що з цього можна зробити у ролі? Викликати side effects — будь ласка, це і є суть всього Ансібла — робити сайд-ефекти. Мати side causas? Елементарно. А ось з "передати значення і повернути його" - ось тут і немає. По-перше, ви не можете передати значення у роль. Ви можете виставити глобальну змінну з терміном життя розміром у play у секції vars для участі. Ви можете виставити глобальну змінну з терміном життя у play усередині ролі. Або навіть із терміном життя плейбуки (set_fact/register). Але ви не можете мати "локальні змінні". Ви не можете "приймати значення" та "повертати його".

З цього випливає головне: не можна на ansible написати щось і не викликати сайд-ефекти. Зміна глобальних змінних - це завжди side effect для функції. У Rust, наприклад, зміна глобальної змінної - це unsafe. А в Ансібл — єдиний спосіб вплинути на значення для участі. Зверніть увагу на слова, що використовуються: не "передати значення в роль", а "змінити значення, які використовує роль". Між ролями немає ізоляції. Між тягами та ролями немає ізоляції.

Разом: роль - це не функція.

Що ж хорошого є у ролі? По-перше, роль має default values ​​(/default/main.yaml), по-друге, у ролі є додаткові каталоги для складання файлів.

Чим же хороші default values? Тим, що в піраміді Маслоу досить перекрученої таблиці пріоритетів змінних у Ансібла, role defaults — найнепріоритетніші (за винятком параметрів командного рядка ансибла). Це означає, що якщо вам треба надати значення за замовчуванням і не переживати, що вони переб'ють значення з інвенторів або групових змінних, то дефолти ролі - це єдине правильне місце для вас. (Я трохи брешу - є ще |d(your_default_here), але якщо говорити про стаціонарні місця – то лише дефолти ролей).

Що ще хорошого у ролях? Тим, що вони мають свої каталоги. Це каталоги для змінних, як постійних (тобто обчислюваних для ролі), так і для динамічних (є такий чи то патерн, чи то анти-патерн — include_vars разом з {{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml.). Це каталоги для files/, templates/. Ще, воно дозволяє мати ролі свої модулі та плагіни (library/). Але, в порівнянні з тягами у playbook'і (у якої теж все це може бути), користь тут тільки в тому, що файли звалені не в одну купу, а кілька окремих купок.

Ще одна деталь: можна намагатися виконувати ролі, які будуть доступні для перевикористання (через galaxy). Після появи колекцій поширення ролей вважатимуться майже забутим.

Таким чином, ролі мають дві важливі особливості: у них є дефолти (унікальна особливість) і вони дозволяють структурувати код.

Повертаючись до вихідного питання: коли робити таски, а коли ролі? Таски у плейбуці найчастіше використовуються або як "клей" до/після ролей, або як самостійний будівельний елемент (тоді в коді не повинно бути ролей). Груда нормальних тасок у перемішування з ролями - це однозначна неохайність. Слід дотримуватися конкретного стилю - або таски, або ролі. Ролі дають поділ сутностей та дефолти, таски дозволяють прочитати код швидше. Зазвичай в ролі виносять "стаціонарніший" (важливий і складний) код, а в стилі тасок пишуть допоміжні скрипти.

Існує можливість робити import_role як таску, але якщо ви таке пишете, то будьте готові до пояснювальної для власного прекрасного почуття, навіщо ви це хочете робити.

В'їдливий читач може сказати, що ролі можуть імпортувати ролі, у ролей може бути залежність через galaxy.yml, а ще є страшний та жахливий include_role — нагадую, ми підвищуємо навички у базовому Ансиблі, а не у фігурній гімнастиці.

Хендлери та таски

Давайте обговоримо ще одну очевидну річ: хендлер. Вміння їх правильно використовувати – це майже мистецтво. У чому різниця між хендлером та тяганням?

Оскільки ми згадуємо основи, то приклад:

- hosts: group1
  tasks:
    - foo:
      notify: handler1
  handlers:
     - name: handler1
       bar:

У ролі handler'и лежать у rolename/handlers/main.yaml. Handler'и нишпоряться між усіма учасниками play: pre/post_tasks можуть смикати handler'и ролі, а роль може смикати handler'и з плей. Однак, "крос-рольові" виклики handler'ів викликають куди більший wtf, ніж повтор тривіального handler'а. (Ще один елемент best practices – намагатися не робити повторів імен handler'ів).

Основна відмінність у тому, що тяга виконується (ідемпотентно) завжди (плюс/мінус теги та when), А хендлер - по зміні стану (notify спрацьовує тільки якщо був changed). Чим це загрожує? Наприклад, тим, що при повторному запуску, якщо не було змінено, то не буде і handler. А чому може бути так, що нам потрібно виконати handler коли не було changed у породжуючою тяги? Наприклад, тому що щось зламалося і changed був, а до хендлера виконання не дійшло. Наприклад, тому, що мережа тимчасово лежала. Конфіг змінився, обслуговування не перезапущено. При наступному запуску конфіг не змінюється, і сервіс залишається зі старою версією конфіга.

Ситуація з конфігом не розв'язувана (точніше, можна самим винайти спеціальний протокол перезапуску з файловими прапорами і т.д., але це вже не 'basic ansible' в жодному вигляді). Зате є інша часта історія: ми поставили додаток, записали його .service-Файл, і тепер хочемо його daemon_reload и state=started. І натуральне місце для цього, здається, хендлер. Але якщо зробити його не хендлером, а тягарем в кінці таскліста або ролі, то він буде ідемпотентно виконуватися щоразу. Навіть якщо плейбук зламався на середині. Це не вирішує проблеми restarted (не можна робити тягу з атрибутом restarted, т.к. втрачається ідемпотентність), але однозначно варто робити state=started, загальна стабільність плейбуки зростає, т.к. зменшується кількість зв'язків та динамічного стану.

Ще одна позитивна властивість handler'а полягає в тому, що він не засмічує висновок. Не було змін - немає зайвих skipped чи ok у висновку - легше читати. Воно ж є і негативною властивістю — якщо друкарську помилку в лінійно виконуваній task'і ви знайдете на перший же прогін, то handler'и будуть виконані тільки при changed, тобто. за деяких умов – дуже рідко. Наприклад, перший раз у житті через п'ять років. І, зрозуміло, там буде друкарська помилка в імені і все зламається. А вдруге їх не запустити — changed ні.

Окремо слід говорити про доступність змінних. Наприклад, якщо ви notify для таски з циклом, то що буде у змінних? Можна аналітичним шляхом здогадатися, але не завжди це тривіально, особливо якщо змінні приходять з різних місць.

…Так що handler'и куди менш корисні і набагато проблемніші, ніж здається. Якщо можна щось красиво (без викрутасу) написати без хендлерів краще робити без них. Якщо гарно не виходить – краще з ними.

В'їдливий читач справедливо зазначає, що ми не обговорили listen, що handler може викликати notify для іншого handler'а, що handler може включати в себе import_tasks (який може робити include_role c with_items), що система хендлерів в Ансіблі тьюрінг-повна, що хендлери з include_role найцікавіше перетинаються з х .д. - Все це явно не "основи").

Хоча є один певний WTF, який насправді фіч, і про який треба пам'ятати. Якщо у вас тяга виконується з delegate_to і вона має notify, то відповідний хендлер виконується без delegate_to, тобто. на хості, на якому призначено play. (Хоча у хендлера, зрозуміло, може бути delegate_to теж).

Окремо хочу сказати пару слів про reusable roles. До появи колекцій була ідея, що можна зробити універсальні ролі, які можна ansible-galaxy install та поїхав. Працює усім ОС всіх варіантів переважають у всіх ситуаціях. Так от моя думка: це не працює. Будь-яка роль з масовим include_vars, підтримкою 100500 випадків приречена на безодні corner case багів. Їх можна затикати масованим тестуванням, але як із будь-яким тестуванням, або у вас декартове твір вхідних значень і тотальна функція, або у вас "покриті окремі сценарії". Моя думка — набагато краща, якщо роль лінійна (цикломатична складність 1).

Чим менше if'ів (явних чи декларативних - у формі when або формі include_vars по набору змінних), краще роль. Іноді доводиться робити розгалуження, але, повторюю, що їх менше, то краще. Так що ніби гарна роль з galaxy (працює ж!) з купою when може бути менш кращою, ніж "своя" роль з п'яти тасок. Момент, коли роль з galaxy краща — коли ви починаєте щось писати. Момент, коли вона стає гіршою - коли щось ламається, і у вас є підозра, що це через "ролю з galaxy". Ви її відкриваєте, а там п'ять інклюдів, вісім таск-листів та стопка whenТо... І в цьому треба розібратися. Замість 5 тасок лінійним списком, в якому й ламатися нічому.

У наступних частинах

  • Трохи для інвенторів, групові змінні, host_group_vars plugin, hostvars. Як зі спагетті зв'язати Гордієв вузол. Scope та precedence змінних, модель пам'яті Ansible. "То де ж таки зберігати ім'я користувача для бази даних?".
  • jinja: {{ jinja }} - nosql notype nosense м'який пластилін. Воно всюди, навіть там, де ви на нього не очікуєте. Трохи про !!unsafe та смачний yaml.

Джерело: habr.com

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