La gestion de l’infrastructure est l’un des process les plus importants du développement logiciel.

Contrairement à la gestion d’infrastructure traditionnelle, l’infrastructure programmable (ou Infrastructure as code IaC) consiste à automatiser le provisionnement, la configuration et le déploiement uniquement via du code, souvent sous forme de fichiers de configuration ou de scripts.

L’avantage de l’IaC est de réduire les interventions humaines, de minimiser les erreurs, ainsi que d’augmenter l’agilité de l’équipe en lui facilitant la création, le déploiement et la surveillance des applications.

Plusieurs solutions ont été développées pour automatiser le déploiement des serveurs d’une infrastructure, telles que Terraform ou AWS CDK pour la création des serveurs dans un environnement cloud, et puppet, chef ou Ansible pour la configuration.

Dans cet article, nous allons découvrir Ansible, son fonctionnement et les notions de bases pour débuter avec cet outil.

Ansible

Ansible est un logiciel open source qui permet de gérer la configuration des serveurs et le déploiement d’applications à distance. Il a été créé en 2012 par Michael DeHaan, puis il a été racheté par Red Hat en 2015.

Ansible fonctionne en mode push. Ce mode consiste à contrôler les serveurs (nodes) depuis un poste local qu’on appelle un node manager ou un control node.

Le node manager doit être une machine UNIX disposant d’une version d’Ansible installée.

Une fois Ansible installé sur cette machine, les tâches à exécuter sont écrites dans des fichiers de configuration en yaml, ces tâches sont converties par Ansible vers des modules python, puis sont poussées grâce à une connexion SSH pour être exécutées sur les serveurs distants.

La connexion SSH est maintenue jusqu’à ce que les tâches soient totalement exécutées et que les résultats soient renvoyés vers le node manager.

Fonctionnement d'Ansible en mode push

Remarque : Dans certains cas, Ansible peut aussi être utilisé en mode pull. Dans ce mode, Ansible doit être installé sur tous les serveurs, et chacun de ces serveurs “tire” les fichiers de configuration depuis un repository distant pour les convertir en tâches et les exécuter.

Inventaire

Avant de configurer les tâches, il est nécessaire de définir l’inventaire Ansible. Il s’agit de l’ensemble des serveurs qu’on souhaite gérer. L’inventaire peut contenir un ou plusieurs serveurs. On peut regrouper ces serveurs dans des sous-groupes pour en faciliter la gestion.

Le but de l’inventaire est de pouvoir cibler les serveurs pour lesquels les tâches Ansible seront exécutées, ainsi que de définir des variables pour chaque serveur.

Il est possible de définir les serveurs de trois manières :

  • un fichier INI statique,
  • un fichier YAML statique,
  • un script qui génère l’inventaire sous forme de JSON d’une manière dynamique.

Les deux premières méthodes statiques sont à privilégier quand on dispose de serveurs physiques. En revanche, le script dynamique est très recommandé pour lister les serveurs virtuels créés dans un cloud.

Exemple de fichier INI :

web_1 ansible_host=35.1.1.1
web_2 ansible_host=35.1.1.2
lb ansible_host=35.1.1.3
db ansible_host=35.1.1.4

[webs]
web_1
web_2

[dbs]
db

[lbs]
lb

Dans l’exemple ci-dessus, l’inventaire contient 2 serveurs web, un serveur load balancer et un serveur database. Les serveurs web appartiennent au groupe webs, et les serveurs load balancer et database appartiennent respectivement aux groupes lbs et dbs.

Modules

Ansible fournit plus de 3000 modules qui permettent d’exécuter des tâches sur chaque serveur.

La commande ansible-doc -l liste l’ensemble de ces modules.

Ansible fournit aussi une documentation détaillée pour chaque module via la commande ansible-doc <module>.

Chaque module peut être exécuté sous forme d’une tâche. Cette dernière est définie par :

  • sa configuration (un ensemble de paramètres qui ne dépendent pas du module),
  • le module Ansible choisi,
  • la configuration du module (un ensemble d’arguments propre au module).

