Aller au contenu

Ecriture, Exécution et Debug de playbook Ansible

Mise à jour :

logo

Dans le guide précédent sur la découverte d’Ansible, nous avons exploré les bases de cet outil puissant d’automatisation, en expliquant comment il permet de gérer la configuration des serveurs de manière simple et efficace. Nous avons également abordé la configuration initiale d’Ansible, son installation et les concepts clés tels que les inventaires, les modules et les playbooks.

Dans ce guide, nous allons passer en revue chaque élément essentiel à la maîtrise d’Ansible. Après avoir exploré les bases dans notre guide précédent, nous allons ici entrer plus en profondeur dans la structure d’un projet complet. Voici un aperçu des chapitres que nous allons aborder :

  1. Structure d’un projet Ansible Nous commencerons par une étude détaillée de l’architecture typique d’un projet Ansible, incluant la configuration, les inventaires, les variables de groupes et d’hôtes, les rôles et les playbooks. Vous comprendrez comment organiser efficacement vos fichiers pour une meilleure lisibilité et maintenabilité.
  2. Syntaxe complète des playbooks Dans ce chapitre, nous approfondirons la syntaxe des playbooks Ansible, en expliquant comment structurer des tâches, utiliser les boucles, conditionner l’exécution de tâches, gérer les dépendances et les rôles. Nous fournirons également des exemples concrets de playbooks utilisés dans des scénarios réels.
  3. Gestion des variables Nous vous montrerons comment utiliser les variables dans Ansible, avec un focus sur les facts Ansible.
  4. Gestion des erreurs et commandes de débogage Même avec la meilleure des configurations, des erreurs peuvent survenir. Dans ce chapitre, nous apprendrons à gérer les erreurs dans Ansible, à utiliser des options de débogage avancées et à exploiter les outils intégrés pour identifier et résoudre les problèmes efficacement.

À la fin de ce guide, vous serez non seulement capable de créer et gérer des projets Ansible complexes, mais aussi d’optimiser votre flux de travail grâce aux commandes de débogage, à la gestion fine des variables et à la création de rôles modulaires et robustes.

Structure d’un projet Ansible

Avant de démarrer l’écriture de playbooks, il faut dans un premier temps créer un certain nombre de fichiers et de repertoires pour organiser votre projet d’automatisation. Voici un exemple de structure :

  • Répertoireprojet_ansible
    • Répertoireinventory
      • hosts
    • Répertoiregroup_vars
      • all.yml
      • webservers.yml
    • Répertoirehost_vars
      • server1.yml
      • server2.yml
    • Répertoirefiles
      • sample_file.txt
    • Répertoiretemplates
      • service.j2
    • ansible.cfg
    • install_apache.yml
    • configure_apache.yml

Description des fichiers et répertoires :

  • ansible.cfg : Fichier de configuration d’Ansible. Il permet de personnaliser le comportement d’Ansible, comme le chemin vers l’inventaire, les privilèges de sudo, etc. C’est ici que vous pouvez définir des options globales pour Ansible. Il est optionnel.
  • inventory/ : Répertoire contenant les fichiers d’inventaire. Un inventaire spécifie les groupes d’hôtes et les serveurs sur lesquels Ansible doit agir.
    • hosts : Fichier d’inventaire principal où sont définis les hôtes et leurs groupes. Par exemple, vous y déclarez vos groupes d’hôtes comme webservers, dbservers et les serveurs qui leur sont associés.
  • group_vars/ : Ce répertoire contient des fichiers YAML définissant des variables pour des groupes d’hôtes. Chaque fichier porte le nom d’un groupe, ou peut s’appeler all.yml pour des variables globales.
    • all.yml : Variables globales appliquées à tous les hôtes.
    • webservers.yml : Variables spécifiques au groupe webservers.
  • host_vars/ : Ce répertoire contient des fichiers YAML définissant des variables spécifiques à un hôte individuel.
    • server1.yml : Variables pour l’hôte server1.
    • server2.yml : Variables pour l’hôte server2.
  • files/ : Répertoire contenant des fichiers à copier directement sur vos hôtes.
    • sample_file.txt : Un fichier exemple qui pourrait être utilisé pour des copies directes, comme un fichier de configuration ou des binaires statiques.
  • templates/ : Le dossier des templates Jinja
    • service.j2 : Un template Jinja de services utilisant des variables et conditions.
  • install_apache.yml : Playbook pour installer Apache.
  • configure_apache.yml : Playbook pour configurer Apache, en utilisant un template par exemple.

Cette structure vous permet de bien organiser vos projets Ansible, en séparant les rôles, les variables et les tâches spécifiques aux serveurs ou groupes de serveurs. Cela rend votre infrastructure plus maintenable et évolutive.

