Ansible grunder, utan vilka dina spelböcker blir en klump klibbig pasta

Jag gör många recensioner av andras Ansible-kod och skriver mycket själv. Under analysen av misstag (både andras och mina egna), samt ett antal intervjuer, insåg jag det största misstaget som Ansible-användare gör - de hamnar i komplexa saker utan att bemästra de grundläggande.

För att rätta till denna universella orättvisa bestämde jag mig för att skriva en introduktion till Ansible för dem som redan kan det. Jag varnar dig, det här är inte en återberättelse av män, det här är en longread med många bokstäver och inga bilder.

Läsarens förväntade nivå är att flera tusen rader yamla redan har skrivits, något är redan i produktion, men "på något sätt är allt snett."

namn

Det största misstaget en Ansible-användare gör är att inte veta vad något heter. Om du inte kan namnen kan du inte förstå vad dokumentationen säger. Ett levande exempel: under en intervju kunde en person som tycktes säga att han skrev mycket i Ansible inte svara på frågan "vilka delar består en spelbok av?" Och när jag föreslog att "svaret förväntades att spelboken består av lek", följde den fördömande kommentaren "det använder vi inte". Folk skriver Ansible för pengar och använder inte spel. De använder det faktiskt, men vet inte vad det är.

Så låt oss börja med något enkelt: vad heter det? Kanske vet du detta, eller kanske inte, eftersom du inte var uppmärksam när du läste dokumentationen.

ansible-playbook kör playbook. En playbook är en fil med tillägget yml/yaml, inuti vilken det finns något sånt här:

---
- hosts: group1
  roles:
    - role1

- hosts: group2,group3
  tasks:
    - debug:

Vi har redan insett att hela den här filen är en spelbok. Vi kan visa var rollerna finns och var uppgifterna finns. Men var är leken? Och vad är skillnaden mellan lek och roll eller lekbok?

Allt finns i dokumentationen. Och de saknar det. Nybörjare - för det är för mycket och du kommer inte ihåg allt på en gång. Erfaren - eftersom "triviala saker". Om du är erfaren, läs dessa sidor igen minst en gång var sjätte månad, så kommer din kod att bli klassledande.

Så kom ihåg: Playbook är en lista som består av lek och import_playbook.
Detta är en pjäs:

- hosts: group1
  roles:
    - role1

och det här är också en annan pjäs:

- hosts: group2,group3
  tasks:
    - debug:

Vad är lek? Varför är hon det?

Lek är ett nyckelelement för en spelbok, eftersom lek och bara lek förknippar en lista med roller och/eller uppgifter med en lista över värdar som de måste utföras på. I djupet av dokumentationen kan du hitta omnämnande av delegate_to, plugins för lokalsökning, nätverksspecifika inställningar för cli, hoppvärdar, etc. De låter dig ändra platsen där uppgifterna utförs något. Men glöm det. Var och en av dessa smarta alternativ har mycket specifika användningsområden, och de är definitivt inte universella. Och vi pratar om grundläggande saker som alla borde känna till och använda.

Om du vill framföra "något" "någonstans" skriver du pjäs. Inte en roll. Inte en roll med moduler och delegater. Du tar den och skriver pjäs. I vilket, i värdfältet, listar du var du ska köra, och i roller/uppgifter - vad som ska utföras.

Enkelt, eller hur? Hur kunde det vara annorlunda?

Ett av de karakteristiska ögonblicken när människor har en önskan att göra detta inte genom lek är "rollen som sätter upp allt." Jag skulle vilja ha en roll som konfigurerar både servrar av den första typen och servrar av den andra typen.

Ett arketypiskt exempel är övervakning. Jag skulle vilja ha en övervakningsroll som konfigurerar övervakning. Övervakningsrollen tilldelas övervakningsvärdar (enligt spel). Men det visar sig att för övervakning måste vi leverera paket till de värdar som vi övervakar. Varför inte använda delegat? Du måste också konfigurera iptables. delegera? Du måste också skriva/korrigera en konfiguration för DBMS för att möjliggöra övervakning. delegera! Och om kreativiteten saknas, då kan du göra en delegation include_role i en kapslad loop med ett knepigt filter på en lista med grupper, och inuti include_role du kan göra mer delegate_to igen. Och vi går...

En god önskan - att ha en enda övervakande roll, som "gör allt" - leder oss in i ett helvete från vilket det oftast bara finns en utväg: att skriva om allt från grunden.

