Día 93 - Using Ansible Conditionals (when + ansible_nodename + facts)
Problema / Desafío
El equipo Nautilus quiere ejercitar todas las formas que ofrece Ansible para una misma tarea. Hoy toca usar condicionales when. El inventario ya existe. La tarea:
Crear /home/thor/ansible/playbook.yml usando when para:
- Copiar
/usr/src/devops/blog.txt(del jump host) a App Server 1 en/opt/devops/→ ownertony:tony, permisos0777 - Copiar
/usr/src/devops/story.txta App Server 2 → ownersteve:steve, permisos0777 - Copiar
/usr/src/devops/media.txta App Server 3 → ownerbanner:banner, permisos0777
Restricciones del lab:
- Usar la variable
ansible_nodename(de los facts) en loswhen - Usar
- hosts: all(correr el play sobre todos los hosts) - Validación:
ansible-playbook -i inventory playbook.yml— sin args extra;becomedentro del playbook
Mismo objetivo que el Día 90 (cada archivo a un host distinto), pero por una tercera vía: un solo play sobre
all+whenpor task. El lab obliga este enfoque para enseñar condicionales.
Conceptos clave
when — ejecutar una task solo si se cumple una condición
when es el condicional de Ansible: la task corre solo si la expresión es verdadera para ese host. Si es falsa, la task se salta (skipped):
- name: copy blog.txt
copy:
src: /usr/src/devops/blog.txt
dest: /opt/devops/blog.txt
when: ansible_nodename == "stapp01" # ← solo en stapp01
Como el play corre sobre all, los tres hosts evalúan las tres tasks; cada when deja pasar solo la que le toca. El resultado neto es idéntico a tres plays separados, pero con un único play.
Detalle de when |
Comportamiento |
|---|---|
| Expresión verdadera | La task se ejecuta (ok/changed) |
| Expresión falsa | La task se salta (skipped) |
when: sin {{ }} |
Se evalúa como expresión Jinja2 directamente (sin llaves) |
| Lista de condiciones | Se combinan con AND implícito |
Gotcha sutil: en
whenno se ponen las{{ }}. Se escribewhen: ansible_nodename == "stapp01", nowhen: "{{ ansible_nodename }}" == ....whenya espera una expresión Jinja2, así que las llaves sobran (y dan warning).
★ ansible_nodename — el tercer "nombre del host"
El lab pide ansible_nodename específicamente. Es la tercera variable tipo-hostname del journal, y conviene tener clara la diferencia:
| Variable | Origen | ¿Necesita facts? | Equivalente shell |
|---|---|---|---|
inventory_hostname |
Nombre en el inventario (Día 92) | No (magic) | — |
ansible_hostname |
Hostname corto del SO (Día 92) | Sí (fact) | hostname -s |
ansible_nodename |
Node name de la red (hoy) | Sí (fact) | uname -n / hostname |
ansible_fqdn |
Nombre completo con dominio | Sí (fact) | hostname -f |
ansible_nodename es un fact: sale del Gathering Facts (internamente uname -n). En estos labs coincide con stapp01, pero conceptualmente es lo que el kernel reporta como nombre del nodo.
★ La consecuencia: NO deshabilitar gather_facts
Como ansible_nodename es un fact, debe recolectarse antes de evaluar el when. Si se pone gather_facts: no, la variable queda indefinida y todos los when fallan (o dan error de variable undefined) → no se copia nada.
Por defecto gather_facts está en yes, así que dejarlo implícito es correcto. La lección: un when basado en un fact crea una dependencia obligatoria con el fact-gathering. Si se usara inventory_hostname (magic var), no habría esa dependencia — pero el lab pidió ansible_nodename.
copy con src local — push desde el control node
Los .txt están en el jump host (control node). El módulo copy toma el archivo de ahí y lo empuja al remoto:
copy:
src: /usr/src/devops/blog.txt # ← ruta en el CONTROL NODE (jump host)
dest: /opt/devops/blog.txt # ← ruta en el host REMOTO
| Parámetro | Significado |
|---|---|
src (default) |
Archivo en el control node → se empuja al remoto |
remote_src: yes |
El src ya está en el host remoto (copia local allá) |
Aquí no se usa remote_src porque el origen está en el jump host, no en los app servers. (Día 84 ya usó copy así.)
ansible_user para el owner — el sudo user por host
El inventario define ansible_user por host (tony/steve/banner). Igual que el Día 85/92, owner: "{{ ansible_user }}" da el dueño correcto por host sin hardcodear:
Detalle elegante: cuando la task de blog.txt corre en stapp01, ansible_user ya es tony — así que el when selecciona el host y el ansible_user da el owner correcto, ambos de la misma fuente.
when vs multi-play vs host_vars+loop — tres formas, mismo resultado
| Enfoque | Cómo | Día | Mejor para |
|---|---|---|---|
| Multi-play | Un play hosts: stappNN por host |
90 | Pocos hosts, config muy distinta |
host_vars + loop |
hosts: all + datos en host_vars + un solo task |
90 (variante) | Muchos hosts, misma forma de task |
when conditionals |
hosts: all + N tasks filtradas por when |
93 | Lógica condicional explícita |
when brilla cuando la decisión es condicional ("haz X solo si el SO es CentOS", "instala Y solo si la versión < Z"). Para puro routing de "archivo→host", multi-play o host_vars suelen ser más limpios — pero el lab usa when para enseñar el mecanismo.
Pasos
- Login al jump host como
thor;cd ~/ansible - Revisar el inventario (
cat inventory) y que los.txtexistan en/usr/src/devops/ - Crear
playbook.ymlconhosts: all,become: truey 3 taskscopyfiltradas porwhen: ansible_nodename == ... - Correr
ansible-playbook -i inventory playbook.yml - Validar en cada host
Comandos / Código
Inventario (referencia)
stapp01 ansible_host=stapp01 ansible_ssh_pass=Ir0nM@n ansible_user=tony
stapp02 ansible_host=stapp02 ansible_ssh_pass=Am3ric@ ansible_user=steve
stapp03 ansible_host=stapp03 ansible_ssh_pass=BigGr33n ansible_user=banner
Playbook (solución utilizada)
# /home/thor/ansible/playbook.yml
- hosts: all
become: true
tasks:
- name: copy blog.txt
copy:
src: /usr/src/devops/blog.txt
dest: /opt/devops/blog.txt
mode: '0777'
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
when: ansible_nodename == "stapp01"
- name: copy story.txt
copy:
src: /usr/src/devops/story.txt
dest: /opt/devops/story.txt
mode: '0777'
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
when: ansible_nodename == "stapp02"
- name: copy media.txt
copy:
src: /usr/src/devops/media.txt
dest: /opt/devops/media.txt
mode: '0777'
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
when: ansible_nodename == "stapp03"
Ejecutar
Output real del lab (cada host evalúa las 3 tasks, ejecuta 1 y salta 2):
TASK [Gathering Facts] *****************************************
ok: [stapp01]
ok: [stapp03]
ok: [stapp02]
TASK [copy blog.txt] *******************************************
skipping: [stapp02]
skipping: [stapp03]
changed: [stapp01]
TASK [copy story.txt] ******************************************
skipping: [stapp01]
skipping: [stapp03]
changed: [stapp02]
TASK [copy media.txt] ******************************************
skipping: [stapp01]
skipping: [stapp02]
changed: [stapp03]
PLAY RECAP *****************************************************
stapp01 : ok=2 changed=1 skipped=2 unreachable=0 failed=0
stapp02 : ok=2 changed=1 skipped=2 unreachable=0 failed=0
stapp03 : ok=2 changed=1 skipped=2 unreachable=0 failed=0
skipped=2por host es la firma visible de los condicionales: cada host saltó las dos tasks que no le tocaban.ok=2= Gathering Facts + la task que sí corrió.
Verificación
Output real del lab:
stapp01 | CHANGED | rc=0 >>
-rwxrwxrwx 1 tony tony 35 ... blog.txt
stapp02 | CHANGED | rc=0 >>
-rwxrwxrwx 1 steve steve 27 ... story.txt
stapp03 | CHANGED | rc=0 >>
-rwxrwxrwx 1 banner banner 22 ... media.txt
Cada host tiene solo su archivo, con el owner correcto (de ansible_user) y 0777 (-rwxrwxrwx). Los tres requisitos cumplidos.
Variantes (referencia)
Combinar condiciones (AND / OR)
# AND: lista de condiciones (todas deben cumplirse)
when:
- ansible_nodename == "stapp01"
- ansible_distribution == "CentOS"
# OR: con 'or' explícito
when: ansible_nodename == "stapp01" or ansible_nodename == "stapp02"
Condicionales sobre el resultado de otra task (register)
- name: chequear si existe el archivo
ansible.builtin.stat:
path: /opt/devops/blog.txt
register: blog
- name: copiar solo si no existe
copy: { src: ..., dest: ... }
when: not blog.stat.exists
Condicional por número de fact
Condicional con in (pertenencia)
Troubleshooting
| Problema | Causa | Solución |
|---|---|---|
| Todas las tasks se saltan; no se copia nada | gather_facts: no → ansible_nodename indefinido |
Dejar gather_facts en su default (yes) |
'ansible_nodename' is undefined |
Facts deshabilitados o when sobre un host sin facts |
No deshabilitar facts; o usar ansible_nodename is defined como guarda |
Warning delimiters ... when statements should not include {{ }} |
Se escribió when: "{{ ansible_nodename }}" == ... |
Quitar las {{ }}: when: ansible_nodename == "stapp01" |
src ... could not be found |
El .txt no está en el control node, o ruta mal |
Verificar que existe en /usr/src/devops/ del jump host |
| Copia local en el remoto en vez de push | Se puso remote_src: yes por error |
Sin remote_src — el origen está en el control node |
Owner queda root |
No se usó ansible_user o se hardcodeó |
owner: "{{ ansible_user }}" |
Destination /opt/devops not writable |
Falta become o el directorio no existe |
become: true en el play; crear el dir con file si hace falta |
Funciona con --become pero no sin args |
La validación corre sin --become |
become: true dentro del play |
Conexión con días anteriores
- Día 90 (multi-play ACLs): mismo objetivo "cada archivo a un host distinto", resuelto con plays separados. Hoy es la tercera forma (un play +
when) — útil comparar los tres enfoques. - Día 92 (
inventory_hostname): hoy apareceansible_nodename, otro "nombre del host" pero fact (no magic var). Completa la trilogía inventory_hostname / ansible_hostname / ansible_nodename. - Día 84 (
copy): mismo módulocopyconsrclocal empujando al remoto; hoy se le suma el filtrowhen. - Día 85 (
ansible_userpor host): el owner por host vía magic var, exactamente el mismo patrón. - Patrón "traducir requisito literal" (Días 65, 67, 90, 92): el lab pide
ansible_nodenameyhosts: allexplícitamente — cumplir la instrucción exacta aunque haya alternativas equivalentes.