Cette formation vous a menés du premier ansible -m ping jusqu’à un playbook d’orchestration multi-rôles complet. Pour clore le parcours, voici un cas réel, tel qu’il peut tourner chaque nuit sur un parc de serveurs Docker : un playbook qui met à jour et redémarre plusieurs dizaines de stacks, dans un ordre précis, sans jamais tout casser d’un coup.
ℹ️ Cette leçon est une lecture commentée, pas un TP à reproduire à l’identique — le playbook manipule de vrais volumes Docker et mérite d’être compris avant d’être testé. Un TP dérivé, plus simple, suit dans le grand TP final.
Le contexte : pourquoi ce playbook existe
Reprenez l’histoire racontée dans l’introduction de cette formation : un script shell qui plantait à moitié parcours sur un parc de serveurs Docker, laissant la moitié du parc à jour et l’autre moitié dans un état incohérent. Ce playbook est la réponse concrète à ce problème — construit avec tout ce que vous avez appris depuis la section 3.
Voici sa structure, tâche par tâche.
1. Auditer avant d’agir
- name: Détecter les doublons de fichiers compose
ansible.builtin.find:
paths: "{{ item.path }}"
patterns:
- "compose.yml"
- "compose.yaml"
- "docker-compose.yml"
- "docker-compose.yaml"
depth: 1
loop: "{{ (docker_stacks | default([])) + (docker_stacks_priority | default([])) }}"
loop_control:
label: "{{ item.path }}"
register: compose_files_audit
changed_when: false
Trois réflexes vus tout au long de cette formation se combinent ici :
loop(section 4) parcourt deux listes fusionnées avec+—docker_stacksetdocker_stacks_priority, exactement le pattern qu’on avait identifié en 2.4 comme relevant dehost_vars.default([])protège contre une variable qui n’existerait pas sur un hôte donné — un réflexe de robustesse qu’on n’avait pas encore rencontré : sidocker_stacks_priorityn’est pas défini pour un hôte, on utilise une liste vide plutôt que de faire planter la tâche.changed_when: false(vu en 3.3) : cette tâche ne fait que lire, jamais modifier — elle ne doit donc jamais afficherchanged, quel que soit son résultat.
2. Traiter les stacks prioritaires, avec du Jinja2 à l’intérieur d’un shell
- name: "PRIORITAIRE | Pull → Down → Post-cmds → Up : {{ item.path }}"
ansible.builtin.shell: |
set -e
cd {{ item.path }}
{% if item.compose_files is defined %}
FARGS="{{ item.compose_files | map('regex_replace', '^', '-f ') | join(' ') }}"
{% else %}
FARGS=""
{% endif %}
echo ">>> Pull : {{ item.path }}"
docker compose $FARGS pull
echo ">>> Down : {{ item.path }}"
docker compose $FARGS down -v
sleep 1
echo ">>> Up : {{ item.path }}"
docker compose $FARGS up -d
sleep {{ item.extra_sleep | default(5) }}
loop: "{{ docker_stacks_priority | default([]) }}"
loop_control:
label: "{{ item.path }}"
Voilà une utilisation du module shell qu’on n’avait pas encore vue : la syntaxe Jinja2 ({% if %}, vue en 5.2 pour les templates) fonctionne aussi à l’intérieur d’un bloc shell, pas seulement dans un fichier .j2. Ansible évalue tout le bloc comme un template avant de l’envoyer au shell distant — c’est ce qui permet de construire dynamiquement les arguments -f selon que item.compose_files existe ou non pour cette stack précise.
Remarquez aussi item.extra_sleep | default(5) : exactement le filtre default déjà vu, mais appliqué cette fois à une propriété d’un dictionnaire dans une boucle — chaque stack prioritaire peut avoir sa propre pause de sécurité après redémarrage (15 secondes pour Traefik, 60 pour Authentik, comme on l’a vu en 2.4), sinon 5 secondes par défaut.
⚠️ On utilise ici shell plutôt qu’un module Docker dédié (community.docker.docker_compose_v2, par exemple) — un choix pragmatique pour orchestrer une séquence pull/down/up complète en une seule tâche lisible, plutôt qu’idéal du point de vue idempotence pure. Le compromis est volontaire : cette tâche a vocation à s’exécuter une fois par nuit, sur une action de maintenance, pas à être rejouée en continu comme un playbook de configuration classique.
3. Ne traiter que les stacks réellement actives
- name: "STANDARD | Vérifier les stacks démarrées"
ansible.builtin.shell: |
cd {{ item.path }}
docker compose ps -q 2>/dev/null || true
loop: "{{ docker_stacks | default([]) }}"
loop_control:
label: "{{ item.path }}"
register: stack_status
changed_when: false
failed_when: false
- name: "STANDARD | Définir les stacks actives"
ansible.builtin.set_fact:
active_stacks: >-
{{ stack_status.results
| selectattr('stdout', '!=', '')
| map(attribute='item')
| list }}
C’est le passage le plus avancé du playbook, et il combine deux notions qu’on a vues séparément :
register(croisé en 4.3 et 7.5) capture le résultat de toutes les itérations de la boucle dansstack_status.results— une liste de résultats, un par stack vérifiée.set_factcrée une nouvelle variable à la volée, à partir d’un calcul — quelque chose qu’on n’avait pas encore rencontré. Ici, elle filtrestack_status.resultsavecselectattr(ne garder que les résultats dontstdoutn’est pas vide, c’est-à-dire les stacks qui ont bien des conteneurs actifs), puismap(attribute='item')pour ne récupérer que l’élément d’origine (le dictionnaire{path: ...}) de chaque résultat filtré.
Le résultat : active_stacks ne contient plus que les stacks réellement démarrées — inutile de faire un pull/down/up sur une stack déjà arrêtée intentionnellement.
4. Nettoyer en fin de traitement
- name: Pause avant nettoyage Docker
ansible.builtin.pause:
seconds: 10
- name: Nettoyage Docker (images/volumes/réseaux orphelins)
ansible.builtin.command: docker system prune --all --force
Deux dernières notions : ansible.builtin.pause marque une pause explicite et documentée dans le déroulé (plus lisible qu’un simple sleep shell pour une pause au niveau du playbook plutôt qu’à l’intérieur d’une commande), et docker system prune referme la boucle en récupérant l’espace disque libéré par toute la nuit de mises à jour.
Ce que cette étude de cas illustre
Ce playbook n’a rien de théorique — c’est du code qui tourne réellement, chaque nuit, sur un vrai parc de serveurs Docker. Et pourtant, chaque brique qui le compose a été vue dans cette formation : loop (4.4), register/changed_when (3.3, 4.3), Jinja2 (5.2), default et filtres de liste, set_fact. La seule vraie nouveauté, c’est la combinaison de ces briques pour résoudre un problème de production concret — exactement la trajectoire qu’on vous souhaite pour vos propres playbooks Nordika.
Ce qu’il faut retenir
- Jinja2 (
{% if %},{% for %}) fonctionne à l’intérieur d’un blocshell, pas seulement dans un fichier.j2. set_fact+selectattr/mappermettent de filtrer dynamiquement une liste de résultats, pour n’agir que sur ce qui est réellement concerné.- Un traitement “prioritaire” séparé du traitement “standard” (comme en 9.2 avec
serial) reste une bonne pratique même en dehors d’Ansible : traiter d’abord ce qui est critique, dans un ordre maîtrisé, avant le reste du parc. - Même un playbook de production avancé reste lisible une fois qu’on connaît les briques de base — rien dans ce fichier n’est plus complexe que ce que vous avez déjà pratiqué dans cette formation.