Gestion des rôles sous Ansible

  • Adrien 

Cet article vise principalement à compiler un ensemble de bonnes pratiques explicitées dans la documentation officielle d’Ansible. Nous avons eu l’idée d’écrire cet article suite à notre difficulté initiale à synthétiser l’ensemble des informations disponibles sur le sujet afin d’aboutir à une utilisation simple, efficace et valable sur le long terme d’Ansible.

Les rôles Ansible, une bonne pratique ?

Ansible est un outil de gestion de configuration habituellement utilisé pour le provisioning d’environnement, le déploiement logiciel et l’orchestration de processus.

Son approche sans agent, sa syntaxe simple (YAML) et sa modularité font de lui un outil polyvalent et complexe. Il est donc facile de se retrouver avec de multiples implémentations, techniquement fonctionnelles, pour un même projet. Notre tâche reste donc à déterminer lesquelles de ces implémentations remplissent les critères suivants, dits de « bonnes pratiques » :

  • Réutilisabilité du code
  • Lisibilité du code et de la structure des fichiers
  • Flexibilité/Adaptabilité du projet face aux évolutions futures

Pour en revenir à l’intitulé de cet article, les rôles définis par Ansible sont un ensemble de tâches qui s’assurent de la présence/absence d’une fonctionnalité spécifique (allant d’un utilisateur linux à un cluster Kubernetes d’une centaine de nœuds).

Chaque rôle doit être capable de fonctionner en autonomie (sans compter des dépendances vers d’autres rôles) et inclut pour ça, dans son arborescence, un certain nombre de choses :

├── README.md
├── defaults
└── main.yml
├── files
├── handlers
└── main.yml
├── meta
└── main.yml
├── tasks
└── main.yml
├── templates
└── vars
└── main.yml
Nous avons donc un README.md pour documenter l’utilisation du rôle, un dossier defaults pour définir les variables par défaut. Un dossier files
où l’on mettra les fichiers hors playbooks dont on aura besoin (par exemple des fichiers de configuration). handlers recensera les événements Ansible liés à ce rôle. meta contient, comme son nom l’indique, les métadonnées de ce rôle telles que son auteur ou ses dépendances. Le dossier le plus important tasks contient les tâches à exécuter. On mettra dans le dossier template les templates Jinja2 qui permettent de générer des fichiers textes de façon procédurale. Et enfin on déclare les variables du rôle dans vars (ce qui supplante évidemment les définitions du dossier defaults).
L’utilisation des rôles Ansible semble donc parfaite pour s’assurer qu’une partie du code sera le plus générique possible et réutilisable à l’infini, pour peu que l’on suive quelques principes de base.

Principes de base

Quitte à enfoncer des portes ouvertes, parlons de quelques principes à respecter pour l’écriture de vos rôles.

Le concept de feature unique permet de maximiser la réutilisabilité du rôle dans divers scénarios tout en maintenant sa complexité au strict minimum. Ainsi, il vaut mieux tabler sur une dépendance entre rôles (explicitée dans un README et le fichier de métadonnées) plutôt que d’inclure un nouvel ensemble de tâches à chaque nouveau besoin d’environnement.

Dans le même registre, pour atteindre un niveau de généralisation correct il faudra passer par l’abstraction de toute variable spécifiant le contexte de déroulement du rôle dans un playbook (les utilisateurs, leurs permissions, le nombre de nœuds dans un cluster, les configurations d’un outil, etc). Nous verrons dans une prochaine section comment sont gérées les variables en dehors du rôle lui-même.

Ensuite, tout ensemble de tâches n’est pas transformable en rôle ou même judicieux à transformer en rôle. Si la fonctionnalité n’est pas généralisable, ou alors si simple qu’un module Ansible est suffisant pour l’implémenter, alors il semble raisonnable de ne pas utiliser un rôle. Tâches et rôles peuvent parfaitement cohabiter au sein d’un projet Ansible, le tout est de trouver le bon équilibre suivant les besoins du projet.

Enfin, garder une structure formalisée à l’intérieur d’un rôle est important afin de faciliter leur compréhension et leur maintenance. On se base pour ça sur la structure donnée par Ansible dans sa page dédiée aux bonnes pratiques et explicitée dans la section précédente de cet article. Il est possible de générer automatiquement un squelette de cette structure grâce à la CLI ansible-galaxy :

ansible-galaxy init <nom_du_rôle>

La réutilisation en pratique

Maintenant que les bases sont posées, il faut s’assurer du meilleur moyen de réutiliser ces rôles dans différents projets, et ce, peu importe l’outil de versioning utilisé (de même si le projet n’est pas versionné).

Plusieurs choix s’offrent alors à nous :

  • Copier les rôles dans le répertoire cible à côté des playbooks appelé par la chaîne de CI/CD.
  • Utiliser Ansible Galaxy.
  • Utiliser un répertoire pour chaque rôle sur un hôte tel que Github, GitLab, BitBucket, etc.

La première approche n’est définitivement pas la bonne en termes de maintenabilité et de gestion de notre ensemble de rôles (se retrouvant ainsi éparpillé dans les différents projets). Ansible Galaxy est une bonne plateforme pour rendre ses rôles accessibles au plus grand nombre, cependant cela force à gérer un compte supplémentaire (si vous n’utilisez pas déjà Github) en parallèle d’un outil pour gérer le code source (dans notre cas GitLab). Par commodité, nous avons décidé de stocker nos rôles sur notre outil de versioning (même si Ansible Galaxy reste une très bonne alternative).

Dans le cadre du troisième scénario, il suffit donc d’utiliser la CLI ansible-galaxy pour arriver au résultat escompté (tel décrit dans la documentation officielle) :

ansible-galaxy install -r chemin/vers/requirements.yml -p dossier/où/installer/le/rôle

Cette commande est appelée en amont de l’exécution du playbook par notre chaîne de CI/CD.

C’est le fichier requirements.yml qui contient les arguments qui nous intéressent. Il permet, entre autres, d’importer plusieurs rôles de sources pouvant être différentes. Voici un petit exemple avec un rôle utilisé pour le déploiement de Sphinx Search conteneurisé :

---
- name : sphinxse
  src : git@gitlab.com:nom/de/notre/répertoire/sphinxse.git
  scm: git
  version : origin/master

Le rôle correspondant peut être trouvé ici et nous avons un rôle pour générer une configuration SphinxSE, pour ceux que ça intéresse. pour ceux que ça intéresse. Cependant attention, pour « pull » un répertoire au sein d’un job automatisé il vous faudra ajouter la clef ssh de l’hôte lançant le playbook (ou celle de votre image Ansible dockerisée) aux deploy keys de votre projet. La plupart des outils de versioning ont cette option de nos jours (par exemple GitLab et Bitbucket). Sur GitLab l’option est trouvable sous Settings -> Repository -> Deploy Keys.

Comme expliqué ici, il est possible de définir de la même façon des rôles de dépendance à importer, dans le fichier meta/main.yml du rôle principal. Rien de bien compliqué ici, en suivant les indications de la documentation en plus du fichier créé par ansible-galaxy init il est très simple d’établir un arbre de dépendance relativement complexe et versatile (vis-à-vis des sources utilisées) :

dependencies: []
  # List your role dependencies here, one per line. Be sure to remove the '[]' above,
  # if you add dependencies to this list.

You specify role dependencies in the meta/main.yml file by providing a list of roles. If the source of a role is Galaxy, you can simply specify the role in the format username.role_name. The more complex format used in requirements.yml is also supported, allowing you to provide srcscmversion, and name.

Gestion des variables

Un coup d’œil à la page de documentation des variables et il apparait clairement que la gestion des variables sous Ansible est un sujet aussi délicat qu’important. En effet, entre les différents types de variables (les facts, les registered variables et variables définies statiquement) et la portée des variables statiques allant de l’ordre du playbook à celui de la tâche selon l’endroit où elles sont défini, il est très facile de se sentir un peu perdu. Surtout lorsque l’on commence à utiliser des rôles, ce qui introduit des problématiques supplémentaires (nommage des variables pour éviter les « collisions » par exemple => en ayant var_1 dans rôle_1 et var_2 dans rôle_2 on se retrouvera avec ce que l’on appelle une collision, car les deux variables ont le même nom).

Stratégie de base

