Aller au contenu
Infrastructure as Code medium

Débogueur Ansible interactif : debugger: on_failed et REPL au runtime

75 min de lecture

Logo Ansible

Quand une tâche échoue dans un playbook de 100 lignes, relancer tout est lent et frustrant. Ansible fournit un débogueur interactif qui ouvre un REPL Python au moment de l'échec : on inspecte les variables (p task_vars['x']), on modifie les arguments à chaud (task.args['name'] = 'nginx'), on rejoue la tâche modifiée (redo), on continue le playbook (continue). Activable au niveau task ou play avec debugger: on_failed.

Cette page détaille les 6 commandes du REPL, le workflow recommandé pour fixer une variable au runtime, et les 2 cas où ne pas activer le débogueur (CI/CD, AWX/AAP, cron).

  • Activer debugger: on_failed au niveau task ou play.
  • Inspecter variables, arguments, résultat avec p.
  • Modifier les args (task.args['x'] = 'y') et rejouer avec redo.
  • Injecter une variable manquante via task_vars.
  • Quitter proprement (continue, quit).
  • Quand NE PAS utiliser le débogueur (interactif requis).

Trois niveaux d'activation, du plus fin au plus large.

C'est le réglage le plus sûr, et celui à privilégier quand vous savez déjà quelle tâche pose problème. Le mot-clé debugger est un keyword d'héritage Ansible standard : il s'écrit au même niveau que name ou when, et il ne concerne que la tâche qui le porte. Aucune autre tâche du playbook ne s'arrêtera, même si elle échoue.

tasks:
- name: Tâche fragile
ansible.builtin.dnf:
name: nginx
state: present
debugger: on_failed # ← s'active uniquement sur cette tâche

Posé sur le play, le mot-clé est hérité par toutes les tâches, y compris celles des rôles importés. C'est le bon réglage quand vous ne savez pas encore où ça casse, mais il a un coût : sur un playbook qui traite plusieurs dizaines d'hôtes, chaque échec ouvre un prompt et bloque l'exécution tant que vous n'avez pas répondu. Les niveaux intermédiaires block et role acceptent le même mot-clé et permettent de cibler une portion du play sans l'armer en entier.

- name: Déployer avec le débogueur armé
hosts: webservers
debugger: on_failed # ← s'active sur toutes les tasks de ce play
tasks:
- name: Installer nginx
ansible.builtin.dnf:
name: nginx
state: present

Cette variable correspond au paramètre ENABLE_TASK_DEBUGGER de la configuration Ansible. Elle arme le débogueur sur toutes les tâches de tous les playbooks lancés depuis ce shell, sans toucher au moindre fichier YAML. C'est précisément ce qui en fait le bon choix pour une session de dépannage ponctuelle : rien à écrire, donc rien à oublier de retirer avant de commiter.

Fenêtre de terminal
ANSIBLE_ENABLE_TASK_DEBUGGER=True ansible-playbook lab.yml

Granularité : task > block > role > play > configuration globale. Le niveau le plus proche de la tâche gagne, et un debugger: never posé sur une tâche neutralise donc la variable d'environnement.

Ansible n'accepte que ces cinq chaînes et rien d'autre : une faute de frappe déclenche un AnsibleParserError au chargement du playbook, pas au moment de l'échec. Vous le saurez donc immédiatement. Dans la pratique, seules deux valeurs servent : on_failed pour le dépannage, et never pour désarmer explicitement une tâche sensible dans un play où le débogueur est armé plus haut.

ValeurEffet
alwaysOuvre le REPL après chaque tâche (TDD, très lent)
neverDésactive (default sauf si globalement activé)
on_failedRecommandé, REPL uniquement en cas d'échec
on_unreachableREPL uniquement si l'host devient unreachable
on_skippedREPL si la tâche est skippée (rare)

Quand une tâche échoue, vous voyez :

TASK [Installer nginx-impossible] ***
fatal: [db1.lab]: FAILED! => {"msg": "...nginx-impossible..."}
[db1.lab] TASK: Installer nginx-impossible (debug)>

Le prompt (debug)> indique que vous êtes dans le REPL. Tapez help pour la liste.

Deux lignes de ce tableau ne sont pas des commandes du débogueur mais du Python brut. Le REPL fonctionne ainsi : tout ce qu'il ne reconnaît pas comme une commande est compilé et exécuté dans un espace de noms qui contient task, task_vars, host, play_context et result. C'est ce qui permet d'écrire une affectation directe, mais aussi de provoquer une exception si vous vous trompez de nom ; le REPL affiche alors la classe de l'erreur et reste ouvert.

Deux pièges à connaître avant de manipuler ces commandes. quit ne rend pas la main proprement au shell : il termine ansible-playbook avec le code de sortie 99, ce qui fera échouer un script appelant. Et redo ne rejoue pas forcément la tâche dans l'instant : la tâche est replanifiée, son résultat n'est donc pas nécessairement le prochain que vous verrez passer.

CommandeEffet
p <expr>Pretty-print d'une expression Python, ex p task, p task.args, p task_vars['x'], p result._result
task.args['x'] = 'y'Modifie un argument de la tâche en mémoire
task_vars['x'] = 'y'Injecte une variable dans le scope de la tâche
update_task (u)Recrée la tâche avec les nouvelles variables/args
redo (r)Rejoue la tâche modifiée
continue (c) / quit (q)Continuer / abandonner le playbook

La séquence ci-dessous est la boucle de base du débogueur : observer, corriger, rejouer. Elle vaut d'être exécutée au moins une fois sur un lab, car elle fixe le réflexe qui compte. À l'étape 2, p task.args affiche les arguments après templating, c'est-à-dire les valeurs réellement envoyées au module, pas ce que vous avez écrit dans le YAML. C'est souvent là que le problème saute aux yeux, quand une variable s'est résolue en chaîne vide.

  1. La tâche échoue :

    fatal: [db1.lab]: FAILED! => {"msg": "Failed to install: nginx-impossible..."}
    [db1.lab] TASK: Installer nginx-impossible (debug)>
  2. Inspecter ce qui a été passé :

    (debug)> p task.args
    {'name': 'nginx-impossible', 'state': 'present'}
  3. Modifier l'argument :

    (debug)> task.args['name'] = 'nginx'
  4. Rejouer :

    (debug)> redo
    changed: [db1.lab]
  5. Continuer :

    (debug)> continue
    TASK [Tâche suivante] ***
    ...

Ce qu'il faut retenir : le bug est corrigé sans modifier le YAML ni relancer le playbook depuis le début. Sur un parc de 50 hôtes où une seule tâche plante en fin de course, cela évite de tout rejouer. La correction reste évidemment en mémoire uniquement : le fichier YAML contient toujours l'erreur, et il faudra la corriger avant le prochain run.

Le cas précédent modifiait un argument déjà présent. Celui-ci est différent : la variable n'existe pas du tout, et changer task.args ne suffirait pas puisque le template {{ target_dir }} échouerait à nouveau. Il faut donc injecter la variable dans le scope, puis demander à Ansible de reconstruire la tâche. C'est le rôle exact de update_task, qui recharge la tâche depuis sa structure de données d'origine et la re-template avec le task_vars mis à jour.

- name: Déposer un fichier
ansible.builtin.copy:
dest: "{{ target_dir }}/file.txt"
content: "Hello"
mode: "0644"

Si target_dir n'est pas défini :

fatal: [db1.lab]: FAILED! => {"msg": "...'target_dir' is undefined..."}
[db1.lab] TASK: copy (debug)>

Au REPL :

(debug)> p task_vars.get('target_dir', 'undefined')
'undefined'
(debug)> task_vars['target_dir'] = '/tmp'
(debug)> update_task
(debug)> redo
changed: [db1.lab]

L'ordre des trois commandes n'est pas interchangeable : task_vars['x'] = 'y' injecte la variable, update_task (raccourci u) recrée la tâche et la re-template avec le nouveau scope, puis redo la replanifie. Sauter update_task rejouerait la tâche avec l'ancien templating, donc avec la même erreur undefined variable.

- hosts: webservers
strategy: free # ← DANGER avec le débogueur
debugger: on_failed

Le prompt (debug)> bloque la boucle principale de la stratégie, pas les processus de travail déjà lancés. Avec strategy: linear, cela suffit : tous les hôtes avancent au même rythme, ils sont donc tous à l'arrêt sur la même tâche pendant que vous inspectez. Avec strategy: free, chaque hôte progresse à sa vitesse : pendant que vous lisez task.args pour web1.lab, les tâches déjà distribuées à web2.lab s'exécutent, et l'état que vous observez ne correspond plus à celui du parc.

Le code d'Ansible confirme cette contrainte : la restauration d'état déclenchée par redo sur une tâche run_once n'est prise en charge que pour la stratégie linear. Gardez strategy: linear (la valeur par défaut) dès que le débogueur est armé.

Le débogueur est interactif. À ne jamais activer dans :

  • CI/CD (GitHub Actions, GitLab CI) : pas de stdin → le pipeline freeze indéfiniment.
  • AWX / AAP : les jobs n'ont pas de prompt utilisateur.
  • Cron / systemd timers : pareil.
  • Production : laisser debugger: on_failed dans un playbook commit en prod = bombe à retardement.

Pour ces contextes, préférer :

  • -vvv + logs détaillés (page Verbosité).
  • ANSIBLE_KEEP_REMOTE_FILES=1 + inspection forensic.
  • ansible-navigator replay <artifact.json> (page EE) pour rejouer un échec sans relancer.

Le REPL ne s'apprend pas en lecture : il faut une tâche réellement en échec devant soi. Le lab arme debugger: on_failed sur un playbook qui casse volontairement, d'abord sur un nom de paquet inexistant, puis sur une variable non définie que rien ne fournit. Vous lisez au prompt ce qu'Ansible a réellement envoyé au module, corrigez en mémoire, rejouez, et constatez pourquoi strategy: linear reste obligatoire dès que le débogueur est armé. Le challenge final rejoue le second défaut, un répertoire cible absent à combler au runtime.

  • debugger: on_failed au niveau task ou play, pas en production.
  • p print accepte n'importe quelle expression Python (vars, args, result).
  • task.args['x'] = ... + redo modifie un argument et rejoue.
  • task_vars['x'] = ... + update_task + redo injecte une variable manquante.
  • strategy: linear obligatoire en debug : avec free, les autres hôtes continuent d'avancer pendant que vous êtes au prompt.
  • quit termine ansible-playbook avec le code de sortie 99.
  • Jamais de débogueur en CI, AAP, cron, ou prod (interactivité requise).
  • Découvrir une collection : Retrouver le module derrière un nom qualifié, cause fréquente des tâches qui échouent.
  • Pipeline CI matrice : En CI, le débogueur est proscrit : ce sont les tests automatisés qui prennent le relais.
  • Doc pendant l'examen : Retrouver le nom exact d'un paramètre en moins de trente secondes, jusque depuis le prompt du débogueur.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn