10 astuces Ansible

S’il vous arrive de faire un peu d’Ansible, vous avez sûrement de bonnes pratiques qui se sont progressivement imposées, surtout si vous travaillez en équipe autour du même code. Dans mon expérience de “développeur” Ansible, j’ai pu ainsi en recueillir certaines que je mets en oeuvre régulièrement quand j’écris des scripts. Ces bonnes pratiques vous feront très certainement gagner un temps précieux.

Je présuppose que vous connaissez Ansible, les playbooks, la syntaxe YAML et les rôles d’Ansible Galaxy.

1 – Toujours tout nommer

Il est possible d’invoquer des Playbooks ou des Tasks sans les nommer :

---
- hosts: local
  tasks:
  - user:
      name: testuser1
      state: present
      groups: wheel

Qui, lors du lancement d’Ansible, va donner :

PLAY ***************************************************************************
TASK [user] *******************************************************************
[...]

Peu d’indication sur ce qui se passe, pas tellement d’information. On peut faire mieux.

Par exemple :

---
- name: "Prepare localhost"
  hosts: local
  tasks:
  - name: "Create testuser1"
    user:
      name: testuser1
      state: present
      groups: wheel

Va donner lors de l’exécution :

PLAY [Prepare localhost] ******************************************************
TASK [Create testuser1] *******************************************************
[...]

Ici, on s’aperçoit que la lisibilité est bien meilleure. Pour faire encore mieux, il ne faut pas hésiter à mettre des informations de DEBUG dans les noms des Tasks, comme par exemple :

- name: "Create folder {{ target_folder }}"
  file:
    path: "{{ target_folder }}"
    recurse: yes
    state: directory

Comme toujours, les messages informatifs doivent contenir autant d’informations que possible, pour permettre le DEBUG en cas d’erreur. J’essaye toujours de mettre le maximum de contexte dans les noms des tâches afin de faciliter l’analyse, plus tard. On ne sait jamais, vous pourriez être vraiment pressé de comprendre ce qui ne fonctionne pas bien dans vos opérations.

Toujours mettre un nom aux Tasks Ansible !!!

2 – Utiliser la syntaxe YAML et pas la syntaxe Ansible

Ansible vous permet d’utiliser un mélange entre deux syntaxes lorsque vous rédigez votre code. Vous pouvez soit utiliser du YAML pur :

- name: add user testuser1
  user:
    name: testuser1
    state: present
    groups: wheel

Ou vous pouvez aussi utiliser la syntaxe hybride YAML et Ansible :

- name: add user testuser1
  user: name=testuser1 state=present groups=wheel

Ces deux exemples sont rigoureusement identiques en terme de description Ansible. Dans le premier cas, Ansible parse tout en YAML puis exécute le code. Dans le second cas, Ansible va en plus parser name=testuser1 state=present groups=wheel avant d’appeler la tâche user. Cette syntaxe est pratique, mais elle oblige à une certaine gymnastique avant d’arriver à faire fonctionner une tâche, surtout quand celle-ci est complexe.

La bonne pratique consiste à toujours utiliser la syntaxe YAML pour éviter la syntaxe hybride avec les = : elle permet une détection d’erreurs un peu plus tôt, et augmente la lisibilité de votre code Ansible.

Avec l’expérience, elle devient plus facile à écrire que le mélange entre la syntaxe d’assignation YAML et celle supportée par Ansible.

Toujours préférer la syntaxe YAML !!!

3 – Documenter les variables

Ansible supporte la surcharge de variables suivant l’endroit où vous les déclarez. Dans la documentation officielle, on explique la priorité des déclarations des variables comme suit :

        role defaults [1]
        inventory vars [2]
        inventory group_vars
        inventory host_vars
        playbook group_vars
        playbook host_vars
        host facts
        play vars
        play vars_prompt
        play vars_files
        registered vars
        set_facts
        role and include vars
        block vars (only for tasks in block)
        task vars (only for the task)
        extra vars (always win precedence)

Comme on peut le voir, la liste est particulièrement fournie et vous permet de surcharger une variable n’importe où, grosso modo. Du coup, il faut faire attention :

  • à ne pas trop disperser les déclarations de variables dans le code que vous utilisez. Évidemment, si c’est vous qui écrivez tout votre code, c’est plus facile à faire que si vous utilisez le code commis par d’autres (au hasard, en récupérant du code depuis Galaxy)
  • à documenter précisément les variables que vous déclarez dans vos rôles, ou ailleurs.

Personnellement, j’utilise des variables de groupes, de rôles autant que possible avant d’ajouter d’autres niveaux de surcharge.

Ensuite, je documente toujours tous les réglages (exemple avec un vrai fichier de la vraie vie) :

---
# file: group_vars/all
# For data synchronization from the server to localhost
local_source_folder: /Users/octplane/src/wiseman_r
remote_production_folder: /home/oct/prod

# app name to look for in the local registry
app_name: wiseman
# image name to search for in the local image registry
image_name: "octplane/{{ app_name }}"
# version to search for in the local image registry
version: 4
# where to export the docker image
export_folder: /tmp
# Exported file from docker after zipping
artefact_name: "{{ app_name }}-{{ version }}.docker.gz"

# where to copy the data on the remote server
# where is deployed the test application
remote:
    landing_folder: /home/oct/tmp
    test_folder: /home/oct/data/wiseman.test/

Ainsi, quand je reviens dans mon code, plus tard, ou si quelqu’un d’autre tombe sur mon code, il peut le comprendre plus facilement que si il doit aller jongler en permanence avec les rôles et les variables.

Toujours documenter les variables !!!

Minimiser le nombre d’emplacements où les variables sont assignées !!!

4 – Utiliser les asserts pour valider les paramètres

En complément de la documentation que vous avez écrite précédemment, valider les paramètres avant de s’en servir est aussi une excellente pratique à mettre en place. En faisant des contrôles de conformité, on a ainsi l’opportunité d’échouer plus tôt dans les traitements des commandes, et c’est une bonne chose.

- name: "Validate version is a number, > 0"
  assert:
    that:
      - "{{ version | int }} > 0"
    msg: "'version' must be a number and > 0, is \"{{version}}\""

Comme toujours, faites des messages d’erreur utiles. Il y a une grande différence entre un message d’erreur générique produit par Ansible et un message d’erreur que vous avez vous même préparé tel : 'version' must be a number and > 0, got "coucou" pour la compréhension.

Utiliser des asserts pour planter tôt en cas de paramètres invalides !!!

Faire des messages d’erreur explicites !!!

5 – Changer le logger stdout par défaut

Par défaut, le logger stdout d’Ansible est un peu moisi. Il vaut mieux en utiliser un plus compact, et plus utile. Je vous propose de tester ce logger :

https://github.com/octplane/ansiblestdoutcompact_logger

Écrit par votre serviteur, il a plusieurs fonctionnalités :

  • il est beaucoup plus compact par défaut
  • il indique la durée d’exécution des tâches et horodate sa sortie
  • en mode -v, il affiche le contenu des messages des tâches de manière structurée et lisible (le YAML est indenté, les retours chariots sont appliqués…)
  • il affiche les champs importants en premier (stdout, stderr, au hasard)
  • dès que l’on dépasse le mode -vv, il rebascule sur le logger habituel d’Ansible.
  • il contient sûrement des bugs, mais c’est ça la beauté de l’open source !

Aller voir ailleurs ce qui existe !!!

Contribuer à l’open source, quand on s’en sert !!!

6 – Écrire les rôles avec un sens technique avant tout

Dès qu’on commence à écrire un rôle Ansible, la tentation est grande de fourrer dedans un peu tout pour avoir une espèce de boîte à outils réutilisable.

En réalité, il faut essayer de séparer le contenu des rôles par objet technique : un rôle “MongoDB”, un rôle “HAProxy”, un rôle “Tomcat” et utiliser ensuite la glue fournie par les playbooks pour faire l’assemblage simplement et rapidement.

Cette séparation vous permet de regrouper au sein du même rôle toutes les tâches qui peuvent être effectuées pour le composant concerné : l’installation, la configuration, le démarrage, l’arrêt, la maintenance, la mise à jour… et seulement de ce composant.

Si votre code doit toucher à un autre composant technique, essayez de le mettre dans un autre rôle dès que possible. Et si vous tenez absolument à orchestrer ces tâches depuis un rôle, écrivez un rôle transverse qui va faire appel aux différentes étapes de vos rôles techniques.

L’objectif premier est de bien séparer les actions qui sont effectuées dans un rôle donné, diminuant ainsi ses dépendances, et le nombre de variables dont il a besoin. Le second objectif est bien évidemment la réutilisabilité, qui est primordiale, pour tout code.

Un rôle ne doit manipuler qu’un seul composant !!!

Écrire des rôles transverses pour appeler les rôles techniques si nécessaire !!!

7 – Pouvoir utiliser un rôle pour plusieurs opérations

Un rôle présente techniquement un point d’entrée unique via son tasks/main.yml. À cause de cette façon de fonctionner, on pourrait croire qu’il est difficile de faire faire plusieurs opérations à un rôle (sans passer par les tags, cf. ci-dessous, ou des includes).

Et pourtant, il existe une manière d’écrire un rôle Ansible qui le rend facilement ré-utilisable, y compris pour plusieurs opérations qui n’ont rien à voir.

En utilisant une des variables du rôle pour déterminer les actions à effectuer, vous pouvez ainsi construire un mini-routeur d’opérations dans votre rôle :

---
# roles/service/vars/main.yml
# by default, we ensure that service is present, configured and running.
# allowed values: present, absent, install, configure, start, stop
state: present
---
# roles/service/tasks/main.yml
- include: "{{ state }}.yml"
---
# roles/service/tasks/present.yml
- include: "install.yml"
- include: "configure.yml"
- include: "start.yml"
---
# roles/service/tasks/install.yml
- name: add user testuser1
  user:
    name: testuser1
    state: present
    groups: wheel

Permettre à un rôle de remplir simplement plusieurs fonctions !!!

8 – Attention aux set_fact

Les set_fact vous permettent de créer facilement des variables pendant qu’Ansible fonctionne pour rendre son comportement plus dynamique. C’est très pratique. Cependant, ces variables créées à la volée ont plusieurs inconvénients, et il faut bien réfléchir à leur utilisation avant d’en créer une :

  • sa priorité est assez élevée
  • elle est créée dynamiquement : vous devez être sûr que sa création n’échouera pas et ne donnera pas un résultat aberrant (par exemple, un shell qui échoue pour une raison mystérieuse).
  • elle est créée dynamiquement : elle n’existe pas avant sa création. Cela peut paraître évident, mais la plupart des autres variables Ansible existent pendant tout le temps de run.
  • l’utilisateur de votre code ne peut pas forcément la surcharger facilement s’il le veut.

La morale de cette histoire est bien de faire attention aux set_fact qui peuvent, tel un cadeau empoisonné, venir vous exploser à la figure au mauvais moment.

Limiter les set_fact aux cas où ils sont absolument nécessaires au fonctionnement du rôle !!!

9 – Utiliser les tags avec modération

Dès qu’on produit un rôle qui remplit plusieurs usages, il est tentant d’utiliser les tags pour filtrer seulements certaines tâches lors de l’exécution. Ca fonctionne dans la plupart des cas et vous permet de lancer vos déploiements via l’option -t pour choisir les tags à utiliser ( ou --skip-tags pour les ignorer).

On peut néanmoins identifier deux petits problèmes qui peuvent surgir en utilisant les tags dans Ansible :

  • les tags peuvent être dispersés dans l’ensemble de vos rôles et entrer en collision les uns avec les autres, vous empêchant de cibler précisément les tags qui vous intéressent
  • cette dispersion rend l’audit de ce que font précisément les tags assez difficile à faire

Au final, on préfèrera utiliser l’exemple de routage expliqué ci-dessus qui a l’avantage de ne pas utiliser de fonctionnalité supplémentaire dans Ansible et qui oblige le développeur du rôle à séparer précisément les différentes fonctionnalités de celui-ci. On réservera les tags à l’utilisation ‘interactive’ d’Ansible en ligne de commande, par opposition à l’utilisation en mode piloté depuis un script ou un orchestrateur par exemple.

Utiliser les tags sans trop d’excès !!!

10 – Ne pas hésiter à reconfigurer Ansible

Grâce à un modèle de surcharge simple, il est possible de donner à Ansible un fichier de configuration ansible.cfg dans lequel vous pouvez reconfigurer partiellement Ansible pour vos besoins.

Ansi, qu’il s’agisse de spécifier un hostfile alternatif pour ne plus avoir à préciser -i meshostsà chaque lancement, ou bien supprimer les inutiles fichiers .retry, ou toute autre option Ansible, vous n’avez bien souvent qu’à créer un fichier ansible.cfg là ou vous lancez vos playbooks pour qu’Ansible aille automatiquement chercher ce fichier en premier lors de son démarrage. L’ordre de recherche est celui-ci :

* ANSIBLE_CONFIG (an environment variable)
* ansible.cfg (in the current directory)
* .ansible.cfg (in the home directory)
* /etc/ansible/ansible.cfg

(source http://docs.ansible.com/ansible/intro_configuration.html)

À vous la personnalisation de votre outil !

Personnaliser son utilisation d’Ansible avec un fichier ansible.cfg local !!!

Conclusion et emballage

Comme nous l’avons vu ensemble, l’utilisation régulière d’Ansible permet de dégager des bonnes pratiques qui rendent le fonctionnement de cet outil plus pratique au quotidien. Ces trucs sont évidemment issus de mon expérience et mes discussions avec d’autres développeurs Ansible, mais j’ai sûrement exagéré l’avantage de certaines astuces et j’en ai sûrement oublié d’autres. Récapitulons ensemble :

  • Toujours mettre un nom aux Tasks Ansible.
  • Toujours préférer la syntaxe YAML.
  • Toujours documenter les variables.
  • Minimiser le nombre d’emplacements où les variables sont assignées.
  • Utiliser des asserts pour planter tôt en cas de paramètres invalides.
  • Faire des messages d’erreur explicites.
  • Aller voir ailleurs ce qui existe.
  • Contribuer à l’open source, quand on s’en sert.
  • Un rôle ne doit manipuler qu’un seul composant.
  • Écrire des rôles transverses pour appeler les rôles techniques si nécessaire.
  • Permettre à un rôle de remplir simplement plusieurs fonctions.
  • Limiter les set_fact aux cas où ils sont nécessaires au fonctionnement du rôle.
  • Utiliser les tags sans trop d’excès.
  • Personnaliser son utilisation d’Ansible avec un fichier ansible.cfg local.

Et vous, comment utilisez-vous Ansible ? Avez-vous des trucs et astuces à transmettre à la communauté ? À vous !