Nozioni di base di Ansible, senza le quali i tuoi playbook saranno un pezzo di pasta appiccicosa

Faccio molte revisioni del codice Ansible di altre persone e scrivo molto anch'io. Nel corso dell'analisi degli errori (sia miei che di altre persone), nonché di una serie di interviste, mi sono reso conto dell'errore principale commesso dagli utenti Ansible: entrano in cose complesse senza padroneggiare quelle di base.

Per correggere questa ingiustizia universale, ho deciso di scrivere un'introduzione ad Ansible per chi già lo conosce. Ti avverto, questa non è una rivisitazione di Mans, è una lettura lunga con molte lettere e nessuna immagine.

Il livello atteso dal lettore è che siano già state scritte diverse migliaia di righe di yamla, qualcosa è già in produzione, ma "in qualche modo tutto è storto".

nomi

L'errore principale commesso da un utente Ansible è non sapere come si chiama qualcosa. Se non conosci i nomi, non puoi capire cosa dice la documentazione. Un esempio vivente: durante un'intervista, una persona che sembrava dire di aver scritto molto in Ansible non ha saputo rispondere alla domanda "in quali elementi è composto un playbook?" E quando ho suggerito che "la risposta era che il playbook consistesse nel gioco", è seguito il commento schiacciante "non lo usiamo". Le persone scrivono Ansible per soldi e non usano il gioco. In realtà lo usano, ma non sanno cosa sia.

Allora cominciamo con una cosa semplice: come si chiama? Forse lo sai, o forse no, perché non hai prestato attenzione quando hai letto la documentazione.

ansible-playbook esegue il playbook. Un playbook è un file con estensione yml/yaml, all'interno del quale c'è qualcosa del genere:

---
- hosts: group1
  roles:
    - role1

- hosts: group2,group3
  tasks:
    - debug:

Ci siamo già resi conto che l'intero file è un playbook. Possiamo mostrare dove sono i ruoli e dove sono i compiti. Ma dov'è il gioco? E qual è la differenza tra gioco e ruolo o playbook?

È tutto nella documentazione. E gli manca. Principianti: perché ce n'è troppo e non ricorderai tutto in una volta. Esperto - perché "cose ​​banali". Se hai esperienza, rileggi queste pagine almeno una volta ogni sei mesi e il tuo codice diventerà leader della categoria.

Quindi, ricorda: Playbook è un elenco composto da play e import_playbook.
Questa è una commedia:

- hosts: group1
  roles:
    - role1

e anche questa è un'altra commedia:

- hosts: group2,group3
  tasks:
    - debug:

Cos'è il gioco? Perché è lei?

Il gioco è un elemento chiave per un playbook, perché play e only play associa un elenco di ruoli e/o attività a un elenco di host su cui devono essere eseguiti. Nelle profondità della documentazione puoi trovare menzione di delegate_to, plug-in di ricerca locale, impostazioni specifiche del client di rete, host jump, ecc. Ti consentono di modificare leggermente il luogo in cui vengono eseguite le attività. Ma dimenticatelo. Ognuna di queste opzioni intelligenti ha usi molto specifici e sicuramente non sono universali. E parliamo di cose basilari che tutti dovrebbero conoscere e utilizzare.

Se vuoi eseguire “qualcosa” “da qualche parte”, scrivi spettacolo. Non un ruolo. Non un ruolo con moduli e delegati. Lo prendi e scrivi la commedia. In cui, nel campo host elenchi dove eseguire e in ruoli/attività cosa eseguire.

Semplice, vero? Come potrebbe essere altrimenti?

Uno dei momenti caratteristici in cui le persone hanno il desiderio di farlo non attraverso il gioco è il “ruolo che imposta tutto”. Vorrei avere un ruolo che configuri sia i server del primo tipo che i server del secondo tipo.

Un esempio archetipico è il monitoraggio. Vorrei avere un ruolo di monitoraggio che configurerà il monitoraggio. Il ruolo di monitoraggio è assegnato agli host di monitoraggio (in base al gioco). Ma si scopre che per il monitoraggio dobbiamo consegnare i pacchetti agli host che stiamo monitorando. Perché non utilizzare il delegato? È inoltre necessario configurare iptables. delegare? È inoltre necessario scrivere/correggere una configurazione per il DBMS per abilitare il monitoraggio. delegare! E se manca la creatività, allora puoi fare una delega include_role in un ciclo nidificato utilizzando un filtro complicato su un elenco di gruppi e all'interno include_role puoi fare di più delegate_to Ancora. E andiamo via...

Un buon augurio – avere un unico ruolo di controllo, che “fa tutto” – ci porta nell'inferno più completo da cui il più delle volte c'è solo una via d'uscita: riscrivere tutto da zero.

Dove è successo l'errore qui? Nel momento in cui hai scoperto che per fare il compito "x" sull'host X dovevi andare sull'host Y e fare "y" lì, dovevi fare un semplice esercizio: andare a scrivere play, che sull'host Y fa y. Non aggiungere qualcosa a "x", ma scrivilo da zero. Anche con variabili hardcoded.

Sembra che tutto nei paragrafi precedenti sia detto correttamente. Ma questo non è il tuo caso! Perché vuoi scrivere codice riutilizzabile che sia DRY e simile a una libreria e devi cercare un metodo su come farlo.

È qui che si nasconde un altro grave errore. Un errore che ha trasformato molti progetti da scritti in modo accettabile (potrebbe essere migliore, ma tutto funziona ed è facile da finire) in un completo horror che nemmeno l'autore riesce a capire. Funziona, ma Dio ti proibisce di cambiare qualcosa.

L'errore è: role è una funzione di libreria. Questa analogia ha rovinato così tanti buoni inizi che è semplicemente triste da guardare. Il ruolo non è una funzione della libreria. Non può fare calcoli e non può prendere decisioni a livello di gioco. Ricordami quali decisioni prende il gioco?

Grazie, hai ragione. Il gioco prende una decisione (più precisamente, contiene informazioni) su quali attività e ruoli eseguire su quali host.

Se deleghi questa decisione a un ruolo, e anche con i calcoli, condanni te stesso (e colui che proverà ad analizzare il tuo codice) a un'esistenza miserabile. Il ruolo non decide dove verrà eseguito. Questa decisione viene presa giocando. Il ruolo fa quello che gli viene detto, dove viene detto.

Perché è pericoloso programmare in Ansible e perché COBOL è migliore di Ansible ne parleremo nel capitolo sulle variabili e sui jinja. Per ora, diciamo una cosa: ciascuno dei tuoi calcoli lascia una traccia indelebile dei cambiamenti nelle variabili globali e non puoi farci nulla. Non appena le due “tracce” si sono incrociate, tutto è scomparso.

Nota per gli schizzinosi: il ruolo può sicuramente influenzare il flusso di controllo. Mangiare delegate_to e ha usi ragionevoli. Mangiare meta: end host/play. Ma! Ricordi che insegniamo le basi? Dimenticato di delegate_to. Stiamo parlando del codice Ansible più semplice e bello. Che è facile da leggere, facile da scrivere, facile da eseguire il debug, facile da testare e facile da completare. Quindi, ancora una volta:

play e solo play decide su quali host cosa viene eseguito.

In questa sezione ci siamo occupati dell’opposizione tra gioco e ruolo. Ora parliamo della relazione tra compiti e ruoli.

Compiti e ruoli

Considera il gioco:

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

Diciamo che devi fare foo. E sembra foo: name=foobar state=present. Dove dovrei scrivere questo? nel pre? inviare? Creare un ruolo?

...E dove sono finiti i compiti?

Ricominciamo dalle basi: il dispositivo di gioco. Se ti soffermi su questo argomento, non potrai usare il gioco come base per tutto il resto, e il tuo risultato sarà "traballante".

Dispositivo di gioco: direttiva host, impostazioni per il gioco stesso e sezioni pre_tasks, attività, ruoli, post_tasks. I restanti parametri di gioco non sono importanti per noi adesso.

L'ordine delle loro sezioni con compiti e ruoli: pre_tasks, roles, tasks, post_tasks. Poiché semanticamente l'ordine di esecuzione è compreso tra tasks и roles non è chiaro, le migliori pratiche dicono che stiamo aggiungendo una sezione tasks, solo in caso contrario roles. Se c'è roles, quindi tutte le attività allegate vengono inserite in sezioni pre_tasks/post_tasks.

Non resta che che tutto sia semanticamente chiaro: primo pre_tasks, quindi roles, quindi post_tasks.

Ma non abbiamo ancora risposto alla domanda: dov’è la chiamata del modulo? foo scrivere? Dobbiamo scrivere un intero ruolo per ogni modulo? O è meglio avere un ruolo di spessore per tutto? E se non un ruolo, dove dovrei scrivere: in pre o post?

Se non c'è una risposta ragionata a queste domande, allora questo è un segno di mancanza di intuizione, cioè di quelle stesse "fondamenta traballanti". Scopriamolo. Innanzitutto, una domanda di sicurezza: se il gioco ha pre_tasks и post_tasks (e non ci sono compiti o ruoli), allora qualcosa può rompersi se eseguo il primo compito da post_tasks Lo sposto fino alla fine pre_tasks?

