Ansible: playbook o ruolo, quale scegliere?
Scopri quando basta un playbook Ansible e quando serve un ruolo: struttura delle directory, ansible-galaxy init, richiami e precedenza delle variabili.
Differenza tra playbook e ruolo Ansible
Un playbook Ansible è il file che esegui con ansible-playbook. Associa un gruppo di host alle operazioni da eseguire. Un ruolo Ansible è una directory con una struttura fissa che contiene task, template, handler e variabili predefinite. Un playbook richiama il ruolo per nome. La sintassi dei task è identica in entrambi i casi. La differenza non riguarda quindi ciò che puoi esprimere, ma il riutilizzo.
Inizia con un playbook semplice. Un singolo site.yml che contiene un elenco tasks: è la struttura corretta per la prima automazione e rimane adatta più a lungo di quanto ci si aspetti. Converti il playbook in un ruolo 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 riprendi da qui quando inizia a crescere.
Quando la risposta corretta è un playbook piatto
Un playbook piatto è la scelta corretta quando l'attività viene eseguita una sola volta, su un solo host oppure quando nessun altro lo leggerà. Il provisioning di un singolo application server o l'applicazione di patch a un server prima di una finestra di manutenzione non giustificano una struttura di directory. Un ruolo aggiunge sette directory e un livello di indirezione. Se l'unico chiamante è il playbook che si trova accanto al ruolo, questa indirezione non offre alcun vantaggio e richiede un passaggio in più ogni volta che si vuole leggere ciò che viene effettivamente eseguito.
Il playbook piatto smette di essere la scelta corretta in un momento preciso, facile da individuare. Si copia un blocco di attività 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 realmente 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 il ruolo viene richiamato; tutte le altre directory sono facoltative.defaults/main.ymlcontiene le variabili che il chiamante dovrebbe sovrascrivere. È la fonte con la priorità più bassa in Ansible, quindi quasi qualsiasi altra impostazione ha la precedenza.vars/main.ymlcontiene le variabili che il chiamante non dovrebbe sovrascrivere. Ha una priorità superiore a quella dell'inventory, una scelta che va fatta solo in casi motivati. Usatela raramente.handlers/main.ymlcontiene i task attivati danotify. Un handler viene eseguito al termine 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, entrambi si referenziano 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 preferenza stilistica. Ansible cerca i contenuti 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'intero scheletro in roles/common, incluse directory che non utilizzerai e stub main.yml contenenti soltanto ---. Elimina quelli che lasci vuoti. Un vars/main.yml vuoto non causa problemi ad Ansible, ma nasconde quali file del ruolo sono effettivamente importanti.
Ora compila i file che eseguono il lavoro. 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 racchiuso tra virgolette 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 indica l'unità errata fallisce soltanto quando qualcosa modifica effettivamente il template. Per questo l'errore emerge spesso dopo diverse settimane.
La riga validate è l'elemento più utile di quel task. Ansible genera il 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 nel template una direttiva non valida ed esegui di nuovo il task: il task fallisce con failed to validate, il vero /etc/ssh/sshd_config.d/99-hardening.conf rimane intatto e puoi ancora accedere al server. Tieni presente che il controllo verifica più della sola sintassi. Se sshd -t non riesce a leggere le chiavi host, 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 un ruolo può essere usato per 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, e Ansible li esegue in quell'ordine indipendentemente dall'ordine in cui li hai scritti nel file. Inserisci tasks: prima di roles:, ma i ruoli vengono comunque eseguiti per primi. Quindi, 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 in fase di analisi e i relativi task diventano parte del play. Di conseguenza, ansible-playbook --list-tasks site.yml li elenca e un tag sull'importazione viene applicato a ogni task incluso. include_role è dinamico. Il contenuto non viene letto finché il task non viene eseguito. Questo permette di determinare il nome del ruolo tramite una variabile o un ciclo. Il compromesso è che questi task non sono visibili a --list-tasks e a --start-at-task.
Qui esiste un errore comune. Un when: su 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 quella 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 valori predefiniti del ruolo per i valori utilizzati dal ruolo stesso.
Quale variabile prevale: valori predefiniti, group_vars, vars, extra vars
Ansible documenta più di venti livelli di precedenza delle variabili. Quattro di questi risolvono quasi ogni caso reale. Sono elencati dal livello più debole a quello più forte.
roles/<name>/defaults/main.ymlsi trova nella parte bassa della gerarchia. Quasi qualsiasi valore impostato altrove lo sovrascrive. Per questo è il posto corretto per i parametri regolabili di un ruolo.group_vars/ehost_vars/si trovano nella parte centrale. Qui vanno inseriti i valori specifici del sito, che sovrascrivono in modo chiaro i valori predefiniti del ruolo.roles/<name>/vars/main.ymlha una precedenza maggiore dihost_vars. Un valore impostato qui non può essere sovrascritto dall'inventory. Usalo per gli 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 in cui il ruolo viene chiamato prevale su
vars/main.yml, mentre-enella riga di comando prevale su tutto, inclusi i parametri del ruolo.
Puoi osservare il risultato in circa un minuto. Crea un ruolo semplice con 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 sovrascritta dalla variabile del ruolo. La seconda esecuzione stampa internal=from-cli, perché gli extra vars si trovano al livello più alto e nessun valore sottostante può prevalere su di essi. Per lo stesso motivo, -e è adatto a un'esecuzione occasionale, ma non a uno script che mantieni nel tempo: sovrascrive silenziosamente ogni decisione considerata nel repository.
La regola pratica è questa: se vuoi che un valore possa essere modificato, inseriscilo in defaults/. Inserirlo in vars/ comunica a ogni futuro utente del ruolo che l'inventory non può modificarlo. A volte è la scelta corretta, ma nella maggior parte dei casi è un errore accidentale.
Verifica che il ruolo sia idempotente: eseguilo due volte
Un'esecuzione Ansible affidabile produce lo stesso risultato anche la seconda volta e segnala che non è cambiato nulla. Esegui il playbook due volte e leggi 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 significa che ogni modulo ha verificato lo stato corrente e ha rilevato che il lavoro era già stato completato. changed=2 in una seconda esecuzione significa che due task non riescono a distinguere lo stato corrente da quello desiderato, quindi continueranno a riscrivere i file e a riavviare i servizi senza fine. Il colpevole abituale è command o shell, perché Ansible non può sapere che cosa abbia 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.txtEsegui quel playbook due volte, quindi conta le righe con wc -l /tmp/grow.txt /tmp/guarded.txt. /tmp/grow.txt contiene due righe e /tmp/guarded.txt ne contiene una. Nella 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 consente al modulo di cercare prima un prodotto visibile. Quando un comando non lascia un prodotto di questo tipo, registra il relativo output e valuta autonomamente il risultato con changed_when.
ansible-playbook --check --diff site.yml prevede le modifiche senza applicarle, mentre --diff stampa le righe esatte che un template riscriverebbe. Leggi l'output tenendo presente una limitazione: i task shell e command vengono saltati in check mode, quindi un piano che sembra completo può comunque nascondere del lavoro.
Anche un'altra colonna del riepilogo richiede la stessa attenzione: un host a cui Ansible non è riuscito a connettersi viene conteggiato in unreachable invece che in failed e nessuno dei suoi task è stato eseguito. Prima di applicare questo ruolo a più di un paio di macchine, decidi in anticipo se un host irraggiungibile debba interrompere l'intera esecuzione.
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/lonelyIl messaggio indica che site.yml e roles/ non sono più allineati e mostra i percorsi che Ansible ha provato. Mantieni i due elementi nella stessa directory. Puoi eseguire il comando da una directory padre, perché ciò che conta è il percorso del playbook:
ansible-playbook -i infra/inventory.ini infra/site.ymlEsiste una variante meno evidente dello stesso problema. Ansible ignora un ansible.cfg nella directory corrente quando questa è scrivibile da tutti, perché qualsiasi utente del server 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 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 integrati. Controllali entrambi quando un'esecuzione si comporta come se la configurazione non esistesse.
Condivisione dei ruoli: requirements.yml e versione fissata
Un ruolo scritto da terzi 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 questa opzione viene usato il contenuto del branch predefinito disponibile nel giorno in cui esegui il comando. Di conseguenza, un deployment che funzionava il mese scorso può interrompersi 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é quel 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 sono download riproducibili fissati a un tag.
Quando i ruoli non sono più la soluzione
Un ruolo è un'unità di riutilizzo all'interno di una singola esecuzione di Ansible. Non crea server né record DNS presso il provider. Cercare di fargli svolgere queste attività trasforma i playbook in codice che nessuno vuole più mantenere. Prima di iniziare, conviene leggere come suddividere il lavoro tra Ansible e Terraform. Un ruolo non sostituisce nemmeno la progettazione dell'inventory: superato un numero limitato di macchine, come raggruppare e raggiungere quei server diventa più importante di come sono organizzate le attività.
Anche l'hardening installato da questo ruolo common richiede decisioni specifiche. Il drop-in precedente imposta soltanto due direttive. Prima di decidere cosa includere nel ruolo per ogni host gestito, 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 una seconda play o su un secondo gruppo di host. Copiare task tra playbook è il segnale: 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 della stessa play?
Sì. Ansible esegue pre_tasks, quindi tutto ciò che è elencato sotto roles:, poi tasks: e infine post_tasks:; ignora l'ordine in cui queste chiavi compaiono nel file. Scrivere tasks: sopra roles: non fa eseguire prima quei task. Se qualcosa deve essere eseguito prima di un ruolo, inseriscilo in pre_tasks:.
Perché il valore in group_vars non sovrascrive quello del ruolo?
Controlla se la variabile è impostata in vars/main.yml del ruolo invece che in defaults/main.yml. vars/ ha una precedenza maggiore di 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 è la posizione corretta per qualsiasi valore che il chiamante debba poter modificare. Per verificare che la causa sia la precedenza e non un errore di battitura, esegui una volta il playbook 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 mostra i percorsi provati, 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 dipendi da roles_path di 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.
Mi 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 costituiscono già un ruolo funzionante. ansible-galaxy init --init-path roles common evita di digitare manualmente la struttura e crea lo scheletro completo, inclusi meta/main.yml e un modello di README. Elimina le directory che lasci vuote, perché un vars/main.yml vuoto nasconde quali file del ruolo eseguono effettivamente delle operazioni.