Ansible-basisprincipes, zonder deze zullen je draaiboeken een klomp plakkerige pasta zijn

Ik doe veel recensies van de Ansible-code van anderen en schrijf veel zelf. Tijdens het analyseren van fouten (zowel die van anderen als die van mij), evenals een aantal interviews, realiseerde ik me de belangrijkste fout die Ansible-gebruikers maken: ze komen in complexe dingen terecht zonder de basis te beheersen.

Om dit universele onrecht recht te zetten, heb ik besloten een inleiding voor Ansible te schrijven voor degenen die het al kennen. Ik waarschuw je, dit is geen hervertelling van mensen, dit is een longread met veel brieven en geen afbeeldingen.

Het verwachte niveau van de lezer is dat er al enkele duizenden regels yamla zijn geschreven, dat er al iets in productie is, maar “op de een of andere manier is alles krom.”

Namen

De belangrijkste fout die een Ansible-gebruiker maakt, is dat hij niet weet hoe iets heet. Als u de namen niet kent, begrijpt u ook niet wat er in de documentatie staat. Een levend voorbeeld: tijdens een interview kon iemand die leek te zeggen dat hij veel in Ansible schreef, de vraag “uit welke elementen bestaat een draaiboek?” niet beantwoorden. En toen ik suggereerde dat ‘het antwoord verwachtte dat het draaiboek uit spel bestaat’, volgde de vernietigende opmerking ‘dat gebruiken we niet’. Mensen schrijven Ansible voor geld en gebruiken geen spel. Ze gebruiken het daadwerkelijk, maar weten niet wat het is.

Laten we dus beginnen met iets simpels: hoe heet het. Misschien weet u dit, of misschien ook niet, omdat u niet goed opgelet heeft bij het lezen van de documentatie.

ansible-playbook voert het playbook uit. Een playbook is een bestand met de extensie yml/yaml, waarin zich zoiets als dit bevindt:

---
- hosts: group1
  roles:
    - role1

- hosts: group2,group3
  tasks:
    - debug:

We realiseerden ons al dat dit hele bestand een draaiboek is. Wij kunnen laten zien waar de rollen liggen en waar de taken liggen. Maar waar is spelen? En wat is het verschil tussen spel en rol of draaiboek?

Het staat allemaal in de documentatie. En ze missen het. Beginners - omdat er te veel is en je niet alles in één keer zult onthouden. Ervaren - omdat “triviale dingen”. Als u ervaren bent, lees deze pagina's dan minstens één keer per zes maanden opnieuw, en uw code zal toonaangevend worden.

Onthoud dus: Playbook is een lijst die bestaat uit play en import_playbook.
Dit is één toneelstuk:

- hosts: group1
  roles:
    - role1

en dit is ook een ander toneelstuk:

- hosts: group2,group3
  tasks:
    - debug:

Wat is spelen? Waarom is zij?

Spelen is een sleutelelement voor een draaiboek, omdat spelen en alleen spelen een lijst met rollen en/of taken associeert met een lijst met hosts waarop ze moeten worden uitgevoerd. In de diepten van de documentatie kunt u melding maken van delegate_to, lokale opzoekplug-ins, netwerkcli-specifieke instellingen, jumphosts, enz. Hiermee kunt u de plaats waar taken worden uitgevoerd enigszins wijzigen. Maar vergeet het maar. Elk van deze slimme opties heeft zeer specifieke toepassingen, en ze zijn zeker niet universeel. En we hebben het over basiszaken die iedereen zou moeten weten en gebruiken.

Als je “iets” “ergens” wilt uitvoeren, schrijf je spel. Geen rol. Geen rol met modules en afgevaardigden. Je neemt het en schrijft toneelstuk. Waarin u in het hosts-veld vermeldt waar u moet uitvoeren, en in rollen/taken - wat u moet uitvoeren.

Simpel, toch? Hoe zou het anders kunnen?

Een van de karakteristieke momenten waarop mensen het verlangen hebben om dit niet door middel van spel te doen, is de ‘rol die alles in gang zet’. Ik zou graag een rol willen hebben die zowel servers van het eerste type als servers van het tweede type configureert.

Een archetypisch voorbeeld is monitoring. Ik zou graag een monitoringrol willen hebben die de monitoring configureert. De monitoringrol is toegewezen aan het monitoren van hosts (volgens spel). Maar het blijkt dat we voor monitoring pakketten moeten afleveren bij de hosts die we monitoren. Waarom gebruik je niet delegeren? U moet ook iptables configureren. delegeren? U moet ook een configuratie voor het DBMS schrijven/corrigeren om monitoring in te schakelen. delegeren! En als de creativiteit ontbreekt, kun je een delegatie maken include_role in een geneste lus met behulp van een lastig filter op een lijst met groepen, en daarbinnen include_role je kunt meer doen delegate_to opnieuw. En daar gaan we...

Een goede wens – om één enkele toezichthoudende rol te hebben, die “alles doet” – leidt ons naar een complete hel waaruit meestal maar één uitweg bestaat: alles helemaal opnieuw schrijven.

Waar is hier de fout gebeurd? Op het moment dat je ontdekte dat je om taak "x" op host X uit te voeren naar host Y moest gaan en daar "y" moest doen, moest je een eenvoudige oefening doen: ga play schrijven, wat op host Y y doet. Voeg niets toe aan "x", maar schrijf het helemaal opnieuw op. Zelfs met hardgecodeerde variabelen.

Het lijkt erop dat alles in de bovenstaande paragrafen correct is gezegd. Maar dit is niet jouw geval! Omdat je herbruikbare code wilt schrijven die DROOG en bibliotheekachtig is, en je moet zoeken naar een methode om dat te doen.

Dit is waar een andere ernstige fout op de loer ligt. Een fout die ervoor zorgde dat veel projecten van redelijk geschreven (het kan beter, maar alles werkt en is gemakkelijk af te maken) veranderden in een complete horror waar zelfs de auteur niet achter kan komen. Het werkt, maar God verhoede dat je iets verandert.

De fout is: rol is een bibliotheekfunctie. Deze analogie heeft zoveel goede beginpunten verpest, dat het gewoon triest is om te zien. De rol is geen bibliotheekfunctie. Ze kan geen berekeningen maken en geen beslissingen nemen op speelniveau. Herinner me eraan welke beslissingen het spel neemt?

Bedankt, je hebt gelijk. Play neemt een beslissing (meer precies, het bevat informatie) over welke taken en rollen op welke hosts moeten worden uitgevoerd.

Als je deze beslissing aan een rol delegeert, en zelfs met berekeningen, veroordeel je jezelf (en degene die zal proberen je code te ontleden) tot een ellendig bestaan. De rol bepaalt niet waar deze wordt uitgevoerd. Deze beslissing wordt door spel genomen. De rol doet wat hem wordt verteld, waar hem wordt verteld.

Waarom is het gevaarlijk om in Ansible te programmeren en waarom COBOL beter is dan Ansible, we zullen het hebben in het hoofdstuk over variabelen en jinja. Laten we voor nu één ding zeggen: al uw berekeningen laten een onuitwisbaar spoor van veranderingen in globale variabelen achter, en u kunt er niets aan doen. Zodra de twee ‘sporen’ elkaar kruisten, was alles verdwenen.

Opmerking voor de preutsen: de rol kan zeker de controlestroom beïnvloeden. Eten delegate_to en het heeft redelijke toepassingen. Eten meta: end host/play. Maar! Weet je nog dat we de basis onderwijzen? Vergeten delegate_to. We hebben het over de eenvoudigste en mooiste Ansible-code. Dat is gemakkelijk te lezen, gemakkelijk te schrijven, gemakkelijk te debuggen, gemakkelijk te testen en gemakkelijk te voltooien. Dus nogmaals:

spelen en alleen spelen bepaalt welke hosts wat wordt uitgevoerd.

In deze paragraaf hebben we de tegenstelling tussen spel en rol behandeld. Laten we het nu hebben over de relatie tussen taken en rollen.

Taken en rollen

Overweeg spelen:

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

Laten we zeggen dat je foo moet doen. En het lijkt erop foo: name=foobar state=present. Waar moet ik dit schrijven? vooraf? na? Een rol aanmaken?

