Základy Ansible, bez kterých jsou vaše playbooky kusem slepených těstovin

Dělám mnoho recenzí pro cizí kód v Ansible a také hodně píšu sám. Během analýzy chyb (jak cizích, tak svých) a některých pohovorů jsem pochopil hlavní chybu, které se uživatelé Ansible dopouštějí — snaží se pustit do složitých věcí, aniž by ovládli základy.

Abych opravil tuto celosvětovou nespravedlnost, rozhodl jsem se napsat úvod do Ansible pro ty, kteří ho už znají. Varuji, není to shrnutí manuálů, ale dlouhý text, ve kterém je mnoho písmen a žádné obrázky.

Očekávaná úroveň čtenáře — už napsal několik tisíc řádků YAML, už něco v produkci, ale "všechno je nějak křivé".

Názvy

Hlavní chybou uživatele Ansible je neznat názvy. Pokud neznáte názvy, nemůžete rozumět tomu, co je napsáno v dokumentaci. Živý příklad: na pohovoru osoba, která se zdála, že hodně psala v Ansible, nedokázala odpovědět na otázku "z čeho se skládá playbook?". A když jsem naznačil, že "očekávaná odpověď byla, že playbook se skládá z play", přišel vražedný komentář "to nepoužíváme". Lidé píší v Ansible za peníze a nepoužívají play. Ve skutečnosti se používá, ale nevědí, co to je.

Takže začneme s jednoduchým: jak se co nazývá. Možná to víte, a možná ne, protože jste si toho nevšimli, když jste četli dokumentaci.

ansible-playbook provádí playbook. Playbook — je to soubor s příponou yml/yaml, uvnitř kterého je něco takového:

---
- hosts: group1
  roles:
    - role1

- hosts: group2,group3
  tasks:
    - debug:

Už jsme pochopili, že celý tenhle soubor — je playbook. Můžeme ukázat, kde jsou role (roles), kde jsou úkoly (tasks). Ale kde je tady play? A čím se play liší od role nebo playbooku?

O tom všechno je v dokumentaci. A to lidé přeskakují. Začátečníci — protože tam je příliš mnoho a všechno si hned nezapamatujete. Zkušení — protože "triviální věci". Pokud jste zkušení — přečtěte si tyto stránky alespoň jednou za půl roku a váš kód se stane o třídu lepším.

Tak si zapamatujte: Playbook — je seznam, který se skládá z play a import_playbook.
Tohle — je jeden play:

- hosts: group1
  roles:
    - role1

a tohle je také ještě jeden play:

- hosts: group2,group3
  tasks:
    - debug:

Co tedy play je? K čemu je?

Play — je klíčový prvek pro playbook, protože play a pouze play spojuje seznam rolí a/nebo úkolů se seznamem hostitelů, na kterých je třeba je vykonat. V hlubinách dokumentace je zmínka o delegate_to, lokální lookup pluginy, specifické nastavení pro network-cli, jump hosty atd. Umožňují mírně změnit místo vykonávání úloh. Ale na to zapomeňte. Každá z těchto chytrých možností má velmi speciální použití a rozhodně nejsou univerzální. A my se bavíme o základních věcech, které by měli znát a používat všichni.

Pokud chcete "něco" vykonat "někde" — napíšete play. Ne roli. Ne roli s moduly a delegáty. Vezmete si a napíšete play. V něm v poli hosts uvedete, kde se má vykonávat, a v roles/tasks — co se má vykonávat.

Snadné, že? A jak by to mohlo být jinak?

Jedním z charakteristických momentů, kdy lidé chtějí udělat něco jinak než přes play, je "role, která vše nastaví". Chtělo by to mít roli, která nastavuje jak serveru prvního typu, tak servery druhého typu.

Archetypálním příkladem je monitorování. Chtělo by to mít roli monitoring, která nastaví monitorování. Role monitoring je přiřazena k hostům monitorování (ve vhodném play). Ale zjistí se, že pro monitorování musíme nainstalovat balíčky na hostech, které monitorujeme. Proč nevyužít delegaci? A také je třeba nastavit iptables. Delegát? A také je potřeba napsat/upravit konfiguraci pro DBMS, aby monitoring povolil. Delegát! A pokud nápad frčí, může se udělat delegace include_role ve vnořeném cyklu podle chytrého filtru na seznam skupin, a uvnitř include_role můžeme znovu provádět delegate_to a jde to dál…

Dobré přání — mít jednu jedinou roli monitoring, která "všechno dělá" — nás vede do pekla, z něhož často existuje pouze jeden východ: všechno přepsat od začátku.

