Ansible: Variables, conditions, boucles et handlers
Ce document présente une série de manipulations permettant de comprendre comment Ansible applique les variables provenant de différentes sources.
Nous allons redéfinir progressivement la même variable, app_port, à différents niveaux, puis exécuter un même playbook pour observer laquelle est prise en compte.
Préparation de l'environnement
Machines virtuelles
Pour ces manipulations, vous aurez besoin d'une machine de contrôle Ansible et de deux nœuds gérés.
Vous pouvez utiliser les machines virtuelles fournies ici (ce sont les mêmes que la semaine dernière) :
Démarrez les machines virtuelles, et faites la commande ip a sur chaque nœud pour obtenir leur adresse IP. Prenez-en note.
Mise en place du projet Ansible
Sur votre contrôleur Ansible, créez un répertoire de travail pour les manipulations :
cd ~
mkdir ansible_semaine11
cd ansible_semaine11
Dans ce répertoire, créez une arborescence de travail (nous utiliserons des sous-répertoires pour les variables) :
mkdir -p demo_vars/{group_vars,host_vars,vars_files}
cd demo_vars
Créez ensuite un fichier d'inventaire minimal (en remplaçant les adresses IP par celles de vos nœuds) à l'endroit demo_vars/hosts.ini :
[webservers]
web1 ansible_host=192.168.1.51
web2 ansible_host=192.168.1.52
Créez également un fichier ansible.cfg dans le répertoire demo_vars avec le contenu suivant pour spécifier l'inventaire par défaut :
[defaults]
inventory = ./hosts.ini
Testez la connexion aux nœuds :
ansible all -m ping
Créez un playbook de base nommé play.yml dans le répertoire demo_vars :
---
- name: Démonstration des sources de variables
hosts: webservers
gather_facts: false
tasks:
- name: Afficher la valeur actuelle de app_port
debug:
msg: "Host={{ inventory_hostname }} app_port={{ app_port }} (source={{ variable_source }})"
Exécutez-le :
ansible-playbook play.yml
Ansible devrait indiquer que les variables app_port est indéfinie. (Par ailleurs, la variable variable_source n'est pas encore définie non plus, mais le playbook échoue dès qu'il rencontre une variable non définie dans une expression.)
Nous allons maintenant définir cette variable à différents endroits pour en observer l’effet.
La variable inventory_hostname utilisée dans le message de débogage est une variable spéciale prédéfinie par Ansible qui contient le nom de l'hôte tel qu'il est défini dans l'inventaire. Celle-ci est une variable qui est toujours disponible dans les playbooks et les tâches, et elle permet d'identifier facilement l'hôte en cours de traitement.
Vous aurez peut-être également remarqué la mention gather_facts: false dans le playbook. Cela empêche Ansible de collecter les faits système au début du playbook, ce qui accélère l'exécution pour cette démonstration où nous ne dépendons pas des faits.
1. Définir une variable de groupe directement dans l’inventaire (hosts.ini)
Modifier hosts.ini pour rajouter la partie [webservers:vars] :
[webservers]
web1 ansible_host=192.168.1.51
web2 ansible_host=192.168.1.52
[webservers:vars]
app_port=9000
variable_source="hosts.ini (vars de groupe)"
Exécuter :
ansible-playbook play.yml
Les deux hôtes devraient maintenant afficher 9000 et la source comme hosts.ini (vars de groupe).
2. Définir une variable dans group_vars/all
Créer le fichier :
# group_vars/all.yml
app_port: 8000
variable_source: "group_vars/all"
Exécuter :
ansible-playbook play.yml
Tous les hôtes devraient afficher app_port = 8000 et la source comme group_vars/all. Cela montre que les variables définies dans group_vars/all ont priorité sur celles définies dans l'inventaire.
3. Définir une variable dans group_vars/
Créer le fichier :
# group_vars/webservers.yml
app_port: 8080
variable_source: "group_vars/webservers"
Re-exécuter le playbook.
La valeur doit maintenant être 8080, puisque les variables spécifiques au groupe ont priorité sur celles définies dans group_vars/all.
4. Définir une variable d’hôte directement dans l’inventaire
Modifier la ligne web1 :
web1 ansible_host=192.168.1.51 app_port=9100 variable_source="hosts.ini (vars d'hôte)"
Tester :
ansible-playbook play.yml
Cette fois :
web1affichera9100web2affichera9000
Cela permet d’observer la priorité des variables définies spécifiquement pour un hôte.
5. Définir une variable dans host_vars/<hôte>.yml
Créer le fichier :
# host_vars/web1.yml
app_port: 9200
variable_source: "host_vars/web1.yml"
Tester à nouveau.web1 devrait maintenant afficher 9200, car les variables définies dans host_vars ont priorité sur celles définies dans l’inventaire.
6. Définir des variables dans le playbook (section vars)
Modifier play.yml pour ajouter la section suivante sous hosts: webservers :
vars:
app_port: 9300
variable_source: "vars dans le playbook"
Tester :
ansible-playbook play.yml
Les deux hôtes devraient afficher 9300.
Les variables définies dans le playbook ont priorité sur tout ce qui vient de l’inventaire.
7. Définir des variables via vars_files
Commenter la section précédente et créer le fichier :
# vars_files/app.yml
app_port: 9400
variable_source: "vars_files/app.yml"
Modifier le playbook :
vars_files:
- vars_files/app.yml
Exécuter :
ansible-playbook play.yml
Les deux hôtes devraient afficher 9400.
8. Définir une variable à l’exécution avec set_fact
Ajouter dans play.yml, après la première tâche :
tasks:
- name: Afficher la valeur actuelle de app_port
debug:
msg: "Host={{ inventory_hostname }} app_port={{ app_port }} (source={{ variable_source }})"
- name: Définir app_port dynamiquement
set_fact:
app_port: 9500
variable_source: "set_fact"
Tester :
ansible-playbook play.yml
Le résultat devrait toujours afficher 9400, car les variables définies avec set_fact n'ont pas encore été appliquées au moment où la tâche de débogage est exécutée.
Déplacez la tâche set_fact avant la tâche de débogage :
tasks:
- name: Définir app_port dynamiquement
set_fact:
app_port: 9500
variable_source: "set_fact"
- name: Afficher la valeur actuelle de app_port
debug:
msg: "Host={{ inventory_hostname }} app_port={{ app_port }} (source={{ variable_source }})"
Le résultat devrait maintenant afficher 9500, car set_fact définit les variables au moment de l’exécution, ce qui surpasse les définitions du playbook.
set_fact est souvent utilisé pour définir des variables dynamiquement en fonction de conditions ou de résultats de tâches précédentes avec le mot-clé when, que nous verrons plus tard... mais en voici un aperçu:
- name: Définir app_port dynamiquement selon l'hôte
set_fact:
app_port: 9600
variable_source: "set_fact avec condition"
when: inventory_hostname == 'web1'
9. Définir une variable en ligne de commande (--extra-vars)
Exécutez :
ansible-playbook play.yml -e "app_port=9600 variable_source='extra vars'"
Le résultat doit indiquer 9600 sur tous les hôtes.
Les valeurs passées avec -e ont la priorité la plus élevée.
Conditions, boucles et handlers
Faisons un peu le ménage avant de passer aux conditions.
- Dans le fichier
play.yml, supprimez la sectionvars_filesdu playbook ainsi que la tâcheset_fact. - Supprimez le fichier
vars_files/app.yml - Supprimez le fichier
host_vars/web1.yml. - Supprimez les fichiers
group_vars/all.ymletgroup_vars/webservers.yml. - Supprimez les variables
app_portetvariable_sourcedu fichierhosts.ini.
Pour la suite, nous allons définir une variable app_port via la section vars du playbook :
- name: Démonstration des sources de variables
hosts: webservers
gather_facts: false
vars:
app_port: 9300
tasks:
[Voir ci-dessous]
Conditions avec when
Il est possible d'exécuter des tâches conditionnellement en utilisant le mot-clé when. Par exemple :
- name: Afficher si le port est élevé
debug:
msg: "Le port {{ app_port }} est supérieur à 9000"
when: app_port > 9000
Exécutez le playbook avec la valeur de app_port définie à 9300 et observez que le message s'affiche.:
ansible-playbook play.yml
Vous verrez le message s'afficher, car 9300 est supérieur à 9000. Changez la valeur pour 8500 dans la commande et réexécutez pour voir que le message ne s'affiche pas.
Rajoutez maintenant une deuxième variable app_environment dans la section vars :
vars:
app_port: 9300
app_environment: "production"
Modifiez la tâche conditionnelle pour inclure cette nouvelle variable :
- name: Afficher si le port est élevé en production
debug:
msg: "Le port {{ app_port }} est élevé en environnement {{ app_environment }}"
when: app_port > 9000 and app_environment == "production"
Exécutez le playbook. Le message devrait s'afficher. Changez app_environment à "development" et réexécutez pour voir que le message ne s'affiche pas.
Revenons un peu en arrière en exécutant la commande ansible web1 -m setup | less pour voir toutes les variables factuelles collectées par Ansible sur l'hôte web1.
Note: la partie | less permet de paginer la sortie pour une lecture plus facile. Il se peut que less ne soit pas installé par défaut sur votre machine; dans ce cas, vous pouvez l'installer avec sudo apt install less. Sinon, vous pouvez simplement exécuter ansible web1 -m setup et faire défiler la sortie dans le terminal.
Parmi les nombreuses variables, vous trouverez des informations sur le système d'exploitation, l'adresse IP, la mémoire, etc. Vous pouvez utiliser ces variables factuelles dans vos conditions. Par exemple, pour exécuter une tâche uniquement si l'hôte utilise Ubuntu :
- name: Afficher un message si l'hôte est Ubuntu
debug:
msg: "Cet hôte utilise Ubuntu."
when: ansible_facts['os_family'] == "Debian"
Pour cela, il faut d'abord activer la collecte des faits en supprimant ou en mettant à true la ligne gather_facts: false dans le playbook.
Conditions avec code de retour
Il est parfois utile d'exécuter une tâche en fonction du succès ou de l'échec d'une tâche précédente. Pour cela, on peut utiliser le module register pour capturer le résultat d'une tâche, puis utiliser ce résultat dans une condition when. Par exemple :
- name: Vérifier que /bin/true s'exécute correctement
command: /bin/true
register: check_true
failed_when: false # Empêche l'échec du playbook si la commande échoue
- name: Afficher message si la commande a réussi
debug:
msg: "La commande a réussi"
when: check_true.rc == 0
Ici, la première tâche exécute la commande /bin/true, qui réussit toujours, et enregistre le résultat dans la variable check_true. La deuxième tâche affiche un message uniquement si le code de retour (rc) de la commande est 0, ce qui indique un succès.
Maintenant, modifiez la première tâche pour exécuter /bin/false à la place, puis réexécutez le playbook. Vous verrez que le message ne s'affiche pas cette fois, car la commande a échoué.
Note #1: La commande /bin/true est une commande Unix qui ne fait rien et retourne toujours un code de retour de 0, indiquant un succès. Inversement, /bin/false est une commande qui ne fait rien mais retourne toujours un code de retour non nul (généralement 1), indiquant un échec.
Note #2: La directive failed_when: false est utilisée pour empêcher le playbook de s'arrêter en cas d'échec de la commande, ce qui nous permet de gérer l'échec de manière conditionnelle dans la tâche suivante. Autrement, si la commande échoue, Ansible arrête l'exécution du playbook avant d'atteindre la tâche conditionnelle.
Boucles avec loop
Ansible permet d'exécuter une tâche plusieurs fois en utilisant le mot-clé loop. Par exemple, pour créer une série de fichiers :
- name: Créer des fichiers temporaires
file:
path: "/tmp/testfile_{{ item }}"
state: touch
loop:
- a
- b
- c
Il s'agit ici de créer trois fichiers : /tmp/testfile_a, /tmp/testfile_b et /tmp/testfile_c. Exécutez le playbook et vérifiez que les fichiers ont été créés sur les nœuds.
Vous pouvez également utiliser des boucles avec des listes de dictionnaires pour des tâches plus complexes. Par exemple, pour créer plusieurs utilisateurs :
- name: Créer plusieurs utilisateurs
user:
name: "{{ item.name }}"
state: present
loop:
- { name: 'user1' }
- { name: 'user2' }
- { name: 'user3' }
Exécutez le playbook une première fois et vous verrez une erreur de permission. Ajoutez become: yes au niveau de la tâche pour obtenir les privilèges administratifs nécessaires à la création des utilisateurs, puis réexécutez le playbook.
Vous verrez ensuite une deuxième erreur indiquant "Missing sudo password". Pour résoudre ce problème, modifiez votre fichier d'inventaire hosts.ini pour inclure la ligne suivante sous le groupe [webservers:vars] :
ansible_become_pass=Ansible123!
Pour confirmer la création des utilisateurs, vous pouvez vous connecter à l'un des nœuds et exécuter la commande id user1 (ou id user2, etc.) pour vérifier que les utilisateurs ont bien été créés.
changed_when et failed_when
À chaque exécution d'un playbook, nous voyons l'état de la tâche. Vous aurez peut-être remarqué que certaines tâches dans vos playbooks indiquent "changed" même si elles n'ont pas réellement modifié quoi que ce soit sur le système (par exemple, le module command avec une commande telle que hostname). De même, Ansible peut indiquer "failed" pour une tâche même si vous souhaitez qu'elle soit considérée comme réussie dans certains cas.
Il est possible de contrôler cet état en utilisant les directives changed_when et failed_when. Par exemple :
- name: Exécuter une commande sans changer l'état
command: hostname
changed_when: false
Cela indique à Ansible de ne pas marquer cette tâche comme "changed", même si la commande a été exécutée avec succès. De même, pour forcer une tâche à être considérée comme réussie même si elle échoue :
- name: Exécuter une commande qui peut échouer
command: /bin/false
failed_when: false
Ici, même si la commande /bin/false échoue, Ansible considérera la tâche comme réussie grâce à failed_when: false.
failed_when est particulièrement utile lorsque vous souhaitez capturer l'échec d'une commande mais ne pas arrêter l'exécution du playbook. Par exemple :
- name: Cette commande échoue mais ne stoppe pas le playbook
command: /bin/false
register: cmd_fail
failed_when: false
- name: Réagir à l'échec capturé
debug:
msg: "Échec détecté, mais toléré"
when: cmd_fail.rc != 0
Ici, la première tâche exécute /bin/false et enregistre le résultat dans cmd_fail, mais n'échoue pas grâce à failed_when: false. La deuxième tâche affiche un message si la commande a effectivement échoué, en utilisant la condition when.
Handlers
Les handlers sont des tâches spéciales qui ne s'exécutent que lorsqu'elles sont "notifiées" par d'autres tâches. Ils sont souvent utilisés pour redémarrer des services ou effectuer des actions similaires après une modification de configuration. Par exemple, pour redémarrer le service Apache uniquement si une installation a eu lieu:
tasks:
- name: Installer Apache
apt:
name: apache2
state: present
notify: Redémarrer Apache
handlers:
- name: Redémarrer Apache
service:
name: apache2
state: restarted
Exécutez le playbook. Si Apache n'était pas installé, la tâche d'installation le fera et notifiera le handler pour redémarrer Apache. Si Apache était déjà installé, le handler ne sera pas exécuté. Pour forcer l'exécution du handler, vous pouvez modifier la tâche d'installation pour toujours indiquer un changement :
- name: Installer Apache
apt:
name: apache2
state: present
changed_when: true # Force le changement pour tester le handler
notify: Redémarrer Apache
Exécutez à nouveau le playbook et vous verrez que le handler est exécuté cette fois, car la tâche d'installation indique un changement même si Apache était déjà installé.
Handlers avec listen
Les handlers peuvent également utiliser le mot-clé listen pour permettre à plusieurs tâches de notifier le même handler sous différents noms. Par exemple :
tasks:
- name: Installer Apache
apt:
name: apache2
state: present
changed_when: true # Force le changement pour tester le handler
notify: restart_web_server
- name: Mettre à jour la configuration du pare-feu
ufw:
rule: allow
name: 'Apache Full'
notify: restart_web_server
handlers:
- name: Redémarrer le serveur web
listen: restart_web_server
service:
name: apache2
state: restarted
Ici, les deux tâches peuvent notifier le même handler Redémarrer le serveur web en utilisant le nom restart_web_server. Si l'une ou l'autre des tâches indique un changement, le handler sera exécuté une seule fois à la fin du playbook.
Handlers avec meta: flush_handlers
Par défaut, les handlers sont exécutés à la fin du playbook, après que toutes les tâches ont été traitées. Cependant, il est possible de forcer l'exécution immédiate des handlers notifiés jusqu'à ce point en utilisant la directive meta: flush_handlers. Par exemple :
tasks:
- name: Installer Apache
apt:
name: apache2
state: present
changed_when: true # Force le changement pour tester le handler
notify: Redémarrer Apache
- name: Forcer l'exécution des handlers
meta: flush_handlers
- name: Effectuer une autre tâche
debug:
msg: "Tâche après le flush des handlers"
handlers:
- name: Redémarrer Apache
service:
name: apache2
state: restarted