Var hände felet här? I samma ögonblick som du upptäckte att för att göra uppgift "x" på värd X måste du gå till värd Y och göra "y" där, var du tvungen att göra en enkel övning: gå och skriv pjäs, vilket på värd Y gör y. Lägg inte till något i "x", utan skriv det från början. Även med hårdkodade variabler.

Det verkar som att allt i styckena ovan är korrekt sagt. Men detta är inte ditt fall! Eftersom du vill skriva återanvändbar kod som är DRY och biblioteksliknande, och du måste leta efter en metod för hur du gör det.

Det är här ett annat allvarligt misstag lurar. Ett fel som förvandlade många projekt från drägligt skrivna (det kunde vara bättre, men allt fungerar och är lätt att avsluta) till en fullständig skräck som inte ens författaren kan lista ut. Det fungerar, men Gud förbjude dig att ändra något.

Felet är: rollen är en biblioteksfunktion. Denna liknelse har förstört så många bra början att det helt enkelt är sorgligt att se. Rollen är inte en biblioteksfunktion. Hon kan inte göra beräkningar och hon kan inte fatta beslut på spelnivå. Påminn mig om vilka beslut leken fattar?

Tack, du har rätt. Play fattar ett beslut (mer exakt, den innehåller information) om vilka uppgifter och roller som ska utföras på vilka värdar.

Om du delegerar detta beslut till en roll, och även med beräkningar, dömer du dig själv (och den som ska försöka analysera din kod) till en eländig tillvaro. Rollen bestämmer inte var den ska utföras. Detta beslut fattas genom lek. Rollen gör vad den sägs, där den sägs.

Varför är det farligt att programmera i Ansible och varför COBOL är bättre än Ansible kommer vi att prata om i kapitlet om variabler och jinja. För nu, låt oss säga en sak - var och en av dina beräkningar lämnar efter sig ett outplånligt spår av förändringar i globala variabler, och du kan inte göra något åt ​​det. Så fort de två "spåren" korsades var allt borta.

Notera för den bråkiga: rollen kan säkert påverka kontrollflödet. Äta delegate_to och det har rimliga användningsområden. Äta meta: end host/play. Men! Kommer du ihåg att vi lär ut grunderna? Glömde delegate_to. Vi pratar om den enklaste och vackraste Ansible-koden. Som är lätt att läsa, lätt att skriva, lätt att felsöka, lätt att testa och lätt att slutföra. Så, än en gång:

lek och bara lek avgör på vilka värdar vad som exekveras.

I detta avsnitt behandlade vi motsättningen mellan lek och roll. Låt oss nu prata om uppgifter kontra rollförhållande.

Uppgifter och roller

Överväg att spela:

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

Låt oss säga att du måste göra foo. Och det ser ut som foo: name=foobar state=present. Var ska jag skriva detta? i pre? posta? Skapa en roll?

...Och vart tog uppgifterna vägen?

Vi börjar med grunderna igen - lekenheten. Om du flyter på den här frågan kan du inte använda spelet som grund för allt annat, och ditt resultat blir "skakigt".

Spelenhet: värddirektiv, inställningar för själva spelet och pre_tasks, uppgifter, roller, post_tasks sektioner. De återstående parametrarna för spel är inte viktiga för oss nu.

Ordningen på deras sektioner med uppgifter och roller: pre_tasks, roles, tasks, post_tasks. Eftersom semantiskt är exekveringsordningen mellan tasks и roles är inte klart, så säger bästa praxis att vi lägger till ett avsnitt tasks, bara om inte roles... Om det är roles, sedan placeras alla bifogade uppgifter i sektioner pre_tasks/post_tasks.

Allt som återstår är att allt är semantiskt klart: först pre_tasksrolespost_tasks.

Men vi har fortfarande inte svarat på frågan: var är modulanropet? foo skriva? Behöver vi skriva en hel roll för varje modul? Eller är det bättre att ha en tjock roll för allt? Och om inte en roll, var ska jag då skriva - i för- eller inlägg?

Om det inte finns något motiverat svar på dessa frågor, är detta ett tecken på brist på intuition, det vill säga samma "skakiga grunder." Låt oss ta reda på det. Först en säkerhetsfråga: Om spelet har pre_tasks и post_tasks (och det finns inga uppgifter eller roller), då kan något gå sönder om jag utför den första uppgiften från post_tasks Jag flyttar det till slutet pre_tasks?

Frågans formulering antyder förstås att den kommer att gå sönder. Men vad exakt?

... Hantare. Att läsa grunderna avslöjar ett viktigt faktum: alla hanterare spolas automatiskt efter varje avsnitt. De där. alla uppgifter från pre_tasks, sedan alla hanterare som meddelades. Sedan exekveras alla roller och alla hanterare som aviserats i rollerna. Efter post_tasks och deras handläggare.

Alltså, om du drar en uppgift från post_tasks в pre_tasks, då kommer du potentiellt att köra det innan hanteraren exekveras. till exempel om i pre_tasks webbservern är installerad och konfigurerad, och post_tasks något skickas till den, överför sedan denna uppgift till sektionen pre_tasks kommer att leda till det faktum att servern inte körs vid tidpunkten för "sändning" och allt kommer att gå sönder.

Låt oss nu tänka igen, varför behöver vi pre_tasks и post_tasks? Till exempel för att slutföra allt som behövs (inklusive hanterare) innan du fullgör rollen. A post_tasks kommer att tillåta oss att arbeta med resultaten av exekverande roller (inklusive hanterare).

En skarpsinnig Ansible-expert kommer att berätta för oss vad det är. meta: flush_handlers, men varför behöver vi flush_handlers om vi kan lita på exekveringsordningen för avsnitt i spel? Dessutom kan användningen av meta: flush_handlers ge oss oväntade saker med dubbletthanterare, vilket ger oss konstiga varningar när de används when у block etc. Ju bättre du kan det ansible, desto fler nyanser kan du nämna för en "knepig" lösning. Och en enkel lösning – att använda en naturlig uppdelning mellan för/roller/post – orsakar inga nyanser.

Och tillbaka till vår "foo". Var ska jag lägga den? I före, post eller roller? Uppenbarligen beror detta på om vi behöver resultaten av hanteraren för foo. Om de inte finns där behöver foo inte placeras i vare sig före eller efter - dessa avsnitt har en speciell betydelse - att utföra uppgifter före och efter huvuddelen av koden.

Nu kommer svaret på frågan "roll eller uppgift" ner till vad som redan är i spel - om det finns uppgifter där, måste du lägga till dem i uppgifter. Om det finns roller måste du skapa en roll (även från en uppgift). Låt mig påminna om att uppgifter och roller inte används samtidigt.

Att förstå grunderna i Ansible ger rimliga svar på till synes smakfrågor.

Uppgifter och roller (del två)

Låt oss nu diskutera situationen när du precis har börjat skriva en lekbok. Du måste göra foo, bar och baz. Är det här tre uppgifter, en roll eller tre roller? För att sammanfatta frågan: vid vilken tidpunkt ska man börja skriva roller? Vad är poängen med att skriva roller när man kan skriva uppgifter?... Vad är en roll?

Ett av de största misstagen (jag har redan pratat om detta) är att tro att en roll är som en funktion i ett programs bibliotek. Hur ser en generisk funktionsbeskrivning ut? Den accepterar inmatningsargument, interagerar med sidoorsaker, gör biverkningar och returnerar ett värde.

Nu, uppmärksamhet. Vad kan man göra av detta i rollen? Du är alltid välkommen att ringa biverkningar, detta är kärnan i hela Ansible – att skapa biverkningar. Har du sidoorsaker? Elementärt. Men med "passera ett värde och returnera det" - det är där det inte fungerar. För det första kan du inte överföra ett värde till en roll. Du kan ställa in en global variabel med en livslängd för spel i vars-sektionen för rollen. Du kan ställa in en global variabel med en livstid i spel i rollen. Eller till och med med spelböckernas livstid (set_fact/register). Men du kan inte ha "lokala variabler". Du kan inte "ta ett värde" och "lämna tillbaka det".

Det viktigaste följer av detta: du kan inte skriva något i Ansible utan att orsaka biverkningar. Att ändra globala variabler är alltid en bieffekt för en funktion. I Rust, till exempel, är att ändra en global variabel unsafe. Och i Ansible är det den enda metoden att påverka värderingarna för en roll. Notera orden som används: inte "överlåta ett värde till rollen", utan "ändra värdena som rollen använder". Det finns ingen isolering mellan rollerna. Det finns ingen isolering mellan uppgifter och roller.

