Guía Práctica para Depurar Módulos Shell y Command Fallidos
Depura fallos en Ansible shell y command con ejemplos de register, stdout, stderr, rc, failed_when y changed_when.
Guía Práctica para Depurar Módulos Shell y Command Fallidos
Los módulos command y shell de Ansible son útiles cuando no existe un módulo específico, pero pueden ser difíciles de depurar. Una tarea fallida puede mostrar solo un código de retorno a menos que captures la salida del comando tú mismo.
Esta guía te muestra cómo depurar módulos shell y command fallidos verificando rc, stdout y stderr, y luego usando failed_when y changed_when para que Ansible reporte el resultado real.
Command vs. Shell: Entendiendo la Diferencia
Antes de profundizar en la depuración, es vital entender la diferencia fundamental entre los dos módulos, ya que su entorno de ejecución afecta los modos de fallo.
ansible.builtin.command
Este módulo ejecuta el comando directamente, omitiendo el entorno shell estándar. Esto lo hace más seguro y predecible, ya que evita características del shell como interpolación de variables, expansión de comodines, tuberías (|) y redirección (>).
Mejor práctica: Usa command cuando la tarea sea simple y no requiera características del shell.
ansible.builtin.shell
Este módulo ejecuta el comando a través del shell estándar del host remoto (/bin/sh o equivalente). Esto es necesario para operaciones complejas, variables de entorno o cuando se usa sintaxis estándar del shell (por ejemplo, cd /tmp && ls -l).
Advertencia: Dado que shell depende del entorno, es más propenso a fallos impredecibles relacionados con la configuración de PATH, variables de entorno ocultas o comillas complejas.
La Anatomía de un Fallo de Comando en Ansible
Por defecto, Ansible determina el éxito o fracaso de una tarea de los módulos command o shell basándose en el código de retorno (RC) del proceso.
| Código de Retorno (RC) | Interpretación |
|---|---|
rc = 0 |
Éxito (La tarea continúa) |
rc != 0 |
Fallo (La tarea se detiene inmediatamente, el host se marca como fallido) |
Sin embargo, esta verificación simple a menudo no captura los matices de los scripts del mundo real. Un comando puede devolver un RC de 0 pero aún así producir un resultado no deseado (un fallo lógico), o un comando puede devolver un RC no cero esperado (por ejemplo, grep devuelve 1 si no encuentra coincidencias).
Para manejar estos matices, debemos capturar la salida y controlar condicionalmente el estado de fallo.
Paso 1: Capturando la Salida del Comando con register
El primer paso para una depuración efectiva es capturar todos los flujos de salida disponibles en una variable de Ansible usando la palabra clave register. Esto permite inspeccionar el código de retorno, la salida estándar y el error estándar.
Para evitar que el playbook se detenga inmediatamente ante un código de retorno no cero durante las pruebas iniciales, a menudo es útil usar temporalmente ignore_errors: yes.
- name: Ejecutar un comando potencialmente no confiable y capturar resultados
ansible.builtin.shell: |
/usr/local/bin/check_config.sh 2>&1 || exit 1
register: cmd_output
ignore_errors: yes # Permitir temporalmente RC != 0 para continuar
Una vez registrada, la variable cmd_output contendrá varias claves útiles, destacando:
cmd_output.rc: El código de retorno entero.cmd_output.stdout: El flujo de salida estándar.cmd_output.stderr: El flujo de error estándar.cmd_output.failed: Un booleano que indica si Ansible considera actualmente la tarea como fallida.
Paso 2: Inspeccionando Datos Capturados con debug
Usa el módulo debug inmediatamente después de la tarea fallida para inspeccionar el contenido de la variable registrada. Esto ayuda a distinguir entre un verdadero fallo técnico (por ejemplo, comando no encontrado) y un fallo lógico (por ejemplo, el script se ejecutó pero reportó un error interno).
- name: Mostrar la salida capturada completa para depuración
ansible.builtin.debug:
var: cmd_output
# Usar 'when' para mostrar solo si la tarea falló, limpiando la salida
when: cmd_output.failed is defined and cmd_output.failed
- name: Resaltar el contenido de stderr
ansible.builtin.debug:
msg: "STDERR capturado: {{ cmd_output.stderr }}"
when: cmd_output.stderr | length > 0
Al inspeccionar la salida completa, puedes identificar el mensaje de error o patrón específico que indica un verdadero fallo.
Paso 3: Sobrescribiendo el Comportamiento de Fallo Predeterminado con failed_when
La condicional failed_when es la herramienta más poderosa para depurar y gestionar resultados complejos de módulos shell. Te permite definir lógica personalizada, usando expresiones Jinja2, para determinar si una tarea debe marcarse como fallida, independientemente del código de retorno predeterminado.
Escenario A: Manejando un Código de Retorno No Cero Esperado
Algunas utilidades devuelven un código no cero para un resultado esperado. Por ejemplo, grep devuelve 1 cuando no encuentra coincidencias y mayor que 1 para errores reales.
- name: Verificar si existe una configuración, pero no fallar si está ausente
ansible.builtin.command: grep -q '^feature_enabled=true' /etc/myapp.conf
register: grep_result
failed_when: grep_result.rc > 1
changed_when: false
Escenario B: Fallando por Errores Lógicos (RC=0, pero Mala Salida)
Si un script siempre devuelve RC=0 incluso cuando ocurre un error interno, pero imprime una cadena de error específica en stdout o stderr, usa failed_when para capturar esa cadena.
- name: Validar script de conectividad de base de datos
ansible.builtin.shell: /opt/scripts/db_connect_test.sh
register: db_result
# Verificar tanto stdout como stderr en busca de frases de error comunes
failed_when: >
('Conexión rechazada' in db_result.stderr) or
('Fallo de autenticación' in db_result.stdout)
Escenario C: Combinando Verificaciones de RC y Salida
Para verificaciones robustas, combina el código de retorno y las verificaciones de contenido usando operadores lógicos (and, or, paréntesis).
- name: Verificar registros de despliegue
ansible.builtin.shell: tail -n 50 /var/log/deployment.log
register: log_check
# Fallar si el RC es no cero O si la salida exitosa contiene la palabra 'FATAL'
failed_when: log_check.rc != 0 or 'FATAL' in log_check.stdout
Consejo: Al usar
failed_when, generalmente debes eliminarignore_errors: yesa menos que explícitamente quieras que el fallo se registre pero el play continúe.
Mejores Prácticas para una Ejecución Confiable de Comandos
Para minimizar la necesidad de depuración compleja, sigue estos estándares al escribir tareas que usen command o shell:
1. Usa Siempre Rutas Absolutas
No confíes en el $PATH del usuario remoto. Especifica siempre la ruta completa al ejecutable (por ejemplo, /usr/bin/python, no solo python). Esto evita fallos causados por entornos inconsistentes o diferencias sutiles en la ruta de ejecución.
2. Aprovecha las Condicionales sobre la Lógica del Shell
En lugar de usar lógica compleja del shell como || o && dentro del módulo shell, utiliza las condicionales nativas de Ansible (when:, failed_when:, changed_when:) y la palabra clave register. Esto mantiene la lógica del playbook transparente y más fácil de depurar.
3. Controla Explícitamente la Detección de Cambios (changed_when)
Por defecto, command y shell marcan una tarea como changed si el código de retorno es 0. Si tu script se ejecuta pero no realiza cambios en el sistema (por ejemplo, una verificación de estado simple), debes definir manualmente cuándo la tarea resulta en un cambio usando changed_when.
- name: Verificar espacio en disco (no debe resultar en 'changed')
ansible.builtin.command: df -h /data
changed_when: false
4. Usa Módulos de Estado Cuando Sea Posible
Si te encuentras usando shell para verificar la existencia de archivos, iniciar/detener servicios o instalar paquetes, detente y busca un módulo dedicado de Ansible (por ejemplo, ansible.builtin.stat, ansible.builtin.service, ansible.builtin.package). Los módulos dedicados manejan la idempotencia y la verificación de errores internamente, reduciendo significativamente el esfuerzo de depuración.
Conclusión Final
Cuando una tarea shell o command falla, captura el resultado primero, inspecciona rc, stdout y stderr, luego codifica la condición de éxito real en failed_when. Una vez que la tarea sea estable, agrega changed_when para que las verificaciones de estado no muestren cambios falsos en cada ejecución del playbook.