Même si la définition d’une variable dépend grandement de son utilisation, il est nécessaire de choisir une stratégie qui conviendra à la majorité des cas sans rajouter de complexité additionnelle. Heureusement pour nous, la page des bonnes pratiques officielles d’Ansible aborde ce sujet épineux. Il est recommandé d’utiliser un dossier group_vars au niveau de la racine de notre projet et de déclarer l’ensemble de nos variables à l’intérieur dans deux fichiers distincts vars et vault (ce dernier étant crypté par ansible-vault et contenant nos informations sensibles).

Cette approche, en l’état, introduit une nouvelle couche de complexité lorsque l’on doit gérer plusieurs environnements. En effet, un rôle appelé dans plusieurs environnements devra différencier ses paramètres d’entrée au niveau de leur dénomination, en plus d’alourdir inutilement les fichiers de variables avec des duplicatas. Se pose également la question de l’organisation de nos variables à l’intérieur même des fichiers. Doit-on les trier par environnement ? Par fonctionnalité ? Ou alors diviser les fichiers de variables en plusieurs fichiers ?

La bonne approche serait donc de diviser nos fichiers et c’est encore une fois la documentation Ansible sur les variables qui nous donne un élément de réponse :

Regional information might be defined in a group_vars/region variable.

Le but est ici de séparer les définitions des variables par région et par hôte (qui sont précisés dans l’inventaire du projet Ansible). Voici un exemple d’arborescence pour mieux se représenter la chose :

group_vars
├── all
├── vars
└── vault
├── région_1
├── vars
└── vault
└── région_2
├── vars
└── vault

host_vars
├── hôte_1
├── vars
└── vault
└── hôte_2
├── vars
└── vault

On notera que plus un groupe est spécifique (all > région > hôte), plus sa priorité sera grande au niveau de la définition d’une variable. C’est-à-dire que la définition dans le fichier vars d’une variable var_1 dans all sera supplantée par la définition de la région_1 et région_2 (les deux peuvent cohabiter), de même s’il y a redéfinition de var_1 dans hôte_1 et hôte_2.

Règles de rédaction des fichiers de variables

Pour ordonner cette pléthore de variables il devient nécessaire de s’imposer quelques règles de rédaction pour nos fichiers de variables. Les règles explicitées ci-après sont totalement arbitraires et sont utilisées dans notre cycle de développement en interne. Voyez-les plus comme des exemples d’organisation et non comme des règles gravées dans le marbre.

Nous avons choisi de nommer nos variables avec la convention suivante nomRôle_nomVar et nomRôle_nomVar_vault si la variable est un secret à stocker dans un vault. Cela évite les « collisions » de variables se retrouvant avec une dénomination identique. En parallèle, nous structurons nos fichiers de variables de la façon suivante :

---
playbook_var_1: "test playbook var 1"

# Rôle 1 variables
role_1_var_1: "test rôle 1 variable 1"

# Rôle 2 variables
role_2_var_1: "test rôle 2 variable 1"

Même si cela engendre une duplication de variables pour des services qui pourraient être mutualisés (se connecter au même répertoire d’images Docker par exemple), il reste impératif de garder la dé-corrélation des rôles au maximum afin d’éviter les effets de bords lors de changement de définition d’une variable.

Adaptation à un cas spécifique

Imaginons que l’on utilise un rôle provenant d’Ansible Galaxy ou d’un répertoire git quelconque. Dans ce cas, nous n’avons aucun contrôle sur le nommage des variables au sein du rôle. Ceci peut parfois contredire les règles de rédaction que l’on a défini en interne et une solution doit être trouvée. Soit nous prenons le même nom de variable et abandonnons notre système de nommage pour ce rôle ci, soit nous effectuons une correspondance de nos définitions de variables avec celles du rôle. Par exemple :

roles:
   - { role: role_1, var_definie_par_role_1: "{{ role_1_var_redefinie_vault }}" }
   - role: role_2
         var_definie_par_role_2: "{{ role_2_var_redefinie }}"

Pour conclure

J’espère que cet article a pu vous éclairer sur la manière utiliser Ansible et plus particulièrement sur l’utilisation de ses rôles. Il sera mis à jour si l’approche décrite dans cet article venait à changer.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.