Le basi di Ansible, senza le quali i vostri playbook saranno un groviglio di spaghetti appiccicati

Faccio molte revisioni del codice di altri su Ansible e scrivo molto. Analizzando gli errori (sia degli altri che miei) e attraverso un certo numero di colloqui, ho capito un errore fondamentale che commettono gli utenti di Ansible: si avventurano in cose complesse senza aver prima padroneggiato le basi.

Per correggere questa ingiustizia universale, ho deciso di scrivere un'introduzione ad Ansible per coloro che lo conoscono già. Avviso, non è un riassunto delle guide, è un lungo articolo con molte parole e senza immagini.

Il livello previsto del lettore è: sono già state scritte alcune migliaia di righe di YAML, sono già operativi, ma "ah, sembra tutto un po' storto".

Nomi

L'errore principale dell'utente di Ansible è non sapere come si chiamano le cose. Se non conosci i nomi, non puoi comprendere ciò che è scritto nella documentazione. Un esempio vivo: durante un colloquio, una persona, che sembrava aver scritto molto su Ansible, non è riuscita a rispondere alla domanda "di quali elementi si compone un playbook?". E quando ho suggerito che "la risposta attesa era che un playbook è composto da play", ha seguito un commento fatale "noi non usiamo questa cosa". Le persone scrivono su Ansible per denaro e non usano i play. In realtà li usano, ma non sanno cosa siano.

Quindi iniziamo con qualcosa di semplice: come si chiamano le cose. Forse lo sapete già, oppure no, perché non ci avete prestato attenzione mentre leggevate 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:

Abbiamo già capito che l'intero file è il playbook. Possiamo mostrare dove ci sono i ruoli (roles) e dove ci sono i task (tasks). Ma dove sono i play? E in cosa si differenzia un play da un role o da un playbook?

Tutto questo è presente nella documentazione. E viene ignorato. I principianti, perché ci sono troppe informazioni e non riesci a ricordarle tutte. Gli esperti, perché si tratta di "cose banali". Se sei esperto, leggi di nuovo queste pagine almeno una volta ogni sei mesi e il tuo codice diventerà un classico migliore.

Quindi, memorizza: il Playbook è un elenco composto da play e import_playbook.
Questa è una play:

- hosts: group1
  roles:
    - role1

E anche questa è un'altra play:

- hosts: group2,group3
  tasks:
    - debug:

Cos'è allora una play? A cosa serve?

Play è un elemento chiave per il playbook, perché il play, e solo il play, collega l'elenco dei ruoli e/o delle attività con l'elenco degli host sui quali devono essere eseguiti. Nelle profondità della documentazione si può trovare un riferimento a delegate_to, plugin di lookup locali, impostazioni specifiche per network-cli, jump host, ecc. Questi permettono di modificare leggermente il luogo di esecuzione delle attività. Ma dimenticateli. Ognuna di queste opzioni astute ha applicazioni molto specifiche e non sono affatto universali. Noi parliamo di cose fondamentali che tutti dovrebbero conoscere e utilizzare.

Se vuoi eseguire "qualcosa" "da qualche parte" — scrivi un play. Non un ruolo. Non un ruolo con moduli e delegati. Prendi e scrivi un play. In cui, nel campo hosts, elenchi dove eseguire, e in roles/tasks — cosa eseguire.

Facile, giusto? E come potrebbe essere altrimenti?

Uno dei momenti caratteristici in cui le persone desiderano farlo in modo diverso dal play è "un ruolo che configura tutto". Si desidera avere un ruolo che configuri e server il primo tipo e server del secondo tipo.

Un esempio archetipico è il monitoraggio. Si desidera avere un ruolo di monitoring che configuri il monitoraggio. Il ruolo di monitoring viene assegnato agli host di monitoraggio (nella relativa play). Ma, si scopre che per monitorare dobbiamo installare pacchetti sugli host che monitoriamo. Perché non usare un delegate? E deve anche configurare iptables. Delegate? E bisogna scrivere/modificare la configurazione per il DBMS, affinché il monitoraggio possa avviarsi. Delegate! E se la creatività è esplosa, si può fare delega include_role in un ciclo interno con un filtro astuto sulla lista dei gruppi, e all'interno include_role si possono ancora fare delegate_to di nuovo. E si parte...