Ecriture d’un playbook Ansible

Un playbook Ansible est un fichier écrit en YAML qui permet de décrire un ensemble de tâches à exécuter sur un ou plusieurs hôtes. L’intérêt principal d’un playbook est d’automatiser des tâches répétitives, qu’il s’agisse de configurer des serveurs, de déployer des applications ou de gérer des infrastructures. Dans ce chapitre, je vais détailler les éléments essentiels d’un playbook et montrer comment le structurer.

Un playbook est constitué de plusieurs parties principales :

  • Les hôtes (hosts) : Ce sont les machines sur lesquelles les tâches vont s’exécuter.
  • Les tâches (tasks) : Ce sont les actions spécifiques à effectuer sur ces hôtes qui font appel aux modules.

Voici un exemple basique de playbook Ansible :

---
- hosts: webservers
tasks:
- name: Installer Apache
ansible.builtin.apt:
name: apache2
state: present

Décomposons cet exemple.

La première ligne du playbook spécifie sur quels hôtes le playbook doit s’exécuter. Cela peut être un ou plusieurs serveurs, regroupés sous un inventaire ou un groupe de serveurs (comme ici, le groupe webservers). Ansible se base sur cet inventaire pour savoir quelles machines doivent recevoir les actions définies dans le playbook.

- hosts: webservers

Dans cet exemple, webservers fait référence à un groupe d’hôtes défini dans votre fichier d’inventaire, où les adresses IP ou les noms des serveurs sont spécifiés. Voici un exemple simple de fichier d’inventaire (hosts).

[webservers]
192.168.1.10
192.168.1.11

Les tasks sont les étapes spécifiques à exécuter sur les hôtes définis. Chaque tâche comporte un nom (name), qui sert de description humaine, et un module Ansible, qui exécute l’action. Dans cet exemple, le module utilisé est apt, qui est responsable de la gestion des paquets sur les systèmes basés sur Debian (comme Ubuntu).

tasks:
- name: Installer Apache
ansible.builtin.apt:
name: apache2
state: present

Les modules sont au cœur des tâches dans un playbook. Ils permettent d’exécuter une grande variété d’actions, telles que :

Exécution du playbook

Pour exécuter un playbook, il suffit d’utiliser la commande ansible-playbook l’inventaire à utiliser uivie du nom du fichier YAML contenant le playbook.

Par exemple :

Terminal window
ansible-playbook -i hosts install_apache.yml

Dans cet exemple, -i hosts spécifie le fichier d’inventaire, et install_apache.yml est le playbook à exécuter.

Utilisation des variables Ansible

Les facts d’Ansible

Les « gather facts », sont des variables qu’Ansible recueille sur les machines cibles : le système d’exploitation, les adresses IP, la mémoire, le disque, etc. Ensuite, il stocke ces informations dans des variables qu’on nomme facts. Le moyen le plus simple est de lancer le module setup sur localhost : ansible -m setup -i localhost, -c local localhost.

Ces variables prédéfinies peuvent ensuite être utilisées dans les playbook ansible. Pour cela, il suffit de les mettre entre doubles accolades :

- name: Show ansible_selinux var
ansible.builtin.debug:
msg: "{{ ansible_selinux }}"

Ce qui donne :

Terminal window
ok: [localhost] => {
"msg": {
"config_mode": "enforcing",
"mode": "enforcing",
"policyvers": 28,
"status": "enabled",
"type": "targeted"
}
}

Dans le cas où vous n’utilisez pas ces facts vous pouvez désactiver sa collecte en ajoutant dans l’entête de votre playbook Ansible [gather_facts: no]. Cela va accélérer l’exécution de votre playbook ansible.

Vos propres variables

Le plus simple pour ajouter une variable à votre playbook ansible est de définir une section vars. Ensuite, vous pouvez les récupérer comme pour les facts avec des doubles accolades.

---
- hosts: all
vars:
nginx_ssl: /etc/nginx/ssl
nginx_conf_file: /etc/nginx/nginx.conf

Il est possible de définir des variables Ansible dans un fichier séparé en ajoutant la section vars_files.

---
hosts: all
vars_files:
- myvarsfile.yml

Création de variables pour les groupes et les hôtes des inventaires Ansible

Les variables dans Ansible sont utilisées pour rendre les playbooks plus dynamiques, flexibles et réutilisables. Elles permettent de personnaliser l’exécution des tâches selon le contexte, que ce soit au niveau d’un groupe d’hôtes (groupes) ou pour un hôte spécifique (hosts). Dans cette section, je vais expliquer comment déclarer et utiliser des variables pour des groupes et des hôtes dans Ansible.

