Idempotencia
Una task idempotente deja el sistema en el mismo estado deseado aunque la ejecutes 1 o N veces. La segunda run debe reportar changed=0 (o solo cambios reales si el estado derivo). Sin idempotencia, los playbooks son scripts fragiles disfrazados de YAML.
Por que importa
Run 1: instala nginx, escribe config, reload -> changed
Run 2: nada que hacer -> ok
Run 3: igual -> okBeneficios:
- Re-aplicar tras fallo parcial sin duplicar usuarios, lineas en ficheros o reinicios.
--checky CI nightly utiles.- Deploys repetibles entre nodos del mismo grupo.
Modulos vs command/shell
Los modulos pensados para estado (package, file, user, copy, template, service, lineinfile, cron) comparan estado actual vs deseado.
# Idempotente
- name: Usuario deploy presente
ansible.builtin.user:
name: deploy
state: present
shell: /bin/bash
# NO idempotente por defecto
- name: Anadir usuario a mano
ansible.builtin.shell: useradd deployLa segunda forma falla en la segunda run (user already exists) o marca changed siempre.
changed_when y failed_when
Cuando debes usar command/shell, declara cuando hay cambio real:
- name: Aplicar migraciones Django
ansible.builtin.command: /opt/myapp/venv/bin/python manage.py migrate --noinput
args:
chdir: /opt/myapp
register: migrate
changed_when: "'Applying' in migrate.stdout"
failed_when:
- migrate.rc != 0
- "'No migrations to apply' not in migrate.stdout"Otro patron: marca sin cambio si el comando es solo lectura:
- name: Validar config nginx
ansible.builtin.command: nginx -t
changed_when: falsecreates y removes
- name: Extraer release una sola vez
ansible.builtin.unarchive:
src: "https://example.com/myapp-{{ app_version }}.tar.gz"
dest: "/opt/myapp-{{ app_version }}"
remote_src: true
creates: "/opt/myapp-{{ app_version }}/bin/myapp"Equivalente con command:
- name: Compilar extension
ansible.builtin.command: make install
args:
chdir: /usr/src/mymod
creates: /usr/lib/mymod.socreates salta el comando si el path existe; removes lo ejecuta solo si existe.
lineinfile y blockinfile
# Mal: echo >> file (duplica lineas)
- name: Sysctl ip forward
ansible.builtin.lineinfile:
path: /etc/sysctl.d/99-app.conf
line: net.ipv4.ip_forward=1
create: true
state: present
- name: Bloque de config app
ansible.builtin.blockinfile:
path: /etc/myapp/app.conf
marker: "# {mark} ANSIBLE MANAGED BLOCK myapp"
block: |
listen_port={{ app_port }}
env={{ app_env }}Sin marker estable, blockinfile puede duplicar bloques al cambiar el contenido de forma ambigua.
check_mode consciente
Algunos modulos simulan bien; otros no. Fuerza comportamiento:
- name: Reinicio real solo fuera de check
ansible.builtin.reboot:
when: not ansible_check_modeO declara que la task no soporta check:
- name: Operacion externa
ansible.builtin.uri:
url: "https://hooks.example.com/deploy"
method: POST
check_mode: falseContadores en la practica
ansible-playbook playbooks/site.yml
# PLAY RECAP * : ok=12 changed=5 failed=0
ansible-playbook playbooks/site.yml
# PLAY RECAP * : ok=17 changed=0 failed=0Si la segunda run sigue con changed>0, investiga:
- Template con fact inestable (timestamp, orden de dict no determinista).
commandsinchanged_when.copycon contenido generado distinto cada vez.- Timeouts / reloads que siempre notifican handlers por error de reporte.
Handlers e idempotencia
Notificar Restart service en cada run (porque la task padre siempre marca changed) reinicia en caliente sin necesidad. Arregla la task padre, no elimines el handler.
# Evita reiniciar siempre
- name: Desplegar unit systemd
ansible.builtin.template:
src: myapp.service.j2
dest: /etc/systemd/system/myapp.service
mode: "0644"
notify:
- Reload systemd
- Restart myappSolo si el template cambia habra restart.
Prueba mental
Antes de mergear una task, pregunta:
- Si el objeto ya existe, esta task no hace nada danino?
- Si falta, lo crea al estado correcto?
- Si el atributo difiere (modo 0644 vs 0755), converge?
- La segunda run en CI quedaria en
changed=0?
Errores comunes
shell: echo foo >> /etc/hostsen un role "temporal" que llega a prod.- Plantillas con
{{ ansible_date_time.epoch }}(cambia cada run). state: latesten paquetes en prod (cambia bajo tus pies); usa version fija opresent.- Contar
okcomo exito de negocio: un play puede seroky dejar el servicio caido si no hay asserts. - Confundir idempotencia con "no hace nada": a veces debe cambiar (drift); lo indeseable es cambiar sin drift.
Buenas practicas
- Prefiere modulos de estado;
command/shellcon justificacion ychanged_when. - Fija versiones de paquetes en prod cuando la reproducibilidad importe.
- Tras escribir un role, corre dos veces en lab y pega el recap en el PR.
- Usa
check_mode+--diffen ficheros de config. - Asserts al final del play (capitulo 7) para validar estado, no solo ausencia de failed.
Ejercicios
- Escribe una task
usery unashell: useradd; corre ambas dos veces y compara resultados. - Envuelve un
commandde migracion conchanged_whenbasado en stdout. - Introduce a proposito
{{ ansible_date_time.iso8601 }}en un template; observachangedeterno; quitalo. - Corre el playbook con
--checky anota que tasks no reportan bien; ajustachanged_whenocheck_mode.
Siguiente paso
El capitulo 7 cierra con lint, molecule, asserts y habitos de equipo.
