ITSkillsCenter
Blog

Installer Ansible et exécuter son premier playbook pas à pas

14 دقائق للقراءة

📍 Article principal : Ansible 2026 : la stack pratique pour automatiser Linux et Windows

Ce tutoriel pose les fondations. Avant d’écrire un rôle, de monter un inventaire dynamique ou de chiffrer des secrets, il faut une installation propre et un premier playbook qui tourne.

Ce que vous aurez à la fin

Une installation d’ansible-core 2.20 sur votre poste, un inventaire de deux machines de test, et un playbook qui installe Nginx puis vérifie que le service écoute. À chaque étape, on vérifie ce qui s’est réellement passé — c’est cette habitude qui distingue un sysadmin qui produit du code Ansible jetable d’un sysadmin qui livre des automatisations fiables.

Prérequis

  • Un poste sous Linux ou macOS comme contrôleur Ansible (ou WSL2 sur Windows). Windows comme contrôleur natif n’est pas supporté.
  • Python 3.12 minimum sur le contrôleur — ansible-core 2.20 supporte Python 3.12 à 3.14 et a abandonné Python 3.11. Pour rester en 3.11, installer ansible-core 2.18 ou 2.19 plutôt que 2.20.
  • Deux machines cibles accessibles en SSH avec un compte sudo. Une cible Ubuntu 22.04 ou 24.04 fait parfaitement l’affaire pour ce premier tour ; deux VPS Hetzner à 4 € par mois suffisent largement.
  • Une clé SSH déjà présente dans ~/.ssh/id_ed25519.pub (sinon : ssh-keygen -t ed25519).
  • Niveau attendu : avoir déjà ouvert un terminal et compris ce qu’est SSH.
  • Temps : 45 à 60 minutes en lecture active.

Étape 1 — Installer ansible-core proprement

La première décision à prendre concerne la méthode d’installation. Trois voies coexistent et chacune a son contexte d’usage. Pour un poste de travail durable, on évite le paquet système qui livre souvent une version figée plusieurs majeures en arrière, et on préfère un environnement Python isolé via pipx. C’est l’approche recommandée par l’équipe Ansible elle-même depuis la séparation entre ansible-core et le package communautaire.

Sur Debian, Ubuntu ou tout dérivé récent, on commence par s’assurer que pipx est disponible :

sudo apt update
sudo apt install -y pipx python3-venv
pipx ensurepath
exec $SHELL -l

Le dernier exec $SHELL -l recharge la session pour que la modification du PATH faite par pipx ensurepath soit prise en compte. Sans ça, la commande ansible ne sera pas trouvée à la prochaine étape — c’est l’erreur la plus fréquente sur cette première installation.

On installe maintenant ansible-core dans un environnement Python isolé que pipx gère pour nous :

pipx install ansible-core

C’est la commande documentée officiellement par l’équipe Ansible. Elle installe ansible-core dans son propre environnement virtuel et expose automatiquement les binaires associés (ansible, ansible-playbook, ansible-galaxy, ansible-vault, ansible-config, ansible-doc, ansible-inventory) dans votre PATH, parce que ce sont les apps du paquet lui-même. Si vous voulez plutôt la version méta avec toutes les collections communautaires intégrées, la commande équivalente est pipx install --include-deps ansible — le drapeau est ici nécessaire car les outils CLI proviennent de la dépendance ansible-core.

On vérifie immédiatement :

ansible --version

La sortie doit afficher ansible [core 2.20.x] avec la version de Python utilisée et le chemin de configuration. Si Python est en 3.10 ou 3.11, ansible-core 2.20 refuse de tourner — il exige 3.12 minimum sur le contrôleur. Dans ce cas, mettez à jour Python ou installez ansible-core 2.19 (qui supporte 3.11) ou 2.18 (qui supporte 3.10).

Étape 2 — Préparer la connectivité SSH avec les cibles

Ansible ne fait pas de magie. Si un ssh utilisateur@machine ne fonctionne pas en ligne de commande, Ansible ne fonctionnera pas non plus sur cette cible. Cette étape consiste à valider la connectivité SSH avant même de toucher à Ansible — c’est cinq minutes de gagnées sur les deux heures de debug que coûte un problème SSH masqué par un message Ansible cryptique.

Sur chaque machine cible, créez un utilisateur dédié à l’automatisation. Le pattern classique en 2026 consiste à éviter de se connecter en root par SSH. On crée un utilisateur ansible, on lui donne sudo sans mot de passe pour l’usage automatisé, et on autorise notre clé publique :

sudo useradd -m -s /bin/bash ansible
echo 'ansible ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/ansible
sudo install -d -m 700 -o ansible -g ansible /home/ansible/.ssh
sudo install -m 600 -o ansible -g ansible /dev/stdin /home/ansible/.ssh/authorized_keys <<< "$(cat ~/.ssh/id_ed25519.pub)"

Cette dernière commande copie la clé publique du contrôleur dans le authorized_keys de l’utilisateur ansible sur la cible, avec les bonnes permissions du premier coup. Beaucoup de tutoriels passent par ssh-copy-id mais cette commande échoue souvent quand l’authentification par mot de passe est désactivée — la version install ci-dessus est plus fiable.

On valide ensuite depuis le contrôleur :

ssh ansible@<ip-cible-1> 'sudo whoami'

La sortie doit être root. Si elle retourne ansible, c’est que sudo demande encore un mot de passe — vérifiez le fichier /etc/sudoers.d/ansible. Si elle retourne une erreur de connexion, c’est que la clé n’est pas en place ou que SSH écoute sur un port non-standard.

Étape 3 — Construire l’inventaire

L’inventaire est la première source de vérité d’un projet Ansible. Pour ce premier tour, on reste sur un fichier statique, mais on adopte d’emblée la structure de répertoires qui sera la nôtre tout le projet :

mkdir -p ansible-lab/{inventory,playbooks,group_vars,host_vars}
cd ansible-lab

On crée le fichier inventory/hosts.yml au format YAML. Le format INI fonctionne aussi mais YAML est plus extensible — on peut y mettre des variables sans avoir à les sortir dans des fichiers séparés :

all:
  children:
    web:
      hosts:
        web1:
          ansible_host: 5.75.10.10
        web2:
          ansible_host: 5.75.10.11
      vars:
        ansible_user: ansible
        ansible_python_interpreter: /usr/bin/python3

Quelques points à comprendre dans cet inventaire. Le groupe all est implicite — on n’a pas besoin de le déclarer, mais l’écrire explicitement aide la lecture. Le sous-groupe web contient deux hôtes nommés web1 et web2 ; ces noms sont symboliques, ils servent de clé dans Ansible. ansible_host contient l’IP réelle. ansible_user, défini au niveau du groupe, s’applique aux deux hôtes. ansible_python_interpreter évite les ambiguïtés d’auto-détection sur les distributions où plusieurs Python coexistent.

On vérifie la structure avec :

ansible-inventory -i inventory/hosts.yml --graph

La sortie doit afficher un arbre @all → @web → web1, web2. Si un hôte manque ou si le YAML est mal indenté, l’erreur apparaît ici — c’est le bon moment pour la corriger, avant le premier playbook.

Étape 4 — Le ping module — premier dialogue avec les cibles

Le module ansible.builtin.ping n’envoie pas un paquet ICMP. Il vérifie quelque chose de bien plus utile : que SSH fonctionne, que Python est présent sur la cible et qu’Ansible peut y exécuter du code. C’est le premier diagnostic à lancer sur tout nouvel inventaire.

ansible -i inventory/hosts.yml web -m ansible.builtin.ping

Le résultat attendu, pour chaque hôte :

web1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

Si la sortie est UNREACHABLE, c’est un problème SSH — port fermé, mauvais user, clé non autorisée. Si la sortie est FAILED avec un message Python, c’est que l’interpréteur Python est absent ou mal détecté sur la cible. Le code retour de la commande est non-zéro tant que tous les hôtes n’ont pas répondu SUCCESS, ce qui le rend exploitable directement dans un script d’init de runner CI.

Étape 5 — Le premier playbook

Un playbook est un fichier YAML qui décrit une suite d’actions à appliquer à un groupe d’hôtes. On crée playbooks/install_nginx.yml :

---
- name: Installer et démarrer Nginx
  hosts: web
  become: true
  tasks:

    - name: Installer le paquet nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true
        cache_valid_time: 3600

    - name: Activer et démarrer le service nginx
      ansible.builtin.systemd_service:
        name: nginx
        enabled: true
        state: started

    - name: Déposer une page d'accueil minimale
      ansible.builtin.copy:
        dest: /var/www/html/index.html
        content: |
          <h1>Nginx piloté par Ansible</h1>
          <p>Hôte : {{ inventory_hostname }}</p>
        owner: www-data
        group: www-data
        mode: '0644'