Naturalmente, la formulazione della domanda suggerisce che si romperà. Ma cosa esattamente?

... Gestori. La lettura delle nozioni di base rivela un fatto importante: tutti i gestori vengono svuotati automaticamente dopo ogni sezione. Quelli. tutte le attività da pre_tasks, quindi tutti i gestori che sono stati avvisati. Quindi vengono eseguiti tutti i ruoli e tutti i gestori notificati nei ruoli. Dopo post_tasks e i loro gestori.

Pertanto, se trascini un'attività da post_tasks в pre_tasks, quindi potenzialmente lo eseguirai prima che venga eseguito il gestore. ad esempio, se in pre_tasks il server web è installato e configurato e post_tasks gli viene inviato qualcosa, quindi trasferisci questa attività nella sezione pre_tasks porterà al fatto che al momento dell'invio il server non sarà ancora in esecuzione e tutto si romperà.

Ora pensiamo di nuovo, perché ne abbiamo bisogno pre_tasks и post_tasks? Ad esempio, per espletare tutto il necessario (compresi i referenti) prima di ricoprire il ruolo. UN post_tasks ci consentirà di lavorare con i risultati dell'esecuzione dei ruoli (inclusi i gestori).

Un astuto esperto di Ansible ci dirà di cosa si tratta. meta: flush_handlers, ma perché abbiamo bisogno di flush_handlers se possiamo fare affidamento sull'ordine di esecuzione delle sezioni in gioco? Inoltre, l'uso di meta: flush_handlers può darci cose inaspettate con gestori duplicati, dandoci strani avvisi quando usato when у block eccetera. Quanto meglio conosci l'ansible, tante più sfumature puoi nominare per una soluzione "complicata". E una soluzione semplice – utilizzando una divisione naturale tra pre/ruoli/post – non provoca sfumature.

E torniamo al nostro "foo". Dove dovrei metterlo? In pre, post o ruoli? Ovviamente, questo dipende dalla necessità o meno dei risultati del gestore foo. Se non sono presenti, allora foo non ha bisogno di essere inserito né in pre né in post - queste sezioni hanno un significato speciale - eseguendo attività prima e dopo il corpo principale del codice.

Ora la risposta alla domanda "ruolo o compito" si riduce a ciò che è già in gioco: se ci sono compiti lì, è necessario aggiungerli alle attività. Se sono presenti ruoli, è necessario creare un ruolo (anche da un'attività). Permettimi di ricordarti che compiti e ruoli non vengono utilizzati contemporaneamente.

Comprendere le basi di Ansible fornisce risposte ragionevoli a domande apparentemente di gusto.

Compiti e ruoli (seconda parte)

Ora discutiamo della situazione in cui stai appena iniziando a scrivere un playbook. Devi fare foo, bar e baz. Questi tre compiti sono un ruolo o tre ruoli? Per riassumere la domanda: a che punto dovresti iniziare a scrivere ruoli? Che senso ha scrivere ruoli quando puoi scrivere compiti?... Cos'è un ruolo?

Uno degli errori più grandi (ne ho già parlato) è pensare che un ruolo sia come una funzione nella libreria di un programma. Che aspetto ha una descrizione generica della funzione? Accetta argomenti di input, interagisce con cause collaterali, produce effetti collaterali e restituisce un valore.

Ora, attenzione. Cosa si può fare da questo nel ruolo? Sei sempre il benvenuto a chiamare effetti collaterali, questa è l'essenza dell'intero Ansible: creare effetti collaterali. Hanno cause collaterali? Elementare. Ma con “passa un valore e restituiscilo” – è qui che non funziona. Innanzitutto, non puoi passare un valore a un ruolo. Puoi impostare una variabile globale con una durata di gioco nella sezione vars per il ruolo. Puoi impostare una variabile globale con una vita in gioco all'interno del ruolo. O anche con la vita dei playbook (set_fact/register). Ma non puoi avere "variabili locali". Non puoi "prendere un valore" e "restituirlo".

La cosa principale ne consegue: non puoi scrivere qualcosa in Ansible senza causare effetti collaterali. La modifica delle variabili globali è sempre un effetto collaterale per una funzione. In Rust, ad esempio, modificare una variabile globale è unsafe. E in Ansible è l’unico metodo per influenzare i valori di un ruolo. Da notare le parole usate: non “passare un valore al ruolo”, ma “cambiare i valori che il ruolo utilizza”. Non c’è isolamento tra i ruoli. Non c’è isolamento tra compiti e ruoli.

Totale: un ruolo non è una funzione.

Cosa c'è di buono nel ruolo? Innanzitutto, il ruolo ha valori predefiniti (/default/main.yaml), in secondo luogo, il ruolo dispone di directory aggiuntive per l'archiviazione dei file.

Quali sono i vantaggi dei valori predefiniti? Perché nella piramide di Maslow, la tabella piuttosto distorta delle priorità variabili di Ansible, i ruoli predefiniti hanno la priorità più bassa (meno i parametri della riga di comando di Ansible). Ciò significa che se devi fornire valori predefiniti e non preoccuparti che sovrascrivano i valori dell'inventario o delle variabili di gruppo, allora i valori predefiniti dei ruoli sono l'unico posto giusto per te. (Sto mentendo un po', ce ne sono altri |d(your_default_here), ma se parliamo di luoghi stazionari, allora viene impostato solo il ruolo).

Cos'altro c'è di bello nei ruoli? Perché hanno i loro cataloghi. Si tratta di directory per variabili, sia costanti (cioè calcolate per il ruolo) che dinamiche (esiste un pattern o un anti-pattern - include_vars con {{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml.). Queste sono le directory per files/, templates/. Inoltre, ti consente di avere i tuoi moduli e plugin (library/). Ma, rispetto alle attività in un playbook (che può anche avere tutto questo), l'unico vantaggio qui è che i file non vengono scaricati in una pila, ma in diverse pile separate.

Un ulteriore dettaglio: puoi provare a creare ruoli che saranno disponibili per il riutilizzo (tramite galaxy). Con l'avvento delle collezioni, la distribuzione dei ruoli può essere considerata quasi dimenticata.

Pertanto, i ruoli hanno due caratteristiche importanti: hanno valori predefiniti (una caratteristica unica) e ti consentono di strutturare il tuo codice.

Ritornando alla domanda iniziale: quando svolgere compiti e quando svolgere ruoli? Le attività in un playbook vengono spesso utilizzate come "collante" prima/dopo i ruoli o come elemento di costruzione indipendente (quindi non dovrebbero esserci ruoli nel codice). Un mucchio di compiti normali mescolati a ruoli è una negligenza inequivocabile. Dovresti aderire a uno stile specifico: un compito o un ruolo. I ruoli forniscono la separazione delle entità e dei valori predefiniti, le attività consentono di leggere il codice più velocemente. Di solito, nei ruoli viene inserito codice più “stazionario” (importante e complesso) e gli script ausiliari vengono scritti in stile attività.

È possibile eseguire import_role come attività, ma se scrivi questo, preparati a spiegare al tuo senso della bellezza perché vuoi farlo.

Un lettore attento potrebbe dire che i ruoli possono importare ruoli, i ruoli possono avere dipendenze tramite galaxy.yml, e c'è anche un terribile e terribile include_role — Ti ricordo che stiamo migliorando le competenze nell'Ansible di base e non nella ginnastica artistica.

Gestori e compiti

Parliamo di un'altra cosa ovvia: gli handler. Saperli utilizzare correttamente è quasi un'arte. Qual è la differenza tra un handler e un drag?

Poiché stiamo ricordando le nozioni di base, ecco un esempio:

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

I gestori del ruolo si trovano in rolename/handlers/main.yaml. Gli handler frugano tra tutti i partecipanti al gioco: pre/post_tasks possono estrarre gestori di ruolo e un ruolo può estrarre handler dal gioco. Tuttavia, le chiamate "con ruoli incrociati" ai gestori causano molto più peso rispetto alla ripetizione di un gestore banale. (Un altro elemento delle migliori pratiche è cercare di non ripetere i nomi dei gestori).

La differenza principale è che l'attività viene sempre eseguita (idempotentmente) (tag più/meno e when) e il gestore - in base al cambiamento di stato (notifica viene attivato solo se è stato modificato). Cosa significa questo? Ad esempio, il fatto che al riavvio, se non sono state apportate modifiche, non ci sarà alcun gestore. Perché potrebbe essere necessario eseguire il gestore quando non è stata apportata alcuna modifica all'attività di generazione? Ad esempio, perché qualcosa si è rotto e cambiato, ma l'esecuzione non ha raggiunto il conduttore. Ad esempio, perché la rete era temporaneamente inattiva. La configurazione è cambiata, il servizio non è stato riavviato. La prossima volta che lo avvii, la configurazione non cambia più e il servizio rimane con la vecchia versione della configurazione.

La situazione con la configurazione non può essere risolta (più precisamente, puoi inventarti uno speciale protocollo di riavvio con flag di file, ecc., Ma questo non è più "ansible di base" in nessuna forma). Ma c'è un'altra storia comune: abbiamo installato l'applicazione, l'abbiamo registrata .service-file, e ora lo vogliamo daemon_reload и state=started. E il luogo naturale per questo sembra essere l'handler. Ma se lo rendi non un gestore ma un'attività alla fine di un elenco di attività o di un ruolo, verrà eseguito ogni volta in modo idempotente. Anche se il playbook si è rotto a metà. Questo non risolve affatto il problema riavviato (non puoi eseguire un'attività con l'attributo restarted, perché l'idempotenza viene persa), ma vale sicuramente la pena fare state=started, la stabilità complessiva dei playbook aumenta, perché il numero di connessioni e lo stato dinamico diminuiscono.

Un'altra proprietà positiva del gestore è che non intasa l'output. Non sono state apportate modifiche, nessun extra saltato o ok nell'output, più facile da leggere. È anche una proprietà negativa: se trovi un errore di battitura in un'attività eseguita linearmente alla prima esecuzione, i gestori verranno eseguiti solo quando modificati, ad es. in alcune condizioni - molto raramente. Ad esempio, per la prima volta nella mia vita cinque anni dopo. E, naturalmente, ci sarà un errore di battitura nel nome e tutto si romperà. E se non li esegui la seconda volta, non ci sono cambiamenti.

Separatamente, dobbiamo parlare della disponibilità delle variabili. Ad esempio, se notifichi un'attività con un ciclo, cosa conterrà le variabili? Puoi indovinarlo analiticamente, ma non è sempre banale, soprattutto se le variabili provengono da luoghi diversi.

... Quindi gli handler sono molto meno utili e molto più problematici di quanto sembri. Se puoi scrivere qualcosa di bello (senza fronzoli) senza gestori, è meglio farlo senza di loro. Se non funziona magnificamente, è meglio con loro.

Il lettore corrosivo fa giustamente notare che non abbiamo discusso listenche un gestore può chiamare notify per un altro gestore, che un gestore può includere import_tasks (che può fare include_role con with_items), che il sistema di gestione in Ansible è Turing-complete, che i gestori di include_role si intersecano in modo curioso con i gestori di play, ecc. .d. - tutto questo chiaramente non è le “basi”).

Sebbene esista un WTF specifico che in realtà è una caratteristica che devi tenere a mente. Se la tua attività viene eseguita con delegate_to e ha ricevuto notifica, il gestore corrispondente viene eseguito senza delegate_to, cioè. sull'host a cui è assegnata la riproduzione. (Anche se il conduttore, ovviamente, potrebbe averlo fatto delegate_to anche).

Separatamente, voglio dire alcune parole sui ruoli riutilizzabili. Prima che apparissero le collezioni, c'era l'idea che si potessero creare ruoli universali che potevano essere ansible-galaxy install e andò. Funziona su tutti i sistemi operativi di tutte le varianti in tutte le situazioni. Quindi, la mia opinione: non funziona. Qualsiasi ruolo con massa include_vars, che supporta 100500 casi, è condannato all'abisso di bug dei casi limite. Possono essere coperti con test di massa, ma come con qualsiasi test, o si ha un prodotto cartesiano di valori di input e una funzione totale, oppure si hanno “singoli scenari coperti”. La mia opinione è che sia molto meglio se il ruolo è lineare (complessità ciclomatica 1).

Meno se (espliciti o dichiarativi - nella forma when o forma include_vars per insieme di variabili), migliore è il ruolo. A volte bisogna fare dei rami, ma, ripeto, meno ce ne sono e meglio è. Quindi sembra un buon ruolo con Galaxy (funziona!) con un sacco di when potrebbe essere meno preferibile del “proprio” ruolo tra cinque compiti. Il momento in cui il ruolo con Galaxy è migliore è quando inizi a scrivere qualcosa. Il momento in cui le cose peggiorano è quando qualcosa si rompe e hai il sospetto che sia a causa del “ruolo con Galaxy”. Lo apri e ci sono cinque inclusioni, otto fogli di attività e una pila when'ov... E dobbiamo capirlo. Invece di 5 compiti, un elenco lineare in cui non c'è nulla da interrompere.

Nelle parti seguenti

  • Un po' di inventario, variabili di gruppo, plugin host_group_vars, hostvars. Come fare il nodo gordiano con gli spaghetti. Variabili di ambito e precedenza, modello di memoria Ansible. "Allora dove memorizziamo il nome utente per il database?"
  • jinja: {{ jinja }} - nosql notype nosense plastilina morbida. È ovunque, anche dove non te lo aspetti. Un po' circa !!unsafe e delizioso igname.

Fonte: habr.com

Aggiungi un commento