Kde došlo k chybě? V okamžiku, kdy jste zjistili, že pro vykonání úkolu "x" na hostu X potřebujete jít na host Y a udělat tam "y", měli jste provést jednoduché cvičení: jít a napsat play, která na hostu Y dělá y. Ne dopisovat něco do "x", ale napsat znovu od nuly. I s hardcoded proměnnými.

Vypadá to, že v předchozích odstavcích je všechno řečeno správně. Ale to přece není váš případ! Protože chcete napsat opětovně použitelný kód, který je DRY a podobný knihovně, a je třeba hledat metodu, jak to udělat.

Tady se skrývá ještě jedna zásadní chyba. Chyba, která přeměnila spoustu projektů z víceméně podařených (dalo by se to udělat lépe, ale vše funguje a dá se snadno doplnit) v naprostý chaos, ve kterém ani autor nemůže najít směr. Funguje to, ale bůh chraň něco změnit.

Tato chyba zní takto: role je knihovní funkce. Tato analogie zabila tolik dobrých začátků, že je z toho smutno. Role není knihovní funkce. Nemůže provádět výpočty a nemůže přijímat rozhodnutí na úrovni play. Připomeňte mi, jaká rozhodnutí play přijímá?

Děkuji, máte pravdu. Play přijímá rozhodnutí (přesněji řečeno, obsahuje informace) o tom, které úkoly a role mají být vykonány na jakých hostitelích.

Pokud svěřujete toto rozhodnutí roli, a ještě k tomu s výpočty, odsuzujete sebe (a toho, kdo se pokusí váš kód rozluštit) k žebráckému existenci. Role nerozhoduje, kde má být vykonávána. Toto rozhodnutí přijímá play. Role dělá to, co jí bylo řečeno, tam, kde jí bylo řečeno.

Proč je programování v Ansible nebezpečné a jak je COBOL lepší než Ansible, o tom si povíme v kapitole o proměnných a jinja. Zatím jedno — každý váš výpočet zanechává nezmazatelnou stopu v podobě změny globálních proměnných, se kterými s tím nic nezmůžete. Jakmile se dvě "stopy" překrývají — všechno je ztraceno.

Poznámka pro pečlivé: role může samozřejmě ovlivňovat řízení toku. Existuje delegate_to a má rozumné aplikace. Existuje meta: end host/play. Ale! Pamatujte, učíme se základy? Zapomněli jste na delegate_to. Mluvíme o nejjednodušším a nejčistším kódu v Ansible. Který je snadno čitelný, snadno psaný, snadno laditelný, snadno testovatelný a snadno doplnitelný. Takže, ještě jednou:

play a jen play rozhoduje, na kterých hostitelích se co vykonává.

V této části jsme se zabývali konfliktem mezi play a role. Teď si promluvme o vztazích tasks vs role.

Úkoly a role

Podívejme se na play:

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

Představte si, že potřebujete udělat foo. A vypadá to jako foo: name=foobar state=present. Kam to napsat? do pre? post? Vytvořit roli?

… A kam zmizely úkoly?

Znovu začínáme od základů — struktura play. Pokud se v této otázce plácáte, nemůžete použít play jako základ pro všechno ostatní, a váš výsledek je "křehký".

Hra play: direktiva hosts, nastavení samotné hry a sekcí pre_tasks, tasks, roles, post_tasks. Ostatní parametry pro hru nejsou nyní důležité.

Pořadí jejich sekcí s úlohami a rolemi: pre_tasks, role, tasks, post_tasks. Protože mezi tasks a role není jasný pořádek vykonávání, doporučuje nejlepší praxe přidat sekci tasks, pouze pokud neexistuje role. Pokud je role, pak se všechny připojené úkoly umístí do sekcí pre_tasks/post_tasks.

Zůstává jen to, co je semanticky jasné: nejprve pre_tasks, pak role, pak post_tasks.

Ale stále jsme neodpověděli na otázku: kam napsat volání modulu foo ? Musíme pro každý modul psát celou roli? Nebo je lepší mít tlustou roli pro vše? A pokud ne roli, kam psát – do pre nebo do post?

Pokud na tyto otázky není argumentovaná odpověď, je to známka absence intuice, tedy oněch "křehkých základů". Pojďme se na to podívat. Nejprve kontrolní otázka: Pokud má play pre_tasks a post_tasks (a nejsou ani úkoly, ani role), může se něco pokazit, když první úkol z post_tasks přesunu na konec pre_tasks?

Samozřejmě, formulace otázky naznačuje, že něco se pokazí. Ale co přesně?

... Handlery. Čtení základů odhaluje důležitý fakt: všechny handlery se automaticky flushují po každé sekci. To znamená, že se provedou všechny úkoly z pre_tasks, pak všechny handlery, které byly notify. Poté se provedou všechny role a všechny handlery, které byly notify v rolích. A pak post_tasks a jejich handlery.

Takže pokud úkol přesunete z post_tasks v pre_tasks, potenciálně jej provedete před vykonáním handleru. Například, pokud v pre_tasks se instaluje a konfiguruje webový server, a v post_tasks do něj něco posílá, pak přesunutí tohoto úkolu do sekce pre_tasks povede k tomu, že v okamžiku "posílání" server ještě nebude spuštěn a všechno se pokazí.

A teď se pojďme znovu zamyslet, proč nám pre_tasks a post_tasks? Например, для того, чтобы выполнить всё нужное (включая хэндлеры) до выполнения роли. А post_tasks umožní pracovat s výsledky vykonání rolí (včetně handlerů).

Důvtipný znalec Ansible nám řekne, že existuje meta: flush_handlers, ale proč potřebujeme flush_handlers, když se můžeme spolehnout na pořadí vykonávání sekcí v play? Navíc použití meta: flush_handlers nám může přinést nečekané problémy s opakujícími se handlery a způsobit divné varování v případě použití when u block atd. Čím lépe znáte Ansible, tím více nuancí budete schopni uvést pro "chytré" řešení. A jednoduché řešení – použití přirozeného rozdělení mezi pre/roles/post – nezpůsobuje žádné nuance.

A vracíme se k našemu 'foo'. Kam ho umístit? Do pre, post nebo do roles? Zjevně to závisí na tom, zda potřebujeme výsledky práce handleru pro foo. Pokud ne, pak foo není potřeba dávat ani do pre, ani do post — tyto sekce mají zvláštní význam — provádění úloh před a po hlavním bloku kódu.

Teď odpověď na otázku "role nebo úloha" závisí na tom, co už je v play — pokud tam jsou tasks, je třeba dopsat do tasks. Pokud jsou tam roles — je třeba vytvořit roli (i když jen z jedné úlohy). Připomínám, tasks a roles se současně nepoužívají.

Pochopení základů Ansible dává opodstatněné odpovědi na, zdánlivě, otázky vkusu.

Úlohy a role (druhá část)

Teď probereme situaci, když právě začínáte psát playbook. Musíte udělat foo, bar a baz. Jsou to tři úlohy, jedna role nebo tři role? Obecně: kdy začít psát role? Jaký je smysl psát role, když je možné psát úlohy?… A co to vlastně role je?

Jedna z největších chyb (o tom jsem už mluvil) — myslet si, že role je jako funkce v knihovně programu. Jak vypadá obecný popis funkce? Přijímá argumenty na vstupu, interaguje s vedlejšími příčinami, dělá vedlejší účinky a vrací hodnotu.

Teď, pozor. Co z toho lze udělat v roli? Vyvolat vedlejší účinky — vždy prosím, to je podstatou Ansible — dělat vedlejší účinky. Mít vedlejší příčiny? Elementární. Ale s "předat hodnotu a vrátit ji" — zde to nejde. Za prvé, nemůžete předat hodnotu do role. Můžete nastavit globální proměnnou s životností v play v sekci vars pro roli. Můžete nastavit globální proměnnou s životností v play uvnitř role. Nebo dokonce s životností playbooku (set_fact/register). Ale nemůžete mít "lokální proměnné". Nemůžete "přijmout hodnotu" a "vrátit ji".

Z toho vyplývá hlavní: nelze napsat v ansible něco a nevyvolat vedlejší účinky. Změna globálních proměnných — to je vždy vedlejší účinek pro funkci. V Rustu, například, změna globální proměnné — to je unsafe. A v Ansible — jediný způsob, jak ovlivnit hodnoty pro roli. Všimněte si použitých slov: ne "předat hodnotu do role", ale "změnit hodnoty, které role používá". Mezi rolemi není izolace. Mezi úlohami a rolemi není izolace.

Celkem: role — to není funkce.

Jaké výhody má role? Za prvé, role má výchozí hodnoty (/default/main.yaml), a za druhé, role má další adresáře pro ukládání souborů.

Jaké jsou tedy výhody výchozích hodnot? To, že v Maslowově pyramidě je v poměrně pokřivené tabulce priorit proměnných v Ansible, výchozí hodnoty rolí jsou nejméně prioritní (s výjimkou parametrů příkazového řádku Ansible). To znamená, že pokud potřebujete poskytnout výchozí hodnoty a nechcete se bát, že přepíšou hodnoty z inventáře nebo skupinových proměnných, pak výchozí hodnoty role jsou jediné správné místo pro vás. (Trochu lžu — existuje ještě |d(your_default_here), ale pokud mluvíme o stacionárních místech — tak jen výchozí hodnoty rolí).

Jaké další výhody mají role? To, že mají své vlastní adresáře. Jsou to adresáře pro proměnné, jak trvalé (tj. vyhodnocované pro roli), tak pro dynamické (existuje takový vzor nebo anti-vzor — include_vars spolu s {{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml.). Jsou to adresáře pro files/, templates/. Ještě umožňuje mít role vlastní moduly a pluginy (library/). Ale ve srovnání s úkoly v playbooku (které to také mohou mít), výhoda zde spočívá pouze v tom, že soubory nejsou hromadně smíchány, ale jsou rozdány do několika oddělených hromádek.Další detail: můžete se snažit vytvářet role, které budou dostupné pro opětovné použití (přes galaxy). Po vzniku kolekcí se šíření rolí dá téměř považovat za zapomenuté.

Tímto způsobem mají role dvě důležité rysy: mají výchozí hodnoty (unikátní vlastnost) a umožňují strukturovat kód.

Vrátíme se k původní otázce: kdy dělat úkoly a kdy role? Úkoly v playbooku se nejčastěji používají buď jako "lepidlo" před/s během rolí, nebo jako samostatná stavební jednotka (pak by v kódu neměly být role). Hromada normálních úkolů mixovaných s rolemi — to je jednoznačně neuspořádané. Měli byste se držet konkrétního stylu — buď úkoly, nebo role. Role přinášejí rozdělení entit a výchozí hodnoty, úkoly umožňují rychleji číst kód. Obvykle se do rolí umísťuje více "stacionární" (důležitý a složitý) kód, zatímco ve stylu úkolů se píší pomocné skripty.

Když se vrátíme k původní otázce: kdy používat úkoly a kdy role? Úkoly v playbooku se nejčastěji používají buď jako "lepidlo" před/po rolích, nebo jako samostatný stavební blok (tehdy v kódu by neměly být role). Hromada běžných úkolů smíchaných s rolemi je jednoznačně nepořádek. Měli bychom se držet konkrétního stylu — buď úkoly, nebo role. Role poskytují oddělení entit a výchozí hodnoty, úkoly usnadňují rychlejší čtení kódu. Obvykle se v rolích vyčleňují více „stacionární“ (důležitý a složitý) kód, zatímco ve stylu úkolů se píší pomocné skripty.

Existuje možnost dělat import_role jako úkol, ale pokud to děláte, buďte připraveni na vysvětlení pro své vlastní estetické cítění, proč to chcete dělat.

Zvědavý čtenář může říct, že role mohou importovat role, role mohou mít závislost přes galaxy.yml, a ještě je zde děsivý a hrozný. include_role — připomínám, že zvyšujeme dovednosti v základním Ansible, a ne v gymnastice.

Handlery a úkoly

Pojďme probrat ještě jednu očividnou věc: handlery. Schopnost je správně používat je téměř umění. Jaký je rozdíl mezi handlerem a úkolem?

Protože si připomínáme základy, zde je příklad:

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

V roli se handlery nacházejí v rolename/handlers/main.yaml. Handlery sdílejí všichni účastníci play: pre/post_tasks mohou volat handlery role, a role může volat handlery z play. Nicméně, "kros-role" volání handlerů vyvolávají mnohem větší zmatení, než opakování triviálního handlera. (Další prvek osvědčených postupů — snažit se nedělat opakování názvů handlerů).

Hlavní rozdíl spočívá v tom, že úkol se vykonává (idempotentně) vždy (plus/mínus tagy a when), zatímco handler — na změnu stavu (notify se aktivuje pouze pokud došlo ke změně). Co to může způsobit? Například to, že při opakovaném spuštění, pokud nebylo changed, tak nebude ani handler. A proč může být tak, že potřebujeme vykonat handler, když u tvořícího úkolu nebylo changed? Například proto, že něco přestalo fungovat a došlo ke změně, ale k handleru se provádění nedostalo. Například proto, že bylo dočasně přerušeno spojení. Konfigurace se změnila, ale služba nebyla restartována. Při dalším spuštění se konfigurace už nemění a služba zůstává se starou verzí konfigurace.

Situace s konfigurací není řešitelná (přesněji, můžeme si vymyslet speciální protokol restartování s flagy souborů atd., ale to už není ‚základní ansible‘ v žádné podobě). Ale existuje další častý příběh: nainstalovali jsme aplikaci, zapsali jsme ji do .service-souboru a nyní ji chceme daemon_reload a state=started. A přirozené místo pro to se zdá být handler. Ale pokud to uděláte ne jako handler, ale jako úkol na konci seznamu úkolů nebo rolí, bude to idempotentně prováděno pokaždé. I když se playbook rozpadl v průběhu. To samozřejmě neřeší problém restarted (nelze udělat úkol s atributem restarted, protože se ztrácí idempotentnost), ale rozhodně stojí za to mít state=started, celková stabilita playbooků se zvyšuje, protože se snižuje počet spojení a dynamického stavu.

Další pozitivní vlastností handleru je, že nezaplavuje výstup. Nebyly provedeny změny — nejsou žádné zbytečné skipped nebo ok ve výstupu — je to snazší k přečtení. To je také negativní vlastnost — pokud najdete překlep v lineárně vykonávaném úkolu při prvním běhu, handler budou provedeny pouze při changed, tzn. za určitých podmínek — velmi zřídka. Například poprvé za život po pěti letech. A samozřejmě, tam bude překlep v názvu a všechno se rozpadne. Při druhém běhu je nemůžete spustit — changed totiž není.

Samostatně musíme hovořit o dostupnosti proměnných. Například, pokud máte notifikaci pro úkol s cyklem, co bude v proměnných? Můžete to analyticky zjistit, ale není to vždy triviální, zvláště když proměnné přicházejí z různých míst.

… Takže handlery jsou mnohem méně užitečné a mnohem problematičtější, než se zdá. Pokud můžete něco napsat pěkně (bez obstrukcí) bez handlerů, je lepší to udělat bez nich. Pokud se to pěkně nedaří — je lepší s nimi.

Pozorný čtenář správně poznamenává, že jsme neprobrali listen, že handler může vyvolat notify pro jiný handler, že handler může obsahovat import_tasks (který může vykonávat include_role s with_items), že systém handlerů v Ansible je Turingovsky úplný, že handlery z include_role se zajímavě překrývají s handlery z play atd. — to všechno rozhodně není 'základy'.

I když je tu jedno specifické WTF, které je ve skutečnosti funkcí, a na které je třeba pamatovat. Pokud máte úkol, který se vykonává s delegate_to a má notifikaci, příslušný handler se vykonává bez delegate_to, tzn. na hostiteli, na kterém je přiřazen play. (I když handler může mít samozřejmě delegate_to také).

Samostatně bych chtěl říci pár slov o reusable roles. Před vznikem kolekcí byl nápad, že můžete vytvořit univerzální role, které lze ansible-galaxy install a jelo. Funguje na všech operačních systémech ve všech situacích. Takže, můj názor je: to nefunguje. Jakákoli role s masivní include_varspodporou 100500 případů je odsouzena k beznadějným corner case chybám. Ty mohou být ucpány masivním testováním, ale jako u každého testování, buď máte dekartský produkt vstupních hodnot a totální funkci, nebo máte "pokryté jednotlivé scénáře". Můj názor — je mnohem lepší, pokud je role lineární (cyklomatická složitost 1).

Čím méně ifů (výslovných nebo deklarativních — ve formě when nebo formě include_vars na základě proměnných), tím lepší je role. Někdy musí docházet k větvení, ale znovu opakuji, čím jich je méně, tím lépe. Takže vypadá to, že dobrá role s galaxy (však funguje!) s množstvím when může být méně preferována než "vlastní" role z pěti úkolů. Moment, kdy je role s galaxy lepší — když začínáte něco psát. Moment, kdy se stává horší — když něco přestane fungovat a máte podezření, že je to kvůli "roli s galaxy". Otevřete ji a tam je pět inkluzí, osm seznamů úkolů a hromada when‘ů… A v tom se musíte vyznat. Místo 5 úkolů v lineárním seznamu, ve kterém ani není co pokazit.

V následujících částech

  • trochu o inventáři, skupinových proměnných, pluginu host_group_vars, hostvars. Jak propojit spaghetti ve Gordionův uzel. Rozsah a precedence proměnných, model paměti Ansible. "Tak kde vlastně uchovávat uživatelské jméno pro databázi?"
  • jinja: {{ jinja }} — nosql notype nosense měkký plastelín. Je všude, i tam, kde ho neočekáváte. Trochu o !!unsafe a chutném yaml.

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster