Guide pratique pour déboguer les modules Shell et Command en échec
Déboguez les échecs des modules shell et command d'Ansible avec des exemples de register, stdout, stderr, rc, failed_when et changed_when.
Guide pratique pour déboguer les modules Shell et Command en échec
Les modules command et shell d'Ansible sont utiles lorsqu'aucun module spécialisé n'existe, mais ils peuvent être difficiles à déboguer. Une tâche en échec peut seulement afficher un code de retour, à moins que vous ne capturiez vous-même la sortie de la commande.
Ce guide vous montre comment déboguer les modules shell et command en échec en vérifiant rc, stdout et stderr, puis en utilisant failed_when et changed_when pour qu'Ansible rapporte le résultat réel.
Command vs. Shell : Comprendre la différence
Avant de plonger dans le débogage, il est essentiel de comprendre la différence fondamentale entre les deux modules, car leur environnement d'exécution impacte les modes de défaillance.
ansible.builtin.command
Ce module exécute la commande directement, en contournant l'environnement shell standard. Cela le rend plus sûr et plus prévisible, car il évite les fonctionnalités du shell comme l'interpolation de variables, l'expansion de globes, les pipes (|) et les redirections (>).
Meilleure pratique : Utilisez command lorsque la tâche est simple et ne nécessite pas de fonctionnalités shell.
ansible.builtin.shell
Ce module exécute la commande via le shell standard de l'hôte distant (/bin/sh ou équivalent). Cela est nécessaire pour les opérations complexes, les variables d'environnement, ou lors de l'utilisation de la syntaxe shell standard (par exemple, cd /tmp && ls -l).
Attention : Puisque shell dépend de l'environnement, il est plus sujet à des échecs imprévisibles liés à la configuration du PATH, aux variables d'environnement cachées ou à un quoting complexe.
L'anatomie d'un échec de commande Ansible
Par défaut, Ansible détermine le succès ou l'échec d'une tâche utilisant les modules command ou shell en fonction du code de retour (RC) du processus.
| Code de retour (RC) | Interprétation |
|---|---|
rc = 0 |
Succès (La tâche continue) |
rc != 0 |
Échec (La tâche s'arrête immédiatement, l'hôte est marqué en échec) |
Cependant, cette simple vérification ne capture souvent pas la nuance des scripts réels. Une commande peut retourner un RC de 0 mais produire un résultat indésirable (un échec logique), ou une commande peut retourner un RC non nul attendu (par exemple, grep retourne 1 s'il ne trouve aucune correspondance).
Pour gérer ces nuances, nous devons capturer la sortie et contrôler conditionnellement l'état d'échec.
Étape 1 : Capturer la sortie de la commande avec register
La première étape d'un débogage efficace est de capturer tous les flux de sortie disponibles dans une variable Ansible en utilisant le mot-clé register. Cela permet d'inspecter le code de retour, la sortie standard et l'erreur standard.
Pour éviter que le playbook ne s'arrête immédiatement en cas de code de retour non nul lors des tests initiaux, il est souvent utile d'utiliser temporairement ignore_errors: yes.
- name: Exécuter une commande potentiellement peu fiable et capturer les résultats
ansible.builtin.shell: |
/usr/local/bin/check_config.sh 2>&1 || exit 1
register: cmd_output
ignore_errors: yes # Permet temporairement à RC != 0 de continuer
Une fois enregistrée, la variable cmd_output contiendra plusieurs clés utiles, notamment :
cmd_output.rc: Le code de retour entier.cmd_output.stdout: Le flux de sortie standard.cmd_output.stderr: Le flux d'erreur standard.cmd_output.failed: Un booléen indiquant si Ansible considère actuellement la tâche comme échouée.
Étape 2 : Inspecter les données capturées avec debug
Utilisez le module debug immédiatement après la tâche en échec pour inspecter le contenu de la variable enregistrée. Cela aide à distinguer entre une véritable défaillance technique (par exemple, commande introuvable) et une défaillance logique (par exemple, le script s'est exécuté mais a signalé une erreur interne).
- name: Afficher la sortie capturée complète pour le débogage
ansible.builtin.debug:
var: cmd_output
# Utiliser 'when' pour n'afficher que si la tâche a échoué, nettoyant ainsi la sortie
when: cmd_output.failed is defined and cmd_output.failed
- name: Mettre en évidence le contenu de stderr
ansible.builtin.debug:
msg: "STDERR capturé : {{ cmd_output.stderr }}"
when: cmd_output.stderr | length > 0
En inspectant la sortie complète, vous pouvez identifier le message d'erreur ou le motif spécifique qui indique une véritable défaillance.
Étape 3 : Remplacer le comportement d'échec par défaut avec failed_when
Le conditionnel failed_when est l'outil le plus puissant pour déboguer et gérer les résultats complexes des modules shell. Il vous permet de définir une logique personnalisée, en utilisant des expressions Jinja2, pour déterminer si une tâche doit être marquée comme échouée, indépendamment du code de retour par défaut.
Scénario A : Gérer un code de retour non nul attendu
Certains utilitaires retournent un code non nul pour un résultat attendu. Par exemple, grep retourne 1 lorsqu'il ne trouve aucune correspondance et plus de 1 pour des erreurs réelles.
- name: Vérifier si un paramètre existe, mais ne pas échouer en cas d'absence
ansible.builtin.command: grep -q '^feature_enabled=true' /etc/myapp.conf
register: grep_result
failed_when: grep_result.rc > 1
changed_when: false
Scénario B : Échouer sur des erreurs logiques (RC=0, mais mauvaise sortie)
Si un script retourne toujours RC=0 même lorsqu'une erreur interne se produit, mais imprime une chaîne d'erreur spécifique sur stdout ou stderr, utilisez failed_when pour attraper cette chaîne.
- name: Valider le script de connectivité à la base de données
ansible.builtin.shell: /opt/scripts/db_connect_test.sh
register: db_result
# Vérifier à la fois stdout et stderr pour les phrases d'erreur courantes
failed_when: >
('Connexion refusée' in db_result.stderr) or
('Échec d'authentification' in db_result.stdout)
Scénario C : Combiner les vérifications de RC et de sortie
Pour des vérifications robustes, combinez le code de retour et les vérifications de contenu en utilisant des opérateurs logiques (and, or, parenthèses).
- name: Vérifier les journaux de déploiement
ansible.builtin.shell: tail -n 50 /var/log/deployment.log
register: log_check
# Échouer si le RC est non nul OU si la sortie réussie contient le mot 'FATAL'
failed_when: log_check.rc != 0 or 'FATAL' in log_check.stdout
Astuce : Lorsque vous utilisez
failed_when, vous devriez généralement supprimerignore_errors: yessauf si vous voulez explicitement que l'échec soit enregistré mais que le play continue.
Meilleures pratiques pour une exécution fiable des commandes
Pour minimiser le besoin de débogage complexe, suivez ces normes lors de l'écriture de tâches utilisant command ou shell :
1. Toujours utiliser des chemins absolus
Ne vous fiez pas au $PATH de l'utilisateur distant. Spécifiez toujours le chemin complet vers l'exécutable (par exemple, /usr/bin/python, pas seulement python). Cela évite les échecs causés par des environnements incohérents ou des différences subtiles dans le chemin d'exécution.
2. Tirer parti des conditionnels plutôt que de la logique shell
Au lieu d'utiliser une logique shell complexe comme || ou && à l'intérieur du module shell, utilisez les conditionnels natifs d'Ansible (when:, failed_when:, changed_when:) et le mot-clé register. Cela rend la logique du playbook transparente et plus facile à déboguer.
3. Contrôler explicitement la détection de changement (changed_when)
Par défaut, command et shell marquent une tâche comme changed si le code de retour est 0. Si votre script s'exécute mais n'apporte aucune modification au système (par exemple, une simple vérification de statut), vous devez définir manuellement quand la tâche entraîne un changement en utilisant changed_when.
- name: Vérifier l'espace disque (ne devrait pas entraîner de 'changed')
ansible.builtin.command: df -h /data
changed_when: false
4. Utiliser des modules d'état lorsque c'est possible
Si vous vous surprenez à utiliser shell pour vérifier l'existence d'un fichier, démarrer/arrêter des services ou installer des paquets, arrêtez-vous et cherchez un module Ansible dédié (par exemple, ansible.builtin.stat, ansible.builtin.service, ansible.builtin.package). Les modules dédiés gèrent l'idempotence et la vérification des erreurs en interne, réduisant considérablement l'effort de débogage.
Conclusion finale
Lorsqu'une tâche shell ou command échoue, capturez d'abord le résultat, inspectez rc, stdout et stderr, puis encodez la condition de succès réelle dans failed_when. Une fois la tâche stable, ajoutez changed_when pour que les vérifications de statut n'affichent pas de faux changements à chaque exécution du playbook.