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é et là 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 src
, scm
, version
, 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 :
├── all
│ ├── vars
│ └── vault
├── région_1
│ ├── vars
│ └── vault
└── région_2
├── vars
└── vault
├── 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.
3 commentaires
Cyrille P. · 23 juin 2021 à 15 h 42 min
@Tuxmika > Les rôles Ansible s’utilisent depuis un playbook Ansible. Je vous invite à regarder la documentation Ansible pour les différentes méthodes d’accès aux rôles : https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html#using-roles
Tuxmika · 23 juin 2021 à 13 h 49 min
Bonjour
Quelle est la syntaxe pour exécuter un rôle ?
Cdt
Stéphane ROBERT · 16 février 2021 à 17 h 12 min
Merci pour ce billet