Ansible: Variables et logique

Notions abordées

  • Variables dans Ansible
  • Structures conditionnelles (if, when)
  • Boucles (loops)
  • Handlers

Suivi d'un laboratoire intégrant ces notions.

Variables

Dans Ansible, dès que les playbooks deviennent légèrement plus complexes, il devient primordial de déclarer et d'utiliser des variables. En effet, les variables permettent de rendre les playbooks plus dynamiques, réutilisables et faciles à maintenir.

Prenons l'exemple d'une entreprise qui déploie une application web sur plusieurs serveurs. Chaque serveur peut avoir des configurations différentes, telles que le port d'écoute, le nom de la base de données, ou encore les chemins des fichiers de configuration. Nous pourrions définir ces informations directement dans les playbooks, comme nous l'aurions fait en utilisant les notions apprises la semaine dernière:

---
- name: Déployer l'application web
  hosts: webservers
  tasks:
    - name: Configurer le port d'écoute
      lineinfile:
        path: /etc/myapp/config.ini
        regexp: '^port='
        line: 'port=8080'
    - name: Configurer la base de données
      lineinfile:
        path: /etc/myapp/config.ini
        regexp: '^database='
        line: 'database=mydb'

Cependant, cette façon de faire comporte ses limitations:

  • Si nous devons changer le port d'écoute ou le nom de la base de données, nous devons modifier manuellement chaque playbook où ces valeurs sont définies.
  • Si nous avons plusieurs serveurs avec des configurations différentes, nous devrons créer des playbooks distincts pour chaque serveur, ce qui entraîne une duplication de code et complique la maintenance.
  • Retrouver ces valeurs dans un long playbook peut devenir fastidieux.

C'est là qu'entre en jeu les variables.En utilisant des variables, nous pouvons écrire un seul playbook qui s'adapte à chaque serveur en fonction de ses besoins spécifiques.

Hiérarchie des variables

Il existe plusieurs façons de définir des variables dans Ansible. Ils sont listés ici par ordre de priorité, de la plus basse à la plus haute (c'est-à-dire que les variables définies plus bas dans la liste écraseront les variables définies plus haut) :

  1. Variables définies dans les rôles dans defaults (defaults/main.yml)
  2. Variables définies dans les fichiers d'inventaire:
    1. Variables définies dans le répertoire d'inventaire hosts.ini ou hosts.yml pour un groupe spécifique
    2. Variables définies dans le répertoire group_vars/all
    3. Variables définies dans le répertoire group_vars/<nom_du_groupe>
    4. Variables définies dans le répertoire d'inventaire hosts.ini ou hosts.yml pour un hôte spécifique
    5. Variables définies dans le répertoire host_vars/<nom_de_l_hôte>
  3. Variables définies dans les playbooks
    1. avec le mot-clé vars
    2. avec le mot-clé vars_files
  4. Variables définies dans les rôles dans vars (vars/main.yml)
  5. Variables définies avec set_fact ou register
  6. Variables définies via la ligne de commande avec l'option -e ou --extra-vars

Puisque nous verrons les rôles au prochain cours, nous verrons les points 2 à 7 (sauf 4) dans ce cours.

Variables dans les fichiers d'inventaire

Nous avons déjà vu comment définir des groupes et des hôtes dans un fichier d'inventaire, que nous avions appelé hosts.ini. Nous pouvons également définir des variables spécifiques à un hôte ou à un groupe d'hôtes dans ce fichier.

Les variables les plus communes à définir dans un fichier d'inventaire sont les informations de connexion, telles que l'utilisateur SSH, le mot de passe, ou la clé privée.

Par exemple:

[webservers]
web1 ansible_host=192.168.1.10 ansible_user=alice ansible_ssh_private_key_file=/path/to/key
web2 ansible_host=192.168.1.11 ansible_user=bob ansible_ssh_private_key_file=/path/to/key
[dbservers]
db1 ansible_host=192.168.1.20 ansible_user=carol ansible_ssh_private_key_file=/path/to/key

[webservers:vars]
http_port=80
max_clients=200

[all:vars]
ntp_server=ntp.example.com

Ici, les variables ansible_user et ansible_ssh_private_key_file sont définies pour chaque hôte individuellement, les variables http_port et max_clients sont définies pour tous les hôtes du groupe webservers, tandis que ntp_server est définie pour tous les hôtes de l'inventaire.

⚠️ S'il y a conflit entre des variables définies dans le fichier d'inventaire, alors la priorité sera accordée aux variables définies dans les sections spécifiques aux hôtes par rapport aux sections spécifiques aux groupes.

Variables définies dans les dossiers host_vars et group_vars

Ansible permet également de définir des variables dans des fichiers séparés situés dans les dossiers host_vars et group_vars. Ces dossiers doivent être situés dans le même répertoire que votre fichier d'inventaire ou dans le répertoire racine de votre projet Ansible. Ce sont des dossiers spéciaux qu'Ansible reconnaît automatiquement.

Supposons que nous gardons le fichier d'inventaire indiqué plus haut, voici comment organiser ces dossiers :

├── hosts.ini
├── host_vars/
│   ├── web1.yml
│   └── db1.yml
└── group_vars/
    ├── webservers.yml
    └── dbservers.yml

Les fichiers YAML dans host_vars et group_vars contiennent des paires clé-valeur définissant les variables. Comme leurs noms l'indiquent, les fichiers dans host_vars sont spécifiques à un hôte, tandis que ceux dans group_vars sont spécifiques à un groupe d'hôtes. Par exemple, le fichier host_vars/web1.yml pourrait contenir :

---
apache_port: 8080
document_root: /var/www/html/webserver1

Et le fichier host_vars/webserver2.yml pourrait contenir :

---
apache_port: 9090
document_root: /var/www/html/webserver2

Et le fichier group_vars/webservers.yml pourrait contenir :

---
max_clients: 300
enable_ssl: true

Ici, apache_port et document_root sont des variables spécifiques à l'hôte web1, tandis que max_clients et enable_ssl sont des variables communes à tous les hôtes du groupe webservers. Si un conflit de noms de variables survient, les variables définies dans host_vars auront la priorité sur celles définies dans group_vars.

Variables définies dans les playbooks

Les variables peuvent également être définies directement dans les playbooks Ansible en utilisant les mots-clé vars ou vars_files. Par exemple, dans un playbook, vous pouvez définir des variables comme suit :

---
- name: Introduction aux variables
  hosts: localhost
  vars:
    app_name: "MonApplication"
    app_version: "1.2.3"
    app_port: 8080
  
  tasks:
    - name: Afficher les informations de l'application
      debug:
        msg: "L'application {{ app_name }} version {{ app_version }} écoute sur le port {{ app_port }}"

Ici, les variables app_name, app_version, et app_port sont définies dans le playbook et peuvent être utilisées dans les tâches du playbook.

Il est aussi possible de définir un fichier externe contenant des variables et de l'inclure dans le playbook avec vars_files :

---
- name: Utiliser des variables depuis un fichier
  hosts: localhost
  vars_files:
    - vars/app_config.yml
  
  tasks:
    - name: Afficher la configuration de la base de données
      debug:
        msg: "DB: {{ database.user }}@{{ database.host }}:{{ database.port }}/{{ database.name }}"
    
    - name: Afficher la configuration de backup
      debug:
        msg: "Backup activé: {{ backup.enabled }}, Rétention: {{ backup.retention_days }} jours"

Avec un fichier vars/app_config.yml comme suit :

---
database:
  host: "db.example.com"
  port: 5432
  name: "production_db"
  user: "app_user"

backup:
  enabled: true
  retention_days: 30
  destination: "/backup/db"

Variables définies avec set_fact ou register

Ansible permet également de définir des variables dynamiquement pendant l'exécution d'un playbook en utilisant le module set_fact ou register. Cela est utile lorsque vous souhaitez créer ou modifier des variables en fonction des résultats des tâches précédentes.

Nous avons vu au cours précédent l'utilisation de register pour capturer la sortie d'une commande. Par exemple :

---
- name: Capturer la sortie d'une commande
  hosts: localhost
  tasks:
    - name: Obtenir la date actuelle
      command: date
      register: current_date
    - name: Afficher la date actuelle
      debug:
        msg: "La date actuelle est : {{ current_date.stdout }}"

Ici, la tâche command: date exécute la commande date et stocke sa sortie dans la variable current_date. La tâche suivante utilise cette variable pour afficher la date actuelle.

Le module set_fact permet également de définir des variables personnalisées. Par exemple :

---
- name: Définir des variables dynamiques avec set_fact
  hosts: localhost
  tasks:
    - name: Définir une variable dynamique
      set_fact:
        dynamic_variable: "Valeur dynamique"
    - name: Afficher la variable dynamique
      debug:
        msg: "La variable dynamique est : {{ dynamic_variable }}"

Il est utile d'utiliser set_fact lorsque vous souhaitez créer des variables basées sur des conditions ou des résultats de tâches précédentes. Nous verrons les conditions plus en détail dans la section suivante. En cas de conflit de noms, les variables définies avec set_fact auront la priorité sur celles définies avec register.

Variables passées via la ligne de commande

Il est également possible de passer des variables directement via la ligne de commande lors de l'exécution d'un playbook en utilisant l'option -e ou --extra-vars. Par exemple :

ansible-playbook playbook.yml -e "app_port=9090 app_debug=true"

Cela définit les variables app_port et app_debug pour le playbook en cours d'exécution. Les variables passées de cette manière ont la plus haute priorité et écraseront toutes les autres définitions de variables.

Conditions et boucles

Ansible permet d'utiliser des conditions et des boucles pour contrôler le flux d'exécution des tâches dans un playbook. Les conditions sont définies à l'aide du mot-clé when, tandis que les boucles sont définies à l'aide du mot-clé loop.

Conditions avec when

Nous utilisons le mot-clé when pour exécuter une tâche uniquement si une condition spécifique est remplie. Par exemple :

---
- name: Exemple de condition avec when
  hosts: localhost
  vars:
    is_production: true
  tasks:
    - name: Tâche exécutée uniquement en production
      debug:
        msg: "Ceci est une tâche de production."
      when: is_production

Ici, la tâche ne sera exécutée que si la variable is_production est vraie. La valeur de la condition peut être basée sur n'importe quelle variable définie dans le playbook, l'inventaire, ou même des faits collectés à partir des hôtes. Voici un exemple comportant différentes conditions :

---
- name: Conditions basiques
  hosts: localhost
  vars:
    environment: "production"
    enable_monitoring: true
    app_version: 2.5
  
  tasks:
    - name: Tâche seulement en production
      debug:
        msg: "Configuration de production activée"
      when: environment == "production"
    
    - name: Tâche seulement si monitoring activé
      debug:
        msg: "Installation des outils de monitoring"
      when: enable_monitoring
    
    - name: Vérifier la version de l'application
      debug:
        msg: "Version récente détectée"
      when: app_version >= 2.0
    
    - name: Conditions multiples (AND)
      debug:
        msg: "Production avec monitoring"
      when:
        - environment == "production"
        - enable_monitoring
    
    - name: Conditions multiples (OR)
      debug:
        msg: "Environnement de développement ou de test"
      when: environment == "dev" or environment == "test"

Explications des conditions:

  • La tâche "Tâche seulement en production" s'exécute uniquement si l'environnement est "production" (comparaison de chaînes).
  • La tâche "Tâche seulement si monitoring activé" s'exécute si la variable enable_monitoring est vraie (variable booléenne).
  • La tâche "Vérifier la version de l'application" s'exécute si la version de l'application est supérieure ou égale à 2.0 (comparaison numérique).
  • La tâche "Conditions multiples (AND)" s'exécute si les deux conditions sont vraies (utilisation de listes pour AND).
  • La tâche "Conditions multiples (OR)" s'exécute si l'une des conditions est vraie (utilisation de l'opérateur or).

Comparaison avec variables facts

Ansible collecte automatiquement des informations sur les hôtes appelées facts. Vous l'avez remarqué dans le cours précédent avec la tâche gather_facts qui collecte ces faits. Il est possible de utiliser ces faits dans des conditions when. Par exemple :

---
- name: Exemple avec des facts
  hosts: all
  tasks:
    - name: Tâche exécutée uniquement sur les hôtes Linux
      debug:
        msg: "Ceci est un hôte Linux."
      when: ansible_facts['os_family'] == "Debian"

Ici, la tâche ne sera exécutée que sur les hôtes dont la famille d'OS est "Debian". Les facts disponibles peuvent être consultés en exécutant la commande ansible -m setup <nom_de_l_hôte>. Le résultat sera assez long!

Conditions avec des variables enregistrées

Il est également possible d'utiliser des variables enregistrées avec register dans des conditions when. Par exemple :

---- 
name: Exemple avec des variables enregistrées
  hosts: localhost
    tasks:
        - name: Exécuter une commande et enregistrer la sortie
        command: /bin/echo "Hello World"
        register: command_result
    
        - name: Tâche exécutée si la commande a réussi
        debug:
            msg: "La commande a réussi avec la sortie : {{ command_result.stdout }}"
        when: command_result.rc == 0

Ici, la tâche de débogage ne sera exécutée que si la commande précédente a réussi (code de retour rc égal à 0). Pour cela, il faut d'accéder aux attributs de la variable enregistrée. Pour une commande, les attributs courants sont :

  • stdout : la sortie standard de la commande
  • stderr : la sortie d'erreur de la commande
  • rc : le code de retour de la commande (0 indique généralement le succès)

Cependant, les attributs disponibles peuvent varier en fonction du module utilisé. Pour connaître les attributs spécifiques disponibles pour une tâche enregistrée, vous pouvez consulter la documentation du module Ansible correspondant.

Les mots-clés failed_when et changed_when

Par défaut, une tâche Ansible est considérée comme ayant échoué si le code de retour (rc) est différent de zéro. De plus, une tâche est considérée comme ayant modifié l'état du système (c'est-à-dire "changed") si elle effectue une action qui modifie effectivement quelque chose sur l'hôte cible.

Cependant, il est possible de personnaliser cette logique en utilisant les mots-clés failed_when et changed_when.

Par exemple :

---
- name: Exemple avec failed_when
  hosts: localhost
  tasks:
    - name: Exécuter une commande personnalisée
      command: /bin/false
      register: command_result
      failed_when: command_result.rc != 0 and "'specific error' in command_result.stderr"

Ici, la tâche sera considérée comme ayant échoué uniquement si le code de retour est différent de zéro et que la sortie d'erreur contient la chaîne "specific error". Sinon, la tâche sera considérée comme réussie, même si le code de retour est différent de zéro. Pourquoi définir l'état d'échec est important? Parce que cela affecte le flux d'exécution du playbook. Si une tâche échoue, Ansible arrêtera l'exécution du playbook par défaut, sauf si vous utilisez ignore_errors: yes ou gérez les erreurs avec des blocs rescue. Il est également possible de simplement mettre failed_when: false pour forcer une tâche à toujours réussir, quel que soit son résultat.

De même, le mot-clé changed_when permet de contrôler si une tâche est considérée comme ayant modifié l'état du système. Par exemple :

--- 
name: Exemple avec changed_when
  hosts: localhost
    tasks:
        - name: Exécuter une commande
        command: /bin/true
        register: command_result
        changed_when: false

Ici, la tâche sera toujours considérée comme n'ayant pas modifié l'état du système. Par défaut, le module command considère qu'il a modifié l'état du système, mais en utilisant changed_when: false, nous annulons ce comportement. Cela aura un impact sur les rapports d'Ansible, car les tâches "changed" sont souvent utilisées pour indiquer que quelque chose a été modifié sur l'hôte cible.

Exemple complet avec conditions

Voici un exemple complet combinant plusieurs types de conditions :

---
- name: Conditions avec register
  hosts: localhost
  
  tasks:
    - name: Vérifier si un service existe
      stat:
        path: /etc/systemd/system/nginx.service
      register: nginx_service
    
    - name: Message si le service existe
      debug:
        msg: "Nginx est installé en tant que service"
      when: nginx_service.stat.exists
    
    - name: Exécuter une commande
      command: echo "test"
      register: cmd_result
      changed_when: false
    
    - name: Vérifier si la commande a réussi
      debug:
        msg: "Commande réussie avec code: {{ cmd_result.rc }}"
      when: cmd_result.rc == 0
    
    - name: Test avec failed_when
      command: /bin/false
      register: failing_command
      failed_when: false  # Empêcher l'échec du playbook
    
    - name: Réagir à l'échec
      debug:
        msg: "La commande a échoué comme prévu"
      when: failing_command.rc != 0

Explications :

  • La première tâche utilise le module stat pour vérifier si le service Nginx existe et enregistre le résultat dans nginx_service. La tâche suivante affiche un message si le service existe, en utilisant une condition when et l'attribut exists de la variable enregistrée.
  • La troisième tâche exécute une commande simple et enregistre le résultat dans cmd_result. Cette tâche sera considérée comme n'ayant pas modifié l'état du système grâce à changed_when: false.
  • La quatrième tâche affiche un message si la commande précédente a réussi, en vérifiant le code de retour avec une condition when.
  • La cinquième tâche exécute une commande qui échoue, mais grâce à failed_when: false, elle ne provoque pas l'échec du playbook.
  • La dernière tâche affiche un message si la commande a effectivement échoué, en utilisant une condition when basée sur le code de retour.

Boucles avec loop

Ansible permet d'exécuter une tâche plusieurs fois en utilisant des boucles définies avec le mot-clé loop. Par exemple :

---
- name: Exemple de boucle avec loop
  hosts: localhost
  tasks:
    - name: Installer plusieurs paquets
      apt:
        name: "{{ item }}"
        state: present
      loop:
        - git
        - curl
        - vim

La syntaxe {{ item }} est utilisée pour référencer l'élément courant de la boucle. Dans cet exemple, la tâche installe les paquets git, curl, et vim un par un.

Il est possible d'itérer sur des listes plus complexes, comme des dictionnaires. Par exemple :

---
- name: Exemple de boucle avec dictionnaires
    hosts: localhost
    tasks:
        - name: Créer plusieurs utilisateurs
        user:
            name: "{{ item.name }}"
            state: present
            uid: "{{ item.uid }}"
        loop:
            - { name: "alice", uid: 1001 }
            - { name: "bob", uid: 1002 }
            - { name: "carol", uid: 1003 }

La syntaxe {{ item.name }} et {{ item.uid }} est utilisée pour accéder aux clés du dictionnaire courant dans la boucle.

Boucle avec condition

Il est également possible de combiner des boucles avec des conditions when. Par exemple :

- name: Boucle avec condition
  hosts: localhost
  tasks:
    - name: Installer des paquets conditionnellement
      apt:
        name: "{{ item }}"
        state: present
      loop:
        - git
        - curl
        - vim
      when: item != "vim"

Ici, la tâche installe les paquets git et curl, mais pas vim, grâce à la condition when qui vérifie que l'élément courant n'est pas "vim".

Le même principe est aussi applicable aux boucles sur des dictionnaires.

---
- name: Boucles conditionnelles
  hosts: localhost
  vars:
    users:
      - { name: 'alice', admin: true, active: true }
      - { name: 'bob', admin: false, active: true }
      - { name: 'charlie', admin: true, active: false }
      - { name: 'dave', admin: false, active: true }
  
  tasks:
    - name: Traiter seulement les utilisateurs actifs
      debug:
        msg: "Configuration de {{ item.name }}"
      loop: "{{ users }}"
      when: item.active
    
    - name: Traiter seulement les admins actifs
      debug:
        msg: "Droits admin pour {{ item.name }}"
      loop: "{{ users }}"
      when:
        - item.active
        - item.admin

Handlers

Les handlers sont des tâches spéciales dans Ansible qui ne s'exécutent que lorsqu'elles sont "notifiées" par d'autres tâches. Ils sont généralement utilisés pour effectuer des actions qui doivent être effectuées après qu'une modification a été apportée, comme redémarrer un service après la mise à jour de sa configuration. Par exemple, si vous modifiez un fichier de configuration pour un service web, vous pouvez notifier un handler pour redémarrer ce service afin que les modifications prennent effet.

---
- name: Exemple avec handlers
  hosts: localhost
  tasks:
    - name: Mettre à jour le fichier de configuration
      template:
        src: webserver.conf.j2
        dest: /etc/webserver/webserver.conf
      notify: Redémarrer le service webserver
  handlers:
    - name: Redémarrer le service webserver
      service:
        name: webserver
        state: restarted

À remarquer:

  • La mention notify: Redémarrer le service webserver dans la tâche indique qu'elle notifie le handler nommé "Redémarrer le service webserver" si la tâche modifie le fichier de configuration.
  • Le handler "Redémarrer le service webserver" est défini dans la section handlers et utilise le module service pour redémarrer le service webserver.
  • L'indentation est importante : les handlers doivent être définis au même niveau que les tâches, mais dans une section distincte appelée handlers.

Voici quelques points importants à retenir concernant les handlers :

  • Les handlers ne seront exécutés que si une tâche les notifie ET que cette tâche a effectivement apporté une modification. Si une tâche ne modifie rien (par exemple, si le fichier de configuration est déjà à jour, le paquet est déjà installé, etc.), alors le handler ne sera pas exécuté. De plus, si une tâche qui devrait notifier un handler n'est pas exécutée (par exemple à cause d’une condition when), alors le handler ne sera pas exécuté.
  • Les handlers s’exécutent une seule fois, et uniquement à la fin d’un play. Si vous devez absolument changer ce comportement et exécuter les handlers au milieu d’un playbook, vous pouvez utiliser le module meta, par exemple :
- meta: flush_handlers
  • Si le play échoue sur un hôte (ou tous les hôtes) avant que les handlers ne soient notifiés, les handlers ne seront jamais exécutés. Si vous souhaitez que les handlers s’exécutent même après l’échec du playbook, vous pouvez utiliser le module meta comme dans l’exemple ci-dessus dans une tâche séparée, ou utiliser l’option de ligne de commande :
--force-handlers

Notez toutefois que les handlers ne s’exécuteront jamais sur les hôtes devenus injoignables pendant l’exécution du playbook.

Exemple complet avec handlers et conditions :

---
- name: Handlers multiples
  hosts: localhost
  
  tasks:
    - name: Modifier la configuration Apache
      debug:
        msg: "Modification de httpd.conf"
      changed_when: true
      notify:
        - reload_apache
        - notify_monitoring
    
    - name: Modifier un certificat SSL
      debug:
        msg: "Installation du nouveau certificat"
      changed_when: true
      notify:
        - reload_apache
        - send_slack_notification
  
  handlers:
    - name: reload_apache
      debug:
        msg: ">>> Rechargement d'Apache <<<"
    
    - name: notify_monitoring
      debug:
        msg: ">>> Notification au système de monitoring <<<"
    
    - name: send_slack_notification
      debug:
        msg: ">>> Envoi de notification Slack <<<"

Dans cet exemple :

  • La tâche "Modifier la configuration Apache" notifie deux handlers : reload_apache et notify_monitoring.
  • La tâche "Modifier un certificat SSL" notifie également le handler reload_apache ainsi que send_slack_notification.
  • Le handler reload_apache sera exécuté une seule fois à la fin du play, même s'il est notifié par deux tâches différentes.
  • Même si les tâches utilisent le module debug qui ne modifie rien, nous forçons le changement avec changed_when: true pour illustrer le fonctionnement des handlers. Ainsi, les handlers seront bien exécutés.

Utilisation de listen pour les handlers

Ansible permet également d'utiliser le mot-clé listen pour regrouper plusieurs notifications sous un même nom de handler. Cela permet de simplifier la gestion des notifications lorsque plusieurs tâches doivent notifier le même handler. Voici un exemple :

---
- name: Handlers avec listen
  hosts: localhost
  
  tasks:
    - name: Modifier la configuration web
      debug:
        msg: "Modification de la config web"
      changed_when: true
      notify: restart_web_stack
    
    - name: Mettre à jour le certificat
      debug:
        msg: "Mise à jour du certificat"
      changed_when: true
      notify: restart_web_stack
  
  handlers:
    - name: Arrêter nginx
      debug:
        msg: ">>> Arrêt de Nginx <<<"
      listen: restart_web_stack
    
    - name: Vider le cache
      debug:
        msg: ">>> Vidage du cache <<<"
      listen: restart_web_stack
    
    - name: Démarrer nginx
      debug:
        msg: ">>> Démarrage de Nginx <<<"
      listen: restart_web_stack

Dans cet exemple :

  • Les deux tâches "Modifier la configuration web" et "Mettre à jour le certificat" notifient le même handler restart_web_stack.
  • Le handler restart_web_stack est défini avec plusieurs sous-tâches utilisant le mot-clé listen. Ainsi, lorsque restart_web_stack est notifié, toutes les sous-tâches associées seront exécutées dans l'ordre où elles sont définies.
  • Cela permet de regrouper plusieurs actions liées à une même notification sous un seul nom de handler, simplifiant ainsi la gestion des notifications dans le playbook.

Forcer l'exécution des handlers avec meta: flush_handlers

Par défaut, les handlers sont exécutés à la fin d'un play. Cependant, il est possible de forcer l'exécution des handlers à un moment spécifique dans le playbook en utilisant le module meta avec l'option flush_handlers. Voici un exemple :

---
- name: Handlers avec meta flush
  hosts: localhost
  
  tasks:
    - name: Modifier la configuration critique
      debug:
        msg: "Modification critique de la config"
      changed_when: true
      notify: restart_critical_service
    
    - name: Forcer l'exécution des handlers maintenant
      meta: flush_handlers
    
    - name: Tâche qui dépend du redémarrage
      debug:
        msg: "Cette tâche s'exécute après le redémarrage"
    
    - name: Autre modification
      debug:
        msg: "Autre modification"
      changed_when: true
      notify: restart_critical_service
  
  handlers:
    - name: restart_critical_service
      debug:
        msg: ">>> REDÉMARRAGE DU SERVICE CRITIQUE <<<"

Dans cet exemple :

  • La tâche "Modifier la configuration critique" notifie le handler restart_critical_service.
  • La tâche suivante utilise meta: flush_handlers pour forcer l'exécution immédiate des handlers notifiés jusqu'à ce point.
  • La tâche "Tâche qui dépend du redémarrage" s'exécute après que le handler a été exécuté.
  • La dernière tâche notifie à nouveau le handler restart_critical_service, mais cette fois, il sera exécuté à la fin du play, car il n'y a pas de nouveau meta: flush_handlers après cette tâche.