LightcodeSysadmin Open Source

Ansible : conseils d'organisation

Lorsque l’on crée des fichiers de configuration avec Ansible, on se perd rapidement à cause de la grande liberté offerte par le logiciel. C’est pourquoi après vous avoir présenté les gestionnaires de configuration et Ansible, je décide de vous parler de bonnes pratiques mais surtout de vous donner un modèle pour vous organiser lors de l’écriture de fichier de configuration avec Ansible.

Attention, cet article n’est pas un tutoriel, il est normal que vous ne compreniez pas tout si vous ne connaissez pas bien Ansible. Je vous conseille de vous référer à la documentation d’Ansible pour en savoir plus sur les conseils donnés.

Organiser ses tâches

Inclure des tâches

Dans l’article précédent, nous avons définit toutes les tâches directement dans le playbook, mais dans la réalité on se retrouvera avec un playbook très long et difficile à maintenir. Ansible offre la possibilité d’inclure des fichiers contenants plusieurs tâches.

Exemple n°1 - Inclure des fichiers de tâches :

- hosts: os_RedHat
  tasks:
    - include: "tasks/epel7.yml"
      when: "ansible_distribution_major_version|int == 7"
    - include: "tasks/epel6.yml"
      when: "ansible_distribution_major_version|int == 6"

Dans l’exemple ci-dessus, j’utilise l’inclusion de tâches directement dans le playbook pour inclure le fichier de tâches d’installation du dépôt EPEL sur une distribution Linux type “RedHat”. Le mot-clé include désigne le chemin vers le fichier à inclure. On peut voir un deuxième mot-clé, optionnel, qui est when et qui permet de mettre une condition sur les tâches incluses. Par exemple, si j’utilise ce playbook sur CentOS 7, je vais inclure les deux fichiers mais uniquement les tâches dans epel7.yml seront exécutées, celles qui sont dans epel6.yml seront toutes marquées comme “skipped”.

Créer un rôle

Dans Ansible, un rôle permet de regrouper des tâches, mais également des handlers, des variables, des fichiers et des templates. Tout cela est mis dans un même dossier portant le nom du rôle. On crée un rôle dès que l’on veut regrouper plusieurs éléments qui sont dépendant entre eux. Par exemple, si vous créez un rôle “NTP”, qui permettra de configurer un client NTP sur votre serveur, vous allez mettre dans un même dossier :

  • Les variables utilisées, par exemple le nom du serveur NTP pour se mettre à l’heure ;
  • Le fichier de configuration principal, sous la forme d’un template dans lequel on injectera les variables précédemment définies ;
  • Un handler qui permettra de recharger le service quand des tâches auront modifiés la configuration ;

Garder son rôle paramétrable

Ansible nous met à disposition de nombreux endroits où définir des variables qui permettent de paramétrer l’exécution de nos playbooks. Pour moi, les variables les plus importantes pour la configuration de notre infrastructure sont celles associées aux groupes et aux hôtes (nous verrons plus tard comment s’en servir).

C’est pour cette raison que le rôle que l’on crée doit rester paramétrable. En gardant vos rôles configurables depuis l’extérieur, vous pourrez les réutiliser plus facilement avec d’autres machines ou sur d’autres infrastructures. Il faut donc penser à utiliser les variables et ne pas tomber dans le piège des variables de rôles. En effet, il est possible de définir des variables dans deux dossiers différents au sein d’un rôle (dans chacun des dossiers, le fichier main.yml est inclus automatiquement) :

  • roles/mon_role/vars : placer dans ce dossier uniquement les variables qui ne doivent pas être changées. Ce seront les constantes de votre rôle et il ne sera pas possible de les modifier depuis l’extérieur, excepté avec le paramètre --extra-vars qui peut être passé à la commande ansible-playbook.
  • roles/mon_role/defaults : les variables placées dans ce dossier seront définies par défaut, elles pourront être facilement modifiées depuis l’extérieur, car elles ont la priorité la plus basse.

En général, on définira nos variables dans le répertoire defaults pour pouvoir les modifier ensuite grâce à nos groupes. Dans les cas où le rôle prend des variables en paramètre qui n’ont pas de valeur par défaut, je place toutes les variables dans le dossier vars en commentaire et je prends soin de les documenter. Cela permettra de retrouver plus facilement quels sont les variables à ajouter dans mes fichiers de variables de groupe.

Gérer plusieurs distributions Linux

Ce point n’est pas obligatoire, mais il peut vous arriver de devoir rendre un de vos rôles exécutables sur plusieurs machines ayant des distributions Linux différentes. Dans l’exemple n°1, nous avons vu une première méthode pour réaliser cette opération. Le mot-clé when peut s’utiliser pour ajouter une condition sur une tâche, mais la tâche reste affichée dans la console même si elle est ignorée, ce n’est pas très propre. Imaginez si vous l’utiliser pour l’inclure des fichiers de tâches complets en fonction des distributions : vous aurez une sortie pas très propre.