...En waar zijn de taken gebleven?

We beginnen weer bij de basis: het speelapparaat. Als je op dit punt blijft drijven, kun je het spel niet als basis voor al het andere gebruiken en zal je resultaat 'wankel' zijn.

Speelapparaat: hosts-richtlijn, instellingen voor het spelen zelf en pre_tasks, taken, rollen, post_tasks-secties. De overige spelparameters zijn nu niet belangrijk voor ons.

De volgorde van hun secties met taken en rollen: pre_tasks, roles, tasks, post_tasks. Omdat de volgorde van uitvoering semantisch gezien tussen tasks и roles niet duidelijk is, zeggen best practices dat we een sectie toevoegen tasks, alleen als dat niet het geval is roles... als er is roles, dan worden alle gekoppelde taken in secties geplaatst pre_tasks/post_tasks.

Het enige dat overblijft is dat alles semantisch duidelijk is: ten eerste pre_tasksdan rolesdan post_tasks.

Maar we hebben de vraag nog steeds niet beantwoord: waar is de moduleoproep? foo schrijven? Moeten we voor elke module een hele rol schrijven? Of is het beter om voor alles een dikke rol te spelen? En als het geen rol is, waar moet ik dan schrijven - in pre of post?

Als er geen beredeneerd antwoord op deze vragen bestaat, is dit een teken van een gebrek aan intuïtie, dat wil zeggen van dezelfde ‘wankele fundamenten’. Laten we het uitzoeken. Ten eerste een veiligheidsvraag: of er sprake is van spel pre_tasks и post_tasks (en er zijn geen taken of rollen), dan kan er iets kapot gaan als ik de eerste taak uitvoer post_tasks Ik verplaats het naar het einde pre_tasks?

Natuurlijk duidt de formulering van de vraag erop dat deze zal breken. Maar wat precies?

... Behandelaars. Als u de basis leest, wordt een belangrijk feit onthuld: alle handlers worden na elke sectie automatisch gespoeld. Die. alle taken van pre_tasksen vervolgens alle afhandelaars die op de hoogte zijn gesteld. Vervolgens worden alle rollen en alle handlers uitgevoerd die in de rollen zijn gemeld. Na post_tasks en hun begeleiders.

Dus als u een taak uit sleept post_tasks в pre_tasks, dan voert u het mogelijk uit voordat de handler wordt uitgevoerd. bijvoorbeeld als in pre_tasks de webserver is geïnstalleerd en geconfigureerd, en post_tasks er wordt iets naartoe gestuurd en breng deze taak vervolgens over naar de sectie pre_tasks zal ertoe leiden dat op het moment van “verzenden” de server nog niet draait en alles kapot gaat.

Laten we nu nog eens nadenken: waarom hebben we dat nodig? pre_tasks и post_tasks? Bijvoorbeeld om al het noodzakelijke (inclusief begeleiders) af te ronden voordat je de rol vervult. A post_tasks zal ons in staat stellen om te werken met de resultaten van het uitvoeren van rollen (inclusief handlers).

Een scherpzinnige Ansible-expert zal ons vertellen wat het is. meta: flush_handlers, maar waarom hebben we flush_handlers nodig als we kunnen vertrouwen op de volgorde van uitvoering van secties in het spel? Bovendien kan het gebruik van meta: flush_handlers ons onverwachte dingen opleveren met dubbele handlers, waardoor we vreemde waarschuwingen krijgen bij gebruik when у block enz. Hoe beter je de weerwort kent, hoe meer nuances je kunt benoemen voor een ‘lastige’ oplossing. En een simpele oplossing – gebruik maken van een natuurlijke scheiding tussen pre/rollen/post – zorgt niet voor nuances.

En terug naar onze 'foo'. Waar moet ik het plaatsen? In pre, post of rollen? Uiteraard hangt dit ervan af of we de resultaten van de handler voor foo nodig hebben. Als ze er niet zijn, hoeft foo niet in pre of post te worden geplaatst - deze secties hebben een speciale betekenis - waarbij taken worden uitgevoerd voor en na de hoofdtekst van de code.

Nu komt het antwoord op de vraag "rol of taak" neer op wat er al in het spel is - als er taken zijn, dan moet je ze aan taken toevoegen. Als er rollen zijn, moet u een rol aanmaken (zelfs vanuit één taak). Ik wil u eraan herinneren dat taken en rollen niet tegelijkertijd worden gebruikt.

Het begrijpen van de basisprincipes van Ansible biedt redelijke antwoorden op ogenschijnlijke smaakvragen.

Taken en rollen (deel twee)

Laten we nu de situatie bespreken waarin u net begint met het schrijven van een draaiboek. Je moet foo, bar en baz maken. Zijn dit drie taken, één rol of drie rollen? Om de vraag samen te vatten: op welk punt moet je beginnen met het schrijven van rollen? Wat heeft het voor zin om rollen te schrijven als je ook taken kunt schrijven?... Wat is een rol?

Een van de grootste fouten (ik heb het hier al over gehad) is te denken dat een rol lijkt op een functie in de bibliotheek van een programma. Hoe ziet een generieke functiebeschrijving eruit? Het accepteert invoerargumenten, werkt samen met nevenoorzaken, heeft bijwerkingen en retourneert een waarde.

Nu, aandacht. Wat kan hieruit worden gedaan in de rol? Je bent altijd welkom om bijwerkingen te noemen, dit is de essentie van de hele Ansible: bijwerkingen creëren. Heeft u nevenoorzaken? Elementair. Maar met “geef een waarde door en retourneer deze” – dat is waar het niet werkt. Ten eerste kunt u geen waarde aan een rol doorgeven. Je kunt een globale variabele instellen met een levenslange speelduur in de vars-sectie voor de rol. U kunt binnen de rol een globale variabele instellen die een leven lang meespeelt. Of zelfs met de levensduur van draaiboeken (set_fact/register). Maar je kunt geen "lokale variabelen" hebben. Je kunt niet 'een waarde nemen' en deze 'teruggeven'.

Het belangrijkste volgt hieruit: je kunt niet iets in Ansible schrijven zonder bijwerkingen te veroorzaken. Het wijzigen van globale variabelen is altijd een neveneffect van een functie. In Rust is het wijzigen van een globale variabele bijvoorbeeld unsafe. En bij Ansible is het de enige manier om de waarden voor een rol te beïnvloeden. Let op de gebruikte woorden: niet ‘geef een waarde door aan de rol’, maar ‘verander de waarden die de rol gebruikt’. Er is geen isolatie tussen rollen. Er is geen isolatie tussen taken en rollen.

Totaal: een rol is geen functie.

Wat is er goed aan de rol? Ten eerste heeft de rol standaardwaarden (/default/main.yaml), ten tweede heeft de rol extra mappen voor het opslaan van bestanden.

Wat zijn de voordelen van standaardwaarden? Omdat in de piramide van Maslow, de nogal vertekende tabel met variabele prioriteiten van Ansible, de standaardrollen de laagste prioriteit hebben (minus de Ansible-opdrachtregelparameters). Dit betekent dat als u standaardwaarden moet opgeven en u zich geen zorgen hoeft te maken dat deze de waarden uit inventaris- of groepsvariabelen overschrijven, de standaardwaarden voor rollen de enige juiste plek voor u zijn. (Ik lieg een beetje - er zijn er meer |d(your_default_here), maar als we het over stationaire plaatsen hebben, dan alleen de standaardrollen).

Wat is er nog meer geweldig aan de rollen? Omdat ze hun eigen catalogi hebben. Dit zijn mappen voor variabelen, zowel constant (dat wil zeggen berekend voor de rol) als dynamisch (er is een patroon of een antipatroon - include_vars met {{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml.). Dit zijn de mappen voor files/, templates/. U kunt er ook uw eigen modules en plug-ins mee hebben (library/). Maar in vergelijking met taken in een draaiboek (dat dit ook allemaal kan bevatten), is het enige voordeel hier dat de bestanden niet op één stapel worden geplaatst, maar op verschillende afzonderlijke stapels.

Nog een detail: je kunt proberen rollen te creëren die beschikbaar zijn voor hergebruik (via galaxy). Met de komst van collecties kan de rolverdeling als bijna vergeten worden beschouwd.

Rollen hebben dus twee belangrijke kenmerken: ze hebben standaardwaarden (een unieke functie) en ze stellen je in staat je code te structureren.

Terugkomend op de oorspronkelijke vraag: wanneer moet je taken uitvoeren en wanneer rollen? Taken in een draaiboek worden meestal gebruikt als “lijm” voor/na rollen, of als een onafhankelijk bouwelement (dan mogen er geen rollen in de code voorkomen). Een stapel normale taken vermengd met rollen is ondubbelzinnige slordigheid. Je moet je aan een specifieke stijl houden: een taak of een rol. Rollen zorgen voor scheiding van entiteiten en standaardwaarden, en met taken kunt u code sneller lezen. Meestal wordt meer “stationaire” (belangrijke en complexe) code in rollen geplaatst en worden hulpscripts in taakstijl geschreven.

Het is mogelijk om import_role als taak uit te voeren, maar als je dit schrijft, wees dan bereid om aan je eigen gevoel voor schoonheid uit te leggen waarom je dit wilt doen.

Een scherpzinnige lezer zou kunnen zeggen dat rollen rollen kunnen importeren, rollen afhankelijkheden kunnen hebben via galaxy.yml, en er is ook een vreselijke en vreselijke include_role — Ik herinner je eraan dat we de vaardigheden in de basis-Ansible verbeteren, en niet in de figuurgymnastiek.

Handelaars en taken

Laten we nog iets voor de hand liggend bespreken: handlers. Weten hoe je ze correct moet gebruiken, is bijna een kunst. Wat is het verschil tussen een handler en een drag?

Omdat we de basisbeginselen onthouden, is hier een voorbeeld:

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

De handlers van de rol bevinden zich in rolnaam/handlers/main.yaml. Handlers snuffelen tussen alle deelnemers aan het spel: pre/post_tasks kunnen rolhandlers trekken, en een rol kan handlers uit het spel halen. Echter, "cross-role" oproepen naar handlers veroorzaken veel meer wtf dan het herhalen van een triviale handler. (Een ander element van best practices is om te proberen de namen van handlers niet te herhalen).

Het belangrijkste verschil is dat de taak altijd (idempotent) wordt uitgevoerd (plus/min-tags en when), en de handler - door statuswijziging (waarschuw branden alleen als deze is gewijzigd). Wat betekent dit? Bijvoorbeeld het feit dat wanneer u opnieuw opstart en er niets is gewijzigd, er geen handler zal zijn. Waarom zou het kunnen dat we handler moeten uitvoeren terwijl er geen verandering is in de genererende taak? Bijvoorbeeld omdat er iets kapot ging en veranderde, maar de uitvoering niet bij de afhandelaar terechtkwam. Bijvoorbeeld omdat het netwerk tijdelijk uitgevallen was. De configuratie is gewijzigd, de service is niet opnieuw gestart. De volgende keer dat u het start, verandert de configuratie niet meer en blijft de service bij de oude versie van de configuratie.

De situatie met de configuratie kan niet worden opgelost (preciezer gezegd, je kunt voor jezelf een speciaal herstartprotocol uitvinden met bestandsvlaggen, enz., maar dit is niet langer een 'fundamentele anible' in welke vorm dan ook). Maar er is nog een veelvoorkomend verhaal: we hebben de applicatie geïnstalleerd en opgenomen .service-bestand, en nu willen we het daemon_reload и state=started. En de natuurlijke plaats hiervoor lijkt de handler te zijn. Maar als je er geen handler van maakt, maar een taak aan het einde van een takenlijst of rol, dan wordt deze elke keer idempotent uitgevoerd. Zelfs als het draaiboek halverwege kapot ging. Dit lost het herstartprobleem helemaal niet op (je kunt geen taak uitvoeren met het herstartattribuut, omdat idempotentie verloren gaat), maar het is zeker de moeite waard om state=started te doen, de algehele stabiliteit van playbooks neemt toe, omdat het aantal verbindingen en de dynamische toestand nemen af.

Een andere positieve eigenschap van de handler is dat deze de uitvoer niet verstopt. Er waren geen wijzigingen - geen extra overgeslagen of ok in de uitvoer - gemakkelijker te lezen. Het is ook een negatieve eigenschap: als u bij de allereerste run een typefout aantreft in een lineair uitgevoerde taak, worden de handlers alleen uitgevoerd als deze worden gewijzigd, d.w.z. onder bepaalde omstandigheden - zeer zelden. Vijf jaar later bijvoorbeeld voor het eerst in mijn leven. En natuurlijk zal er een typefout in de naam zitten en zal alles kapot gaan. En als u ze niet de tweede keer uitvoert, is er niets veranderd.

Afzonderlijk moeten we praten over de beschikbaarheid van variabelen. Als u bijvoorbeeld een taak meldt met een lus, wat staat er dan in de variabelen? Je kunt analytisch raden, maar dat is niet altijd triviaal, vooral als de variabelen van verschillende plaatsen komen.

... Handlers zijn dus veel minder nuttig en veel problematischer dan ze lijken. Als je iets moois (zonder franjes) kunt schrijven zonder handlers, kun je het beter zonder hen doen. Als het niet mooi uitpakt, is het beter met hen.

De bijtende lezer wijst er terecht op dat we er niet over hebben gediscussieerd listendat een handler notificatie kan aanroepen voor een andere handler, dat een handler import_tasks kan opnemen (die include_role kan doen met with_items), dat het handlersysteem in Ansible Turing-complete is, dat handlers van include_role op een merkwaardige manier kruisen met handlers uit het spel, enz. .d. - dit alles is duidelijk niet de “basis”).

Hoewel er één specifieke WTF is, is dit eigenlijk een functie waarmee u rekening moet houden. Als uw taak wordt uitgevoerd met delegate_to en het heeft een melding gedaan, dan wordt de corresponderende handler zonder uitgevoerd delegate_to, d.w.z. op de host waar het spel is toegewezen. (Hoewel de handler dat natuurlijk wel kan hebben delegate_to Dezelfde).

Afzonderlijk wil ik een paar woorden zeggen over herbruikbare rollen. Voordat er collecties verschenen, bestond er een idee dat je universele rollen kon maken ansible-galaxy install en ging. Werkt op alle besturingssystemen van alle varianten in alle situaties. Dus mijn mening: het werkt niet. Elke rol met massa include_vars, dat 100500 gevallen ondersteunt, is gedoemd tot een afgrond van hoekkastbugs. Ze kunnen worden afgedekt met massale tests, maar zoals bij elke test heb je óf een cartesiaans product van invoerwaarden en een totale functie, óf je hebt ‘individuele scenario’s gedekt’. Mijn mening is dat het veel beter is als de rol lineair is (cyclomatische complexiteit 1).

Hoe minder ifs (expliciet of declaratief - in de vorm when of vorm include_vars per set variabelen), hoe beter de rol. Soms moet je takken maken, maar ik herhaal: hoe minder er zijn, hoe beter. Het lijkt dus een goede rol bij Galaxy (het werkt!) met een heleboel when kan minder de voorkeur verdienen dan de ‘eigen’ rol uit vijf taken. Het moment waarop de rol bij Galaxy beter is, is wanneer je iets begint te schrijven. Het moment waarop het erger wordt is wanneer er iets kapot gaat en je het vermoeden hebt dat dit komt door de “rol bij galaxy”. Je opent het en er zijn vijf insluitsels, acht taakbladen en een stapel when'ov... En we moeten dit uitzoeken. In plaats van 5 taken, een lineaire lijst waarin niets te breken valt.

In de volgende delen

  • Iets over inventaris, groepsvariabelen, plug-in host_group_vars, hostvars. Hoe maak je een Gordiaanse knoop met spaghetti? Variabelen voor bereik en prioriteit, Ansible-geheugenmodel. "Dus waar slaan we de gebruikersnaam voor de database op?"
  • jinja: {{ jinja }} — nosql notype nosense zachte plasticine. Het is overal, zelfs waar je het niet verwacht. Een beetje over !!unsafe en heerlijke jam.

Bron: www.habr.com

Voeg een reactie