Aller au contenu
Infrastructure as Code medium

Premier playbook Ansible : installer nginx, ouvrir le port 80, démarrer le service

14 min de lecture

Logo Ansible

Vous savez lancer des commandes ad-hoc ; il est temps de basculer en playbook. Cette page écrit votre premier playbook complet : installation de nginx sur les webservers, ouverture du port 80 dans firewalld, démarrage du service, vérification HTTP. Tout en YAML structuré, exécuté en une commande (ansible-playbook), idempotent (un second passage affiche changed=0). C’est le passage de la commande jetable à l’infrastructure-as-code.

  • Comprendre la structure d’un playbook : name, hosts, become, gather_facts, tasks ;
  • Écrire un playbook YAML qui enchaîne 5 tâches sur le groupe webservers ;
  • Exécuter avec ansible-playbook et lire chaque colonne du PLAY RECAP ;
  • Vérifier l’idempotence en relançant le playbook : changed=0 au second passage ;
  • Tester le résultat avec une tâche uri: qui vérifie le code HTTP retourné.
  • Le lab provisionné, préparé, et la connexion SSH validée (cf. les pages précédentes de cette section) ;
  • L’inventaire YAML chargé via ansible.cfg ;
  • Une compréhension des commandes ad-hoc (cf. Commandes ad-hoc) — un playbook est essentiellement une suite ordonnée de commandes ad-hoc encapsulées en YAML.

Un playbook est un fichier YAML qui contient une liste de plays. Un play cible un groupe d’hôtes et exécute une liste de tâches dans l’ordre. Une tâche appelle un module avec ses paramètres.

playbook
└── play (cible : un groupe d'hôtes)
├── tasks
│ ├── task 1 (module + paramètres)
│ ├── task 2 (module + paramètres)
│ └── ...
└── handlers (optionnel — déclenchés par notify:)

Les clés essentielles d’un play :

CléRôle
name:Description humaine du play (apparaît dans le PLAY RECAP)
hosts:Pattern d’hôtes ciblé (all, webservers, web1.lab:!web2.lab…)
become:Élévation sudo si true
gather_facts:Collecte les facts au début (true par défaut)
vars:Variables locales au play
tasks:Liste ordonnée de tâches
handlers:Tâches déclenchées en fin de play par notify:

Créez le fichier labs/premiers-pas/premier-playbook/site.yml à la racine du repo ansible-training. Ce playbook installe nginx + ouvre le port 80 + démarre le service + vérifie le code HTTP :

---
- name: Déployer nginx sur les webservers
hosts: webservers
become: true
gather_facts: true
tasks:
- name: Installer le paquet nginx
ansible.builtin.dnf:
name: nginx
state: present
- name: Démarrer et activer nginx au boot
ansible.builtin.systemd:
name: nginx
state: started
enabled: true
- name: Ouvrir le port HTTP (80) dans firewalld
ansible.posix.firewalld:
service: http
permanent: true
immediate: true
state: enabled
- name: Vérifier que nginx répond sur localhost
ansible.builtin.uri:
url: http://localhost
status_code: 200
register: nginx_response
- name: Afficher le code HTTP retourné
ansible.builtin.debug:
msg: "nginx répond avec le code {{ nginx_response.status }} sur {{ inventory_hostname }}"

hosts: webservers — cible le groupe défini dans l’inventaire (web1.lab + web2.lab). Pas all, pas db1.lab. Le play ne tourne que sur les hôtes voulus.

become: true — toutes les tâches s’exécutent en root via sudo. Sans cette ligne, dnf install échouerait avec une erreur de permission.

gather_facts: trueAnsible exécute le module setup au démarrage et expose les facts (ansible_distribution, ansible_default_ipv4…). Vous pouvez les désactiver avec false si vous savez que vous n’en avez pas besoin (gain de quelques secondes).

Tâche 1 — ansible.builtin.dnf : installe le paquet nginx (idempotent — n’agit que s’il n’est pas déjà installé). Le FQCN (ansible.builtin.dnf) est la forme recommandée depuis Ansible 2.10 ; c’est l’objectif RHCE.

Tâche 2 — ansible.builtin.systemd : combine enable: true (démarrage au boot) et state: started (démarré maintenant). Idempotent : si le service est déjà dans cet état, changed=false.

Tâche 3 — ansible.posix.firewalld : ouvre le service http (port 80) dans la zone par défaut. permanent: true écrit la règle dans /etc/firewalld/zones/, immediate: true l’applique tout de suite. Sans immediate, la règle ne prend effet qu’au prochain firewall-cmd --reload ou reboot.

Tâche 4 — ansible.builtin.uri : interroge http://localhost côté managed node, vérifie que le code retour est 200. La sortie est stockée dans la variable nginx_response via register:.

Tâche 5 — ansible.builtin.debug : affiche un message contenant {{ nginx_response.status }} (le code HTTP) et {{ inventory_hostname }} (le nom de l’hôte courant). Une interpolation Jinja2 classique.

Deux habitudes qui font la différence entre du code qui démarre et du code qui tient en production — autant les ancrer sur ce premier playbook :

  • FQCN partout (ansible.builtin.dnf, ansible.posix.firewalld). Plus long de 14 caractères, mais c’est l’usage attendu en 2026 et un objectif RHCE explicite. Prenez l’habitude dès la première ligne plutôt que d’avoir à migrer tout votre code dans 6 mois.
  • Jamais shell: dnf install -y nginx quand un module dédié existe. shell: rejoue la commande à chaque exécution et casse l’idempotence. Si le réflexe « passer par shell » revient, c’est qu’il faut chercher le bon module avec ansible-doc -l.

Depuis la racine du repo :

Fenêtre de terminal
ansible-playbook labs/premiers-pas/premier-playbook/site.yml

Sortie attendue au premier passage (sur des managed nodes vierges de nginx) :

PLAY [Déployer nginx sur les webservers] ***************************************
TASK [Gathering Facts] *********************************************************
ok: [web1.lab]
ok: [web2.lab]
TASK [Installer le paquet nginx] ***********************************************
changed: [web1.lab]
changed: [web2.lab]
TASK [Démarrer et activer nginx au boot] ***************************************
changed: [web1.lab]
changed: [web2.lab]
TASK [Ouvrir le port HTTP (80) dans firewalld] *********************************
changed: [web1.lab]
changed: [web2.lab]
TASK [Vérifier que nginx répond sur localhost] *********************************
ok: [web1.lab]
ok: [web2.lab]
TASK [Afficher le code HTTP retourné] ******************************************
ok: [web1.lab] =>
msg: nginx répond avec le code 200 sur web1.lab
ok: [web2.lab] =>
msg: nginx répond avec le code 200 sur web2.lab
PLAY RECAP *********************************************************************
web1.lab : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web2.lab : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Le PLAY RECAP est la ligne la plus importante de toute exécution Ansible. Chaque colonne a un sens précis :

ColonneSens
okTâches exécutées sans erreur (avec ou sans changement)
changedTâches qui ont modifié l’état du managed node
unreachableHôtes injoignables (SSH, DNS, firewall)
failedTâches qui ont échoué (rc != 0, exception)
skippedTâches sautées par when: ou tags:
rescuedTâches récupérées par un rescue: block
ignoredTâches échouées mais ignorées par ignore_errors: true

Lecture de la sortie ci-dessus : sur web1.lab, 5 tâches OK dont 3 ont modifié l’état (installation nginx, démarrage, règle firewall). Les deux autres (uri et debug) sont en ok sans changement — elles lisent, elles ne modifient pas.

Relancez immédiatement la même commande :

Fenêtre de terminal
ansible-playbook labs/premiers-pas/premier-playbook/site.yml

PLAY RECAP attendu :

web1.lab : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web2.lab : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

changed=0 sur les deux hôtes — c’est la preuve d’idempotence. Tous les modules ont vérifié l’état réel, l’ont trouvé conforme à l’état désiré, et n’ont rien modifié. Le playbook est sûr à relancer indéfiniment.

Le playbook a vérifié http://localhost depuis chaque webserver. Vous pouvez aussi tester depuis votre poste de contrôle :

Fenêtre de terminal
curl -I http://10.10.20.21
HTTP/1.1 200 OK
Server: nginx/1.26.1
Date: Sat, 25 Apr 2026 12:48:32 GMT
Content-Type: text/html
...

C’est le résultat tangible : nginx tourne, firewalld autorise le port 80, le service est accessible depuis le réseau du lab.

SymptômeCauseFix
Permission denied (publickey) au début du runMauvais chemin de clé dans l’inventaire ou ansible.cfgVérifier ansible_ssh_private_key_file ; depuis un sous-répertoire, utiliser {{ inventory_dir }}/../ssh/id_ed25519 plutôt que playbook_dir
MODULE FAILURE: No module named 'firewall'python3-firewall non installé sur le managed nodeLancer le playbook labs/bootstrap/prepare-managed-nodes/playbook.yml qui installe les bindings Python
changed=N à chaque relanceModule non-idempotent (shell, command)Bascule sur le module idempotent dédié, ou ajoute creates:/removes:
state: started ne démarre pas le serviceLe service n’existe pas / nom mal orthographiéansible web1.lab -m systemd -a 'name=nginx' | jq .status.LoadState ; ou ssh ansible@web1.lab systemctl status nginx
ConnectionError: Connection timeout sur uri:Port pas ouvert dans firewalldVérifier que la tâche firewalld s’est bien exécutée AVANT uri: (ordre des tâches)
  • Un playbook est un fichier YAML qui contient une ou plusieurs plays, chaque play ciblant un pattern d’hôtes.
  • Les clés essentielles d’un play : name, hosts, become, gather_facts, tasks.
  • Les tâches utilisent des modules (ansible.builtin.dnf, ansible.builtin.systemd, ansible.posix.firewalld, ansible.builtin.uri, ansible.builtin.debug) — la forme FQCN est la norme RHCE.
  • register: capture la sortie d’un module dans une variable, {{ var }} l’interpole dans un message ou un autre paramètre.
  • Le PLAY RECAP est la ligne de vérité — changed=0 au second passage est le signal d’idempotence.
  • Un playbook idempotent est sûr à relancer ; c’est la base du contrat Ansible.

Cette page a un lab d’accompagnement : labs/premiers-pas/premier-playbook/ dans stephrobert/ansible-training. Il contient un README.md guidé, un Makefile (make verify lance les tests), et un challenge final auto-évalué : déployer Apache (httpd) sur db1.lab avec ouverture firewalld port 8080 et page custom.

Une fois le lab provisionné :

Fenêtre de terminal
cd ~/Projets/ansible-training/labs/premiers-pas/premier-playbook/
cat README.md # tuto pas à pas
cat challenge/README.md # consigne du challenge final
pytest -v challenge/tests/ # lancer les tests testinfra

Si les tests passent, vous maîtrisez les concepts couverts dans ce guide. En cas de blocage, docs/troubleshooting.md à la racine du repo couvre les pièges fréquents (rate-limit SSH, clé absente, collection manquante).

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn