Fac multe recenzii pentru codul altora pe Ansible și scriu mult singur. În timpul analizei erorilor (atât ale altora, cât și ale mele), dar și a unor interviuri, am realizat greșeala fundamentală pe care o fac utilizatorii Ansible — se bazează pe complexitate fără a stăpâni bazele.
Pentru a corecta această nedreptate universală, am decis să scriu o introducere în Ansible pentru cei care deja îl cunosc. Vă avertizez, aceasta nu este o reluare a manualelor, este un lung articol în care sunt multe cuvinte și nu sunt imagini.
Nivelul așteptat al cititorului — a scris deja câteva mii de linii de YAML, are deja ceva în producție, dar "totul este oarecum greșit".
Denominări
Principală greșeală a utilizatorului Ansible — este să nu știe cum se numesc lucrurile. Dacă nu știi denumirile, nu poți înțelege ceea ce este scris în documentație. Un exemplu viu: la interviu, o persoană, care se părea că a declarat că a scris mult pe Ansible, nu a putut răspunde la întrebarea "din ce elemente constă un playbook?". Și când am sugerat că "se aștepta un răspuns că playbook-ul constă din play", a urmat comentariul devastator "noi nu folosim asta". Oamenii scriu pe Ansible pentru bani și nu folosesc play. De fapt, îl folosesc, dar nu știu ce este.
Așadar, să începem cu simplu: cum se numesc lucrurile. Poate că știai asta, sau poate nu, pentru că nu ai fost atent când ai citit documentația.
ansible-playbook execută playbook. Playbook — este un fișier cu extensia yml/yaml, în interiorul căruia este ceva de genul:
---
- hosts: group1
roles:
- role1
- hosts: group2,group3
tasks:
- debug:Deja am înțeles că întregul acest fișier este un playbook. Putem arăta unde sunt rolurile (roles), unde sunt sarcinile (tasks). Dar unde este play-ul? Și cu ce se deosebește play de role sau playbook?
Toate acestea sunt în documentație. Și se sar în acest sens. Începătorii — pentru că este prea mult și nu reușesc să rețină totul deodată. Cei experimentați — pentru că sunt "lucruri triviale". Dacă ești experimentat — citește aceste pagini măcar o dată la șase luni și codul tău va deveni cu clasa mai bun.
Așadar, rețineți: Playbook — este o listă, formată din play și import_playbook.
Aceasta este o play:
- hosts: group1
roles:
- role1Și aceasta este o altă play:
- hosts: group2,group3
tasks:
- debug:Ce este play? La ce folosește?
Play — este elementul cheie pentru playbook, pentru că play și numai play leagă lista de roluri și/sau sarcini de lista de gazde pe care trebuie să fie executate. În adâncurile documentației se poate găsi o mențiune despre delegate_to, plugin-uri de căutare locale, setări specifice pentru network-cli, jump hosts etc. Acestea permit o modificare minimă a locului de executare a sarcinilor. Dar, uitați de asta. Fiecare dintre aceste opțiuni avansate are aplicații foarte specifice și cu siguranță nu sunt universale. Și noi vorbim despre elementele de bază pe care ar trebui să le cunoască și să le folosească toți.
Dacă doriți să executați "ceva" "undeva" — scrieți un play. Nu un rol. Nu un rol cu module și delegate. Pur și simplu scrieți un play. În care, în câmpul hosts, enumerați unde să executați, iar în roles/tasks — ce să executați.
E simplu, nu? Și cum altfel ar putea fi?
Unul dintre momentele caracteristice în care oamenii simt dorința de a face acest lucru fără un play este "rolul care configurează totul". Se dorește un rol care să configureze și server servere de tipul unu și servere de tipul doi.
Un exemplu arhetipal este monitorizarea. Se dorește un rol de monitorizare, care să configureze monitorizarea. Rolul de monitorizare este atribuit hosturilor de monitorizare (în cadrul play-ului corespunzător). Dar, se dovedește că pentru monitorizare trebuie să instalăm pachete pe hosturile pe care le monitorizăm. De ce să nu folosim delegate? Și, de asemenea, trebuie să configurăm iptables. delegate? Și, de asemenea, trebuie să scriem/corectăm configurația pentru SGBD, astfel încât monitorizarea să fie permisă. delegate! Și dacă inspirația vine, atunci putem face delegație include_role într-un ciclu interior printr-un filtru inteligent pe lista grupurilor, iar în interior include_role putem face și delegate_to din nou. Și tot așa…
O dorință lăudabilă — de a avea un singur rol de monitorizare care "face totul" — ne conduce în iadul nebuniei din care, de cele mai multe ori, există o singură ieșire: să rescriem totul de la zero.
Unde a apărut greșeala aici? În momentul în care ați descoperit că pentru a îndeplini sarcina "x" pe hostul X trebuie să mergeți pe hostul Y și să faceți "y", ar fi trebuit să efectuați un exercițiu simplu: mergeți și scrieți un play care pe hostul Y face y. Nu adăugați ceva în "x", ci scrieți de la zero. Chiar dacă cu variabile hardcodate.
Pare că în paragrafele de mai sus totul este spus corect. Dar aceasta nu este situația dvs.! Deoarece doriți să scrieți cod reutilizabil, care să fie DRY și să semene cu o bibliotecă, și trebuie să căutați metoda de a face acest lucru.
Aici s-a ascuns o altă greșeală gravă. O greșeală care a transformat numeroase proiecte din scrise rezonabil (se putea mai bine, dar totul funcționează și poate fi continuat ușor) într-un coșmar complet, în care chiar și autorul nu se mai poate descurca. Funcționează, dar Dumnezeule ferește să schimb ceva.
Această greșeală sună astfel: rolul este o funcție de bibliotecă. Această analogie a distrus atât de multe inițiative bune, încât este pur și simplu trist să ne uităm. Rolul nu este o funcție de bibliotecă. Nu poate face calcule și nu poate lua decizii la nivel de play. Amintește-mi, ce decizii ia play?
Mulțumesc, ai dreptate. Play ia decizia (mai exact, conține informația) despre ce sarcini și roluri să fie executate pe ce gazde.
Dacă delegi această decizie unui rol, și încă cu calcule, te condamni (pe tine și pe cel care va încerca să înțeleagă codul tău) la o existență jalnică. Rolul nu decide unde să se execute. Această decizie o ia play. Rolul face ceea ce i s-a spus, acolo unde i s-a spus.
Vom discuta despre de ce programarea în Ansible este periculoasă și de ce COBOL este mai bun decât Ansible în capitolul despre variabile și jinja. Deocamdată să spunem un lucru — fiecare calcul pe care îl faci lasă o urmă de neșters din schimbarea variabilelor globale, și nu poți face nimic în privința asta. De îndată ce două "urme" se intersectează — totul este pierdut.
Observație pentru cei atenți: rolul poate influența, desigur, fluxul de control. Există delegate_to și are aplicații rezonabile. Există meta: end host/play. Dar! Amintiți-vă, învățăm noțiunile de bază? Ați uitat despre delegate_to. Vorbim despre cel mai simplu și frumos cod în Ansible. Care este ușor de citit, ușor de scris, ușor de debuggat, ușor de testat și ușor de continuat. Așadar, încă o dată:
play și doar play decide pe ce gazde se execută ce.
În această secțiune am discutat despre opoziția dintre play și rol. Acum să vorbim despre relația sarcini vs rol.
Sarcini și Roluri
Să luăm în considerare play:
- hosts: somegroup
pre_tasks:
- some_tasks1:
roles:
- role1
- role2
post_tasks:
- some_task2:
- some_task3:Să presupunem că trebuie să faci foo. Și arată așa foo: name=foobar state=present. Unde trebuie să scrii asta? în pre? post? Să creezi un rol?
… Și unde au dispărut sarcinile?
Începem din nou cu noțiunile de bază — structura play. Dacă ești confuz în această privință, nu poți folosi play ca bază pentru tot restul, iar rezultatul tău devine "instabil".
Dispozitivul play: directiva hosts, setările play-ului în sine și secțiunile pre_tasks, tasks, roles, post_tasks. Celelalte parametri pentru play nu sunt importante acum.
Ordinea secțiunilor lor cu sarcini și roluri: pre_tasks, roles, tasks, post_tasks. Deoarece nu este clar ordinea de execuție între tasks și roles best practices ne spun că adăugăm secțiunea tasks, numai dacă nu există roles. Dacă există roles, atunci toate sarcinile atașate sunt plasate în secțiunile pre_tasks/post_tasks.
Rămâne doar ceea ce este clar semnificativ: mai întâi pre_tasks, apoi roles, apoi post_tasks.
Dar încă nu am răspuns la întrebarea: unde să scriem apelul modulului foo ? Trebuie să scriem un rol întreg pentru fiecare modul? Sau este mai bine să avem un rol complex pentru toate? Și dacă nu este rol, unde să scriem – în pre sau în post?
Dacă nu există un răspuns argumentat la aceste întrebări, este un semn al lipsei de intuiție, adică acele "fundamente instabile". Să încercăm să clarificăm. Mai întâi, o întrebare de control: Dacă play-ul are pre_tasks și post_tasks (și nu există sarcini, nici roluri), se poate întâmpla ceva rău dacă mut greaua sarcină din post_tasks la finalul pre_tasks?
Desigur, formularea întrebării sugerează că se va strica ceva. Dar ce anume?
... Manevrelor. Studiul conceptelor de bază dezvăluie un fapt important: toate manevrele se execuțiează automat după fiecare secțiune. Adică, se execută toate sarcinile din pre_tasks, apoi toate manevrele care au fost notify. Apoi se execută toate rolurile și toate manevrele care au fost notify în roluri. Apoi post_tasks și manevrele lor.
Astfel, dacă muți o sarcină din post_tasks în pre_tasks, atunci, potențial, o vei executa înainte de a se executa manevra. De exemplu, dacă în pre_tasks se instalează și se configurează server web, iar în post_tasks se trimite ceva, atunci mutarea acestei sarcini în secțiunea pre_tasks va duce la faptul că, în momentul de "trimite", serverul nu va fi încă pornit și totul se va strica.
Acum să ne gândim din nou, de ce avem nevoie de pre_tasks și post_tasks? Например, для того, чтобы выполнить всё нужное (включая хэндлеры) до выполнения роли. А post_tasks ne va permite să lucrăm cu rezultatele execuției rolurilor (inclusiv manevrele).
Un cunoscător perspicace al Ansible ne va spune că există meta: flush_handlers, dar de ce avem nevoie de flush_handlers, dacă ne putem baza pe ordinea de execuție a secțiunilor în play? Mai mult, utilizarea meta: flush_handlers poate provoca apariția neașteptată a manevrelor repetate, generând avertismente ciudate în cazul utilizării when u block etc. Cu cât înțelegi mai bine Ansible, cu atât mai multe nuanțe vei putea menționa pentru o soluție "înșelătoare". Iar soluția simplă – utilizarea unei separări naturale între pre/roles/post – nu provoacă nuanțe.
Și acum, să revenim la ‘foo’. Unde să-l plasăm? În pre, post sau în roles? Evident, depinde de faptul dacă avem nevoie de rezultatele muncii handler-ului pentru foo. Dacă nu, atunci foo nu trebuie plasat în niciunul din pre sau post — aceste secțiuni au un sens special — executarea sarcinilor înainte și după blocul principal de cod.
Acum, răspunsul la întrebarea "rol sau sarcină" se reduce la ceea ce există deja în play — dacă există tasks, trebuie completat în tasks. Dacă există roles, trebuie creat un rol (chiar și dintr-o singură task). Reamintesc, tasks și roles nu sunt folosite simultan.
Înțelegerea elementelor de bază ale Ansible oferă răspunsuri motivate la întrebări ce păreau subiective.
Sarcini și roluri (partea a doua)
Acum să discutăm despre situația în care abia începi să scrii un playbook. Trebuie să faci foo, bar și baz. Sunt acestea trei sarcini, un rol sau trei roluri? Generalizând întrebarea: când trebuie să începi să scrii roluri? Care este sensul scrierii rolurilor, când poți scrie sarcini?… Și ce este un rol?
Una dintre cele mai grave greșeli (deja am menționat asta) este să consideri că un rol este ca o funcție într-o bibliotecă a unei aplicații. Cum arată o descriere generalizată a unei funcții? Acceptă argumente la intrare, interacționează cu efecte secundare, produce efecte secundare, returnează o valoare.
Acum, atenție. Ce dintre acestea poate fi realizat într-un rol? A produce efecte secundare — întotdeauna, aceasta este esența lui Ansible — a genera efecte secundare. A avea cauze secundare? Elementar. Dar cu "a transmite o valoare și a o returna" — acolo nu. În primul rând, nu poți transmite o valoare într-un rol. Poți seta o variabilă globală cu durata de viață a play-ului în secțiunea vars pentru rol. Poți seta o variabilă globală cu durata de viață în play în interiorul rolului. Sau chiar cu durata de viață a playbook-ului (set_fact/register). Dar nu poți avea "variabile locale". Nu poți "accepta o valoare" și "returna-o".
Din aceasta rezultă concluzia principală: nu poți scrie nimic în Ansible fără a provoca efecte secundare. Schimbarea variabilelor globale este întotdeauna un efect secundar pentru funcție. În Rust, de exemplu, schimbarea unei variabile globale este unsafe. Iar în Ansible — este singura metodă de a afecta valorile pentru rol. Observați cuvintele folosite: nu "a transmite o valoare într-un rol", ci "a schimba valorile pe care le folosește rolul". Nu există izolație între roluri. Nu există izolație între sarcini și roluri.
În total: rolul este nu o funcție.
Ce este bun în rol? În primul rând, rolul are valori implicite (/default/main.yaml), în al doilea rând rolul are directoare suplimentare pentru stocarea fișierelor.
Ce este atât de bun la valorile implicite? Că în piramida lui Maslow, o tabelă destul de distorsionată de priorități a variabilelor în Ansible, valorile implicite ale rolului sunt cele mai puțin prioritizate (cu excepția parametrilor din linia de comandă Ansible). Aceasta înseamnă că, dacă trebuie să oferiți valori implicite fără a vă face griji că ele vor suprascrie valorile din inventar sau din variabilele de grup, atunci valorile implicite ale rolului sunt singurul loc corect pentru dvs. (Mă mint puțin - mai există și |d(your_default_here), dar dacă vorbim despre locuri statice - atunci doar valorile implicite ale rolurilor).
Ce altceva este bun în roluri? Că acestea au directoarele lor. Acestea sunt directoare pentru variabile, atât permanente (adică, calculate pentru rol), cât și pentru cele dinamice (există un tip de model sau anti-model - include_vars împreună cu {{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml.). Acestea sunt directoare pentru files/, templates/. De asemenea, permite rolurilor să aibă module și pluginuri proprii (library/). Dar, în comparație cu sarcinile din playbook-uri (care pot avea și toate acestea), beneficiul constă doar în faptul că fișierele sunt grupate nu într-un singur loc, ci în mai multe grămezi separate.Un alt detaliu: poți încerca să creezi roluri care vor fi disponibile pentru reutilizare (prin galaxy). După apariția colecțiilor, distribuția rolurilor poate fi considerată aproape uitată.
Astfel, rolurile au două caracteristici importante: au valori implicite (o caracteristică unică) și permit structurarea codului.
Revenind la întrebarea inițială: când să faci sarcini și când să folosești roluri? Sarcinile în playbook sunt folosite cel mai adesea fie ca "lipici" înainte/după roluri, fie ca un element de construcție de sine stătător (atunci codul nu ar trebui să conțină roluri). O grămadă de sarcini normale amestecate cu roluri - aceasta este cu siguranță neglijență. Ar trebui să te ții de un stil specific – fie sarcini, fie roluri. Rolurile oferă separarea entităților și valorile implicite, sarcinile permit citirea mai rapidă a codului. De obicei, în roluri sunt extrase coduri mai "statice" (importante și complexe), iar în stilul sarcinilor se scriu scripturi auxiliare.
Revenind la întrebarea inițială: când să facem sarcini și când să folosen roluri? Sarcinile din playbook sunt de obicei folosite fie ca "lipici" înainte/după roluri, fie ca elemente autonome de construcție (atunci nu ar trebui să existe roluri în cod). Un amestec de sarcini normale cu roluri este cu siguranță neglijent. Este important să ne menținem într-un stil specific - fie sarcini, fie roluri. Rolurile oferă separarea entităților și valorile implicite, iar sarcinile permit citirea codului mai rapid. De obicei, rolurile conțin cod mai "staționar" (important și complex), în timp ce sarcinile sunt destinate scripturilor auxiliare.
Există posibilitatea de a face import_role ca o sarcină, dar dacă scrieți așa ceva, fiți pregătit să explicați propriul sentiment estetic despre motivul pentru care doriți să faceți asta.
Un cititor perspicace ar putea spune că rolurile pot importa roluri, rolurile pot avea dependențe prin galaxy.yml, și mai este un lucru înfricoșător și terifiant. include_role — îmi amintesc, ne îmbunătățim abilitățile în Ansible de bază, nu în gimnastică ritmică.
Handleri și sarcini
Să discutăm despre un alt lucru evident: handlerii. Capacitatea de a-i folosi corect este aproape o artă. Care este diferența între un handler și o sarcină?
Așa că, având în vedere că ne amintim de bazele, iată un exemplu:
- hosts: group1
tasks:
- foo:
notify: handler1
handlers:
- name: handler1
bar:Într-un rol, handlerii se află în rolename/handlers/main.yaml. Handlerii sunt partajați între toți participanții play: pre/post_tasks pot apela handlerii rolului, iar rolul poate apela handlerii din play. Totuși, apelurile "între roluri" ale handlerilor provoacă mult mai multe confuzii decât repetarea unui handler trivial. (Un alt element de bune practici — încercați să nu repetați numele handlerilor).
Principala diferență este că o sarcină se execută (idempotent) întotdeauna (plus/minus etichete și when), în timp ce handlerul se declanșează în funcție de schimbarea stării (notify se activează doar dacă a fost modificat). Ce înseamnă asta? De exemplu, că la o nouă rulare, dacă nu a fost modificat, nu va fi nici handler. Și de ce ar putea fi necesar să executăm handlerul când nu a fost modificat la sarcina generatoare? De exemplu, pentru că ceva s-a stricat și a fost modificat, dar execuția nu a ajuns la handler. De exemplu, din cauza că rețeaua a căzut temporar. Configurația s-a schimbat, dar serviciul nu a fost repornit. La următoarea rulare, configurația nu mai este schimbată și serviciul rămâne cu vechea versiune a configurației.
Situația cu configurația nu este rezolvabilă (mai precis, ar putea inventa un protocol special de repornire cu steaguri de fișiere etc., dar asta nu mai e ‘ansible de bază’ în niciun fel). În schimb, există o altă poveste frecventă: am instalat o aplicație, am înregistrat-o în .service-fișier și acum vrem să-i facem daemon_reload și state=started. Și un loc natural pentru asta pare a fi handler-ul. Dar dacă îl transformăm într-o task la sfârșitul listei de task-uri sau rolului, atunci acesta va fi executat idempotent de fiecare dată. Chiar și dacă playbook-ul se întrerupe pe parcurs. Aceasta nu rezolvă deloc problema restarted (nu putem face o task cu atributul restarted, deoarece se pierde idempotenta), dar este clar că ar trebui să folosim state=started, stabilitatea generală a playbook-urilor crește, deoarece se reduce numărul de conexiuni și de stări dinamice.
Un alt aspect pozitiv al handler-ului este că nu aglomerează ieșirea. Nu au fost modificări — nu există skipped sau ok suplimentare în ieșire — mai ușor de citit. Este, de asemenea, o caracteristică negativă — dacă găsiți o greșeală într-o task executată în mod linear la prima rulare, handler-ii vor fi executați doar când este changed, adică în anumite condiții — foarte rar. De exemplu, prima dată în viață, după cinci ani. Și, desigur, acolo va fi o greșeală în nume și totul se va strica. A doua oară nu-i putem porni — nu este changed.
Este important să discutăm despre accesibilitatea variabilelor. De exemplu, dacă folosiți notify pentru o task cu ciclu, ce va fi în variabile? Se poate deduce analitic, dar nu este întotdeauna trivial, mai ales dacă variabilele provin din locuri diferite.
Așadar, handler-ii sunt mult mai puțin utilizați și mai problematici decât par. Dacă se poate scrie ceva frumos (fără complicăciuni) fără handleri, este mai bine să se evite utilizarea lor. Dacă nu reușești să faci asta frumos — mai bine să lucrezi cu ei.
Citiatorul perseverenț își face o observație corectă, că nu am discutat listen, că handler-ul poate să invoce notify pentru un alt handler, că handler-ul poate include import_tasks (care poate face include_role cu with_items), că sistemul de handleri în Ansible este turing-complet, că handler-ii din include_role interacționează interesant cu handler-ii din play și așa mai departe — toate acestea nu sunt "baze" într-adevăr.
Deși există un anumit WTF specific, care este de fapt o caracteristică și de care trebuie să țineți cont. Dacă task-ul dvs. se execută cu delegate_to și are un notify, atunci handler-ul corespunzător se execută fără delegate_to, adică pe hostul pe care este asignat play-ul. (Deși handler-ul, desigur, poate avea delegate_to de asemenea).
În mod separat, vreau să spun câteva cuvinte despre roluri reutilizabile. Până la apariția colecțiilor, exista ideea de a crea roluri universale, care se pot ansible-galaxy install Și am plecat. Funcționează pe toate sistemele de operare în toate variantele, în toate situațiile. Așadar, opinia mea este: nu funcționează. Orice rol cu suport pentru 100500 de cazuri este sortit să se confrunte cu abisurile bug-urilor corner case. Acestea pot fi remediate prin teste extenuante, dar, la fel ca în orice testare, fie aveți un produs cartezian de valori de intrare și o funcție totală, fie aveți "scenarii individuale acoperite". Cred că este cu mult mai bine dacă rolul este liniar (complexitate ciclomatică 1). include_vars, suportul pentru 100500 de cazuri este sortit să se confrunte cu abisurile bug-urilor corner case. Acestea pot fi remediate prin teste extenuante, dar, la fel ca în orice testare, fie aveți un produs cartezian de valori de intrare și o funcție totală, fie aveți "scenarii individuale acoperite". Cred că este cu mult mai bine dacă rolul este liniar (complexitate ciclomatică 1).
Cu cât sunt mai puține if-uri (explicite sau declarative — în formă when sau formă include_vars în funcție de setul de variabile), cu atât mai bine este rolul. Uneori trebuie să facem ramificații, dar, repet, cu cât mai puține sunt, cu atât mai bine. Așadar, o rol bun cu galaxy (funcționează după toate!) cu o mulțime de when poate fi mai puțin preferată decât "rolul propriu" format din cinci sarcini. Momentul în care rolul cu galaxy este mai bun — când începi să scrii ceva. Momentul în care devine mai proastă — când ceva se strică și ai bănci că este din cauza "rolului cu galaxy". O deschizi, iar acolo sunt cinci încluziuni, opt liste de sarcini și o grămadă whende... Și trebuie să te descurci cu asta. În loc de 5 sarcini într-o listă liniară, în care nu sunt cu adevărat puncte de rupt.
În părțile următoare
- Puțin despre inventar, variabile de grup, pluginul host_group_vars, hostvars. Cum să leagă spaghetele într-o ghem de Gordian. Domeniul și precedența variabilelor, modelul de memorie Ansible. "Deci unde ar trebui de fapt să stochez numele utilizatorului pentru baza de date?".
jinja: {{ jinja }}— nosql notype nosense plastic moale. Este peste tot, chiar și acolo unde nu te aștepți. Puțin despre!!unsafeși yaml delicios.
Sursa: habr.com
