Com escriure bé un playbook d’Ansible bàsic
Si portes uns anys automatitzant sistemes, probablement ja hagis llegit dotzenes de tutorials que t’expliquen com escriure el teu primer playbook d’Ansible amb exemples tipus “instal·la Apache a Ubuntu”. I si alguna vegada tractes de fer servir un d’aquests exemples per a alguna cosa real, també sabràs que solen quedar-se curts quan surts del camí més obvi.
Aquest text assumeix que ja saps què és Ansible, com s’executa un playbook, què és un invent i com funcionen els mòduls més comuns.
1. Un playbook bàsic no és un one-liner amb apt: name=nginx
Un dels errors més comuns —i més temptadors al començament— és pensar que un playbook “bàsic” és un que fa molt poc: instal·lar un paquet, tocar un arxiu, reiniciar un servei. Una cosa així:
- hosts: web
become: yes
tasks:
- name: Instala nginx
apt:
name: nginx
state: present
Aquest tipus de fragments són útils per a provar coses, però no serveixen de gaire a un entorn real. No resisteixen un canvi de distribució, ni tan sols una reorganització de l’ inventari. El problema no és que facin poc, sinó que fan poc sense context. I quan automatitzes, el context és tot.
Un playbook bàsic, ben escrit, no és sinònim de minimalisme. És sinònim de tenir l’essencial perquè el que fas funcioni, es pugui repetir i no es trenqui si algú més l’executa amb paràmetres diferents.
2. Defineix variables des del primer moment (encara que sigui només una)
Un dels primers símptomes d’un playbook que no escala és veure rutes, ports, usuaris i noms de serveis escrits a mà en cada tasca. Això sol passar quan un pensa que “encara és molt simple per a variables”. Gran error. Una sola variable ben posada des del començament pot evitar que repeteixis tasques o escriguis condicionals més endavant.
Per exemple, en lloc de:
- name: Assegura que l'arxiu de configuració hi sigui present
copy:
src: files/nginx.conf
dest: /etc/nginx/nginx.conf
Prefereixo començar ja amb això:
vars:
nginx_conf_path: /etc/nginx/nginx.conf
tasks:
- name: Còpia l' arxiu de configuració de nginx
copy:
src: files/nginx.conf
dest: "{{ nginx_conf_path }}"
És redundant? Sí, avui. Però si demà has de fer el mateix per a Alpine, on el path pot canviar, o si fas servir aquest valor en cinc tasques més, t’alegraràs d’ haver-ho fet així.
I ja que estem: evita els set_fact innecessaris. És molt comú veure playbooks que defineixen facts en temps d’execució quan n’hi havia prou amb fer servir vars o defaults. Cada vegada que fas servir set_fact, estàs introduint mutabilitat i potencial confusió en l’execució.
3. Sempre separa lògica de configuració
Quan es comença, és molt comú barrejar lògica i dades. Em refereixo a escriure les rutes, flags, paràmetres i llistes de paquets dins de les tasques. Jo ho he fet, tu ho has fet, tots ho hem fet. Però quan tens més d’un entorn, o més d’un sistema operatiu, això es torna un malson.
El que s’ha de fer: escriure els playbooks com a controladors lògics que carreguen dades de group_vars, host_vars o fins i tot vars_files.
Exemple mínim:
# playbook.yml
- hosts: web
become: yes
vars_files:
- vars/common.yml
roles:
- nginx
I a vars/common.yml:
nginx_packages:
- nginx
- nginx-extras
nginx_conf_path: /etc/nginx/nginx.conf
Això t’obliga a pensar en el teu codi com a una cosa parametritzable des del principi. I això marca la diferència entre un playbook que serveix dues setmanes i un que pots fer servir dos anys.
4. Els handlers no són opcionals
Si fas una tasca que pot requerir reiniciar un servei, fes servir un handler. Fins i tot si el playbook és curt. No els posis com una segona tasca just a sota amb service: state=restarted. No barregis lògica de configuració amb accions irreversibles.
Per què? Perquè els handlers tenen lògica idempotent implícita: només s’executen si les tasques que els notifiquen han fet un canvi. Si ho fas tot com a tasques planes, reiniciaràs serveis encara que no hi hagi res a canviar. En producció, això no és trivial.
Exemple:
tasks:
- name: Copia la configuració de nginx
copy:
src: files/nginx.conf
dest: "{{ nginx_conf_path }}"
notify: Reinicia nginx
handlers:
- name: Reinicia nginx
service:
name: nginx
state: restarted
També és bona idea que el nom del handler coincideixi amb el que fa, no amb la tasca que l’invoca. Així pots tornar-lo a fer servir en diverses tasques sense ambigüitats.
5. Fes servir blocs (block:)
Molta gent evita els blocs perquè “són més text”, però estructuren millor el playbook. Agrupar tasques relacionades sota un block permet afegir when, become, tags o rescue al grup complet. Això t’estalvia duplicacions i et dona control fi quan les coses fallen.
Exemple real: en instal·lar paquets amb apt, és comú voler actualitzar la caché abans. Pots agrupar-ho així:
- name: Instal·la paquets base
block:
- name: Actualitza apt cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Instal·la paquets
apt:
name: "{{ item }}"
state: present
loop: "{{ base_packages }}"
when: ansible_os_family == "Debian"
Si demà necessites que tot aquest bloc no s’executi en un entorn en particular (per exemple, staging), només has de posar un when: env != “staging” al bloc i no en cada tasca.
6. Sempre fes servir tags, encara que no els necessitis avui
No hi ha res més frustrant que haver de provar només la part de configuració de Nginx i adonar-te que el teu playbook no té ni un sol tag. Llavors l’executes complet, triga, canvia coses que no volies i acabes amb un entorn brut.
Posar tags no costa res. T’hi acostumes ràpid, i quan els necessites, t’estalvien temps.
tasks:
- name: Instal·la nginx
apt:
name: nginx
state: present
tags: [nginx, packages]
- name: Còpia l' arxiu de configuració
copy:
src: files/nginx.conf
dest: "{{ nginx_conf_path }}"
tags: [nginx, config]
Després pots executar:
ansible-playbook playbook.yml --tags "config"
I llestos.
7. Els errors subtils no són on creus
Hi ha coses que Ansible permet i funcionen… fins que deixen de fer-ho. Alguns errors comuns:
- No declarar els become: guies al nivell del play: Posar become a cada tasca és temptador al començament, però sempre se t’escapa una, i no sempre falla de forma clara. Declara’ls a dalt, al bloc principal del playbook.
- Fer servir rutes relatives sense pensar-ho: Quan fas src: … /files/x, recorda que el context d’execució pot no estar on esperes si crides el playbook des d’un altre directori. Fes servir {{ role_path }} o rutes absolutes relatives al playbook.
- Repetir tasques que només s’han de fer una vegada: Instal·lar paquets, crear usuaris o tocar configuracions globals és una cosa que hauries de poder executar diverses vegades sense canviar res. Però si no tens cura amb els flags (creates=, state=present, force=no), el que sembla inofensiu pot acabar reescrivint configuracions a cada execució.
8. Fes servir check_mode i –diff com a part del flux de treball
Un bon senyal que el teu playbook està ben escrit és que pots executar-lo amb –check i obtenir una simulació raonable del que canviaria sense trencar res. Molta gent ho ignora perquè no tot funciona en mode check, i és cert. Però això no és excusa per a no intentar-ho.
Hi ha mòduls que no són compatibles amb check_mode, especialment quan invoquen comandaments externs o scripts sense marcar explícitament si canviarien alguna cosa. Però com més estructurat estigui el teu playbook, millor es comportarà en aquesta manera.
Fer servir –diff és igual de valuós. Si has de sobreescriure arxius de configuració amb copy, template o lineinfile, els diffs et permeten veure exactament què ha de canviar. T’estalvia temps de debugging quan un arxiu de configuració deixa de funcionar i l’únic canvi ha de ser un espai mal renderitzat en una plantilla Jinja2.
Exemple real:
- name: Copia la configuració de nginx
template:
src: nginx.conf.j2
dest: "{{ nginx_conf_path }}"
owner: root
group: root
mode: '0644'
notify: Reinicia nginx
tags: [nginx, config]
Amb això, si executem:
ansible-playbook nginx.yml --check --diff --tags config
Tenim una vista clara de si la configuració canviaria, si hi ha errors de sintaxi a la plantilla, i si el reinici es dispararia o no. Molt útil per a revisar sense tocar producció.
9. Templating amb Jinja2: fes servir filtres sempre que puguis
Jinja2 dona per a molt, però on realment estalvia mals de cap és quan fas servir filtres per a sanejar dades abans de fer-les servir. Exemples que eviten errors subtils:
- | default() per assegurar-te que una variable té valor
- | bool per forçar coerció quan hi ha condicionals
- | regex_replace per normalitzar entrades mal formatades
- | to_nice_json en templers que necessites que siguin llegibles
Cas concret: crear un arxiu JSON amb una llista de paràmetres opcionals que venen de múltiples llocs.
{
"feature_flags": {{ feature_flags | default({}) | to_nice_json(indent=2) }}
}
Això evita que el JSON final sigui invàlid si feature_flags no està definit. Sense aquest tipus de filtres, el més probable és que acabis amb plantilles que fallen en temps d’execució per variables mal definides o amb tipus inesperats.
També: mai facis lògica complexa a les plantilles. Si necessites if niats, processament de llistes o condicions complexes, fes-ho al playbook, no al Jinja2. Moure la lògica fora de les plantilles redueix la quantitat d’errors difícils de rastrejar.
10. Evita fer servir shell: o command: si pots
L’ impuls d’ escriure coses com:
- name: Reinicia el servei
shell: systemctl restart nginx
és fort, sobretot quan véns d’escriure scripts. Però a Ansible, cada cop que fas servir shell: o command:, estàs renunciant al fet que l’eina entengui què estàs fent. Perds idempotència, estructura i capacitat d’anàlisi.
Millor fer servir mòduls com a service:, package:, file:, etc. Estan pensats per a operar de forma declarativa, no imperativa. I si realment has d’executar alguna cosa arbitrària, llavors com a mínim:
- name: Executa script de manteniment
shell: /usr/local/bin/fix_permissions.sh
args:
creates: /var/lib/.permissions_fixed
El flag creates: assegura que no s’executarà dues vegades si ja es va fer. És una forma barata d’idempotència. Similar a removes: si fas servir scripts de neteja.
Una altra pràctica: si l’script és extern i complex, versioneu-lo, puja’l com a arxiu amb copy: i executa’l localment. Així controles quina versió s’executa i pots fer-lo servir en entorns diferents sense duplicar lògica.
11. Fes proves sense modificar l’entorn real
Això no és específic d’Ansible, però impacta directament en com escrius els teus playbooks. Tenir una forma ràpida de provar els teus playbooks en entorns d’un sol ús —ja sigui amb Vagrant, contenidors LXC, Docker, o màquines cloud efímeres— canvia la qualitat del que escrius.
Quan saps que pots destruir i tornar a aixecar un entorn amb dos comandaments, t’atreveixes a provar coses que d’una altra manera evitaries. I això permet detectar errors abans que arribin a staging o producció.
Exemple: per a rols compartits, muntem sempre un molecule amb almenys dues plataformes (Debian i RHEL) i proves mínimes. No perquè confiem en les proves com a veritat absoluta, sinó perquè ens serveix per a detectar regressions tontes: rutes mal definides, ús incorrecte de ansible_facts, diferències en els paquets, etc.
I encara que no facis servir Molecule, almenys tens un Vagrantfile o una recepta per a aixecar un entorn de prova que puguis automatitzar. Provar en la teva pròpia màquina amb –limit localhost no és suficient si després executes contra 40 servidors diferents.
12. Decideix com versionar l’inventari des de l’inici
Tot i que això escapa una mica del playbook en si, és una de les decisions més rellevants que afecten l’ús real d’Ansible. On viu el teu inventari? Està versionat amb el codi? Es genera dinàmicament? L’ escriu a mà cada equip?
Podem trobar tres escenaris:
- Inventaris dinàmics mal integrats, on els entorns canvien sota els teus peus i els playbooks deixen de funcionar sense avisar.
- Inventaris estàtics duplicats per entorn, impossibles de mantenir al dia.
- Inventaris compartits però no versionats, on ningú sap quin host està en quin entorn.
El que millor ha funcionat: versionar l’inventari per entorn dins del mateix repositori que els playbooks, amb un esquema tipus:
inventories/
staging/
hosts.yml
group_vars/
production/
hosts.yml
group_vars/
Això dona claredat, traçabilitat, i evita sorpreses. Quan algú revisa un PR que modifica el playbook, també veu quins hosts es veuran afectats.
Conclusió
No hi ha una única forma correcta d’escriure un playbook bàsic. Però hi ha moltes formes en què un de mal plantejat es torni una càrrega. El que comença com un conjunt de tasques simples pot escalar ràpidament a un monstre incontrolable si no apliques des del principi algunes regles d’ordre i separació de responsabilitats.
Escriure playbooks amb sentit pràctic no és qüestió de seguir una guia pas a pas, sinó de construir costums que facin la teva feina més predictible i menys fràgil. De vegades això implica escriure una mica més, posar una variable que avui no sembla necessària, o posar un tag que probablement no faràs servir fins d’aquí algunes setmanes.
Però quan tornes a aquest playbook passats sis mesos, o l’hereta algú més, t’adones que valia la pena.