Totalt: en roll är inte en funktion.

Vad är bra med rollen? För det första har rollen standardvärden (/default/main.yaml), för det andra har rollen ytterligare kataloger för lagring av filer.

Vilka är fördelarna med standardvärden? För i Maslows pyramid, Ansibles ganska förvrängda tabell med variabla prioriteringar, är rollstandarder de mest lågprioriterade (minus Ansibles kommandoradsparametrar). Detta betyder att om du behöver ange standardvärden och inte oroa dig för att de åsidosätter värdena från inventerings- eller gruppvariabler, så är rollstandarder den enda rätta platsen för dig. (Jag ljuger lite - det finns fler |d(your_default_here), men om vi pratar om stationära platser, så är det bara rollförvalda).

Vad mer är bra med rollerna? Eftersom de har sina egna kataloger. Dessa är kataloger för variabler, både konstanta (dvs. beräknade för rollen) och dynamiska (det finns antingen ett mönster eller ett antimönster - include_vars med {{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml.). Det här är katalogerna för files/, templates/. Det låter dig också ha dina egna moduler och plugins (library/). Men i jämförelse med uppgifter i en spelbok (som också kan ha allt detta), är den enda fördelen här att filerna inte dumpas i en hög, utan flera separata högar.

En detalj till: du kan försöka skapa roller som kommer att vara tillgängliga för återanvändning (via galaxy). Med tillkomsten av samlingar kan rollfördelning anses nästan glömd.

Roller har alltså två viktiga funktioner: de har standardvärden (en unik funktion) och de låter dig strukturera din kod.

Återgå till den ursprungliga frågan: när ska man göra uppgifter och när man ska göra roller? Uppgifter i en lekbok används oftast antingen som ”lim” före/efter roller, eller som ett självständigt byggelement (då ska det inte finnas några roller i koden). En hög med normala uppgifter blandat med roller är entydigt slarv. Du bör hålla dig till en specifik stil - antingen en uppgift eller en roll. Roller ger separation av entiteter och standardinställningar, uppgifter gör att du kan läsa kod snabbare. Vanligtvis sätts mer "stationär" (viktig och komplex) kod i roller, och hjälpskript skrivs i uppgiftsstil.

Det är möjligt att göra import_role som en uppgift, men om du skriver detta, var då beredd att förklara för din egen skönhetskänsla varför du vill göra detta.

En skarpsinnig läsare kan säga att roller kan importera roller, roller kan ha beroenden via galaxy.yml, och det finns också en hemsk och hemsk include_role — Jag påminner om att vi förbättrar färdigheter i grundläggande Ansible, och inte i figurgymnastik.

Handläggare och arbetsuppgifter

Låt oss diskutera en annan självklar sak: hanterare. Att veta hur man använder dem rätt är nästan en konst. Vad är skillnaden mellan en hanterare och en dragning?

Eftersom vi kommer ihåg grunderna, här är ett exempel:

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

Rollens hanterare finns i rolename/handlers/main.yaml. Handlers rotar mellan alla lekdeltagare: pre/post_tasks kan dra rollhanterare och en roll kan dra hanterare från pjäsen. Men "korsande" anrop till hanterare orsakar mycket mer wtf än att upprepa en trivial hanterare. (En annan del av bästa praxis är att försöka att inte upprepa hanterarnamn).

Den största skillnaden är att uppgiften alltid utförs (idempotent) (plus/minus-taggar och when), och hanteraren - efter tillståndsändring (meddela bränder endast om den har ändrats). Vad betyder det här? Till exempel det faktum att när du startar om, om det inte gjordes någon förändring, kommer det inte att finnas någon hanterare. Varför kan det vara så att vi behöver köra hanteraren när det inte fanns någon förändring i genereringsuppgiften? Till exempel för att något gick sönder och förändrades, men utförandet nådde inte hanteraren. Till exempel för att nätverket var tillfälligt nere. Konfigurationen har ändrats, tjänsten har inte startats om. Nästa gång du startar den ändras inte längre konfigurationen och tjänsten finns kvar med den gamla versionen av konfigurationen.

Situationen med konfigurationen kan inte lösas (mer exakt, du kan uppfinna ett speciellt omstartsprotokoll för dig själv med filflaggor, etc., men detta är inte längre "basic ansible" i någon form). Men det finns en annan vanlig historia: vi installerade programmet, spelade in det .service-fil, och nu vill vi ha den daemon_reload и state=started. Och den naturliga platsen för detta verkar vara hanteraren. Men om du inte gör det till en hanterare utan till en uppgift i slutet av en uppgiftslista eller roll, kommer den att exekveras idempotent varje gång. Även om spelboken gick sönder i mitten. Detta löser inte det omstartade problemet alls (du kan inte göra en uppgift med attributet omstartad, eftersom idempotens går förlorad), men det är definitivt värt att göra state=started, den övergripande stabiliteten för playbooks ökar, eftersom antalet anslutningar och dynamiskt tillstånd minskar.

En annan positiv egenskap hos hanteraren är att den inte täpper till utgången. Det blev inga ändringar - inga extra överhoppade eller ok i utgången - lättare att läsa. Det är också en negativ egenskap - om du hittar ett stavfel i en linjärt utförd uppgift vid den allra första körningen, kommer hanterarna att exekveras endast när de ändras, d.v.s. under vissa förhållanden - mycket sällan. Till exempel för första gången i mitt liv fem år senare. Och självklart blir det ett stavfel i namnet och allt går sönder. Och om du inte kör dem andra gången är det ingen förändring.

Separat måste vi prata om tillgängligheten av variabler. Till exempel, om du meddelar en uppgift med en loop, vad kommer att finnas i variablerna? Du kan gissa analytiskt, men det är inte alltid trivialt, särskilt om variablerna kommer från olika platser.

... Så hanterare är mycket mindre användbara och mycket mer problematiska än de verkar. Om du kan skriva något vackert (utan krusiduller) utan hanterare, är det bättre att göra det utan dem. Om det inte fungerar vackert är det bättre med dem.

Den frätande läsaren påpekar med rätta att vi inte har diskuterat listenatt en hanterare kan anropa notify för en annan hanterare, att en hanterare kan inkludera import_tasks (som kan göra include_role med with_items), att hanterarsystemet i Ansible är Turing-komplett, att hanterare från include_role korsar sig på ett nyfiket sätt med hanterare från spel, etc. .d. - allt detta är uppenbarligen inte "grunderna").

Även om det finns en specifik WTF som faktiskt är en funktion som du måste ha i åtanke. Om din uppgift utförs med delegate_to och den har meddelat, exekveras motsvarande hanterare utan delegate_to, dvs. på värden där spelet tilldelas. (Även om hanteraren naturligtvis kan ha det delegate_to Samma).

Separat vill jag säga några ord om återanvändbara roller. Innan samlingar dök upp fanns det en idé om att man kunde göra universella roller som kunde vara det ansible-galaxy install och gick. Fungerar på alla OS av alla varianter i alla situationer. Så, min åsikt: det fungerar inte. Vilken roll som helst med massa include_vars, som stöder 100500 1 fodral, är dömd till avgrunden av hörnfodral buggar. De kan täckas med massiva tester, men som med alla tester, antingen har du en kartesisk produkt av ingångsvärden och en total funktion, eller så har du "individuella scenarier täckta." Min uppfattning är att det är mycket bättre om rollen är linjär (cyklomatisk komplexitet XNUMX).

Ju färre om (explicit eller deklarativt - i formen when eller form include_vars efter uppsättning variabler), desto bättre roll. Ibland måste man göra grenar, men jag upprepar, ju färre det är desto bättre. Så det verkar vara en bra roll med galaxy (det fungerar!) med ett gäng when kan vara mindre att föredra än "en egen" roll från fem uppgifter. Det ögonblick då rollen med galaxy är bättre är när du börjar skriva något. Det ögonblick då det blir värre är när något går sönder och du har en misstanke om att det beror på "rollen med galaxen". Du öppnar den och det finns fem inneslutningar, åtta uppgiftsblad och en stack when'ov... Och vi måste ta reda på det här. Istället för 5 uppgifter, en linjär lista där det inte finns något att bryta.

I följande delar

  • Lite om inventering, gruppvariabler, host_group_vars plugin, hostvars. Hur man knyter en gordisk knut med spagetti. Omfattning och prioritetsvariabler, Ansible minnesmodell. "Så var lagrar vi användarnamnet för databasen?"
  • jinja: {{ jinja }} — nosql notype nosense mjuk plasticine. Det finns överallt, även där du inte förväntar dig det. Lite om !!unsafe och läcker yaml.

Källa: will.com

Lägg en kommentar