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

Jag granskar mycket 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 ger sig in i komplexa saker utan att behärska grunderna.

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

Förväntad läsnivå – flera tusen rader Yamla har redan skrivits, något är redan under produktion, men ”på något sätt är allt krokigt”.

Namn

Det största misstaget en Ansible-användare gör är att inte veta vad saker kallas. Om du inte känner till namnen kan du inte förstå vad dokumentationen säger. Ett exempel från verkligheten: under en intervju kunde en person som verkade påstå att han skrev mycket i Ansible inte svara på frågan "vilka element består en handbok av?" Och när jag föreslog att "svaret var förväntat att en spelbok består av pjäser", var den knepiga kommentaren "det använder vi inte". Folk skriver i Ansible för pengar och använder inte play. De använder det faktiskt, men vet inte vad det är.

Så låt oss börja med det enkla: vad kallas det? Kanske vet du detta, kanske inte, för att du inte var uppmärksam när du läste dokumentationen.

ansible-playbook kör en playbook. Playbook är en fil med filändelsen yml/yaml, inuti vilken det finns något liknande:

---
- hosts: group1
  roles:
    - role1

- hosts: group2,group3
  tasks:
    - debug:

Vi förstod redan att hela den här filen är en spelbok. Vi kan visa var rollerna är, var uppgifterna är. Men var är pjäsen här? Och vad är skillnaden mellan en pjäs och en roll eller en spelbok?

Allt finns i dokumentationen. Och detta är missat. Nybörjare - eftersom det är för mycket och man inte kan komma ihåg allt på en gång. Erfaren - eftersom "triviala saker". Om du är erfaren, läs om dessa sidor minst en gång var sjätte månad, så kommer din kod att bli ett klass bättre.

Så kom ihåg: En spelbok är en lista med spel och import_playbook.
Här är en pjäs:

- hosts: group1
  roles:
    - role1

och detta är också en annan pjäs:

- hosts: group2,group3
  tasks:
    - debug:

Vad är lek? Varför henne?

Lek är nyckelelementet i en spelbok eftersom lek och endast lek associerar en lista med roller och/eller uppgifter med en lista med värdar att köra dem på. I dokumentationens djup kan man finna ett omnämnande av delegate_to, lokala sökplugins, nätverksklispecifika inställningar, hoppvärdar etc. De låter dig ändra platsen för övningarna något. Men glöm det. Var och en av dessa smarta alternativ har mycket specifika användningsområden och ä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. Ingen roll med moduler och delegater. Du tar och skriver pjäs. Där du i fältet "värdar" anger var du ska köra, och i "roller/uppgifter" - vad du ska köra.

Enkelt, eller hur? Hur skulle det kunna vara annorlunda?

Ett av de karakteristiska ögonblicken när folk vill göra detta utan att spela är "rollen som sätter allt igång". De vill ha en roll som sätter allt igång och server den första typen och den andra typen av server.

Ett arketypiskt exempel är övervakning. Jag skulle vilja ha en övervakningsroll som konfigurerar övervakning. Övervakningsrollen tilldelas övervakningsvärdar (resp. play). Men det visar sig att för att kunna övervaka måste vi leverera paket till de värdar vi övervakar. Varför inte använda delegat? Och du behöver också konfigurera iptables. delegera? Och du måste också skriva/korrigera konfigurationen för DBMS så att övervakning kan startas. delegera! Och om kreativitet kommer in i bilden, då kan du delegera include_role i en kapslad loop med ett knepigt filter på listan över grupper, och inuti include_role du kan fortfarande göra det delegate_to igen. Och iväg bar det av...

Den goda önskan om att ha en enda övervakande roll som "gör allt" leder oss till ett fullständigt helvete, från vilket det oftast bara finns en väg ut: att skriva om allt från grunden.

Var inträffade misstaget? I det ögonblick du upptäckte att för att göra uppgift "x" på värd X var du tvungen att gå till värd Y och göra "y" där, var du tvungen att utföra en enkel övning: gå och skriv en pjäs som gör y på värd Y. Lägg inte till något till "x", utan skriv från grunden. Även med hårdkodade variabler.

Det verkar som att allt som sägs i styckena ovan är korrekt. Men detta är inte ditt fall! Eftersom du vill skriva återanvändbar kod som är DRY och biblioteksliknande, och du behöver hitta en metod för att göra det.

Här lurar ytterligare ett grovt misstag. Ett misstag som förvandlade många projekt från att vara någorlunda skrivet (det kunde vara bättre, men allt fungerar och är lätt att avsluta) till en fullständig skräckupplevelse som inte ens författaren kan lista ut. Det fungerar, men Gud förbjude att du ändrar något.

Felet låter så här: rollen är en biblioteksfunktion. Den här analogin har förstört så många bra saker att det är bara sorgligt att titta på. En roll är inte en biblioteksfunktion. Hon kan inte göra beräkningar och hon kan inte fatta beslut på spelnivå. Påminn mig vilka beslut lek fattar?

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

Om du delegerar detta beslut till en roll, och med beräkningar som tillskott, fördömer du dig själv (och den som försöker analysera din kod) till en miserabel existens. Rollen avgör inte var den ska utföras. Detta beslut fattas genom lek. Rollen gör vad den blir tillsagd, där den blir tillsagd.

Varför programmering i Ansible är farligt och varför COBOL är bättre än Ansible kommer vi att diskutera i kapitlet om variabler och jinja. För tillfället kan vi bara säga en sak: varje beräkning du gör lämnar efter sig ett outplånligt spår av förändringar i globala variabler, och det finns inget du kan göra åt det. Så fort de två "spåren" korsades försvann allt.

Anmärkning för den kräsne: rollen kan säkert påverka kontrollflödet. Äta delegate_to och den har rimliga tillämpningar. Äta meta: end host/play. Men! Kommer ni ihåg att vi lärde oss grunderna? Glömde bort delegate_to. Vi pratar om den enklaste och vackraste koden i Ansible. Vilken är lätt att läsa, lätt att skriva, lätt att felsöka, lätt att testa och lätt att lägga till. Så, ännu en gång:

spela och endast spela avgör på vilka värdar vad som körs.

I det här avsnittet har vi tittat på motsättningen mellan lek och roll. Nu ska vi prata om förhållandet mellan uppgifter och roller.

Uppgifter och roller

Låt oss titta på pjäsen:

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

Låt oss säga att du behöver göra foo. Och det ser ut så här foo: name=foobar state=present. Var ska jag skriva detta? i förväg? posta? Skapa roll?

...Och vart tog uppgifterna vägen?

Vi börjar om från grunderna – lekanordningen. Om du bara svävar i den här frågan kan du inte använda lek som grund för allt annat, och ditt resultat kommer att bli "skakigt".

Spela upp enhet: hosts-direktiv, inställningar för själva uppspelningen och avsnitten pre_tasks, tasks, roles och post_tasks. Resten av parametrarna för spel är inte viktiga för oss nu.

Ordningen på deras avsnitt med uppgifter och roller: pre_tasks, roles, tasks, post_tasks. Eftersom semantiskt sett är exekveringsordningen mellan tasks и roles det är inte tydligt, då 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 avsnitt pre_tasks/post_tasks.

Det enda som återstår är att allt är semantiskt tydligt: ​​först pre_tasksrolespost_tasks.

Men vi har fortfarande inte svarat på frågan: var är modulens anrop? foo skriva? Behöver vi skriva en hel roll för varje modul? Eller är det bättre att ha en tjock roll under allting? Och om det inte är en roll, var ska man då skriva – i för- eller efterrollen?

Om det inte finns något välgrundat svar på dessa frågor, är detta ett tecken på brist på intuition, det vill säga samma "skakiga grundvalar". Låt oss lista ut det. Först, en säkerhetsfråga: Om spel har pre_tasks и post_tasks (och det inte finns några uppgifter eller roller), kan något då gå sönder om jag först gör en uppgift från post_tasks Jag flyttar den till slutet pre_tasks?

Naturligtvis antyder formuleringen av frågan att den kommer att brytas. Men vad exakt?

… Hanterare. Att läsa grunderna avslöjar ett viktigt faktum: alla hanterare spolas automatiskt efter varje avsnitt. Dessa. alla uppgifter utförs pre_tasks, sedan meddelades alla hanterare som var det. Sedan körs alla roller och alla hanterare som har meddelats i rollerna. Efter post_tasks och deras handläggare.

Så om du drar en uppgift från post_tasks в pre_tasks, då kommer du potentiellt att köra den innan hanteraren körs. till exempel, om i pre_tasks installerad och konfigurerad webbserver, och i post_tasks något skickas till den, sedan överförs denna uppgift till sektionen pre_tasks kommer att leda till att servern ännu inte kommer att startas vid "skickningsögonblicket" och allt kommer att gå sönder.

Nu ska vi tänka om, varför behöver vi det? pre_tasks и post_tasks? Till exempel att utföra allt som krävs (inklusive hanterare) innan rollen utförs. En post_tasks kommer att tillåta oss att arbeta med resultaten av rollexekveringen (inklusive hanterare).

En ivrig Ansible-expert kommer att berätta för oss att det finns meta: flush_handlers, men varför behöver vi flush_handlers om vi kan lita på ordningen i vilken sektioner som spelas upp? Dessutom kan användning av meta: flush_handlers ge oss oväntade resultat med dubbla hanterare, vilket ger oss konstiga varningar vid användning. when у block etc. Ju bättre du känner till ansvarstagandet, desto fler nyanser kan du namnge för en "knepig" lösning. Och den enkla lösningen – att använda en naturlig uppdelning mellan för/roller/post – orsakar inga nyanser.

Och låt oss återgå till vår 'foo'. Var ska man lägga den? I pre-, post- eller i roller? Det beror självklart på om vi behöver resultaten från hanteraren för foo. Om de inte finns där behöver foo inte placeras vare sig i pre eller post - dessa avsnitt har en speciell betydelse - de utför uppgifter före och efter huvudkodens array.

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

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

Uppgifter och roller (del två)

Nu ska vi diskutera situationen när du precis har börjat skriva en handbok. 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: när bör man börja skriva roller? Vad är poängen med att skriva roller när man kan skriva uppgifter?... Och 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 generaliserad beskrivning av en funktion ut? Den tar argument som indata, interagerar med sidoverkningar, gör biverkningar och returnerar ett värde.

Nu, uppmärksamhet. Vad kan man göra med detta i en roll? Att kalla biverkningar är alltid välkommet, det är hela poängen med Ansible – att göra biverkningar. Har det biverkningar? Elementär. Men med "skicka värdet och returnera det" - det är där det inte fungerar. För det första kan du inte skicka ett värde till en roll. Du kan ange en global variabel med en livstids speltid i vars-sektionen för en roll. Du kan ange en global variabel med en livslängd i spel inuti en roll. Eller ens med spelböckernas livslängd (set_fact/register). Men man kan inte ha "lokala variabler". Du kan inte "ta ett värde" och "returnera 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 av en funktion. I Rust, till exempel, är det att ändra en global variabel unsafe. Och i Ansible är det den enda metoden att påverka värdena för en roll. Observera orden som används: inte "skickar ett värde till en roll", utan "ändrar de värden som rollen använder". Det finns ingen isolering mellan roller. Det finns ingen isolering mellan uppgifter och roller.

Totalt: roll är inte en funktion.

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

Vad är bra med standardvärden? Faktum är att i Maslows pyramid, en ganska förvrängd tabell över variabelprioriteter i Ansible, har rollstandardvärden lägst prioritet (minus Ansibles kommandoradsparametrar). Det betyder att om du behöver ange standardvärden och inte oroa dig för att de ska åsidosätta värden från inventering eller gruppvariabler, då är rollstandardvärden det enda rätta stället för dig. (Jag ljuger lite – det finns mer) |d(your_default_here), men om vi pratar om stationära platser, då bara rollstandarder).

Vad mer är bra med rollerna? Att de har sina egna kataloger. Dessa är kataloger för variabler, både konstanta (dvs. beräknade för en roll) 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 tillåter också att roller har sina egna moduler och plugins (library/). Men i jämförelse med uppgifter i en playbook (som också kan ha allt detta) är den enda fördelen här att filerna inte dumpas i en hög, utan i 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). Efter samlingarnas tillkomst kan rollfördelningen anses vara nästan bortglömd.

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

Åter till den ursprungliga frågan: när ska man utföra uppgifter och när roller? Uppgifter i en playbook används oftast antingen som "lim" före/efter roller, eller som en fristående byggsten (i vilket fall det inte ska finnas några roller i koden). En hög med vanliga saker blandat med roller är ett tydligt tecken på slarv. Du bör hålla dig till en specifik stil – antingen en uppgift eller en roll. Roller ger separering av entiteter och standardvärden, uppgifter gör att du kan läsa koden snabbare. Vanligtvis placeras 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 något liknande, var beredd att förklara för din egen skönhetssinne varför du vill göra detta.

En kräsen läsare kanske säger att roller kan importera roller, roller kan ha ett beroende via galaxy.yml, och att det också finns det läskiga och fruktansvärda include_role — Låt mig påminna er om att vi förbättrar våra färdigheter i grundläggande Ansible, inte i figurgymnastik.

Hanterare och uppgifter

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

Eftersom vi bara återger grunderna, här är ett exempel:

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

Rollhanterarna finns i rolename/handlers/main.yaml. Hanterare delas mellan alla speldeltagare: pre/post_tasks kan hämta hanterare från en roll, och en roll kan hämta hanterare från en spelning. Emellertid orsakar "cross-role"-anrop till hanterare mycket mer wtf än att upprepa en trivial hanterare. (En annan bra metod är att försöka att inte upprepa namnen på förare.)

Den största skillnaden är att uppgiften alltid utförs (idempotent) (plus/minus-taggar och when), och hanteraren - vid tillståndsändring (notify utlöses endast om den ändrades). Vilka är konsekvenserna av detta? Till exempel, vid omstart, om det inte gjordes några ändringar, kommer det inte att finnas någon hanterare. Och varför kan det vara så att vi behöver köra en handler när genereringsuppgiften inte har ändrats? Till exempel, för att något gick sönder och det skedde en ändring, men körningen nådde inte hanteraren. Till exempel för att nätverket tillfälligt låg nere. Konfigurationen har ändrats, tjänsten har inte startats om. Nästa gång du startar konfigurationen kommer den inte att ändras, och tjänsten kommer att behålla den gamla versionen av konfigurationen.

Situationen med konfigurationen är inte lösbar (mer exakt kan du uppfinna ett speciellt omstartsprotokoll med filflaggor etc., men detta är inte längre 'grundläggande ansible' i någon form). Men det finns en annan vanlig historia: vi installerade applikationen, spelade in den .service-fil, och nu vill vi ha den daemon_reload и state=started. Och den naturliga platsen för detta verkar vara föraren. Men om du gör den till en uppgift i slutet av en uppgiftslista eller roll, inte en hanterare, så kommer den att köras idempotent varje gång. Även om spelboken brister mitt i. Detta löser inte alls problemet med omstartad (du kan inte skapa en uppgift med attributet omstartad, eftersom idempotensen går förlorad), men det är definitivt värt att göra state=started, den övergripande stabiliteten i playbooken ökar, eftersom antalet anslutningar och det dynamiska tillståndet minskar.

En annan positiv egenskap hos hanteraren är att den inte förorenar utgången. Det gjordes inga ändringar - inget extra överhoppat eller ok i utdata - lättare att läsa. Det är också en negativ egenskap - om du hittar ett stavfel i en linjärt exekverad uppgift vid den allra första körningen, kommer hanterarna endast att exekveras 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 på fem år. Och naturligtvis kommer det att finnas ett stavfel i namnet och allt kommer att gå sönder. Och du kan inte köra dem en andra gång – de har inte ändrats.

Vi behöver prata separat om tillgängligheten av variabler. Om du till exempel meddelar en uppgift med en loop, vad kommer att finnas i variablerna? Man kan gissa analytiskt, men det är inte alltid trivialt, särskilt inte 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 blir vackert, är det bättre med dem.

Den flitige läsaren påpekar med rätta att vi inte har diskuterat listen, att en hanterare kan anropa notify på en annan hanterare, att en hanterare kan include import_tasks (vilket include_role kan göra with_items), att hanterarsystemet i Ansible är Turing-komplett, att hanterare från include_role korsar hanterare från play på ett ytterst märkligt sätt, etc. - allt detta är uppenbarligen inte "grundläggande").

Det finns dock en särskild WTF, som faktiskt är en funktion, och något att tänka på. Om du har en uppgift som körs med delegate_to och den har notifiering, så körs motsvarande hanterare utan delegate_to, d.v.s. på värden där spelet är tilldelat. (Även om föraren naturligtvis kan ha delegate_to Samma).

Separat vill jag säga några ord om återanvändbara roller. Innan samlingarna fanns det en idé om att man kunde skapa universella roller som kunde vara ansible-galaxy install och gick. Fungerar på alla operativsystem av alla varianter i alla situationer. Så här är min åsikt: det fungerar inte. Vilken roll som helst med massa include_vars, att stödja 100500 fall är dömt till en avgrund av buggar i hörnfallet. De kan täppas till med massiv testning, men som med all testning har man antingen en kartesisk produkt av ingångsvärden och en total funktion, eller så har man "enskilda scenarier täckta". Min åsikt är att det är mycket bättre om rollen är linjär (cyklomatisk komplexitet 1).

Ju färre om (explicit eller deklarativ - i formen when eller formulär include_vars av uppsättningen variabler), desto bättre roll. Ibland måste man göra grenar, men jag upprepar, ju färre, desto bättre. Så det verkar som en bra roll med Galaxy (det fungerar!) med en massa when kan vara mindre att föredra än "ens egen" roll från fem uppgifter. Det ögonblick då rollen med Galaxy är bättre är när man börjar skriva något. Det ögonblick det blir värre är när något går sönder och man misstänker att det beror på "galaxrollen". Du öppnar den och där finns fem inkluderingar, åtta uppgiftslistor och en stapel. when'Oj... Och det här måste lösas. 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 inventeringar, gruppvariabler, host_group_vars-pluginet, hostvars. Hur man knyter en gordiansk knut av spaghetti. Variablers omfattning och prioritet, Ansible minnesmodell. "Så var ska användarnamnet för databasen lagras?"
  • jinja: {{ jinja }} — nosql notype nosense mjuk plasticine. Det finns överallt, även där man inte förväntar sig det. Lite om !!unsafe och läcker yaml.

Källa: will.com

Köp pålitlig hosting för webbplatser med DDoS-skydd, VPS VDS-servrar 🔥 Köp pålitlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster