Ik maak veel beoordelingen van andermans code in Ansible en schrijf ook veel zelf. Tijdens het analyseren van fouten (zowel de mijne als die van anderen) en een aantal sollicitatiegesprekken besefte ik de grote fout die Ansible-gebruikers maken: ze duiken in complexe zaken zonder de basis te beheersen.
Om deze universele onrechtvaardigheid te verhelpen, besloot ik een introductie tot Ansible te schrijven voor degenen die het al kennen. Let op, dit is geen samenvatting van de handboeken, dit is een longread met veel tekst en zonder afbeeldingen.
Het verwachte niveau van de lezer is: je hebt al duizenden regels YAML geschreven, al iets in productie, maar "het lijkt allemaal scheef".
Benamingen
De grootste fout van de Ansible-gebruiker is niet weten hoe dingen heten. Als je de namen niet weet, kun je niet begrijpen wat in de documentatie staat. Een levend voorbeeld: tijdens een sollicitatiegesprek kon een persoon die beweerde veel met Ansible te hebben gewerkt, de vraag "uit welke elementen bestaat een playbook?" niet beantwoorden. Toen ik suggereerde dat "het verwachte antwoord was dat een playbook uit plays bestaat", volgde de dodelijke opmerking "dat gebruiken we niet". Mensen schrijven met Ansible voor geld en gebruiken geen plays. In werkelijkheid gebruiken ze het wel, maar weten ze niet wat het is.
Laten we dus beginnen met het eenvoudige: hoe dingen heten. Misschien weet je dit al, of misschien ook niet, omdat je er niet op lette toen je de documentatie las.
ansible-playbook voert het playbook uit. Een playbook is een bestand met de extensie yml/yaml, waarin iets als dit staat:
---
- hosts: group1
roles:
- role1
- hosts: group2,group3
tasks:
- debug:We hebben al begrepen dat dit hele bestand een playbook is. We kunnen laten zien waar de rollen (roles) en taken (tasks) zijn. Maar waar is de play? En wat is het verschil tussen een play, roll of playbook?
Dit staat allemaal in de documentatie. En dit wordt vaak overgeslagen. Beginners — omdat er te veel staat en je kunt niet alles in één keer onthouden. Ervaren gebruikers — omdat het "triviale zaken" zijn. Als je ervaren bent, herlees deze pagina's minstens eens in de zes maanden, en je code zal een stuk beter worden.
Dus onthoud dit: Een playbook is een lijst bestaande uit plays en import_playbook.
Dit is één play:
- hosts: group1
roles:
- role1En dit is ook weer een play:
- hosts: group2,group3
tasks:
- debug:Wat is een play? Wat is het nut ervan?
Play is a key element for the playbook, because play alone connects the list of roles and/or tasks with the list of hosts on which they need to be executed. Deep in the documentation, mention can be found about delegate_to, local lookup plugins, network-cli-specific settings, jump hosts, etc. They allow for a slight change in the execution location of tasks. But forget about that. Each of these clever options has very specific applications, and they are definitely not universal. Here, we are talking about the basics that everyone should know and use.
If you want to execute "something" "somewhere" — you write a play. Not a role. Not a role with modules and delegates. You simply take and write a play. In which, in the hosts field, you list where to execute, and in roles/tasks — what to execute.
Simple, right? How could it be otherwise?
One of the characteristic moments when people want to do this not through a play is "a role that sets everything up." There’s a desire to have a role that configures and de server first type servers as well as second type servers.
An archetypal example is monitoring. There’s a desire to have a monitoring role that sets up monitoring. The monitoring role is assigned to the monitoring hosts (in the corresponding play). But it turns out that we need to install packages on the hosts we are monitoring. Why not use delegate? Also, we need to configure iptables. Delegate? And we need to write/fix the DB config so that monitoring can work. Delegate! And if creativity kicks in, we can delegate include_role in a nested cycle with a clever filter on the group list, and inside include_role you can also do delegate_to again. And it goes on...
The good wish — to have a single monitoring role that "does everything" — leads us to a hell from which the only way out is to rewrite everything from scratch.
Where did the mistake happen? At the moment when you realized that to perform the task "x" on host X, you needed to go to host Y and do "y" there, you should have done a simple exercise: go and write a play that does y on host Y. Not append something to "x" but write from scratch. Even if it has hardcoded variables.
It seems that everything said in the paragraphs above is correct. But that’s not your case! Because you want to write reusable code that is DRY and resembles a library, and you need to find a way to do that.
Hier ligt nog een grove fout verscholen. Een fout die talloze projecten heeft veranderd van redelijk goed geschreven (het kan beter, maar alles werkt en is gemakkelijk uit te breiden) in een volslagen chaos waarin zelfs de auteur niet meer kan volgen. Het werkt, maar je kunt beter niets veranderen.
Deze fout klinkt zo: een rol is een bibliotheekfunctie. Deze analogie heeft zoveel goede initiatieven de das omgedaan dat het gewoon tragisch is om te zien. Een rol is geen bibliotheekfunctie. Het kan geen berekeningen maken en het kan geen beslissingen op het niveau van play nemen. Herinner mij, welke beslissingen neemt play?
Dank je, je hebt gelijk. Play neemt een beslissing (meer precies, bevat informatie) over welke taken en rollen op welke hosts moeten worden uitgevoerd.
Als je deze beslissing aan een rol delegeert, en bovendien met berekeningen, dan geef je jezelf (en degene die je code probeert te begrijpen) een miserable bestaan. Een rol beslist niet waar hij uitgevoerd wordt. Die beslissing neemt play. Een rol doet wat hem is opgedragen, daar waar dat is aangegeven.
Waarom programmeren met Ansible gevaarlijk is en waarom COBOL beter is dan Ansible, daar zullen we het in het hoofdstuk over variabelen en jinja over hebben. Voor nu één ding: elke berekening die je maakt laat een onuitwisbare sporen achter van gewijzigde globale variabelen, en je kunt er niets aan doen. Zodra twee "sporen" elkaar kruisen, is het voorbij.
Opmerking voor de oplettenden: een rol kan inderdaad invloed hebben op de control flow. Er zijn delegate_to en hij heeft redelijkerwijs toepassingen. Er zijn meta: eindhost/play. Maar! Vergeet niet, we leren de basis? Vergeet niet over delegate_to. We hebben het over de eenvoudigste en mooiste code in Ansible. Die gemakkelijk te lezen, gemakkelijk te schrijven, gemakkelijk te debuggen, gemakkelijk te testen en gemakkelijk uit te breiden is. Dus nogmaals:
play en alleen play beslist op welke hosts wat uitgevoerd wordt.
In dit onderdeel hebben we het conflict tussen play en role behandeld. Laten we nu het onderwerp taken versus rol bespreken.
Taken en Rollen
Laten we play bekijken:
- hosts: somegroup
pre_tasks:
- some_tasks1:
roles:
- role1
- role2
post_tasks:
- some_task2:
- some_task3:Stel, je moet foo maken. En het ziet eruit als foo: name=foobar state=present. Waar schrijf je dit? In pre? post? Een rol maken?
… En waar zijn de taken gebleven?
We beginnen weer bij de basis — de structuur van play. Als je hier niet goed mee overweg kunt, kun je play niet als basis gebruiken voor alles eromheen, en je resultaat wordt "fragiel".
Het play-onderdeel: de hosts-directive, de instellingen van het play zelf en de secties pre_tasks, tasks, roles, post_tasks. Andere parameters voor het play zijn nu niet belangrijk voor ons.
De volgorde van hun secties met taken en rollen: pre_tasks, roles, tasks, post_tasks. Aangezien de semantische volgorde van uitvoering tussen tasks en roles onduidelijk is, zegt best practices dat we de sectie taskstoevoegen, alleen als er geen roles. Als er een roles, dan worden alle bijbehorende taken geplaatst in de sectie pre_tasks/post_tasks.
Er blijft alleen over dat semantisch alles duidelijk is: eerst pre_tasks, daarna roles, daarna post_tasks.
Maar we hebben nog niet geantwoord op de vraag: waar moeten we de module-aanroep foo plaatsen? Moeten we voor elke module een hele rol schrijven? Of is het beter om één uitgebreide rol voor alles te hebben? En als het geen rol is, waar moet het dan geschreven worden — in pre of in post?
Als er geen onderbouwd antwoord op deze vragen is, dan is dat een teken van gebrek aan intuïtief begrip, dat wil zeggen de "broze fundamenten". Laten we het uitzoeken. Eerst een controlevraag: Als er een play is pre_tasks en post_tasks (en er zijn geen taken of rollen), kan er dan iets kapotgaan als ik de eerste taak van post_tasks naar het einde verplaats? pre_tasks?
Natuurlijk suggereert de formulering van de vraag dat er iets kapotgaat. Maar wat precies?
… Handlers. Het lezen van de basisprincipes onthult een belangrijk feit: alle handlers worden automatisch geflushd na elke sectie. Dat wil zeggen, alle taken van pre_tasks, dan worden alle handlers uitgevoerd die zijn genotificeerd. Vervolgens worden alle rollen uitgevoerd en alle handlers die zijn genotificeerd in die rollen. Daarna post_tasks en hun handlers.
Daarom, als je een taak verplaatst van post_tasks in pre_tasks, dan voer je deze potentieel uit vóór het uitvoeren van de handler. Bijvoorbeeld, als in pre_tasks wordt geïnstalleerd en geconfigureerd webserver, en in post_tasks en er iets naartoe wordt gestuurd, dan leidt het verplaatsen van deze taak naar de sectie pre_tasks ertoe dat op het moment van "verzending" de server nog niet gestart is en alles kapotgaat.
Laten we nu nog eens nadenken, waarom hebben we pre_tasks en post_tasks? Например, для того, чтобы выполнить всё нужное (включая хэндлеры) до выполнения роли. А post_tasks nodig dat ons in staat stelt om met de resultaten van het uitvoeren van rollen (inclusief handlers) te werken.
Een oplettende expert in Ansible zal ons vertellen dat er meta: flush_handlersis, maar waarom hebben we flush_handlers nodig, als we kunnen rekenen op de volgorde van uitvoering van de secties in het play? Bovendien kan het gebruik van meta: flush_handlers ons in de problemen brengen met herhaalde handlers, en vreemde waarschuwingen veroorzaken bij het gebruik van when ‘ block enzovoort. Hoe beter je Ansible kent, hoe meer nuances je kunt noemen voor een "slimme" oplossing. En de eenvoudige oplossing — het gebruik van een natuurlijke scheiding tussen pre/roles/post — roept geen nuances op.
En, laten we terugkomen op onze 'foo'. Waar plaatsen we het? In pre, post of in roles? Dit hangt duidelijk af van of we de resultaten van de handler voor foo nodig hebben. Als dat niet zo is, hoeft foo niet in pre of post geplaatst te worden — deze secties hebben een speciale betekenis — het uitvoeren van taken voor en na de hoofdcode-array.
Nu is het antwoord op de vraag 'rol of taak' afhankelijk van wat er al in de play staat — als daar taken zijn, moet je die aan taken toevoegen. Als er rollen zijn, moet je een rol maken (ook al is het maar vanuit één taak). Ik herinner je eraan, taken en rollen worden niet tegelijkertijd gebruikt.
Het begrijpen van de basisprincipes van Ansible geeft onderbouwde antwoorden op vragen die schijnbaar om persoonlijke voorkeur draaien.
Taken en rollen (deel twee)
Laten we nu de situatie bespreken waarin je net begint met het schrijven van een playbook. Je moet foo, bar en baz maken. Zijn dit drie taken, één rol of drie rollen? Om de vraag te generaliseren: wanneer moet je beginnen met het schrijven van rollen? Wat is de betekenis van het schrijven van rollen als je ook taken kunt schrijven?… En wat is een rol?
Een van de grootste fouten (daar heb ik het al over gehad) is denken dat een rol als een functie in de bibliotheek van een programma is. Hoe ziet een algemene beschrijving van een functie eruit? Het neemt argumenten als invoer, interacteert met side causes, heeft side effects en geeft een waarde terug.
Nu, let op. Wat hiervan kan in een rol worden gedaan? Side effects oproepen — dat kan altijd, dat is de essentie van Ansible — side effects genereren. Heb je side causes? Elementair. Maar met 'waarde doorgeven en teruggeven' — daar is het een nee. Ten eerste kun je geen waarde naar een rol doorgeven. Je kunt een globale variabele instellen met een levensduur van play in de vars-sectie voor de rol. Je kunt een globale variabele instellen met een levensduur van play binnen de rol. Of zelfs met de levensduur van een playbook (set_fact/register). Maar je kunt geen 'lokale variabelen' hebben. Je kunt geen 'waarde ontvangen' en 'teruggeven'.
Hieruit volgt het belangrijkste: je kunt in Ansible niets schrijven zonder side effects op te roepen. Het wijzigen van globale variabelen is altijd een side effect voor een functie. In Rust, bijvoorbeeld, is het wijzigen van een globale variabele unsafe. En in Ansible — dat is de enige manier om invloed uit te oefenen op waarden voor de rol. Let op de gebruikte woorden: niet 'waarde doorgeven aan rol', maar 'waarden wijzigen die de rol gebruikt'. Er is geen isolatie tussen rollen. Er is geen isolatie tussen taken en rollen.
Kortom: een rol is geen functie.
Wat is er goed aan rollen? Ten eerste hebben rollen standaardwaarden (/default/main.yaml), ten tweede hebben rollen extra mappen om bestanden op te slaan.
Wat is er zo goed aan standaardwaarden? In de behoorlijk verdraaide prioriteitstabel van Maslow voor variabelen in Ansible zijn de rolstandaardwaarden de minst prioritaire (behalve de commandoregelparameters van Ansible). Dit betekent dat als je standaardwaarden moet geven zonder je zorgen te maken dat ze de waarden uit de inventory of groepsvariabelen overschrijven, de standaardwaarden van de rol de enige juiste plek voor je zijn. (Ik lieg een beetje - er is ook nog |d(your_default_here), maar als we het hebben over vaste plekken - dan zijn het alleen de standaardwaarden van de rollen).
Wat is er nog meer goed aan rollen? Ze hebben hun eigen mappen. Dit zijn mappen voor variabelen, zowel statische (d.w.z. berekend voor de rol) als dynamische (er is een of andere patroon, of anti-patroon - include_vars samen met {{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml.). Dit zijn mappen voor files/, templates/. Bovendien stelt het je in staat om je eigen modules en plugins te hebben (library/). Maar in vergelijking met taken in playbooks (waar ook al deze dingen mogelijk zijn), is het voordeel hier alleen dat de bestanden niet in één grote hoop liggen, maar in een paar gescheiden stapels.
Een ander detail: je kunt proberen rollen te maken die beschikbaar zijn voor hergebruik (via galaxy). Na de introductie van collecties kan de verspreiding van rollen als bijna vergeten worden beschouwd.
Dus hebben rollen twee belangrijke kenmerken: ze hebben standaardwaarden (een unieke eigenschap) en ze stellen je in staat om de code te structureren.
Terugkomend op de oorspronkelijke vraag: wanneer taken doen en wanneer rollen gebruiken? Taken in een playbook worden meestal gebruikt als 'lijm' voor of na rollen, of als een zelfstandig bouwsteen (dan mogen er geen rollen in de code staan). Een hoop normale taken in combinatie met rollen is absoluut slordig. Je moet je aan een specifieke stijl houden - of taken of rollen. Rollen zorgen voor scheiding van entiteiten en standaardwaarden, taken zorgen ervoor dat je de code sneller kunt lezen. Gewoonlijk worden in rollen meer 'statische' (belangrijke en complexe) codeelementen geplaatst, terwijl in de stijl van taken ondersteunende scripts worden geschreven.
Er is de mogelijkheid om import_role als taak te maken, maar als je dat schrijft, wees dan bereid om jezelf uit te leggen waarom je dat zou willen doen.
Een kritische lezer kan zeggen dat rollen rollen kunnen importeren, dat rollen afhankelijkheden kunnen hebben via galaxy.yml, en er is ook het gruwelijke en vreselijke include_role — ik herinner je eraan dat we onze vaardigheden in basis Ansible verbeteren, en niet in ritmische gymnastiek.
Handlers en taken
Laten we nog iets voor de hand liggends bespreken: handlers. De vaardigheid om ze correct te gebruiken is bijna een kunst. Wat is het verschil tussen een handler en een taak?
Omdat we de basisherinneringen opfrissen, hier is een voorbeeld:
- hosts: group1
taken:
- foo:
notify: handler1
handlers:
- naam: handler1
bar:In een rol zitten de handlers in rolename/handlers/main.yaml. Handlers worden gedeeld tussen alle deelnemers van de play: pre/post_tasks kunnen de handlers van de rol aanroepen, en de rol kan handlers uit de play aanroepen. Echter, "cross-role" aanroepen van handlers veroorzaken veel meer verwarring dan het herhalen van een triviale handler. (Een ander element van best practices is proberen geen dubbele namen voor handlers te maken).
Het belangrijkste verschil is dat een taak altijd (bijna altijd met uitzondering van tags en when), idempotent wordt uitgevoerd, terwijl de handler wordt uitgevoerd op basis van statuswijzigingen (notify wordt alleen geactiveerd als er iets is veranderd). Wat houdt dat in? Bijvoorbeeld, dat bij een herhaalde uitvoering, als er geen wijzigingen zijn geweest, er ook geen handler zal zijn. Maar waarom kan het zo zijn dat we een handler moeten uitvoeren, terwijl er geen wijzigingen waren bij de veroorzakende taak? Bijvoorbeeld omdat er iets kapot was en er wel wijzigingen waren, maar de uitvoering kwam niet bij de handler. Bijvoorbeeld omdat het netwerk tijdelijk uit viel. De configuratie is veranderd, de service is niet herstart. Bij de volgende uitvoering verandert de configuratie al niet meer, en blijft de service met de oude versie van de configuratie.
De situatie met de configuratie is niet oplosbaar (je kunt een speciaal protocol voor herstarten uitvinden met bestandsvlaggen enzovoort, maar dat is al niet meer ‘basic ansible’ in welke zin dan ook). Er is echter een ander veel voorkomend verhaal: we hebben een applicatie geïnstalleerd, hebben deze .service-bestand, en nu willen we deze daemon_reload en state=started. En een natuurlijke plek hiervoor lijkt de handler. Maar als je het geen handler maar een taak aan het einde van de takenlijst of rol maakt, zal het idempotent worden uitgevoerd elke keer. Zelfs als de playbook halverwege faalt. Dit lost het probleem van restarted absoluut niet op (je kunt geen taak met de eigenschap restarted doen, omdat de idempotentie verloren gaat), maar het is zeker beter om state=started te doen, omdat de algehele stabiliteit van playbooks toeneemt, omdat het aantal afhankelijkheden en dynamische toestand vermindert.
Een ander positief kenmerk van de handler is dat hij de uitvoer niet vervuilt. Geen wijzigingen - geen overbodige skipped of ok in de uitvoer - gemakkelijker te lezen. Dit is ook een negatief kenmerk - als je een typefout in een lineair uitgevoerde taak vindt bij de eerste uitrol, worden handlers alleen uitgevoerd bij changed, d.w.z. onder bepaalde voorwaarden - heel zelden. Bijvoorbeeld, de eerste keer in vijf jaar. En uiteraard zit daar een typefout in de naam en gaat alles stuk. De tweede keer kan je ze niet opnieuw aanroepen - er is immers geen changed.
Er moet apart gesproken worden over de beschikbaarheid van variabelen. Bijvoorbeeld, als je notify voor een taak met een loop, wat zal er dan in de variabelen staan? Je kunt dat analytisch afleiden, maar het is niet altijd triviaal, vooral als de variabelen uit verschillende bronnen komen.
… Dus handlers zijn veel minder nuttig en veel problematischer dan je zou denken. Als je iets mooi (zonder franje) kunt schrijven zonder handlers, is het beter om het zonder hen te doen. Als het niet mooi lukt - dan is het beter met hen.
De oplettende lezer merkt terecht op dat we niet hebben besproken listen, dat een handler notify voor een andere handler kan aanroepen, dat een handler import_tasks kan bevatten (die include_role met with_items kan doen), dat het systeem van handlers in Ansible Turing-compleet is, en dat handlers van include_role interessant overlappen met handlers van plays, enzovoort - dit zijn allemaal duidelijk geen "basisprincipes".
Maar er is één bepaalde WTF die eigenlijk een feature is, en waar je aan moet denken. Als je een taak uitvoert met delegate_to en het heeft notify, dan wordt de bijbehorende handler uitgevoerd zonder delegate_to, dat wil zeggen, op de host waarop de play is toegewezen. (Hoewel een handler uiteraard ook kan hebben delegate_to ook).
Ik wil apart een paar woorden zeggen over herbruikbare rollen. Tot de komst van collecties was er het idee dat je universele rollen kon maken die je kon ansible-galaxy install en verder gegaan. Werkt op alle besturingssystemen in alle varianten in alle situaties. Dus, mijn mening: dit werkt niet. Elke rol met een massale include_vars, ondersteuning van 100500 gevallen is gedoemd om naar de afgronden van corner case bugs te gaan. Je kunt ze verhelpen met massaal testen, maar zoals met elke test, of je hebt het cartesiaanse product van invoerwaarden en een totale functie, of je hebt "bepaalde scenario's gedekt". Mijn mening is — het is veel beter als de rol lineair is (cyclomatische complexiteit 1).
Hoe minder if's (expliciet of declaratief — in de vorm van when of vorm include_vars per set variabelen), hoe beter de rol. Soms moet je takken maken, maar nogmaals, hoe minder, hoe beter. Dus het lijkt alsof een goede rol met galaxy (werkt immers!) met een hoop when minder wenselijk kan zijn dan "je eigen" rol van vijf taken. Het moment waarop een rol met galaxy beter is — als je iets begint te schrijven. Het moment waarop het slechter wordt — als er iets kapotgaat, en je vermoedt dat het door de "rol met galaxy" komt. Je opent het, en daar zijn vijf includes, acht taaklijsten en een stapel when‘s... En daar moet je doorheen zien te komen. In plaats van 5 taken in een lineaire lijst, waarin er niets kapot kan gaan.
In de volgende delen
- Een beetje over inventaris, groepsvariabelen, host_group_vars plugin, hostvars. Hoe je spaghetti kunt verbinden tot een Gordiaanse knoop. Scope en prioriteit van variabelen, geheugenmodel van Ansible. "Dus waar moet je de gebruikersnaam voor de database opslaan?"
jinja: {{ jinja }}— nosql notype nosense zachte klei. Het is overal, zelfs daar waar je het niet verwacht. Een beetje over!!unsafeen smakelijke yaml.
Bron: habr.com