Lisons ce playbook ligne par ligne, parce que chaque détail compte. Le name du play sert de titre lisible dans la sortie. hosts: web cible le groupe défini dans l’inventaire. become: true active la montée en privilèges (sudo) pour toutes les tâches du play — sans elle, l’installation du paquet échouerait avec un refus de permission. La directive tasks liste les actions, exécutées dans l’ordre.

Le module apt installe le paquet ; state: present signifie « assure-toi qu’il soit installé » — pas « installe-le », nuance qui fait toute la différence pour l’idempotence. update_cache: true avec cache_valid_time: 3600 rafraîchit l’index APT mais seulement s’il a plus d’une heure, ce qui évite un appel réseau inutile à chaque exécution.

Le module systemd garantit que le service est activé au boot et démarré. Le module copy dépose un fichier avec un contenu inline ; {{ inventory_hostname }} est une variable de gabarit Jinja2 qui sera remplacée par le nom de l’hôte au moment du rendu, différent pour web1 et web2.

On exécute :

ansible-playbook -i inventory/hosts.yml playbooks/install_nginx.yml

La sortie défile, regroupée par tâche puis par hôte. Les statuts possibles sont ok (état déjà conforme), changed (modification appliquée), failed (erreur), skipped (tâche conditionnelle ignorée). Le récapitulatif de fin (PLAY RECAP) compte chaque catégorie par hôte.

Sur un premier passage, vous verrez environ quatre changed=4 par hôte : la mise à jour APT, l’install nginx, l’activation systemd, le dépôt du fichier. Le service est maintenant en route. Pour le vérifier sans re-SSH :

curl http://5.75.10.10/

La page minimale s’affiche, avec le nom d’hôte injecté par Jinja.

Étape 6 — Tester l’idempotence

L’idempotence est la propriété qui rend Ansible utilisable en production. On la vérifie en relançant exactement le même playbook une seconde fois :

ansible-playbook -i inventory/hosts.yml playbooks/install_nginx.yml

Cette fois, le récap doit montrer changed=0. Toutes les tâches retournent ok parce que l’état observé est déjà conforme à l’état déclaré : nginx est installé, le service tourne, le fichier existe avec le bon contenu et les bonnes permissions. Si une tâche reste systématiquement changed=1 sans raison, c’est qu’elle n’est pas idempotente — c’est presque toujours un usage de command ou shell sans garde, à corriger.

Pour observer le diff exact qu’Ansible appliquerait, on ajoute le drapeau --check --diff :

ansible-playbook -i inventory/hosts.yml playbooks/install_nginx.yml --check --diff

Le mode --check simule sans rien modifier ; --diff montre les changements qui auraient lieu — utile pour repérer une dérive avant un run réel.

Étape 7 — Variables, gabarits et premier raffinement

Le contenu HTML codé en dur dans le playbook est correct pour un test, pas pour un projet sérieux. La bonne pratique consiste à le sortir dans un fichier de gabarit Jinja2 séparé. On crée playbooks/templates/index.html.j2 :

<!doctype html>
<html lang="fr">
<head><meta charset="utf-8"><title>{{ site_title }}</title></head>
<body>
  <h1>{{ site_title }}</h1>
  <p>Servi par {{ inventory_hostname }} ({{ ansible_facts.distribution }} {{ ansible_facts.distribution_version }}).</p>
</body>
</html>

On crée également un fichier de variables d’inventaire group_vars/web.yml :

site_title: "Ferme web pilotée par Ansible"

Et on remplace la tâche copy par une tâche template :

    - name: Déposer la page d'accueil depuis un gabarit
      ansible.builtin.template:
        src: templates/index.html.j2
        dest: /var/www/html/index.html
        owner: www-data
        group: www-data
        mode: '0644'

La différence entre copy et template est précise : template fait passer le fichier source par le moteur Jinja2 et substitue les variables. C’est ce qui permet de générer un fichier de configuration différent par hôte ou par environnement, à partir d’une source unique.

Les variables comme ansible_facts.distribution viennent de la phase fact-gathering qu’Ansible exécute au début de chaque play (sauf si on la désactive avec gather_facts: false). On peut inspecter tous les facts disponibles avec :

ansible -i inventory/hosts.yml web1 -m ansible.builtin.setup

Étape 8 — Précautions pour la suite

Avant de quitter ce premier projet, trois habitudes à prendre dès maintenant.

Première : versionner. Initialisez un dépôt Git, ajoutez un .gitignore qui exclut *.retry (fichiers générés par Ansible en cas d’échec partiel) et .vault_pass, committez. Ce projet va grossir, et l’historique aide à comprendre les régressions.

Deuxième : épingler les versions. Créez un fichier collections/requirements.yml dès maintenant, même s’il est vide pour ce premier playbook. Quand vous ajouterez votre première collection externe, vous y indiquerez la version exacte plutôt que latest. Vous éviterez la régression silencieuse qui casse une CI six mois plus tard.

Troisième : activer ansible-lint. Installez-le avec pipx install ansible-lint et lancez-le sur votre playbook : ansible-lint playbooks/install_nginx.yml. Il signale les anti-patterns (modules non FQCN, idempotence douteuse, indentation YAML approximative). Plus on le branche tôt, moins on dette technique on accumule.

Erreurs fréquentes

Erreur Cause Solution
UNREACHABLE sur le ping SSH ne passe pas avec l’utilisateur, la clé ou le port indiqué Tester ssh ansible@cible manuellement avant de blâmer Ansible
missing sudo password L’utilisateur n’a pas NOPASSWD dans sudoers Vérifier /etc/sudoers.d/ansible ou passer --ask-become-pass
The module copy was not found Typo ou collection non installée Utiliser le FQCN ansible.builtin.copy et vérifier ansible-doc
Une tâche reste changed=1 à chaque run Usage de shell sans garde ou de command non-idempotent Ajouter creates:, removes: ou changed_when:
Indentation YAML refusée Mélange tabulations et espaces Configurer l’éditeur en deux espaces, jamais de tabulation
Failed to parse template Variable Jinja non définie Définir un défaut avec {{ var | default('valeur') }}

Tutoriels frères

Pour explorer plus loin

FAQ

Pourquoi pipx plutôt qu’apt ou pip ?
Le paquet ansible-core d’apt sur Ubuntu 22.04 est figé sur une version ancienne (2.12 ou 2.13 selon les minor releases). Pipx isole ansible-core dans son propre venv, met les binaires dans ~/.local/bin, et permet une mise à jour propre via pipx upgrade ansible-core. Pip global pollue le système Python.

Quelle différence entre ansible et ansible-playbook ?
La commande ansible exécute un module unique en mode ad-hoc — utile pour les vérifications ou les corrections ponctuelles. ansible-playbook exécute un fichier YAML qui orchestre plusieurs tâches, avec gestion des handlers, des facts et de la stratégie d’exécution. Les vrais projets passent par ansible-playbook ; ansible seul reste précieux pour le débogage.

Faut-il systématiquement utiliser become: true ?
Non. On le déclare au plus haut niveau possible (play ou tâche) et on désactive ponctuellement avec become: false les tâches qui n’en ont pas besoin (modules de lecture, tâches utilisateur). Le réflexe inverse — become: true sur chaque tâche — alourdit le YAML pour rien.

Que faire si la cible n’a pas Python installé ?
La plupart des distributions Linux livrent Python 3 par défaut. Si ce n’est pas le cas, deux options : utiliser le module ansible.builtin.raw qui exécute des commandes brutes via SSH sans Python, pour bootstrapper Python sur la cible, puis basculer sur les modules normaux. Sur les images cloud minimales (Alpine, distroless), ce pattern est courant.

Comment cibler un seul hôte du groupe ?
On utilise --limit web1 sur la ligne de commande. Cela restreint l’exécution sans modifier l’inventaire. C’est utile pour tester un changement sur une seule machine avant de le propager au groupe entier.

Le port SSH non standard, comment le déclarer ?
Dans l’inventaire, ajouter ansible_port: 2222 au niveau de l’hôte ou des variables de groupe. Ansible lit cette variable et la passe à SSH automatiquement. Évitez de la hardcoder dans des paramètres SSH externes — l’inventaire reste la source unique de vérité.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité