Grundlæggende om Ansible, uden hvilke dine playbooks blot er en klump sammenklistrede spaghetti

Jeg laver mange anmeldelser af andres kode i Ansible og skriver også meget selv. Gennem fejlanalyse (både af andres og mine egne) og en del interviews har jeg indset den grundlæggende fejl, som Ansible-brugere laver — de dykker ned i det komplekse uden at have forstået det grundlæggende.

For at rette op på denne universelle uretfærdighed har jeg besluttet at skrive en introduktion til Ansible for dem, der allerede kender det. Jeg advarer, dette er ikke en opsummering af manualerne, men en lang artikel med mange ord og ingen billeder.

Det forventede niveau for læseren — du har allerede skrevet flere tusinde linjer YAML, er allerede i produktion, men "det er lidt skævt alt sammen".

Titler

Den største fejl Ansible-brugeren laver — er ikke at vide, hvad det, der hedder, hedder. Hvis du ikke kender betegnelserne, kan du ikke forstå det, der står i dokumentationen. Et levende eksempel: til et interview var der en person, der påstod at have skrevet meget i Ansible, men han kunne ikke svare på spørgsmålet "hvilke elementer består en playbook af?". Og da jeg foreslog, at "svaret, man skulle forvente, var, at playbook består af play", kom den dræbende kommentar "vi bruger ikke det". Folk skriver i Ansible for penge og bruger ikke play. De bruger det faktisk, men ved ikke, hvad det er.

Så lad os starte med det enkle: hvad hedder hvad. Måske ved du det, eller måske ikke, fordi du ikke har lagt mærke til det, når du læste dokumentationen.

ansible-playbook udfører playbook. En playbook er en fil med udvidelsen yml/yaml, inden i hvilken der er noget som dette:

---
- hosts: group1
  roles:
    - role1

- hosts: group2,group3
  tasks:
    - debug:

Vi har allerede forstået, at hele denne fil — er en playbook. Vi kan vise, hvor roljerne (roles) er, og hvor opgaverne (tasks) er. Men hvor er play her? Og hvad adskiller play fra role eller playbook?

Det hele findes i dokumentationen. Og det bliver overset. Nybegyndere — fordi der er for meget, og man kan ikke huske alt på én gang. Erfarne — fordi "trivielle ting". Hvis du er erfaren — læs disse sider igen mindst en gang hver halve år, og din kode vil blive væsentligt bedre.

Så husk: En playbook — er en liste, der består af play og import_playbook.
Dette er en play:

- hosts: group1
  roles:
    - role1

Og dette er også endnu en play:

- hosts: group2,group3
  tasks:
    - debug:

Hvad er en play? Hvad er formålet med den?

En play er den vigtigste del af en playbook, fordi play og kun play forbinder listen af roller og/eller opgaver med listen af værter, hvor de skal udføres. I de dybe afkroge af dokumentationen kan man finde en omtale af delegate_to, lokale lookup-plugins, netværk-cli-specifikke indstillinger, jump-hosts osv. De giver mulighed for let at ændre leddet for opgaveudførelse. Men glem det. Hver af disse snedige muligheder har meget specifikke anvendelser, og de er bestemt ikke universelle. Vi taler om de grundlæggende ting, som alle bør vide og bruge.

Hvis du ønsker at "udføre noget" "et sted" — skriver du et play. Ikke en rolle. Ikke en rolle med moduler og delegater. Du tager og skriver et play. Hvor du under feltet hosts angiver, hvor det skal udføres, og under roles/tasks hvad der skal udføres.

Det er simpelt, ikke? Og hvordan kan det være anderledes?

En af de karakteristiske situationer, hvor folk ønsker at gøre dette uden et play, er "en rolle, der konfigurerer alt". Man ønsker at have en rolle, der konfigurerer og server af første type og servere af anden type.

Et arketypisk eksempel er overvågning. Man ønsker at have en rolle monitoring, der opsætter overvågning. Rollen monitoring tildeles til overvågningshoste (i den relevante play). Men det viser sig, at vi skal installere pakker på de hoste, vi overvåger. Hvorfor ikke bruge delegate? Og desuden skal iptables konfigureres. Delegate? Og vi skal også skrive/revidere konfigurationen for DBMS, så overvågningen kan starte. Delegate! Og hvis kreativiteten strømmer, kan man lave delegation include_role i en indlejret løkke med et snedigt filter på gruppelisten, og indeni include_role kan man også gøre delegate_to igen. Og så ruller det…

Et godt ønske — at have en enkelt rolle monitoring, der "gør alt" — fører os til et kaos, hvor der ofte kun er én udvej: at skrive alt om fra bunden.

Hvor skete der en fejl? I det øjeblik, du fandt ud af, at for at udføre opgaven "x" på host X, skulle du gå til host Y og gøre "y" der, skulle du have udført en simpel øvelse: gå og skrive et play, der på host Y udfører y. Ikke at tilføje noget i "x", men at skrive fra bunden. Selv med hardcodede variabler.

Tilsyneladende er alt sagt korrekt i de foregående afsnit. Men det er ikke dit tilfælde! Fordi du ønsker at skrive genanvendelig kode, der er DRY og ligner et bibliotek, og det er nødvendigt at finde en metode til at gøre dette.

Her ligger der endnu en grov fejl. En fejl, der har forvandlet mange projekter fra tåleligt skrevne (det kunne være bedre, men det fungerer og er nemt at skrive videre på) til en fuldstændig rædsel, som selv forfatteren ikke kan forstå. Det fungerer, men lad være med at ændre noget.

Denne fejl lyder således: en rolle er en biblioteksfunktion. Denne analogi har ødelagt så mange gode initiativer, at det næsten er trist at se. En rolle er ikke en biblioteksfunktion. Den kan ikke udføre beregninger, og den kan ikke træffe beslutninger på play-niveau. Mind mig om, hvilke beslutninger play træffer?

Tak, du har ret. Play træffer beslutningen (eller rettere, indeholder informationen) om, hvilke opgaver og roller der skal udføres på hvilke værter.

Hvis du delegerer denne beslutning til en rolle, og endda med beregninger, dømer du dig selv (og den, der skal forsøge at forstå din kode) til en elendig tilværelse. Rollen beslutter ikke, hvor den skal udføres. Det er play, der træffer denne beslutning. Rollen gør, hvad den bliver bedt om, dér hvor den bliver bedt om det.

Hvorfor det er farligt at programmere med Ansible, og hvorfor COBOL er bedre end Ansible, vil vi tale om i kapitlet om variabler og jinja. Indtil videre kan vi sige én ting — hver beregning efterlader et uudsletteligt spor af ændringer i globale variabler, og du kan ikke gøre noget ved det. Så snart to 'spor' krydser hinanden — er alt tabt.

Bemærkning til de nysgerrige: en rolle kan bestemt påvirke kontrolstrømmen. Der er delegate_to og den har fornuftige anvendelser. Der er meta: end host/play. Men! Husk, vi lærer grundlæggende ting? Glemt om delegate_to. Vi taler om den enkleste og smukkeste kode i Ansible. Som er let at læse, let at skrive, let at fejlrette, let at teste og let at tilføje til. Så endnu en gang:

play og kun play beslutter, hvilke værter hvad udføres på.

I denne sektion har vi undersøgt modstridene mellem play og rolle. Nu vil vi tale om forholdet mellem tasks vs rolle.

Opgaver og Roller

Lad os se på play:

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

Lad os antage, at du skal lave foo. Og det ser således ud foo: name=foobar state=present. Hvor skal dette skrives? i pre? post? Oprette en rolle?

… Og hvor er opgaverne blevet af?

Vi begynder igen fra bunden — strukturen af play. Hvis du er usikker på dette spørgsmål, kan du ikke bruge play som fundament for alt andet, og dit resultat bliver 'usikkert'.

Play-enhed: hosts-direktiv, indstillinger for selve play og sektionerne pre_tasks, tasks, roles, post_tasks. Andre parametre for play er ikke relevante for os lige nu.

Rækkefølgen af deres sektioner med tasks og roller: pre_tasks, roles, tasks, post_tasks. Da den semantiske rækkefølge for udførelsen mellem tasks og roles ikke er klar, siger best practices, at vi kun tilføjer sektionen tasks, hvis der ikke er roles. Hvis der er roles, så placeres alle tilknyttede tasks i sektionerne pre_tasks/post_tasks.

Der er kun tilbage, hvad der semantisk er klart: først pre_tasks, derefter roles, derefter post_tasks.

Men vi har stadig ikke svaret på spørgsmålet: hvor skal modulopkaldet foo skrives? Skal vi skrive en hel rolle for hver modul? Eller er det bedre at have en stor rolle til det hele? Og hvis ikke en rolle, hvor skal vi så skrive det — i pre eller i post?

Hvis der ikke er et velbegrundet svar på disse spørgsmål, er det et tegn på mangel på intuition, de "usikre fundamenter". Lad os finde ud af det. Først et kontrolspørgsmål: Hvis play har pre_tasks og post_tasks (og ingen tasks eller roller), kan det så gå galt, hvis jeg flytter den første task fra post_tasks til slutningen af pre_tasks?

Selvfølgelig antyder formuleringen af spørgsmålet, at det vil gå galt. Men hvad præcist?

… Handlere. At læse det grundlæggende afslører et vigtigt faktum: alle handlere flushes automatisk efter hver sektion. Dvs. alle tasks fra pre_tasksudføres, så alle handlere, der blev notify. Derefter udføres alle roller og alle handlere, der blev notify i rollerne. Så post_tasks og deres handlere.

Derfor, hvis du flytter en task fra post_tasks i pre_tasks, vil du potentielt udføre den før handleren. For eksempel, hvis der i pre_tasks installeres og konfigureres webserveren, og der sendes noget til den, vil flytning af denne task til sektionen post_tasks medføre, at serveren ikke er startet på tidspunktet for "afsendelsen", og alt vil gå galt. pre_tasks Lad os nu tænke over, hvorfor vi skulle have

giver os mulighed for at arbejde med resultaterne af rolleans udførelse (inklusive handlere). pre_tasks og post_tasks? Например, для того, чтобы выполнить всё нужное (включая хэндлеры) до выполнения роли. А post_tasks En nysgerrig Ansible-ekspert vil sige til os, at der findes

meta: flush_handlers , men hvorfor har vi brug for flush_handlers, hvis vi kan stole på rækkefølgen for udførelsen af sektionerne i play? Desuden kan brugen af meta: flush_handlers give os uventede problemer med gentagne handlere, hvilket kan føre til mærkelige advarsler ved brug afwhen block af osv. Jo bedre du kender Ansible, desto flere nuancer kan du nævne for "smart" løsning. Og den enkle løsning — brugen af naturlig adskillelse mellem pre/roles/post — medfører ikke nuancer. osv. Jo bedre du kender Ansible, jo flere nuancer kan du nævne for en 'smart' løsning. Men den enkle løsning — brugen af naturlig opdeling mellem pre/roles/post — skaber ikke nuancer.

Og, lad os vende tilbage til vores 'foo'. Hvor skal vi placere det? I pre, post eller i roller? Det afhænger tydeligvis af, om vi har brug for resultaterne fra handleren til foo. Hvis ikke, så skal foo ikke placeres hverken i pre eller post — disse sektioner har en særlig betydning — udføre tasker før og efter hovedkoden.

Nu er svaret på spørgsmålet "rolle eller opgave" afhængig af, hvad der allerede er i play — hvis der er tasks, skal vi tilføje til tasks. Hvis der er roller, skal vi lave en rolle (selv om det kun er én task). Jeg minder om, at tasks og roller ikke bruges samtidigt.

Forståelsen af Ansible's grundlæggende aspekter giver velbegrundede svar på tilsyneladende smagsspecifikke spørgsmål.

Opgaver og roller (del to)

Lad os nu diskutere situationen, hvor du lige er begyndt at skrive et playbook. Du skal lave foo, bar og baz. Er det tre opgaver, én rolle eller tre roller? For at generalisere spørgsmålet: hvornår skal man begynde at skrive roller? Hvad er meningen med at skrive roller, når man kan skrive opgaver?… Hvad er en rolle?

En af de største fejl (jeg har talt om det tidligere) er at antage, at en rolle er som en funktion i et bibliotek til et program. Hvordan ser en generel beskrivelse af en funktion ud? Den tager argumenter som input, interagerer med sideårsager, skaber sideeffekter og returnerer en værdi.

Nu, vær opmærksom. Hvad af dette kan gøres i en rolle? At skabe sideeffekter — altid muligt, det er essensen af Ansible — at skabe sideeffekter. At have sideårsager? Elementært. Men med "at videregive en værdi og returnere den" — der er det ikke muligt. For det første kan du ikke videregive en værdi til en rolle. Du kan sætte en global variabel med en livscyklus svarende til play i vars sektionen for rollen. Du kan sætte en global variabel med livscyklus i play inden for rollen. Eller endda med livscyklus for playbooks (set_fact/register). Men du kan ikke have "lokale variabler". Du kan ikke "modtage en værdi" og "returnere den".

Derfor følger det vigtigste: Du kan ikke skrive noget i Ansible og undgå at skabe sideeffekter. Ændring af globale variabler er altid en sideeffekt for funktionen. I Rust, for eksempel, er ændring af en global variabel — unsafe. I Ansible er det den eneste måde at påvirke værdier for en rolle. Bemærk de anvendte ord: ikke "at videregive værdi til en rolle", men "ændre værdierne som rollen bruger". Der er ikke isolation mellem roller. Der er ikke isolation mellem opgaver og roller.

I alt: en rolle er ikke en funktion.

Hvad er der godt ved roller? For det første har roller standardværdier (/default/main.yaml), for det andet har roller ekstra kataloger til opbevaring af filer.

Hvad er så godt ved standardværdier? Det er, at i Maslows ret forvrængede tabel over variabelprioriteter i Ansible er rolle-standarder de mindst prioriterede (minus kommandolinjeparametre fra Ansible). Det betyder, at hvis du ønsker at give standardværdier og ikke bekymre dig om, at de overskriver værdier fra inventar eller gruppevariabler, så er rolle-standarder det eneste rigtige sted for dig. (Jeg lyver lidt – der er også |d(your_default_here), men hvis vi taler om stationære steder – så kun rolle-standarder).

Hvad er der ellers godt ved roller? Det er, at de har deres egne kataloger. Disse kataloger er til variabler, både permanente (dvs. beregnede for rollen) og dynamiske (der er en slags mønster eller anti-mønster – include_vars sammen med {{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml.). Dette er kataloger til files/, templates/. Derudover gør det det muligt at have sine egne moduler og plugins i roller (library/). Men i sammenligning med opgaver i playbooks (hvor dette også kan være), er den eneste fordel her, at filerne ikke er samlet i en bunke, men i flere separate bunker.

Endnu en detalje: man kan prøve at lave roller, der vil være tilgængelige for genbrug (gennem galaxy). Efter introduktionen af samlinger kan distributionen af roller betragtes som næsten glemt.

Dermed har roller to vigtige egenskaber: de har standarder (en unik egenskab), og de tillader at strukturere koden.

Når vi vender tilbage til det oprindelige spørgsmål: hvornår skal man lave opgaver, og hvornår roller? Opgaver i en playbook bruges oftest enten som "lim" før/efter roller eller som en selvstændig byggeklods (så skal der ikke være roller i koden). En bunke normale opgaver blandet med roller er helt klart en rodet tilgang. Man bør holde sig til en bestemt stil – enten opgaver eller roller. Roller giver adskillelse af enheder og standardværdier; opgaver gør det muligt at læse koden hurtigere. Normalt placeres mere "stationær" (vigtig og kompleks) kode i roller, mens hjælpe-scripts skrives i stilen med opgaver.

Der er mulighed for at lave import_role som en opgave, men hvis du skriver det, så vær forberedt på at skulle forklare, hvorfor du ønsker at gøre det.

Den nysgerrige læser kan sige, at roller kan importere roller, at roller kan have afhængigheder via galaxy.yml, og der er også den skræmmende og forfærdelige include_role — jeg minder om, at vi forbedrer vores færdigheder i grundlæggende Ansible, ikke i rytmisk gymnastik.

Handlere og opgaver

Lad os diskutere en anden indlysende ting: handlere. At kunne bruge dem korrekt er næsten en kunst. Hvad er forskellen mellem en handler og en opgave?

Da vi minder om grundprincipperne, så her er et eksempel:

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

I en rolle ligger handlerne i rolename/handlers/main.yaml. Handlere deles mellem alle deltagerne i play: pre/post_tasks kan kalde handlerne fra rollen, og rollen kan kalde handlerne fra play. Men "cross-role" kald af handlere skaber meget mere forvirring end gentagelse af trivielle handlere. (Endnu et element i bedste praksis — prøv at undgå at gentage navne på handlere).

Den grundlæggende forskel er, at en opgave altid udføres (idempotent) (plus/minus tags og block), mens en handler — ved ændring af tilstand (notify aktiveres kun, hvis der er sket ændringer). Hvad kan det føre til? For eksempel, at ved genstart, hvis der ikke var sket ændringer, så bliver handleren ikke aktiveret. Og hvorfor kan det være, at vi har brug for at udføre en handler, når der ikke var sket ændringer i den genererende opgave? For eksempel, fordi noget gik i stykker, og der var sket ændringer, men handleren blev ikke nået. Fordi netværket var midlertidigt nede. Konfigurationen blev ændret, tjenesten blev ikke genstartet. Ved næste kørsel ændres konfigurationen ikke, og tjenesten forbliver med den gamle version af konfigurationen.

Situationen med konfigurationen er ikke løselig (eller rettere, man kan opfinde en speciel genstart protokol med filflager osv., men det er ikke længere 'basic ansible' i nogen form). Men der er en anden hyppig historie: vi installerede en applikation, lavede en .service-fil, og nu vil vi gerne have den daemon_reload og state=started. Og det naturlige sted for dette ser ud til at være handleren. Men hvis du gør det til en opgave i slutningen af opgavelisten eller rollen, vil det blive idempotent udført hver gang. Selv hvis playbooken fejler midtvejs. Dette løser bestemt ikke problemerne med restarted (det er ikke muligt at have en opgave med attributten restarted, da idempotens går tabt), men det er helt sikkert værd at angive state=started, den samlede stabilitet af playbooks øges, da antallet af forbindelser og dynamisk tilstand reduceres.

En anden positiv egenskab ved handleren er, at den ikke forstyrrer outputtet. Ingen ændringer – ingen unødvendige skipped eller ok i outputtet – lettere at læse. Det er også en negativ egenskab – hvis du finder en tastefejl i en lineært udført opgave, vil handlerne kun blive udført ved changed, dvs. under visse betingelser – meget sjældent. For eksempel første gang i livet efter fem år. Og naturligvis vil der være en tastefejl i navnet, og alt vil bryde sammen. Og anden gang kan de ikke startes – changed er jo ikke der.

Der skal tales særskilt om tilgængeligheden af variabler. For eksempel, hvis du notifierer for en opgave med en løkke, hvad vil der så være i variablerne? Man kan gætte det analytisk, men det er ikke altid trivielt, især hvis variablerne kommer fra forskellige steder.

… Så handlerne er langt mindre nyttige og mere problematiske, end de ser ud til. Hvis det er muligt at skrive noget smukt (uden triks) uden håndterere, er det bedre at gøre det uden dem. Hvis det ikke kan gøres smukt – er det bedre med dem.

Den nysgerrige læser bemærker med rette, at vi ikke har diskuteret lytt, at handleren kan udløse notifikation for en anden handler, at handleren kan inkludere import_tasks (som kan lave include_role med with_items), at handler-systemet i Ansible er Turing-komplet, at handlerne fra include_role bemærkelsesværdigt krydser med handlerne fra play osv. – alt dette er bestemt ikke "grundlæggende").

Selvom der er en bestemt WTF, som faktisk er en funktion, og som man skal huske. Hvis du har en opgave, der udføres med delegate_to og den har en notifikation, så udføres den tilsvarende handler uden delegate_to, dvs. på den vært, hvortil play er tildelt. (Selvom handleren selvfølgelig også kan have delegate_to det samme).

Separat vil jeg gerne sige et par ord om genanvendelige roller. Inden samlingerne kom, var ideen, at man kunne lave universelle roller, som man kunne ansible-galaxy install Og så kørte det. Det fungerer på alle OS i alle varianter under alle forhold. Sådan er min mening: det virker ikke. Enhver rolle med massiv include_vars, støtte for 100500 tilfælde er dømt til dybderne af corner case fejl. De kan stoppe ved massiv testning, men som med enhver testning, enten har du det kartesiske produkt af indgangsværdier og en totalfunktion, eller også har du "dækkede specifikke scenarier". Min mening er, at det er meget bedre, hvis rollen er lineær (cyklomatisk kompleksitet 1).

Jo færre if'er (åbenlyse eller deklarative - i form af block eller formen include_vars ud fra et sæt variabler), jo bedre er rollen. Nogle gange er det nødvendigt at lave forgreninger, men jeg gentager, jo færre de er, jo bedre. Så en tilsyneladende god rolle med galaxy (for det virker!), med en masse block kan være mindre at foretrække end "din egen" rolle med fem opgaver. Det øjeblik, hvor en rolle med galaxy er bedre, er når du begynder at skrive noget. Det øjeblik, den bliver værre, er når noget går i stykker, og du har en mistanke om, at det skyldes "rollen med galaxy". Du åbner den, og derinde er fem inklusioner, otte opgavelister og en stak block‘er... Og du skal finde ud af det. I stedet for 5 opgaver i en lineær liste, hvor der ikke er noget at gå i stykker.

I de kommende dele

  • Lidt om inventar, gruppevariabler, host_group_vars plugin, hostvars. Hvordan man binder en gordisk knude fra spaghetti. Scope og præcedens for variabler, Ansible's hukommelsesmodel. "Hvor skal man så opbevare brugernavnet til databasen?"
  • jinja: {{ jinja }} — nosql notype nosense blød plastelin. Det er overalt, selv der hvor du ikke forventer det. Lidt om !!unsafe og lækker yaml.

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster