Skip to content

Ansible

Installer des paquets, créer des utilisateurs, déposer des fichiers de configuration, redémarrer des services… Sur un serveur, on peut le faire à la main. Sur dix ou cent serveurs, c’est long, répétitif et source d’erreurs.

Ansible permet de décrire l’état souhaité de ses machines dans des fichiers texte, puis de l’appliquer automatiquement, autant de fois que nécessaire. Cette fiche présente les notions essentielles pour écrire et lancer ses premiers playbooks.

Ansible est un outil open source d’automatisation et de gestion de configuration, maintenu par Red Hat. Il se connecte aux machines en SSH, sans agent à installer, et exécute des tâches décrites en YAML. Ses tâches sont idempotentes : les relancer ne change rien si la machine est déjà dans l’état voulu.

Sans agent

Rien à installer sur les machines gérées : un accès SSH et Python suffisent. On peut commencer à l’utiliser tout de suite sur un parc existant.

Lisible

Les playbooks sont écrits en YAML. Ils se lisent presque comme une liste de consignes, et servent aussi de documentation de l’infrastructure.

Idempotent

On décrit un état (« nginx est installé ») plutôt qu’une action (« installer nginx »). Relancer un playbook est sans danger.

Versionnable

Tout tient dans des fichiers texte : on les range dans Git, on les relit en merge request et on garde l’historique des changements.

C’est ce qu’on appelle l’Infrastructure as Code (IaC) : la configuration de l’infrastructure est décrite dans du code, plutôt que dans des manipulations manuelles dont personne ne garde la trace.

Nœud de contrôle

La machine où Ansible est installé et d’où on lance les commandes : ton poste, ou un serveur dédié. Elle doit tourner sous Linux ou macOS (sous Windows, on passe par WSL).

Nœuds gérés

Les machines que l’on configure. Elles n’ont besoin que d’un accès SSH et de Python. Ansible y envoie de petits programmes (les modules), les exécute, puis les supprime.

Le vocabulaire de base :

Terme Définition
Inventaire La liste des machines gérées, rangées en groupes (web, bdd…).
Module Une unité de travail réutilisable : installer un paquet, copier un fichier, gérer un service… Ansible en fournit des milliers.
Tâche L’appel d’un module avec ses paramètres.
Play Un ensemble de tâches appliquées à un groupe de machines.
Playbook Un fichier YAML qui contient un ou plusieurs plays.
Rôle Un paquet réutilisable de tâches, fichiers, templates et variables.
Collection Un format de distribution qui regroupe des modules, des rôles et des plugins.

Ansible s’installe uniquement sur le nœud de contrôle. La méthode recommandée passe par pipx, qui l’isole dans son propre environnement Python :

Terminal window
pipx install --include-deps ansible
ansible --version

Il existe aussi des paquets pour la plupart des distributions (sudo apt install ansible sur Debian/Ubuntu), mais leur version est souvent plus ancienne.

L’inventaire indique à Ansible quelles machines gérer et comment s’y connecter. Il peut être écrit en format INI ou en YAML :

inventory.yml
all:
children:
web:
hosts:
web1.exemple.fr:
web2.exemple.fr:
bdd:
hosts:
bdd1.exemple.fr:
ansible_host: 192.168.1.20
ansible_user: admin
production:
children:
web:
bdd:
  • Deux groupes existent toujours : all (toutes les machines) et ungrouped (celles qui ne sont dans aucun groupe).
  • Les variables ansible_host, ansible_user ou ansible_port précisent la connexion quand le nom ne suffit pas.
  • Un groupe peut contenir d’autres groupes : ici, production regroupe web et bdd (avec children en YAML, :children en INI). Lancer une commande sur production la lance sur les trois machines.

Pour vérifier ce qu’Ansible comprend de l’inventaire :

Terminal window
ansible-inventory -i inventory.yml --graph

Une commande ad hoc exécute un seul module, directement depuis le terminal, sans écrire de playbook. C’est pratique pour une vérification ou une action ponctuelle.

Terminal window
# Vérifier qu'Ansible peut joindre toutes les machines
ansible all -i inventory.yml -m ansible.builtin.ping
# Afficher l'espace disque des serveurs web
ansible web -i inventory.yml -m ansible.builtin.command -a "df -h"
# Installer un paquet avec les droits administrateur (-b pour become)
ansible web -i inventory.yml -b -m ansible.builtin.apt -a "name=htop state=present"

La structure est toujours la même : ansible <groupe> -m <module> -a "<arguments>".

Pour tout ce qui dépasse une action ponctuelle, on écrit un playbook. Voici un premier exemple, qui installe et démarre nginx sur les serveurs web :

site.yml
- name: Configurer les serveurs web
hosts: web
become: true
tasks:
- name: Installer nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: Démarrer nginx et l'activer au démarrage
ansible.builtin.service:
name: nginx
state: started
enabled: true
  • hosts : le groupe de l’inventaire sur lequel s’applique le play.
  • become: true : les tâches s’exécutent avec les droits administrateur (via sudo par défaut).
  • tasks : la liste des tâches, exécutées dans l’ordre, sur toutes les machines du groupe en parallèle.
  • name : une description lisible, affichée pendant l’exécution. Toujours en mettre une.

On le lance avec :

Terminal window
ansible-playbook -i inventory.yml site.yml

À la fin de l’exécution, Ansible affiche un récapitulatif par machine :

PLAY RECAP *********************************************************
web1.exemple.fr : ok=3 changed=2 unreachable=0 failed=0 skipped=0
web2.exemple.fr : ok=3 changed=0 unreachable=0 failed=0 skipped=0
Statut Signification
ok La tâche s’est bien passée, la machine était déjà dans l’état voulu.
changed Ansible a dû modifier la machine pour atteindre l’état voulu.
failed La tâche a échoué. Par défaut, Ansible arrête de traiter cette machine.
unreachable Ansible n’a pas pu se connecter à la machine.
skipped La tâche a été ignorée, car sa condition n’était pas remplie.

Si on relance le playbook sans rien modifier, toutes les tâches doivent être en ok et aucune en changed : c’est le signe que le playbook est bien idempotent.

Module Usage
ansible.builtin.apt / ansible.builtin.dnf Gérer les paquets (Debian/Ubuntu, Red Hat/Fedora)
ansible.builtin.package Gérer les paquets, quel que soit le gestionnaire du système
ansible.builtin.service Démarrer, arrêter, activer un service
ansible.builtin.copy Copier un fichier vers la machine gérée
ansible.builtin.template Générer un fichier à partir d’un modèle Jinja2
ansible.builtin.file Créer un dossier, un lien, changer des droits
ansible.builtin.lineinfile Ajouter ou modifier une ligne dans un fichier
ansible.builtin.user Gérer les comptes utilisateurs
ansible.builtin.command / ansible.builtin.shell Exécuter une commande (en dernier recours)
ansible.builtin.debug Afficher un message ou une variable

La documentation de chaque module est disponible directement dans le terminal :

Terminal window
ansible-doc ansible.builtin.apt

Les noms de modules comme ansible.builtin.apt sont des noms complets (FQCN, Fully Qualified Collection Name) : ansible.builtin est la collection, apt le module. On peut souvent écrire seulement apt, mais le nom complet évite toute ambiguïté et c’est la pratique recommandée.

Les variables évitent de répéter des valeurs en dur et permettent d’adapter un même playbook à plusieurs environnements.

site.yml
- name: Configurer les serveurs web
hosts: web
become: true
vars:
paquets:
- nginx
- curl
- htop
tasks:
- name: Installer les paquets
ansible.builtin.apt:
name: "{{ paquets }}"
state: present

On appelle une variable avec la syntaxe Jinja2 {{ nom_de_la_variable }}. Quand une valeur YAML commence par {{, il faut l’entourer de guillemets.

Plutôt que de tout mettre dans le playbook, on range généralement les variables dans des dossiers dédiés, à côté de l’inventaire :

  • inventory.yml
  • Directorygroup_vars/
    • all.yml variables de toutes les machines
    • web.yml variables du groupe web
  • Directoryhost_vars/
    • web1.exemple.fr.yml variables d’une seule machine

Ansible les charge automatiquement. Quand une même variable est définie à plusieurs endroits, c’est la définition la plus précise qui l’emporte : host_vars passe avant group_vars/web.yml, qui passe avant group_vars/all.yml. Les variables passées en ligne de commande avec -e l’emportent sur tout le reste.

Au début de chaque play, Ansible collecte automatiquement des informations sur chaque machine : système d’exploitation, adresses IP, mémoire, nombre de processeurs… Ce sont les facts, accessibles dans la variable ansible_facts.

- name: Afficher la distribution
ansible.builtin.debug:
msg: "Cette machine tourne sous {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}"

Pour voir tous les facts d’une machine :

Terminal window
ansible web1.exemple.fr -i inventory.yml -m ansible.builtin.setup

when n’exécute une tâche que si une condition est vraie. Pas besoin de {{ }} : la condition est déjà une expression Jinja2.

- name: Installer nginx sur Debian et Ubuntu
ansible.builtin.apt:
name: nginx
state: present
when: ansible_facts['os_family'] == "Debian"

loop répète une tâche pour chaque élément d’une liste. L’élément en cours est disponible dans la variable item.

- name: Créer les comptes de l'équipe
ansible.builtin.user:
name: "{{ item }}"
groups: sudo
append: true
loop:
- alice
- bob

Le module template génère un fichier de configuration à partir d’un modèle Jinja2 (extension .j2), en remplaçant les variables par leurs valeurs pour chaque machine.

templates/site.conf.j2
server {
listen {{ nginx_port }};
server_name {{ inventory_hostname }};
root /var/www/{{ nom_du_site }};
{% if activer_cache %}
location ~* \.(css|js|jpg|png|webp)$ {
expires 30d;
}
{% endif %}
}
- name: Déposer la configuration du site
ansible.builtin.template:
src: templates/site.conf.j2
dest: /etc/nginx/sites-available/site.conf
mode: "0644"

Jinja2 permet aussi des conditions ({% if %}), des boucles ({% for %}) et des filtres ({{ nom | upper }}, {{ port | default(80) }}).

Certaines actions ne doivent avoir lieu que si quelque chose a changé : on ne redémarre nginx que si sa configuration a été modifiée. C’est le rôle des handlers.

site.yml (extrait)
tasks:
- name: Déposer la configuration du site
ansible.builtin.template:
src: templates/site.conf.j2
dest: /etc/nginx/sites-available/site.conf
notify: Recharger nginx
handlers:
- name: Recharger nginx
ansible.builtin.service:
name: nginx
state: reloaded
  • Une tâche notifie un handler avec notify, en donnant son nom exact.
  • Le handler ne s’exécute que si la tâche est en changed.
  • Il s’exécute une seule fois, à la fin du play, même s’il a été notifié par plusieurs tâches.

Quand un playbook grossit, on le découpe en rôles : chaque rôle regroupe tout ce qu’il faut pour une fonction (installer nginx, configurer une base de données, sécuriser SSH…) et peut être réutilisé d’un projet à l’autre.

Un rôle a une structure de dossiers standard, que l’on peut créer avec :

Terminal window
ansible-galaxy role init roles/nginx
  • Directoryroles/
    • Directorynginx/
      • Directorytasks/
        • main.yml les tâches du rôle
      • Directoryhandlers/
        • main.yml les handlers
      • Directorytemplates/
        • site.conf.j2
      • Directoryfiles/ fichiers copiés tels quels
        • …
      • Directorydefaults/
        • main.yml valeurs par défaut des variables
      • Directoryvars/
        • main.yml variables internes au rôle
      • Directorymeta/
        • main.yml dépendances et informations du rôle

Dans chaque dossier, Ansible charge automatiquement le fichier main.yml. Le playbook principal devient alors très court :

site.yml
- name: Configurer les serveurs web
hosts: web
become: true
roles:
- nginx

Ansible Galaxy est le catalogue public de rôles et de collections partagés par la communauté. Avant d’écrire un rôle, il vaut la peine de vérifier s’il n’existe pas déjà.

Terminal window
# Installer une collection
ansible-galaxy collection install community.general
# Installer toutes les dépendances d'un projet
ansible-galaxy install -r requirements.yml
requirements.yml
collections:
- name: community.general
- name: community.docker

Les mots de passe, clés d’API et autres secrets ne doivent jamais être écrits en clair dans un dépôt Git. Ansible Vault chiffre les fichiers de variables sensibles.

Terminal window
# Créer un fichier chiffré
ansible-vault create group_vars/bdd/vault.yml
# Chiffrer un fichier existant
ansible-vault encrypt group_vars/bdd/vault.yml
# Modifier un fichier chiffré
ansible-vault edit group_vars/bdd/vault.yml
# Lancer un playbook qui utilise des fichiers chiffrés
ansible-playbook -i inventory.yml site.yml --ask-vault-pass

Le fichier chiffré peut être versionné sans risque : sans le mot de passe du vault, son contenu est illisible.

Exemple complet : déployer un site statique avec nginx

Section titled “Exemple complet : déployer un site statique avec nginx”

Pour mettre tout ça en pratique, voici un petit projet qui installe nginx, dépose une configuration générée à partir d’un template et publie une page d’accueil.

  • ansible.cfg
  • inventory.yml
  • site.yml
  • Directorygroup_vars/
    • web.yml
  • Directoryroles/
    • Directorynginx/
      • Directorydefaults/
        • main.yml
      • Directorytasks/
        • main.yml
      • Directoryhandlers/
        • main.yml
      • Directorytemplates/
        • site.conf.j2
        • index.html.j2
  1. Configurer Ansible pour le projet, afin de ne pas répéter l’inventaire à chaque commande :

    ansible.cfg
    [defaults]
    inventory = inventory.yml
  2. Définir les valeurs par défaut du rôle :

    roles/nginx/defaults/main.yml
    nginx_port: 80
    nom_du_site: mon-site
    message_accueil: "Bonjour !"
  3. Personnaliser pour le groupe web :

    group_vars/web.yml
    nom_du_site: je-suis-la
    message_accueil: "Ce serveur a été configuré par Ansible."
  4. Écrire les tâches du rôle :

    roles/nginx/tasks/main.yml
    - name: Installer nginx
    ansible.builtin.apt:
    name: nginx
    state: present
    update_cache: true
    - name: Créer le dossier du site
    ansible.builtin.file:
    path: "/var/www/{{ nom_du_site }}"
    state: directory
    mode: "0755"
    - name: Publier la page d'accueil
    ansible.builtin.template:
    src: index.html.j2
    dest: "/var/www/{{ nom_du_site }}/index.html"
    mode: "0644"
    - name: Déposer la configuration du site
    ansible.builtin.template:
    src: site.conf.j2
    dest: /etc/nginx/sites-available/{{ nom_du_site }}.conf
    mode: "0644"
    notify: Recharger nginx
    - name: Activer le site
    ansible.builtin.file:
    src: /etc/nginx/sites-available/{{ nom_du_site }}.conf
    dest: /etc/nginx/sites-enabled/{{ nom_du_site }}.conf
    state: link
    notify: Recharger nginx
    - name: Désactiver le site par défaut
    ansible.builtin.file:
    path: /etc/nginx/sites-enabled/default
    state: absent
    notify: Recharger nginx
    - name: Démarrer nginx
    ansible.builtin.service:
    name: nginx
    state: started
    enabled: true
  5. Écrire le handler :

    roles/nginx/handlers/main.yml
    - name: Recharger nginx
    ansible.builtin.service:
    name: nginx
    state: reloaded
  6. Écrire les templates :

    roles/nginx/templates/site.conf.j2
    server {
    listen {{ nginx_port }};
    server_name {{ inventory_hostname }};
    root /var/www/{{ nom_du_site }};
    index index.html;
    }
    roles/nginx/templates/index.html.j2
    <!doctype html>
    <html lang="fr">
    <head><meta charset="utf-8"><title>{{ nom_du_site }}</title></head>
    <body>
    <h1>{{ message_accueil }}</h1>
    <p>Machine : {{ inventory_hostname }}</p>
    </body>
    </html>
  7. Assembler le playbook :

    site.yml
    - name: Configurer les serveurs web
    hosts: web
    become: true
    roles:
    - nginx
  8. Tester, puis lancer :

    Terminal window
    ansible-playbook site.yml --check --diff
    ansible-playbook site.yml

En relançant le playbook une seconde fois, toutes les tâches doivent être en ok : rien n’a changé, donc nginx n’est pas rechargé.

Voici les principales. La documentation d’Ansible les détaille dans son guide Tips and tricks (en anglais).

  • Toujours nommer les tâches, pour que la sortie d’Ansible se lise comme un journal.

  • Préférer les modules dédiés à command et shell, pour garder l’idempotence.

  • Utiliser les noms complets des modules (ansible.builtin.copy plutôt que copy).

  • Tester avec --check --diff avant de toucher à la production, et relancer une seconde fois pour vérifier qu’aucune tâche n’est en changed.

  • Ne jamais versionner de secret en clair : utiliser Ansible Vault.

  • Découper en rôles dès que le playbook dépasse quelques dizaines de lignes.

  • Vérifier son code avec ansible-lint, qui repère les erreurs et les mauvaises pratiques :

    Terminal window
    pipx install ansible-lint
    ansible-lint site.yml
Commande Usage
ansible all -m ansible.builtin.ping Tester la connexion à toutes les machines
ansible-inventory --graph Afficher l’inventaire sous forme d’arbre
ansible-playbook site.yml Lancer un playbook
ansible-playbook site.yml --check --diff Simuler l’exécution et afficher les différences
ansible-playbook site.yml --limit web1.exemple.fr Limiter l’exécution à certaines machines
ansible-playbook site.yml -e "nginx_port=8080" Passer une variable en ligne de commande
ansible-playbook site.yml -v (jusqu’à -vvvv) Afficher plus de détails pour déboguer
ansible-doc <module> Lire la documentation d’un module
ansible-galaxy role init <nom> Créer la structure d’un rôle
ansible-vault edit <fichier> Modifier un fichier chiffré