Exemple n°2 - utilisation du mot clé when (déconseillé) :

- name: installation d'Apache (Debian)
  apt: pkg=apache2 state=present
  when: ansible_os_family == 'Debian'
- name: installation d'Apache (RedHat)
  yum: pkg=httpd state=present
  when: ansible_os_family == 'RedHat'

Les deux blocs ne sont pas très différent l’un de l’autre. Ce qui change : le nom du paquet et le nom du gestionnaire de paquet. Le nom du gestionnaire de paquet utilisé par la distribution peut s’obtenir grâce à la variable ansible_pkg_mgr. Note : ces variables créées automatiquement par Ansible à propos de l’hôte sont des facts.

L’autre astuce provient de l’inclusion de fichiers contenant des variables en fonction de certaines conditions. Il nous suffira de créer des fichiers de variable portant le nom <distribution>.yml et d’utiliser ce code :

- name: inclusion du fichier de variables spécifique à l'OS
  include_vars: "{{ item }}"
  with_first_found:
    - "{{ ansible_distribution }}-{{ ansible_distribution_version }}.yml"
    - "{{ ansible_distribution }}.yml"
    - "{{ ansible_os_family }}.yml"
    - "defaults.yml"

Cette tâche que j’ai trouvé sur ce site va chercher parmi les fichiers de variables créés dans le dossier vars. Comme vous pouvez le constater, on va du plus précis jusqu’au plus générique. Si ma distribution est CentOS 7, il va essayer de charger les fichiers dans cet ordre :

  • vars/CentOS-7.yml
  • vars/CentOS.yml
  • vars/RedHat.yml
  • vars/defaults.yml

Le mot clé with_first_found va effectuer la recherche et le mot clé include_vars importera ce fichier. Si on veut supporter l’installation d’Apache sous CentOS et sous Debian, on créera les deux fichiers RedHat.yml et Debian.yml (CentOS utilise les mêmes noms de paquet que RedHat, autant couvrir le plus de cas).

Exemple n°3 - fichier de paramètres pour l’installation d’Apache sous CentOS :

---
apache:
  pkg: httpd
  service: httpd

J’ai choisi de créer un dictionnaire pour stocker toutes mes valeurs sous le nom “apache”, pour accéder au nom du paquet on fera simplement apache.pkg. Si on met les deux astuces ensembles, la tâche d’installation de notre paquet se fera ainsi comme dans l’exemple n° 4.

Exemple n°4 - installation d’Apache supportant le multi distribution :

- name: installation d'Apache
  action: "{{ ansible_pkg_mgr }} pkg={{ apache.pkg }} state=present"

Le mot-clé action s’utilise lorsque l’on veut écrire dynamiquement le nom du module Ansible : action: <module> <paramètres...>. Comme vous le voyez, il est possible de gérer assez simplement l’installation de paquets sur plusieurs distributions. Il est possible d’étendre facilement ce principe à d’autre action, par exemple l’envoie de fichier de configuration spécifique à un OS. Nous verrons dans la suite de l’article une autre méthode pour faire du multi-OS avec la création dynamique de groupes.

Gérer plusieurs environnements

Segmenter ces fichiers d’hôtes

Dans cette partie, je vais vous présenter un moyen de gérer plusieurs environnements. Nous allons voir comment créer un même playbook pour gérer un environnement de test et l’environnement de production. La première chose à faire est de séparer les hôtes de test et les hôtes en production en créant deux fichiers de définition d’hôtes séparés : production et testing. Lors de l’exécution de votre playbook vous devrez préciser quelle infrastructure vous ciblez.

Exemple n°5 - exécution du playbook sur l’infrastructure de test :

$ ansible-playbook -i testing site.yml

Création de groupes

Cela n’est peut-être pas suffisant en fonction des cas, vous pouvez avoir besoin de personnaliser les rôles à exécuter. Pour ce faire, on va créer un groupe d’hôtes pour chaque environnement.

Exemple n°6 - fichier d’hôte de l’infrastructure de test :

[servers]
test.www.example

[admin]
mon-serveur-admin.example

[testing:children]
servers
admin

Vous pouvez voir ici deux groupes contenants des machines : “servers” et “admin”. Un troisième groupe nommé “testing” inclus les deux autres groupes à l’aide de la section [testing:children]. Il est aussi possible d’ajouter des machines dans le groupes [testing] directement. Les groupes permettront de modifier les variables ou à exécuter des rôles ou des tâches sur certains hôtes.

Utilisation des groupes

Nous allons voir un nouveau type de fichier de variables, les group_vars et les host_vars qui sont respectivement définis pour un groupe et pour un hôte. Comme tout à l’heure, les variables ont des priorités suivant l’endroit où elles sont définies. La règle est simple, si la variable est définit dans plusieurs fichiers (de groupe ou d’hôte), la valeur placée dans le fichier qui définit le plus précisément l’hôte sera utilisée. Le fichier de variable de l’hôte aura la priorité sur le reste.

Par exemple, si je définis la variable version = 5 dans le groupe “servers” et version = 6 dans le groupe “testing”, la variable version sera définie à 5 car les valeurs des groupes enfants ont la priorité sur la valeur des groupes parents.

Ces fichiers de variables se définissent dans un fichier YAML dans les fichiers group_vars/<nom_groupe>.yml et host_vars/<nom_hote>.yml. Enfin, il existe un groupe qui est le parent de tous les groupes, il se nomme “all”. Voici comment on peut organiser les variables de ses infrastructures :

  • host_vars/<nom_hote>.yml : on l’utilisera très rarement, uniquement dans les cas où la variable ne s’applique qu’à un hôte ;
  • group_vars/testing.yml : ici on mettra toutes les valeurs spécifiques à la phase de test (on peut faire pareil avec la production) ;
  • group_vars/all.yml : ici on met les valeurs par défaut propres à l’infrastructure pour tous les environnements ;

Gestion des mises à jour

Lors de l’installation d’un paquet, le paramètre state existe la plupart du temps et vous permet de choisir si vous souhaitez installer le paquet uniquement s’il est présent, forcer la mise à jour du paquet ou supprimer ce dernier. Dans tous les cas, il est important de définir à chaque fois ce paramètre même lorsqu’il est facultatif. Lorsque l’on veut installer un paquet, il est conseillé de le mettre avec le paramètre state=present et d'utiliser un playbook séparé pour faire les mises à jour du système. Cela permet d’éviter que des paquets se mettent à jour sans avoir été testé au préalable et perturbent votre service. Il est donc préférable de bien contrôler les mises à jour pour éviter les mauvaises surprises.

Exemple n°7 - playbook de mise à jour, upgrade.yml :

---
- hosts: all
  tasks:
    - name: regroupement par famille d'OS
      group_by: key=os_{{ ansible_os_family }}
- hosts: os_RedHat
  tasks:
    - name: met à jour tous les paquets de RedHat
      yum: name=* state=latest
- hosts: os_Debian
  tasks:
    - name: met à jour tous les paquets de Debian
      apt: upgrade=dist

L’exemple n°7 permet de mettre à jour tous les paquets des distributions de la famille des Debian et des RedHat. La première partie utilise le mot-clé group_by pour regrouper les hôtes en fonction d’une variable d’un “fact”. Dans notre cas, cela permet de créer un groupe portant le nom os_<nom de la famille d'OS>. hosts: <nom du groupe> permet d’appliquer les tâches uniquement pour les hôtes faisant parties du groupe. Ensuite, une tâche est créée pour chacune des familles d’OS pour mettre à jour tous les paquets installés.

Je vais terminer l’article en vous montrant deux commandes, la première permet de vérifier les systèmes à mettre à jour :

$ ansible-playbook -i testing upgrade.yml --check
[...]
TASK: [upgrade all packages (RedHat)] *****************************************
changed: [centos-test.local]

PLAY [os_Debian] **************************************************************

TASK: [upgrade all packages (Debian)] *****************************************
ok: [debian-test.local]
[...]

L’argument --check permet de voir qu’elles sont les tâches qui pourraient modifier le système mais n’effectue aucune des modifications. Les machines marquées comme “changed” ne sont donc pas à jour. Pour limiter l’exécution du playbook de mise à jour à une machine ou un groupe, on utilisera l’argument --limit :

$ ansible-playbook -i testing upgrade.yml --limit centos-test.local

Ici, on mettra à jour uniquement la machine “centos-test.local”.

Conclusion

Cette série de conseils peut s’appliquer à beaucoup d’infrastructures gérées par Ansible. Je vous recommande également de lire les Best Practices dont je me suis également inspiré. Voici un résumé des conseils donnés dans l’article :

  • Créez des rôles pour organiser plus facilement vos fichiers ;
  • Utilisez au maximum les variables dans les rôles, cela permet de modifier les valeurs plus facilement en fonction du contexte de déploiement ;
  • Créez des fichiers d’hôtes séparés pour gérer l’infrastructure de production et celle de test ;
  • Séparez les mises à jour du reste de votre playbook principal pour éviter les mauvaises surprises.