Les tâches peuvent être lancées grâce aux deux commandes suivantes : ansible et ansible-playbook. Nous les détaillons ci-après :

La commande ansible

C’est la commande qui permet de lancer une seule tâche en mode ad-hoc sur un ou plusieurs serveurs. Ce mode est principalement utilisé sur des environnements de développement pour faire des tests ou pour se familiariser avec des nouveaux modules.

On peut l’utiliser ponctuellement en production pour exécuter des tâches non répétitives.

La commande ad-hoc se lance de cette manière :

ansible <PATTERN> -i <INVENTORY> -m <MODULE_NAME> -a <MODULE_ARGS> -b

l’argument pattern permet de définir le nom du serveur ou le groupe de serveurs sur lesquels la tâche sera lancée.

En plus de cet argument principal, on trouve les arguments optionnels suivants :

  • -i ou --inventory : le chemin vers le fichier de l’inventaire,
  • -m ou --module-name : le nom du module Ansible,
  • -a ou --args : l’ensemble des clés/valeurs pour paramétrer le module,
  • -b ou --become: Certains modules nécessitent des droits pour être exécutés. Dans ce cas, ce paramètre doit être rajouté pour l’élévation des privilèges (sudo).

La commande ansible-playbook

Contrairement à la commande ad-hoc, ansible-playbook permet de lancer un ensemble de tâches codées dans un ou plusieurs fichiers yaml, et destinées à être exécutées de manière répétée en production.

Dans la suite de cet article, nous verrons en détail ce qu’est un playbook, les éléments qui le composent et comment ils sont structurés dans un projet.

Exemples

Le module debug

On commence les exemples par le module debug, un module très utile pour déboguer des variables et tester des conditions au milieu d’un playbook.

ansible localhost -m debug -e "my_name=John" -a "msg='My name is {{ my_name }}'"
Résultat de la commande ad-hoc

Le serveur ciblé par cette tâche n’est pas un serveur distant, il s’agit juste du poste de travail local, qui est rajouté automatiquement par Ansible dans l’inventaire.

Dans cette tâche, l’argument -e (ou --extra-vars) est utilisé pour définir la variable my_name. Cette dernière est bien interprétée dans le message affiché grâce au système de template jinja2.

Le module shell

Le module shell donne la possibilité de lancer des commandes shell.
Dans cet exemple, nous créons un nouveau dossier ansible_directory en lançant la commande bash mkdir ansible_directory.

ansible localhost -m shell -a "mkdir ansible_directory"

Bien qu’il soit difficile de connaître tous les modules disponibles sur Ansible, il est conseillé d’explorer la collection des modules afin de trouver celui qui répond au besoin au lieu de tout faire en bash.

Si on reprend l’exemple de création d’un nouveau dossier, il est possible d’exécuter cette tâche en utilisant le module file qui permet la gestion des fichiers et des dossiers.

ansible localhost -m file -a "path=ansible_directory state=directory"

En plus de ce module, on trouve aussi le module copy qui copie un fichier depuis la machine locale vers un serveur distant, ainsi que le module template qui permet de générer un fichier dynamiquement avec Jinja2.

Playbook

Un playbook est une séquence de plusieurs jeux d’instruction (ou play) définis dans un fichier yaml. Chaque play contient lui-même un ensemble de tâches et une liste de serveurs ciblés par ce play.

Lors de l’excécution d’un playbook, les play sont excécutés dans l’ordre de haut en bas. Les tâches de chaque play sont également lancées dans cet ordre.

La liste des serveurs associés peut changer d’un play à l’autre. Par exemple, dans un playbook qui permet l’installation d’une application, on peut imaginer un premier play qui effectue des installations communes de packages sur tous les serveurs, un deuxième play pour la configuration des serveurs de base de données et puis un troisième pour les serveurs Web.

Le playbook est lancé grâce à la commande ansible-playbook, elle prend comme argument le nom de fichier du playbook. Cette commande accepte des arguments optionnels tels que les paramètres --inventory et --extra-vars vus précédemment, mais elle n’accepte pas certains paramètres comme le pattern et le --args car ils sont définis au niveau de chaque play.

Exemple

Si on veut afficher à nouveau le contenu d’une variable my_name dans un message en passant par un playbook, on doit suivre les étapes suivantes :

1- Créer un nouveau fichier yaml playbook_debug.yml à la racine du projet.

2- Dans ce fichier yaml, définir un play "My first play" avec une seule tâche debug.

- name: My first play
  hosts: localhost
  vars:
    my_name: John
  tasks:
    - name: Display my name
      debug:
        msg: 'My name is {{ my_name }}'

Les paramètres du play utilisés sont :

  • name : le nom du play courant, ce nom n’est utilisé que dans le code de retour pour pouvoir repérer facilement les logs associés à ce play,
  • hosts : définition des serveur ou des groupes de serveurs ciblés par les tâches du play,
  • vars : définition des variables,
  • tasks : liste des tâches du play.

3- Lancer la commande :

ansible-playbook playbook_debug.yml

Résultat

Résultat de la commande ansible-playbook

Le code de retour affiche le nom du play suivi des noms des tâches lancées.

En plus de notre tâche Display my name, on remarque qu’Ansible a exécuté une deuxième tâche appelée Gathering Facts non définie dans le play. Il s’agit de la tâche lancée par defaut au début de chaque play. Elle utilise le module setup qui permet de récupérer des variables utiles pour chacun des serveurs définies dans le paramètre hosts.

Rôles

Dans un grand projet, on peut se retrouver face à plusieurs playbooks ayant beaucoup de play et avec des centaines de tâches dedans. Dans ce genre de projets on risque de perdre en lisibilité et de dupliquer des bouts de configuration dans les différents play / playbooks.

Le rôle est une notion Ansible qui permet de pallier ces problèmes. Il consiste à grouper logiquement un ensemble de tâches sous forme de composants réutilisables dans différents playbooks.

Par exemple, la logique de configuration des serveurs de base de données peut être encapsulée dans un rôle db. Ce rôle peut être appelé dans plusieurs playbooks afin d’installer les bases de données pour toutes les applications gérées par le projet.

Structure d’un rôle

Chaque rôle est mis dans un dossier à part. L’ensemble de ces dossiers sont groupés dans un dossier parent appelé roles qui se trouve à la racine du projet.
Voici un exemple de structure d’un rôle:

roles/
    my_first_role/ 
        default/            
            main.yml 
        vars/            
            main.yml 
        tasks/            
            main.yml 
        meta/            
            main.yml      
        handlers/         
            main.yml      
        files/            
            file1.txt       
            file2.sh   
        templates/        
            template.j2   

La présence de tous ces répertoires n’est pas obligatoire, mais leur structure est bien définie et doit respecter certaines règles :

  • Certains répertoires doivent contenir au moins un fichier yaml nommé main.yml
  • Le contenu et l’utilité des fichiers yaml changent en fonction du répertoire :
    • default : sert à définir des variables statiques,
    • vars : sert à définir des variables qui ont tendance à être modifiées par l’utilisateur (un numéro de version par exemple),
    • tasks : le répertoire le plus utilisé dans un rôle, il permet de définir ses tâches principales,
    • meta : permet de définir les métadonnées du rôle. Parfois, l’exécution de certaines tâches (ou rôles) est requise avant de lancer les tâches d’un rôle (installation de dépendances / vérifications …). Dans ce cas, elles sont déclarées ici.
    • handlers : on peut abonner les tâches d’un rôle à des handlers. Ces derniers seront lancés automatiquement si ces tâches sont exécutées.
      L’exemple de handler le plus récurrent est le redémarrage d’un service nécessaire après l’exécution de certaines tâches.
    • files : contient les fichiers à déployer par le rôle.
    • templates : contient les templates jinja2 à déployer par le rôle.
  • Le nom du rôle correspond au nom du dossier qui le contient. Par exemple, on peut nommer le rôle my_first_role dans un play de la manière suivante :
- name: My first play
  hosts: all
  roles:
    - role: my_first_role

Variables

Les variables Ansible sont divisées en deux types :

Les facts
Les données associées aux serveurs et récupérées via la tâche gathering facts tels que l’adresse IP ou le système d’exploitation.

Les variables définies dans le code
Jusqu’à maintenant, on a eu l’occasion de voir 3 manières pour définir une variable :

  • Dans la section vars d’un play,
  • Dans les dossiers default/vars d’un rôle,
  • Dans l’argument --extra-vars de la commande ansible-playbook.

Il existe en tout une vingtaine de manières différentes de définir une variable dans Ansible !

Ansible priorise la définition des variables selon un ordre bien défini (cf. la documentation sur les priorités des variables Ansible).

D’après cette documentation, on voit que les extra-vars sont les plus prioritaires. Ceci est pratique quand on souhaite forcer une variable (un numéro de version par exemple) lors du lancement d’un playbook.

Codes de retour

On a vu précédemment qu’Ansible maintient la connexion SSH entre le node manager et les serveurs distants pour récupérer les résultats. Ces derniers sont affichés sous forme de logs.

Dans les logs on voit principalement les tâches qui ont été lancées et leur status d’exécution.

Les différents status qu’on retrouve en sortie d’Ansible sont :

  • Ok : la tâche a été lancée mais elle n’a pas changé l’état du serveur distant,
  • Changed : la tâche a été lancée et elle a changé l’état du serveur distant,
  • Unreachable : le serveur distant est inaccessible,
  • Failed : la tâche a échoué,
  • Skipped : la tâche n’a pas été lancée,
  • Rescued : un bloc de tâches a été lancé à la suite d’une erreur au niveau d’un autre bloc de tâches,
  • Ignored : la tâche a échoué mais l’erreur a été ignorée.

Exemple

Prenons l’exemple du play suivant:

- name: Ansible output
  hosts: localhost
  tasks:
    - name: Task 1
      file:
      args:
        path: ansible_directory
        state: directory
      when: false
    - name: Task 2
      file:
      args:
        unsupported_parameter: unsupported_parameter
      ignore_errors: true
    - name: Task 3
      file:
      args:
        path: ansible_directory
        state: directory
    - name: Task 4
      file:
      args:
        path: ansible_directory
        state: directory
    - name: Task 5
      file:
      args:
        invalid_argument: invalid_argument

En lançant un ansible-playbook, on obtient les résultats suivants:

  • La tâche 1 n’a pas été exécutée parce qu’elle est conditionnée par le paramètre when qui prend toujours la valeur false. Il doit prendre une valeur true pour que cette tâche soit lancée.
  • La tâche 2 a échoué à cause d’un mauvais paramètre unsupported_parameter, mais l’erreur a été ignorée grace au paramètre ignore_errors: true.
  • La tâche 3 a été lancée avec succès. Le status changed veut dire que le dossier vient d’être créé par cette tâche.
  • La tâche 4 a également été lancée avec succès. Mais le status Ok veut dire que le dossier existait déjà et que cette tâche n’a rien changé.
  • La tâche 5 a échoué à cause d’un mauvais paramètre invalid_argument.

Remarques

  • Pour une meilleure lisibilité des logs, il faut que les noms des tâches soient explicites.
    Une tâche qui s’appelle Create file et qui renvoie le status Ok nous donne l’impression que le fichier a été créé. Par contre, si on la nomme Ensure file is created, on gagne en lisibilité.
  • Pour afficher plus d’informations sur les logs, on peut activer deux niveaux de verbosité en passant l’un des deux paramètres -v ou -vvv dans la commande ansible-playbook.

Conclusion

Chez Deepki, Ansible est utilisé pour automatiser la configuration de nos plateformes et le déploiements de nos applications.
L’automatisation de ces aspects nous offre une grande flexibilité et nous permet de répondre aux besoins de nos clients dans de brefs délais et d’une manière sécurisée.

Le but de cet article est de présenter cet outil et de découvrir ses notions de base.
Il existe d’autres notions plus poussées que nous n’avons pas eu l’occasion de voir et qui seront présentées et détaillées dans de prochains articles.