Il buon proposito — avere un'unica e sola ruolo di monitoring che "fa tutto" — ci conduce in un inferno da cui la maggior parte delle volte c'è solo un'uscita: riscrivere tutto da zero.

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

Sembra che nei paragrafi sopra sia tutto corretto. Ma questo non è il tuo caso! Perché vuoi scrivere codice riutilizzabile, che sia DRY e somigli a una libreria, e devi cercare il metodo per farlo.

Qui si nasconde un altro errore grave. Un errore che ha trasformato molti progetti da scritti in modo accettabile (si può fare meglio, ma funziona e è facile da completare) in un terribile disastro, in cui persino l'autore non riesce a districarsi. Funziona, ma Dio non voglia che cambi qualcosa.

Questo errore suona così: un ruolo è una funzione di libreria. Questa analogia ha distrutto così tante buone iniziative che è solo triste da osservare. Un ruolo non è una funzione di libreria. Non può effettuare calcoli e non può prendere decisioni a livello di play. Mi ricordate quali decisioni prende il play?

Grazie, hai ragione. Il play prende decisioni (piuttosto, contiene informazioni) su quali task e ruoli eseguire su quali host.

Se deleghi questa decisione al ruolo, e ci metti anche dei calcoli, ti condanni (e chiunque cerchi di decifrare il tuo codice) a un'esistenza misera. Il ruolo non decide dove deve essere eseguito. Questa decisione la prende il play. Il ruolo esegue ciò che gli è stato detto, nel luogo in cui gli è stato detto.

Perché programmare in Ansible sia pericoloso e perché COBOL sia migliore di Ansible ne parleremo nel capitolo sulle variabili e jinja. Per ora diciamo solo una cosa: ogni tuo calcolo lascia una traccia indelebile di modifica delle variabili globali, e tu non puoi farci niente. Non appena due "tracce" si incrociano, è finita.

Nota per i pignoli: il ruolo può certamente influenzare il controllo del flusso. Ci sono delegate_to e ha applicazioni ragionevoli. Ci sono meta: end host/play. Ma! Ricorda, stiamo insegnando le basi? Dimenticati di delegate_to. Stiamo parlando del codice più semplice e più bello in Ansible. Che è facile da leggere, scrivere, debuggare, testare e completare. Quindi, ancora una volta:

solo il play decide su quali host ciò viene eseguito.

In questa sezione abbiamo esaminato il contrasto tra play e ruolo. Ora parleremo delle relazioni tra task e ruolo.

Task e Ruoli

Esaminiamo il play:

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

Supponiamo che tu debba fare foo. E appare così foo: name=foobar state=present. Dove dovresti scriverlo? in pre? post? Creare un ruolo?

… E dove sono finiti i task?

Iniziamo di nuovo dalle basi: la struttura del play. Se sei confuso su questo punto, non puoi usare il play come base per tutto il resto, e il tuo risultato risulterà "instabile".

Dispositivo play: direttiva hosts, impostazioni del play stesso e delle sezioni pre_tasks, tasks, roles, post_tasks. Gli altri parametri per il play non ci interessano al momento.

Ordine delle loro sezioni con i task e i ruoli: pre_tasks, roles, tasks, post_tasks. Poiché l'ordine semantico tra tasks e roles non è chiaro, le best practices dicono che dobbiamo aggiungere una sezione tasks, solo se non ci sono roles. Se ci sono roles, allora tutti i task allegati vengono posizionati nelle sezioni pre_tasks/post_tasks.

Rimane solo ciò che è semanticamente chiaro: prima pre_tasks, poi roles, poi post_tasks.

Ma non abbiamo ancora risposto alla domanda: dove dobbiamo scrivere il richiamo del modulo foo ? Dobbiamo scrivere un ruolo intero per ogni modulo? O è meglio avere un ruolo ampio per tutto? E se non è un ruolo, dove scrivere — in pre o in post?

Se a queste domande non c'è una risposta argomentata, questo è un segnale di mancanza di intuizione, cioè quelle "basi instabili". Cerchiamo di capirlo. Prima di tutto, una domanda di controllo: Se il play ha pre_tasks e post_tasks (e non ci sono né tasks né roles), potrebbe rompersi qualcosa se porto il primo task da post_tasks alla fine pre_tasks?

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

… I gestori. La lettura delle basi rivela un fatto importante: tutti i gestori vengono flushati automaticamente dopo ogni sezione. Cioè, vengono eseguiti tutti i task da pre_tasks, poi tutti i gestori che sono stati notificati. Poi vengono eseguiti tutti i ruoli e tutti i gestori che sono stati notificati nei ruoli. Poi post_tasks e i loro gestori.

Così, se trascini un task da post_tasks in pre_tasks, potenzialmente lo esegui prima di eseguire il gestore. Ad esempio, se nel pre_tasks si installa e configura server web, e in post_tasks qualcosa viene inviato lì, allora spostare questo task nella sezione pre_tasks porterà al fatto che al momento dell'"invio" il server non sarà ancora avviato e tutto si romperà.

E ora riflettiamo ancora una volta: perché abbiamo bisogno di pre_tasks e post_tasks? Например, для того, чтобы выполнить всё нужное (включая хэндлеры) до выполнения роли. А post_tasks ci permetterà di lavorare con i risultati dell'esecuzione dei ruoli (inclusi i gestori).

Un esperto di Ansible dirà che c'è meta: flush_handlers, ma perché abbiamo bisogno di flush_handlers, se possiamo contare sull'ordine di esecuzione delle sezioni nel play? Inoltre, l'uso di meta: flush_handlers può causarci sorprese con gestori ripetuti, generare avvertimenti insoliti in caso di utilizzo di when a block ecc. Più conosci bene Ansible, più sfumature potrai nominare per una soluzione "astuta". E la soluzione semplice — usare una divisione naturale tra pre/roles/post — non provoca sfumature.

E torniamo al nostro 'foo'. Dove posizionarlo? In pre, post o in ruoli? Ovviamente, dipende se abbiamo bisogno dei risultati del lavoro del gestore per foo. Se non ci sono, non è necessario inserire foo né in pre né in post: queste sezioni hanno un significato speciale - eseguire compiti prima e dopo l'array di codice principale.

Ora la risposta alla domanda "ruolo o compito" si riduce a ciò che è già presente nel play: se ci sono compiti, bisogna scrivere in tasks. Se ci sono ruoli, bisogna creare un ruolo (anche se da un solo compito). Ricordo che tasks e roles non vengono utilizzati contemporaneamente.

Comprendere le basi di Ansible fornisce risposte fondate a quelli che sembrerebbero essere quesiti soggettivi.

Task e ruoli (parte seconda)

Ora discutiamo della situazione in cui stai appena iniziando a scrivere un playbook. Devi fare foo, bar e baz. Sono tre compiti, un ruolo o tre ruoli? Generalizzando la domanda: quando dovresti iniziare a scrivere ruoli? Qual è il senso di scrivere ruoli quando puoi scrivere compiti?… E cos'è un ruolo?

Uno degli errori più gravi (ne ho già parlato) è considerare un ruolo come una funzione in una libreria di un programma. Come appare una descrizione generale di una funzione? Riceve argomenti in input, interagisce con cause esterne, produce effetti collaterali e restituisce un valore.

Ora, attenzione. Cosa di tutto ciò può essere fatto in un ruolo? Causare effetti collaterali – sempre, grazie, questo è l'essenza di Ansible – causare effetti collaterali. Avere cause esterne? Elementare. Ma nel "trasmettere un valore e restituirlo" – qui non ci siamo. In primo luogo, non puoi trasmettere un valore a un ruolo. Puoi impostare una variabile globale con durata pari al play nella sezione vars per il ruolo. Puoi impostare una variabile globale con durata nel play all'interno del ruolo. O addirittura con durata del playbook (set_fact/register). Ma non puoi avere "variabili locali". Non puoi "ricevere un valore" e "restituirlo".

Da ciò ne deriva la cosa principale: non puoi scrivere nulla in ansible senza causare effetti collaterali. Modificare variabili globali è sempre un effetto collaterale per una funzione. In Rust, per esempio, modificare una variabile globale è unsafe. E in Ansible – è l'unico modo per influenzare i valori per un ruolo. Presta attenzione alle parole usate: non "trasmettere un valore a un ruolo", ma "modificare i valori utilizzati dal ruolo". Tra i ruoli non c'è isolamento. Tra compiti e ruoli non c'è isolamento.

In totale: un ruolo non è una funzione.

Cosa c'è di buono nei ruoli? Innanzitutto, i ruoli hanno valori predefiniti (/default/main.yaml), in secondo luogo i ruoli hanno directory aggiuntive per l'archiviazione dei file.

Cosa c'è di buono nei valori predefiniti? Il fatto che, nella piuttosto distorta gerarchia delle variabili di Maslow, i valori predefiniti dei ruoli di Ansible sono i meno prioritari (esclusi i parametri della riga di comando di Ansible). Questo significa che se devi fornire valori predefiniti senza preoccuparti che questi sovrascrivano i valori provenienti dall'inventario o dalle variabili di gruppo, i valori predefiniti del ruolo sono l'unico posto giusto per te. (Sto un po' mentendo — ce ne sono anche altri |d(your_default_here), ma se si parla di luoghi stabili — allora solo i valori predefiniti dei ruoli).

Cosa c'è di buono nei ruoli? Il fatto che hanno le proprie directory. Queste sono directory per le variabili, sia costanti (cioè calcolate per il ruolo), sia per quelle dinamiche (c'è un qualche pattern o antipattern — include_vars insieme a {{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml. Queste sono directory per files/, templates/. Inoltre, permettono ai ruoli di avere i loro moduli e plugin (library/). Tuttavia, rispetto ai task nei playbook (che possono includere anche tutto questo), il vantaggio qui è solo che i file sono separati in diverse raccolte, anziché essere accumulati tutti insieme.

Un altro aspetto: puoi cercare di creare ruoli che siano riutilizzabili (attraverso galaxy). Dopo l'introduzione delle collezioni, la diffusione dei ruoli può essere considerata quasi dimenticata.

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

Tornando alla domanda iniziale: quando fare task e quando ruoli? I task nei playbook sono spesso utilizzati come "colla" prima/dopo i ruoli, oppure come elementi costruttivi autonomi (in questo caso non dovrebbero esserci ruoli nel codice). Un insieme di task normali mescolati a ruoli è certamente disordinato. È meglio mantenere uno stile specifico — o solo task, o solo ruoli. I ruoli danno separazione delle entità e valori predefiniti, mentre i task permettono di leggere il codice più velocemente. Di solito, nei ruoli si spostano codici più "stabili" (importanti e complessi), mentre negli stili di task si scrivono script di supporto.

Esiste la possibilità di fare import_role come task, ma se scrivi qualcosa del genere, preparati a dare una spiegazione del tuo gusto estetico sul motivo per cui vuoi farlo.

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

Handler e task

Discutiamo di un'altra cosa ovvia: gli handler. Saperli usare correttamente è quasi un'arte. Qual è la differenza tra un handler e un task?

Poiché stiamo rinfrescando le basi, ecco un esempio:

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

Negli handler di un ruolo si trovano in rolename/handlers/main.yaml. Gli handler possono essere condivisi tra tutti i partecipanti al play: pre/post_tasks possono chiamare gli handler del ruolo e il ruolo può chiamare gli handler dal play. Tuttavia, le chiamate "cross-role" agli handler suscitano molto più wtf rispetto alla ripetizione di un handler banale. (Un altro elemento delle best practices è cercare di non ripetere i nomi degli handler).

La principale differenza è che un task viene eseguito (idempotentemente) sempre (più/meno con tag e when), mentre un handler viene eseguito in base alla modifica dello stato (notify scatta solo se c'è stato un cambiamento). Quali problematiche può comportare? Ad esempio, il fatto che se riavvi il task senza cambiamenti, allora non verrà eseguito l'handler. E perché potrebbe essere necessario eseguire un handler quando non c'è stato cambiato nel task generatore? Ad esempio, perché qualcosa si è rotto e c'è stato un cambiamento, ma l'esecuzione non è arrivata all'handler. Ad esempio, perché la rete è andata temporaneamente giù. La configurazione è cambiata, il servizio non è stato riavviato. Al prossimo avvio, la configurazione non cambia più, e il servizio rimane con la vecchia versione della configurazione.

La situazione con la configurazione non è risolvibile (anzi, potresti inventarti un protocollo speciale di riavvio con flag di file, ecc., ma questo non è più ‘ansible di base’ in alcun caso). Tuttavia, c'è un'altra storia frequente: abbiamo installato un'applicazione, abbiamo registrato il suo .service-file, e ora vogliamo daemon_reload e state=started. E il posto naturale per questo sembra essere un handler. Ma se lo si trasforma da handler in un'attività alla fine della lista delle attività o in un ruolo, verrà eseguito idempotentemente ogni volta. Anche se il playbook si interrompe a metà. Questo non risolve affatto il problema di restarted (non si può creare un'attività con l'attributo restarted, poiché si perde l'idempotenza), ma è sicuramente opportuno impostare state=started, la stabilità complessiva dei playbook aumenta, poiché si riduce il numero di collegamenti e stati dinamici.

Un'altra proprietà positiva dell'handler è che non ingombra l'output. Nessuna modifica — niente skipped o ok in più nell'output — è più facile da leggere. Ma questo è anche un attributo negativo — se trovi un errore in un'attività eseguita linearmente al primo passaggio, gli handler verranno eseguiti solo se c'è stato un cambiamento, cioè in determinate condizioni — molto raramente. Ad esempio, la prima volta in vita tua dopo cinque anni. E, naturalmente, ci sarà un errore nel nome e tutto si romperà. E non sarà possibile eseguirli la seconda volta — non ci sono cambiamenti.

È importante parlare della disponibilità delle variabili. Ad esempio, se si invia una notifica per un'attività con un ciclo, cosa accadrà alle variabili? Si può indovinare analiticamente, ma non è sempre banale, soprattutto se le variabili provengono da posti diversi.

... Quindi gli handler sono molto meno utili e molto più problematici di quanto sembri. Se si può scrivere qualcosa in modo elegante (senza complicazioni) senza handler, è meglio farlo senza di essi. Se non si riesce a farlo in modo elegante — è meglio farlo con loro.

Un lettore attento fa notare giustamente che non abbiamo discusso ascolta, che un handler può chiamare notify per un altro handler, che un handler può includere import_tasks (che può fare include_role con with_items), che il sistema di handler in Ansible è Turing-completo, che gli handler da include_role si sovrappongono in modo curioso con gli handler dal play, ecc. — tutto questo non è chiaramente parte delle "basi".

Tuttavia, c'è un certo WTF che è in realtà una funzionalità, e di cui bisogna tenere conto. Se si esegue un'attività con delegate_to e ha un notify, l'handler corrispondente viene eseguito senza delegate_to, cioè sull'host a cui è assegnato il play. (Anche se l'handler, ovviamente, può avere delegate_to anch'esso).

Voglio anche dire qualche parola sui ruoli riutilizzabili. Prima dell'arrivo delle collezioni, c'era l'idea che fosse possibile creare ruoli universali che possono essere ansible-galaxy install E sono partito. Funziona su tutti i sistemi operativi in tutte le varianti in tutte le situazioni. Quindi, la mia opinione è: non funziona. Qualsiasi ruolo con supporto per 100500 casi è destinato a profonde bug corner case. Possono essere tappati con un testing massiccio, ma come con qualsiasi testing, o hai il prodotto cartesiano dei valori di input e una funzione totale, oppure hai "coperti scenari singoli". La mia opinione è: è molto meglio se il ruolo è lineare (complessità ciclomatica 1). include_varsMeno if (espliciti o dichiarativi — sotto forma di

o forma when in base a un insieme di variabili), migliore è il ruolo. A volte è necessario fare delle diramazioni, ma, ripeto, meno ce ne sono, meglio è. Quindi, sembra che un buon ruolo con galaxy (funziona, giusto!) con un sacco di include_vars possa essere meno preferibile rispetto a un "proprio" ruolo di cinque task. Il momento in cui un ruolo con galaxy è migliore — è quando inizi a scrivere qualcosa. Il momento in cui diventa peggiore — è quando qualcosa si rompe, e hai il sospetto che sia a causa del "ruolo con galaxy". Lo apri e ci sono cinque inclusioni, otto elenchi di attività e una pila when di task… E in questo devi fare chiarezza. Invece di 5 task in una lista lineare, in cui non c'è neanche niente da rompere. whenNei prossimi capitoli

Un po' sull'inventario, variabili di gruppo, plugin host_group_vars, hostvars. Come collegare il nodo gordiano in uno spaghetto. Scope e precedenza delle variabili, modello di memoria Ansible. "Ma dove dovremmo davvero conservare il nome utente per il database?".

  • jinja: {{ jinja }}
  • — nosql notype nosense plastilina morbida. È ovunque, anche dove non te lo aspetti. Un po' su !!unsafe e su un yaml gustoso. Rilascio della distribuzione openSUSE Leap 15.2

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster