Асновы Ansible, без якіх вашы плэйбукі - камяк зліплых макароны

Я раблю шмат раўчу для чужога кода на Ансібл і шмат пішу сам. Падчас аналізу памылак (як чужых, так і сваіх), а гэтак жа некаторай колькасці гутарак, я зразумеў асноўную памылку, якую дапушчаюць карыстачы Ансібла - яны лезуць у складанае, не асвоіўшы базавага.

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

Чаканы ўзровень чытача - ужо напісана некалькі тысяч радкоў ямла, ужо нешта ў прадакшэне, але "неяк усё крыва".

назвы

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

Так што пачнем з простага: як што называецца. Можа быць, вы гэта ведаеце, а можа і не, таму што не звярнулі ўвагі, калі чыталі дакументацыю.

ansible-playbook выконвае playbook. Playbook - гэта файл з пашырэннем yml/yaml, усярэдзіне якога нешта такое:

---
- hosts: group1
  roles:
    - role1

- hosts: group2,group3
  tasks:
    - debug:

Мы ўжо зразумелі, што ўвесь гэты файл - плэйбука. Мы можам паказаць дзе тут ролі (roles), дзе цягі (tasks). Але дзе тут play? І чым адрозніваецца 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, потым усе хэндлеры, якія былі notify. Потым выконваюцца ўсе ролі і ўсе хэндлеры, якія былі notify у ролях. Потым 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). Чым гэта багата? Напрыклад, што пры паўторным запуску, калі не было 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

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