Ansible: playbook o role, quale usare e quando?
Scopri quando basta un playbook Ansible e quando conviene un role: struttura delle directory, ansible-galaxy init, richiami e precedenza delle variabili.
Differenze tra playbook e role di Ansible
Un playbook Ansible è il file che esegui con ansible-playbook. Associa un gruppo di host alle attività che devono eseguire. Un role Ansible è una directory con una struttura fissa che contiene task, template, handler e variabili predefinite; un playbook lo richiama per nome. La sintassi dei task è identica in entrambi i casi, quindi non è una questione di ciò che puoi esprimere. È una questione di riutilizzo.
Inizia con un playbook semplice. Un singolo site.yml che contiene un elenco tasks: è la struttura giusta per la prima automazione e rimane adatta più a lungo di quanto ci si aspetti. Converti il contenuto in un role quando lo stesso blocco di task deve essere eseguito per un secondo gruppo di host oppure quando il file supera circa 100 righe e non riesci più a trovare un task scorrendo il contenuto.
Se non ne hai ancora scritto uno, inizia con un primo playbook su un singolo VPS e torna qui quando inizia a crescere.
Quando un playbook semplice è la scelta corretta
Un playbook semplice è corretto quando l'attività viene eseguita una sola volta, su un solo host oppure quando nessun altro lo leggerà. Provisionare un singolo application server o applicare patch a un server prima di una finestra di manutenzione non richiede una struttura di directory. Un role aggiunge sette directory e un livello di indirezione. Se l'unico chiamante è il playbook che si trova accanto, questa indirezione non offre alcun vantaggio e costringe a fare un passaggio in più ogni volta che si vuole leggere ciò che viene effettivamente eseguito.
Il playbook semplice smette di essere la scelta corretta in un momento preciso, facile da individuare. Si copia un blocco di task in un secondo playbook. Questa copia è il segnale. Da quel momento ogni correzione deve essere applicata due volte e, prima o poi, verrà applicata una volta sola.
Cosa contiene effettivamente una directory di ruolo
roles/common/
defaults/main.yml
vars/main.yml
tasks/main.yml
handlers/main.yml
templates/99-hardening.conf.j2
files/
meta/main.ymltasks/main.ymlè il punto di ingresso. Ansible esegue questo file quando viene richiamato il ruolo; tutte le altre directory sono facoltative.defaults/main.ymlcontiene le variabili che il chiamante dovrebbe sovrascrivere. In Ansible ha la priorità più bassa, quindi quasi qualsiasi altra origine ha la precedenza.vars/main.ymlcontiene le variabili che il chiamante non dovrebbe sovrascrivere. Ha una priorità superiore a quella dell'inventory, quindi è una scelta con conseguenze rilevanti. Usatela raramente.handlers/main.ymlcontiene i task attivati danotify. Un handler viene eseguito alla fine del play, una sola volta, indipendentemente dal numero di task che lo hanno notificato.files/contiene i file copiati senza modifiche dal modulocopy, mentretemplates/contiene i template Jinja2 elaborati dal modulotemplate. All'interno di un ruolo, fate riferimento a entrambi usando il solo nome del file, senza il percorso, perché Ansible cerca prima nelle directory del ruolo.meta/main.ymldichiara le dipendenze del ruolo e i metadati letti da Ansible Galaxy.
La struttura non è una semplice preferenza stilistica. Ansible cerca i file in questi percorsi esatti, quindi un template inserito in roles/common/template/ (al singolare) non viene semplicemente trovato.
Creare il ruolo comune con ansible-galaxy init
mkdir -p ~/infra/roles
cd ~/infra
ansible-galaxy init --init-path roles commonQuesto comando scrive l'intera struttura in roles/common, incluse directory che non userai e file main.yml che contengono soltanto ---. Elimina quelli che lasci vuoti. Un vars/main.yml vuoto non crea problemi ad Ansible, ma nasconde quali file del ruolo sono effettivamente importanti.
Ora compila i file che eseguono le operazioni. Inizia dai valori predefiniti, perché costituiscono l'interfaccia pubblica del ruolo.
# roles/common/defaults/main.yml
---
common_packages:
- ufw
- fail2ban
- unattended-upgrades
common_admin_group: admins
common_permit_root_login: "no"
common_password_authentication: "no"Racchiudi "no" e "yes" tra virgolette. Ansible analizza YAML con PyYAML, che interpreta un no non quotato come il valore booleano false; di conseguenza, la riga di configurazione generata diventa PermitRootLogin False e sshd la rifiuta. Le virgolette mantengono il valore come stringa.
# roles/common/tasks/main.yml
---
- name: Install the base packages
ansible.builtin.apt:
name: "{{ common_packages }}"
state: present
update_cache: true
cache_valid_time: 3600
- name: Create the admin group
ansible.builtin.group:
name: "{{ common_admin_group }}"
state: present
- name: Install the sshd hardening drop-in
ansible.builtin.template:
src: 99-hardening.conf.j2
dest: /etc/ssh/sshd_config.d/99-hardening.conf
owner: root
group: root
mode: "0644"
validate: /usr/sbin/sshd -t -f %s
notify: Restart sshd# roles/common/handlers/main.yml
---
- name: Restart sshd
ansible.builtin.service:
name: ssh
state: restarted# roles/common/templates/99-hardening.conf.j2
# Managed by Ansible. Local edits are overwritten on the next run.
PermitRootLogin {{ common_permit_root_login }}
PasswordAuthentication {{ common_password_authentication }}Su Debian e Ubuntu l'unità systemd si chiama ssh, mentre sui sistemi della famiglia RHEL si chiama sshd. Un handler che specifica il nome errato fallisce soltanto quando una modifica effettiva aggiorna il template, perciò il problema in genere emerge dopo alcune settimane.
La riga validate è l'elemento più utile di quel task. Ansible esegue il rendering del template in un file temporaneo, sostituisce %s con il percorso di quel file ed esegue il comando. La destinazione viene sostituita soltanto se il comando termina con codice 0. Inserisci una direttiva non valida nel template ed esegui di nuovo il task: il task fallisce con failed to validate, il vero /etc/ssh/sshd_config.d/99-hardening.conf non viene modificato e puoi ancora accedere al server. Tieni presente che il controllo verifica aspetti ulteriori rispetto alla sintassi. Se sshd -t non riesce a leggere le host key, termina con sshd: no hostkeys available -- exiting. e Ansible segnala lo stesso failed to validate. Prima di attribuire il problema al template, consulta msg del modulo.
Come un playbook richiama un ruolo
# site.yml
---
- name: Base configuration for every server
hosts: all
become: true
roles:
- common# inventory.ini
[local]
localhost ansible_connection=localansible-playbook -i inventory.ini site.ymlIl play deve terminare con failed=0 nel riepilogo. Passa i parametri nel punto della chiamata usando la forma estesa. In questo modo uno stesso ruolo può gestire due gruppi di host:
roles:
- role: common
common_admin_group: ops
common_permit_root_login: prohibit-passwordEsiste una regola sull'ordine che sorprende quasi tutti. Un play può contenere pre_tasks, roles, tasks e post_tasks. Ansible li esegue in quest'ordine, indipendentemente dall'ordine in cui li hai scritti nel file. Inserisci tasks: prima di roles:, ma i ruoli vengono comunque eseguiti per primi. Se un'operazione deve essere eseguita prima di un ruolo, deve trovarsi in pre_tasks:, non all'inizio di tasks:.
- name: Ordering demonstration
hosts: local
gather_facts: false
pre_tasks:
- name: Runs first
ansible.builtin.debug:
msg: pre
roles:
- common
tasks:
- name: Runs after the role
ansible.builtin.debug:
msg: task
post_tasks:
- name: Runs last
ansible.builtin.debug:
msg: postPer richiamare un ruolo dall'interno di un elenco di task invece di usare la chiave roles:, usa import_role o include_role.
tasks:
- name: Static, read when the playbook is parsed
ansible.builtin.import_role:
name: common
- name: Dynamic, resolved when the task runs
ansible.builtin.include_role:
name: postgres
when: "'db' in group_names"import_role è statico. Ansible legge il ruolo durante l'analisi e i relativi task diventano parte del play. Di conseguenza, ansible-playbook --list-tasks site.yml li elenca e un tag applicato all'importazione si applica a ogni task incluso. include_role è dinamico. Ansible non legge nulla finché il task non viene eseguito. Questo consente di determinare il nome del ruolo tramite una variabile o un loop. Lo svantaggio è che questi task non sono visibili a --list-tasks e --start-at-task.
Qui è presente un problema comune. Un when: in un task include_role viene valutato prima che defaults/main.yml del ruolo incluso sia disponibile. Se scrivi when: common_packages | length > 0 sull'include, l'esecuzione si interrompe con 'common_packages' is undefined, anche se la variabile è definita proprio nel ruolo che stai includendo. La soluzione consiste nello spostare l'opzione fuori dal ruolo: inseriscila in group_vars/all.yml, dove è disponibile ovunque, e lascia i default del ruolo per i valori utilizzati dal ruolo stesso.
Quale variabile prevale: defaults, group_vars, vars, extra vars
Ansible documenta più di venti livelli di precedenza delle variabili. Quattro di questi risolvono quasi ogni discussione reale. Eccoli dal più debole al più forte.
roles/<name>/defaults/main.ymlsi trova quasi alla base della gerarchia. Quasi tutto ciò che si imposta altrove lo sovrascrive. Per questo è il posto corretto per i parametri configurabili di un ruolo.group_vars/ehost_vars/si trovano nella parte centrale. Qui vanno inseriti i valori specifici del sito, che sovrascrivono in modo netto i valori predefiniti del ruolo.roles/<name>/vars/main.ymlprevale suhost_vars. Un valore definito qui non può essere sovrascritto dall'inventory. Riservalo agli elementi che il ruolo deve mantenere coerenti internamente, ad esempio un nome di pacchetto che deve corrispondere al nome di un servizio.- Un parametro del ruolo passato nel punto di chiamata prevale su
vars/main.yml, mentre-enella riga di comando prevale su tutto, inclusi i parametri del ruolo.
Puoi osservare questa risoluzione in circa un minuto. Assegna a un piccolo ruolo un valore predefinito e una variabile del ruolo, quindi imposta gli stessi nomi in host_vars.
# roles/prec/defaults/main.yml
---
prec_tunable: from-defaults
prec_internal: from-defaults# roles/prec/vars/main.yml
---
prec_internal: from-rolevars# host_vars/localhost.yml
---
prec_tunable: from-hostvars
prec_internal: from-hostvars# roles/prec/tasks/main.yml
---
- name: Show which value survived
ansible.builtin.debug:
msg: "tunable={{ prec_tunable }} internal={{ prec_internal }}"ansible-playbook -i inventory.ini prec.yml
ansible-playbook -i inventory.ini prec.yml -e prec_internal=from-cliLa prima esecuzione stampa tunable=from-hostvars internal=from-rolevars. L'inventory ha prevalso sul valore predefinito del ruolo, ma è stata a sua volta sovrascritta dalla variabile del ruolo. La seconda esecuzione stampa internal=from-cli, perché le extra vars si trovano al livello più alto e nessun livello inferiore può sovrascriverle. Per lo stesso motivo, -e va bene per un'esecuzione occasionale, ma è una scelta errata in uno script che viene mantenuto: sovrascrive silenziosamente ogni decisione considerata nel repository.
La regola pratica è questa: se vuoi che un valore possa essere impostato, inseriscilo in defaults/. Inserirlo in vars/ comunica a ogni futuro utente del ruolo che l'inventory non può modificarlo. A volte è proprio ciò che serve, ma nella maggior parte dei casi è un errore.
Verificare l’idempotenza del ruolo: eseguirlo due volte
Un’esecuzione di Ansible affidabile produce lo stesso risultato alla seconda esecuzione e segnala che non è cambiato nulla. Eseguire il playbook due volte e leggere il riepilogo.
ansible-playbook -i inventory.ini site.yml
ansible-playbook -i inventory.ini site.ymlIl secondo riepilogo dovrebbe essere simile al seguente:
PLAY RECAP *********************************************************************
localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed=0 indica che ogni modulo ha verificato lo stato corrente e ha rilevato che l’operazione era già stata eseguita. changed=2 alla seconda esecuzione indica che due task non riescono a distinguere lo stato corrente da quello precedente. Di conseguenza, continueranno a riscrivere i file e a riavviare i servizi senza fine. La causa più comune è command o shell, perché Ansible non può sapere che cosa ha fatto un comando arbitrario.
# traps.yml
---
- name: Command modules do not know what they changed
hosts: local
gather_facts: false
tasks:
- name: This appends a line on every run
ansible.builtin.shell: "echo run >> /tmp/grow.txt"
- name: This appends a line only once
ansible.builtin.shell: "echo run >> /tmp/guarded.txt"
args:
creates: /tmp/guarded.txtEseguire il playbook due volte, quindi contare le righe contenenti wc -l /tmp/grow.txt /tmp/guarded.txt. /tmp/grow.txt contiene due righe e /tmp/guarded.txt ne contiene una. Alla seconda esecuzione il task con la condizione non è stato eseguito e il relativo risultato contiene il messaggio skipped, since /tmp/guarded.txt exists, perché creates fornisce al modulo un prodotto visibile da cercare prima. Quando un comando non lascia un prodotto di questo tipo, registrare il relativo output e decidere autonomamente con changed_when.
ansible-playbook --check --diff site.yml prevede le modifiche senza applicarle, mentre --diff stampa le righe esatte che un template riscriverebbe. Leggere l’output tenendo presente un’avvertenza: i task shell e command vengono ignorati in check mode. Di conseguenza, un piano che sembra privo di modifiche può comunque nascondere operazioni da eseguire.
Perché Ansible segnala che il ruolo non è stato trovato
Ansible cerca una directory roles/ accanto al file del playbook, quindi in roles_path. La ricerca segue il playbook, non la shell.
ERROR! the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonelyQuesto messaggio indica che site.yml e roles/ non sono più allineati e mostra i percorsi che Ansible ha verificato. Mantienili nella stessa directory. Puoi eseguire il comando da una directory padre, perché conta il percorso del playbook:
ansible-playbook -i infra/inventory.ini infra/site.ymlEsiste anche una variante meno evidente dello stesso problema. Ansible ignora un ansible.cfg nella directory corrente quando questa è scrivibile da tutti, perché qualsiasi utente del sistema potrebbe inserirvi una configurazione e modificare il comportamento dell'esecuzione.
[WARNING]: Ansible is being run in a world writable directory (/tmp/infra), ignoring it as an ansible.cfg source.Le impostazioni roles_path e inventory risultano quindi assenti senza messaggi espliciti e la ricerca del ruolo non riesce per un motivo che non riguarda i ruoli. ansible --version mostra il config file effettivamente caricato, mentre ansible-config dump --only-changed mostra ogni impostazione diversa dai valori predefiniti incorporati. Controllali entrambi quando un'esecuzione si comporta come se la configurazione non esistesse.
Condividere i ruoli: requirements.yml e una versione fissata
Un ruolo scritto da altri viene installato, non copiato. Dichiaralo una sola volta:
# requirements.yml
---
roles:
- name: postgres
src: https://github.com/example/ansible-role-postgres
scm: git
version: v1.4.0ansible-galaxy install -r requirements.yml -p galaxy_rolesImposta sempre version. Senza questo parametro viene usato il contenuto del branch predefinito disponibile quando esegui il comando. Di conseguenza, un deployment che funzionava il mese scorso può non funzionare più senza alcuna modifica nel tuo repository. Imposta roles_path sulla directory di download ed escludi questa directory da git:
# ansible.cfg
[defaults]
inventory = inventory.ini
roles_path = ./galaxy_rolesI ruoli in roles/ accanto al playbook vengono comunque trovati, perché questo percorso viene sempre cercato in aggiunta a roles_path. In questo modo i tuoi ruoli restano versionati e sottoposti a revisione, mentre i ruoli di terze parti vengono scaricati in modo riproducibile con una versione fissata tramite tag.
Dove i ruoli non sono più la soluzione
Un ruolo è un'unità di riutilizzo all'interno di un'esecuzione di Ansible. Non crea server né record DNS presso il provider. Cercare di fargli svolgere anche queste operazioni è il modo più rapido per trasformare i playbook in qualcosa che nessuno vuole più mantenere. Prima di iniziare, conviene leggere la suddivisione delle attività tra Ansible e Terraform. Un ruolo non sostituisce neppure la progettazione dell'inventory: quando si supera un numero limitato di macchine, il modo in cui si raggruppano e si raggiungono quei server conta più di come vengono organizzate le attività.
Anche le misure di hardening installate da questo ruolo common richiedono decisioni specifiche. Il drop-in precedente imposta soltanto due direttive. Prima di decidere cosa includere nel ruolo per tutti gli host gestiti, leggi quali impostazioni SSH vale davvero la pena modificare e come fare in modo che Ubuntu applichi automaticamente gli aggiornamenti di sicurezza.
FAQ
Quando devo trasformare un playbook Ansible in un ruolo?
Quando lo stesso blocco di task deve essere eseguito in un secondo play o su un secondo gruppo di host. Copiare i task tra playbook è il segnale, perché da quel momento ogni correzione deve essere applicata due volte e prima o poi verrà applicata una sola volta. Un singolo playbook di circa 100 righe o meno, destinato sempre a un solo gruppo, non trae vantaggio da un ruolo; le directory aggiuntive ne rendono più difficile la lettura.
I ruoli vengono eseguiti prima dei task nello stesso play?
Sì. Ansible esegue pre_tasks, poi tutto ciò che è elencato in roles:, quindi tasks: e infine post_tasks:; ignora l'ordine in cui queste chiavi compaiono nel file. Scrivere tasks: sopra roles: non fa eseguire per primi quei task. Se qualcosa deve avvenire prima di un ruolo, inseriscilo in pre_tasks:.
Perché il valore in group_vars non sovrascrive quello del ruolo?
Verifica se la variabile è impostata in vars/main.yml del ruolo invece che in defaults/main.yml. vars/ ha precedenza su group_vars e host_vars nell'ordine di precedenza di Ansible, quindi l'inventory non può sovrascriverlo. Sposta la variabile in defaults/main.yml, che si trova vicino al fondo dell'ordine ed è il posto corretto per tutto ciò che il chiamante deve poter modificare. Per confermare che la causa sia la precedenza e non un refuso, esegui una volta con -e name=value, che ha precedenza su qualsiasi altra origine.
Perché Ansible dice che il ruolo non è stato trovato?
La ricerca inizia accanto al file del playbook, quindi site.yml e roles/ devono trovarsi nella stessa directory. L'errore stampa i percorsi che ha provato, come in the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely. Eseguire il playbook da una directory padre non è un problema, perché la ricerca segue il percorso del playbook e non la directory di lavoro della shell. Se usi roles_path da ansible.cfg, verifica che il file sia stato caricato con ansible --version, perché una directory di lavoro scrivibile da chiunque fa sì che Ansible lo ignori.
Serve ansible-galaxy init per creare un ruolo?
No. Un ruolo è costituito soltanto da directory con i nomi previsti, quindi mkdir -p roles/common/tasks più un tasks/main.yml costituisce già un ruolo funzionante. ansible-galaxy init --init-path roles common evita di scrivere manualmente la struttura e fornisce lo scheletro completo, inclusi meta/main.yml e una bozza del README. Elimina le directory che lasci vuote, perché un vars/main.yml vuoto nasconde quali file del ruolo eseguono effettivamente delle operazioni.