Structure des fichiers de variables

Ansible permet de définir des variables dans plusieurs emplacements, en fonction de leur portée (globale, groupe d’hôtes, ou hôte spécifique). Voici les principaux endroits où les variables peuvent être définies :

  1. Variables globales : définies dans le playbook ou un fichier dédié.
  2. Variables par groupe d’hôtes : appliquées à tous les serveurs appartenant à un groupe d’hôtes.
  3. Variables spécifiques à un hôte : propres à un seul hôte.

Emplacement des fichiers de variables

Les fichiers de variables spécifiques aux groupes et aux hôtes sont généralement placés dans des répertoires spécifiques sous le répertoire group_vars ou host_vars dans votre projet Ansible.

  • Les variables de groupe sont placées dans le répertoire group_vars.
  • Les variables d’hôte sont placées dans le répertoire host_vars.

Variables pour un groupe d’hôtes

Les groupes d’hôtes sont des collections de machines définies dans le fichier d’inventaire. Pour associer des variables à un groupe, vous pouvez créer un fichier YAML portant le nom du groupe dans le répertoire group_vars. Par exemple, si vous avez un groupe nommé webservers, vous pouvez créer un fichier group_vars/webservers.yml pour définir des variables qui s’appliquent à tous les serveurs de ce groupe.

Voici un exemple de fichier group_vars/webservers.yml :

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

Dans ce fichier, deux variables sont définies :

  • apache_port : le port sur lequel Apache écoutera.
  • document_root : le répertoire où seront stockés les fichiers du site Web.

Ces variables seront disponibles pour tous les hôtes appartenant au groupe webservers.

Variables pour un hôte spécifique

Si vous avez besoin de définir des variables spécifiques à un seul serveur, vous pouvez créer un fichier dans le répertoire host_vars. Le fichier doit avoir pour nom l’hôte en question, tel qu’il est défini dans votre fichier d’inventaire. Par exemple, pour un hôte nommé server1, vous pouvez créer un fichier host_vars/server1.yml.

Voici un exemple de fichier host_vars/server1.yml :

---
apache_port: 9090
document_root: /var/www/custom

Dans cet exemple, les variables apache_port et document_root sont définies uniquement pour server1. Ces variables peuvent écraser celles définies au niveau du groupe si l’hôte appartient également à un groupe (par exemple, webservers).

Utilisation des variables dans un playbook

Une fois les variables définies, elles peuvent être utilisées directement dans les tâches de votre playbook en utilisant la syntaxe jinja : {{ variable_name }}.

Par exemple, voici un playbook qui utilise les variables définies pour installer et configurer Apache sur les serveurs du groupe webservers :

---
- hosts: webservers
tasks:
- name: Installer Apache
ansible.builtin.apt:
name: apache2
state: present
- name: Configurer Apache pour écouter sur le port spécifié
ansible.builtin.lineinfile:
path: /etc/apache2/ports.conf
regexp: '^Listen'
line: "Listen {{ apache_port }}"
state: present
- name: Configurer le répertoire racine du serveur
ansible.builtin.copy:
content: |
<VirtualHost *:{{ apache_port }}>
DocumentRoot {{ document_root }}
</VirtualHost>
dest: /etc/apache2/sites-available/000-default.conf
- name: Redémarrer Apache
ansible.builtin.service:
name: apache2
state: restarted

Dans ce playbook :

  • La tâche lineinfile utilise la variable apache_port pour configurer le port sur lequel Apache écoutera.
  • La tâche copy définit le répertoire racine du serveur Web en utilisant la variable document_root.
  • La dernière tâche redémarre Apache pour appliquer les changements.

Sauvegarde du résultat d’une tache Ansible dans une variable

Pour enregistrer une variable, il suffit d’ajouter à votre tâche le mot-clé register. Le résultat sera ensuite utilisé comme un dictionnaire :

---
- hosts: localhost
tasks:
- name: Ping localhost
ansible.builtin.ping:
register: test
- name: display var
ansible.builtin.debug:
msg: "{{ test.ping }}"

Définition d’une variable dans une tâche

Le module set_fact dans Ansible permet de définir des variables dynamiques et des faits directement au sein d’un playbook, pour une utilisation immédiate dans les tâches suivantes du même playbook. Contrairement aux variables définies dans des fichiers de variables ou d’inventaire, les facts créés avec set_fact sont disponibles immédiatement après leur définition et persistent uniquement durant l’exécution du playbook.

Le module est utile lorsque vous devez calculer ou ajuster des valeurs en fonction des résultats d’autres tâches ou des faits récupérés dynamiquement. Ces variables peuvent ensuite être utilisées dans d’autres parties du playbook, ce qui facilite la gestion de la logique conditionnelle, les boucles, ou l’ajustement des paramètres en fonction des conditions rencontrées lors de l’exécution.

- name: Définir un fait personnalisé
ansible.builtin.set_fact:
version_app: "1.2.3"
nombre_serveurs: 4
- name: Afficher les valeurs définies
ansible.builtin.debug:
msg: "La version de l'application est {{ version_app }} et il y a {{ nombre_serveurs }} serveurs."

Points importants :

  • Persistant pendant le playbook : Les variables définies via set_fact sont disponibles pour toutes les tâches subséquentes, jusqu’à la fin du playbook.
  • Conservation entre les hôtes : Les variables définies sont spécifiques à chaque hôte, ce qui signifie qu’elles ne se propagent pas automatiquement à d’autres hôtes dans une exécution parallèle.
  • Utilisation de jinja : set_fact supporte les filtres Jinja, ce qui permet d’effectuer des opérations avancées (concaténation de chaînes, calculs, etc.) lors de la définition des variables.

Les Lookup en Ansible

Un lookup en Ansible permet de récupérer des informations provenant de sources externes, comme des fichiers, des variables d’environnement ou des résultats de commandes, au moment de l’exécution du playbook. Cela permet d’ajouter de la flexibilité aux playbooks, en s’appuyant sur des données dynamiques plutôt que des valeurs statiques.

Pourquoi utiliser un lookup ?

Les lookups sont particulièrement utiles lorsque vos données doivent provenir de sources externes ou être récupérées dynamiquement en fonction du contexte d’exécution. Par exemple, vous pouvez lire des configurations à partir de fichiers ou de variables d’environnement sans avoir à les coder directement dans le playbook.

Exemple :

- hosts: all
vars:
config_data: "{{ lookup('file', '/etc/app/config.txt') }}"
tasks:
- name: Afficher le contenu du fichier de configuration
ansible.builtin.debug:
msg: "{{ config_data }}"

Dans cet exemple, le lookup lit le fichier /etc/app/config.txt et stocke son contenu dans la variable config_data, qui est ensuite utilisée dans une tâche Ansible. Cela permet de centraliser la gestion des données externes tout en maintenant un playbook flexible.

Pour une explication plus complète sur l’utilisation des lookups et les différentes options disponibles, vous pouvez consulter le guide détaillé sur l’utilisation des lookups dans Ansible.

Utilisation des boucles Ansible

Dans certains cas, nous avons besoin de réaliser une tâche sur plusieurs cibles, par exemple installer plusieurs paquets et tout cela en une seule opération. Ansible intègre les boucles de différentes manières :

Les boucles standards : with_items ou loop

Le premier type de boucle permet de répéter une action sur une liste de valeurs, par exemple une liste de packages :

...
- name: add several package
ansible.builtin.package:
name={{ item }}
state=present
with_items:
- nginx
- python

Les boucles imbriquées : with_nested

Cet exemple permettra de comprendre le fonctionnement de ce type de boucle :

vars:
keys:
- key1
- key2
- key3
tasks:
- name: Distribute SSH keys among multiple users
ansible.builtin.lineinfile:
dest: /home/{{ item[0] }}/.ssh/authorized_keys
line: {{ item[1] }}
state: present
with_nested:
- [ ’calvin’, ’josh’, ’alice’ ]
- ’{{ keys }}’

Ici Ansible va boucler sur chaque utilisateur et remplira leur fichier authorized_keys avec les 3 clés définies dans la liste.

Les boucles sur dictionnaire: with_dict

Les boucles sur dictionnaires : En terminologie Python, un dictionnaire est un ensemble défini de variables possédant plusieurs valeurs :

vars:
users:
alice:
name: Alice
telephone: 123-456-7890
bob:
name: Bob
telephone: 987-654-3210
tasks:
- name: Print phone records
ansible.builtin.debug:
msg="User {{ item.key }} is {{ item.value.name }} ({{ item.value.telephone }})"
with_dict: "{{users}}"

Do until

Do until peut être utilisé pour attendre qu’une condition soit vraie où qu’elle ait atteint le nombre maxi d’itérations pour sortir de la boucle.

- ansible.builtin.shell: /usr/bin/foo
register: result
until: result.stdout.find("OK")
retries: 5
delay: 10

Le playbook exemple ci-dessus exécute le module shell de manière répétée jusqu’à ce que le résultat du module retourne “OK” dans sa sortie standard ou que la tâche ait été itérée 5 fois avec un délai de 10 secondes. La variable result aura également une nouvelle clé attemps qui aura le nombre des tentatives effectuées par la boucle.

Et bien d’autres

Il existe toute une série de type de boucle répondant à des besoins spécifiques. Si vous avez des choses complexes à construire n’hésitez pas à jeter un œil à cette section de la documentation Ansible.

Utilisations des conditions d’Ansible: when

Parfois, vous voudrez qu’une tache particulière ne s’exécute ou pas dans certaines conditions. Par exemple ne pas installer un certain paquet si le système d’exploitation correspond à une version particulière, ou encore de procéder à certaines étapes de nettoyage si un système de fichiers est saturé.

tasks:
- name: "shut down all Debian"
ansible.builtin.command: /sbin/shutdown -t now
when: ansible_facts[’os_family’] == "Debian"

La syntaxe des conditions (and or, in, not in, is … ) reprend celle de Jinja qui nous servira à créer par la suite des modèles.

Il est possible de mettre des conditions en fonction de l’exécution ou de l’échec ou du bypass d’une tache précédente. Pour ignorer une erreur d’une tache, il suffit d’ajouter la clé ignore_errors à true (Ne pas utiliser dans tous les cas).

tasks:
- ansible.builtin.command: /bin/false
register: result
ignore_errors: True
- ansible.builtin.command: /bin/something
when: result is failed
- ansible.builtin.command: /bin/something_else
when: result is succeeded
- ansible.builtin.command: /bin/still/something_else
when: result is skipped

On peut regrouper des actions utilisant les mêmes conditions en recourant à des blocks Ansible.

Utilisations de Jinja

Jinja est à la base un module python écrit pour Django permettant de produire rapidement du texte dynamique. L’idée principale est de fournir à un module, un modèle et une liste de valeurs pour qu’il construise dynamiquement un résultat. Jinja fournit des possibilités plus avancées, comme appliquer des filtres sur une variable, appliquer des boucles sur des listes et bien d’autre encore. Et cela va nous servir dans les playbook Ansible.

Les filtres Ansible

Les filtres dans Ansible sont utilisés pour transformer des données. Sa syntaxe est "{{ var | filter }}".

---
tasks:
- ansible.builtin.shell: cat /some/path/to/file.json
register: result
- ansible.builtin.set_fact:
myvar: "{{ result.stdout | from_json }}"

D’autres sont là pour contraindre leur existence, pour assigner une valeur par défaut ou encore leur omission :

---
- name: touch files with an optional mode
ansible.builtin.file:
dest: "{{ item.path | mandatory }}"
state: touch
mode: "{{ item.mode | default(omit) }}"
loop:
- path: /tmp/foo
- path: /tmp/bar
- path: /tmp/baz
mode: "0444"

Il existe toute une série de filtres, des filtres mathématiques, pour des opérations sur des listes, les dictionnaires, les adresses mac ou ip, des [expressions régulières](/docs/developper/expressions-regulieres/, … Leur documentation se trouve dans ses trois billets : 1, 2 et 3

Si vous ne trouvez pas votre bonheur, vous pouvez développer vos propres filtres.

Utilisation de tests

Les tests en Jinja permettent d’évaluer les expressions de modèle et de renvoyer True ou False. La principale différence entre les tests et les filtres réside dans le fait que les tests Jinja sont utilisés à des fins de comparaison, alors que les filtres sont utilisés pour la manipulation de données.

Ils sont tous écrits de la forme valeur is type_de_test (paramètres).

Tests sur les strings

Ansible propose trois types de recherche sur les strings : is match, is search et is regex

  • match retourne vrai s’il trouve le motif au début de la chaîne, contrairement à
  • search qui lui réussit s’il trouve le motif n’importe où dans la chaîne. Ces tests acceptent des arguments ignorecase et multiline.
vars:
url: "http://example.com/users/foo/resources/bar"
tasks:
- ansible.builtin.debug:
msg: "matched pattern 1"
when: url is match("http://example.com/users/.*/resources/.*")
- ansible.builtin.debug:
msg: "matched pattern 2"
when: url is search("/users/.*/resources/.*")
- ansible.builtin.debug:
msg: "matched pattern 3"
when: url is search("/users/")

Les tests sur les booléens

Les tests truthy et falsy acceptent un paramètre facultatif appelé convert_bool qui tentera de convertir les indicateurs en booléens.

- ansible.builtin.debug:
msg: "Truthy"
when: value is truthy
vars:
value: "some string"
- ansible.builtin.debug:
msg: "Falsy"
when: value is falsy(convert_bool=True)
vars:
value: "off"

Comparer des versions

Pour comparer un numéro de version, il existe un test version, On peut ainsi vérifier que la version trouvée ansible_facts['distribution_version'] version est supérieure ou égale à celle attendue.

tasks:
- ansible.builtin.debug:
msg: "Attention votre distribution est trop vieille"
when: ansible_facts['distribution_version'] is version('20.04', '>')

Ce test accepte trois paramètres :

  • operator l’opérateur de comparaison parmi : <, lt, <=, le, >, gt, >=, ge, ==, =, eq, !=, <>, ne.
  • version_type défini le type de versioning et accepte les valeurs suivantes: loose, strict, semver, semantic.
  • strict ne peut être utilisé avec version_type.
tasks:
- ansible.builtin.debug:
msg: "Attention votre distribution est trop vieille"
when: sample_version_var is version('1.0', operator='lt', strict=True) }}

Tester si une liste contient une valeur

contains permet de vérifier l’existence d’une valeur dans une liste. Il accepte les filtres select, reject, selectattr et rejectattr.

vars:
lacp_groups:
- master: lacp0
network: 10.65.100.0/24
gateway: 10.65.100.1
dns4:
- 10.65.100.10
- 10.65.100.11
interfaces:
- em1
- em2
- master: lacp1
network: 10.65.120.0/24
gateway: 10.65.120.1
dns4:
- 10.65.100.10
- 10.65.100.11
interfaces:
- em3
- em4
tasks:
- ansible.builtin.debug:
msg: "{{ (lacp_groups|selectattr('interfaces', 'contains', 'em1')|first).master }}"

Tests sur les ensembles

Pour voir si une liste inclut ou est incluse dans une autre liste, vous pouvez utiliser subset et superset :

vars:
a: [1,2,3,4,5]
b: [2,3]
tasks:
- ansible.builtin.debug:
msg: "A includes B"
when: a is superset(b)
- ansible.builtin.debug:
msg: "B is included in A"
when: b is subset(a)

Tests sur les fichiers

Ces tests permettent de tester le type de fichiers ou son état :

- ansible.builtin.debug:
msg: "path is a directory"
when: mypath is directory
- ansible.builtin.debug:
msg: "path is a file"
when: mypath is file
- ansible.builtin.debug:
msg: "path is a symlink"
when: mypath is link
- ansible.builtin.debug:
msg: "path already exists"
when: mypath is exists
- ansible.builtin.debug:
msg: "path is {{ (mypath is abs)|ternary(’absolute’,’relative’)}}"
- ansible.builtin.debug:
msg: "path is the same file as path2"
when: mypath is same_file(path2)
- ansible.builtin.debug:
msg: "path is a mount"
when: mypath is mount

Tests sur les résultats de taches

On peut contrôler le statut d’une tache parmi : failed, changed, succeeded et skipped.

tasks:
- ansible.builtin.shell: /usr/bin/foo
register: result
ignore_errors: True
- ansible.builtin.debug:
msg: "it failed"
when: result is failed

Utilisation des handlers Ansible

Les handlers d’Ansible permettent de déclencher des événements après qu’une tâche soit passé au status changed. En outre, bien que plusieurs tâches puissent nécessiter une même action, l’action en question ne sera lancée qu’après l’exécution de toutes les tâches.

Pour cela, il suffit d’ajouter un notify à votre tâche Ansible avec le nom du handler et de définir le bloc handlers avec le même nom à la fin du fichier.

- name: enable vhost
ansible.builtin.command: a2ensite test_site
notify:
- restart apache
handlers:
- name: restart apache
ansible.builtin.service:
name: apache2
state: restarted

Si vous souhaitez forcer le déclenchement d’un handler après une tache ajouter la ligne suivante :

- name: enable vhost
ansible.builtin.command: a2ensite test_site
notify:
- restart apache
# Force restart handlers
- meta: flush_handlers

Élévation de privilèges

Avant tout, il faut bien se rappeler qu’Ansible utilise principalement le protocole ssh pour se connecter à vos machines hôtes. La bonne pratique est de créer sur les machines hôtes un user ansible ne possédant pas les droits root, mais de lui donner les droits sudo.

Dans ce cas, il faudra peut-être demander à Ansible de faire ce qu’on appelle de l’élévation de privilèges.

Pour cela, il est possible d’utiliser les paramètres Ansible.

become : mis à true pour activer l’élévation de privilèges become_user : défini l’utilisateur souhaité par défaut, il s’agit de root become_method: la méthode d’élévation de privilège. Par défaut sudo, mais on peut aussi utiliser su ou doas. On peut aussi développer ses propres plugins de connexion.

Exemples

Démarrer le service apache avec sudo

- name: Ensure the httpd service is running
ansible.builtin.service:
name: httpd
state: started
become: true

Utiliser le compte apache

- name: Run a command as the apache user
ansible.builtin.command: somecommand
become: true
become_user: apache

Il est possible de définir ces paramètres directement dans les variables de groupes. Vous pouvez aussi les définir au niveau le plus haut de votre playbook ou dans la commande de lancement de votre playbook (j’aime moins) :

---
- hosts: all
become: true
become_method: enable
tasks:
- name: Install EPEL repo.
ansible.builtin.package:
name: epel-release
state: present

ou dans la ligne de commande :

Terminal window
ansible-playbook -i staging --become --become-method=su --ask-become-pass

Plus d’infos sur become

Debug des playbooks Ansible

Vous avez plusieurs outils à votre disposition pour débuger vos playbooks.

Utilisons ce playbook comme exemple qui ne fonctionne pas bien sur :

---
- hosts: all
gather_facts: true
become: true
vars:
pkg_name: not_exist
tasks:
- name: Install a package
ansible.builtin.package:
name: "{{ pkg_name }}"

Le mode verbose

Un premier moyen de voir ce qui se passe lorsqu’on lance un playbook ansible, avec d’activer le mode verbose -v. Chaque v supplémentaire permet à d’augmenter la verbosité de la sortie.

Terminal window
ansible-playbook -vvv -i inventory --limit host1 test.yml
ansible-playbook 2.9.15
<host1> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o ConnectTimeout=10 -o ControlPath=/home/vagrant/.ansible/cp/5401241998 host1 '/bin/sh -c '"'"'rm -f -r /home/vagrant/.ansible/tmp/ansible-tmp-1614782435.453363-165214-175234282313188/ > /dev/null 2>&1 && sleep 0'"'"''
<host1> (0, b'', b'')
fatal: [host1]: FAILED! => {
"changed": false,
"failures": [
"No package not_exist available."
],
"invocation": {
"module_args": {
"allow_downgrade": false,
"autoremove": false,
"bugfix": false,
"conf_file": null,
"disable_excludes": null,
"disable_gpg_check": false,
"disable_plugin": [],
"disablerepo": [],
"download_dir": null,
"download_only": false,
"enable_plugin": [],
"enablerepo": [],
"exclude": [],
"install_repoquery": true,
"install_weak_deps": true,
"installroot": "/",
"list": null,
"lock_timeout": 30,
"name": [
"not_exist"
],
"releasever": null,
"security": false,
"skip_broken": false,
"state": null,
"update_cache": false,
"update_only": false,
"validate_certs": true
}
},
"msg": "Failed to install some of the specified packages",
"rc": 1,
"results": []
}
PLAY RECAP *************************************************************************************
host1 : ok=1 changed=0 unreachable=0 failed=1 skipped=0 resc

Avec ce niveau, vous obtenez déja bcp d’informations.

Check_mode

Le second moyen est de lancer votre playbook ansible en mode check. En fait, dans ce mode :

  • Les modules qui effectuent une action de modification, d’ajout ou de suppression vont uniquement vérifier que la source et la destination sont présentes, l’action ne sera pas effectuée.
  • Les modules qui effectuent peut-être des actions de modifications, mais sans possibilité de contrôle, comme modules command et shell, seront ignorés.
  • Ceux qui ne font que rendre une information comme les modules find et stat seront tout de même exécutés.
Terminal window
ansible-playbook main.yml --check

Pour contrôler, vous pouvez activer le mode diff et vous ne verrez aucune modification (sauf pour les tasks utilisant shell ou command):

Terminal window
ansible-playbook main.yml --check --diff

Si vous voulez désactiver le mode check sur une tache, il suffit de rajouter ceci à votre task :

check_mode: no

Si certaines de vos taches retourne des erreurs, vous pouvez ajouter ceci à ces tasks pour poursuivre le playbook:

ignore_errors: '{{ ansible_check_mode }}'

Le mode debugger

Vous pouvez activer le mode débogage pour chaque exécution de vos playbooks en mettant la variable enable_task_debugger à True dans le fichier ansible.cfg:

[defaults]
enable_task_debugger = True

Un autre moyen est de le rajouter dans votre playbook :

---
- hosts: all
gather_facts: true
become: true
debugger: on_failed
vars:
pkg_name: not_exist
tasks:
- name: Install a package
ansible.builtin.package:
name: "{{ pkg_name }}"

Lorsqu’on lance ce playbook ansible, qui plante, car le package n’existe pas, on voit apparaître cette ligne :

Terminal window
TASK [Install a package] ***********************************************************************
fatal: [host2]: FAILED! => {"changed": false, "failures": ["No package not_exist available."], "msg": "Failed to install some of the specified packages", "rc": 1, "results": []}
[host2] TASK: Install a package (debug)>

A partir de là, nous pouvons utiliser les commandes suivantes pour debugger :

  • p : affiche des informations utilisées pendant l’exécution par votre tâche module
  • task.args[key] = value: met à jour l’argument du module.
  • task_vars[key] = value: met à jour les variables de votre playbook.
  • u(pdate_task): si vous modifiez les task_vars alors utilisez cette commande pour recréer la tâche à partir de votre nouvelle structure de données.
  • r(edo): exécutez à nouveau la tâche.
  • c(ontinue): passe à la tâche suivante.
  • q(uit): quitte le débogueur. L’exécution du playbook est abandonnée.

Ici modifions le user le nom du package avant de relancer :

Terminal window
TASK [Install a package] ***********************************************************************
fatal: [host2]: FAILED! => {"changed": false, "failures": ["No package not_exist available."], "msg": "Failed to install some of the specified packages", "rc": 1, "results": []}
[host2] TASK: Install a package (debug)> p task.args
{'name': 'not_exist'}
[host2] TASK: Install a package (debug)> task.args['name'] = 'bash'
[host2] TASK: Install a package (debug)> redo
ok: [host2]

Si vous avez plusieurs hosts, il faudra le répéter autant de fois.

Relancer depuis une étape particulière

Parfois si vous voulez relancer un playbook à partir d’une étape particulière et non depuis le début, il suffit d’utiliser l’option --start-at-task.

Terminal window
ansible-playbook playbook.yml --start-at-task="install packages"

Jouer un playbook de manière interactive

Avec cette option, Ansible s’arrête à chaque tâche et demande s’il doit l’exécuter ou pas. Par exemple, si vous avez une tâche appelée configure ssh, l’exécution du playbook s’arrêtera et demandera :

Terminal window
ansible-playbook playbook.yml --step
...
Perform task: configure ssh (y/n/c):

c permet de quitter le mode interactif !

L’utilitaire ansible-console

Je l’ai documenté dans ce billet. Cet outil permet de lancer les modules ansible de manière interactifs.

Changer la sortie d’Ansible

Si comme moi, vous trouvez que la sortie standard de la commande ansible-playbook pas très pratique, vous pouvez en changer.

On va prendre cet exemple pour montrer les différences.

- name: Show ansible_selinux var
ansible.builtin.debug:
msg: "{{ ansible_selinux }}"

donne :

PLAY [Test stdout] *************************************************************
TASK [Gathering Facts] *********************************************************
ok: [localhost]
TASK [Show ansible_selinux var] ************************************************
ok: [localhost] => {
"msg": {
"status": "Missing selinux Python library"
}
}
PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Sortie au format YAML

Il suffit dans le fichier ansible.cfg

[defaults]
stdout_callback = yaml
PLAY [Test stdout] *************************************************************
TASK [Gathering Facts] *********************************************************
ok: [localhost]
TASK [Show ansible_selinux var] ************************************************
ok: [localhost] =>
msg:
status: Missing selinux Python library
PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Sortie au format JSON

[defaults]
stdout_callback = json
{
"custom_stats": {},
"global_custom_stats": {},
"plays": [
{
"play": {
"duration": {
"end": "2021-04-10T12:36:31.761991Z",
"start": "2021-04-10T12:36:30.529488Z"
},
"id": "7d9a4545-499f-5cc6-45f2-000000000005",
"name": "Test stdout"
},
...

D’autres formats de sortie

La liste complète de la communauté des stdout_callback Ansible

D’autres qu’il faut placer dans le répertoire plugins/callback :

Conclusion

En conclusion, la rédaction de playbooks Ansible est un exercice essentiel pour automatiser et orchestrer efficacement la gestion de l’infrastructure. À travers ce guide, nous avons exploré en profondeur les différentes facettes de la syntaxe des playbooks, leur structure et leur organisation. En maîtrisant la logique des tâches, l’utilisation des variables, … vous pouvez transformer des processus manuels complexes en workflows automatiques et reproductibles.

Un playbook bien conçu doit être clair, modulable et maintenable, ce qui facilitera sa réutilisation et son évolution au fil du temps. En suivant les meilleures pratiques que nous avons décrites, vous serez capable d’écrire des playbooks robustes qui répondront aux besoins spécifiques de votre environnement, tout en anticipant les futures extensions de votre infrastructure.

Que vous soyez en phase de déploiement initial ou en gestion continue d’une infrastructure en production, Ansible vous offre une flexibilité et une puissance remarquables pour gérer vos systèmes avec précision et efficacité. La clé du succès réside dans une bonne planification, une organisation rigoureuse et une utilisation judicieuse des ressources offertes par Ansible, notamment à travers les playbooks.

Plus loin

Je vous propose quelques exemples de projets montrant comment développer des playbooks :