Les Playbooks Ansible : Le Guide Complet de A à Z

Maîtrisez les playbooks Ansible : structure YAML, modules essentiels, variables, facts, conditionnels, boucles, handlers, templates Jinja2 et gestion d'erreurs. Guide complet avec exemples.

Qu'est-ce qu'un Playbook Ansible ?

Un playbook Ansible est un fichier YAML qui décrit de manière déclarative l'état désiré de vos systèmes et les actions nécessaires pour y parvenir. C'est le cœur d'Ansible, l'élément qui transforme un simple outil de commandes à distance en une véritable plateforme d'automatisation d'infrastructure.

Si les commandes ad-hoc sont l'équivalent de commandes bash individuelles, les playbooks sont l'équivalent de scripts complets, mais en beaucoup plus puissant. Un playbook peut orchestrer des déploiements multi-serveurs, gérer des configurations complexes, et garantir l'idempotence de chaque opération.

Architecture en un coup d'œil

Diagramme - Les Playbooks Ansible : Le Guide Complet de A à Z

Dans ce guide complet, nous allons explorer en profondeur tous les aspects des playbooks Ansible : leur structure, les modules essentiels, les variables, les conditionnels, les boucles, les handlers, les templates Jinja2 et bien plus encore. À la fin de cet article, vous serez capable d'écrire des playbooks professionnels pour automatiser n'importe quelle tâche d'administration système.

Structure d'un Playbook : Plays, Tasks et Handlers

Un playbook est composé d'un ou plusieurs plays. Chaque play cible un groupe de machines et contient une liste de tasks (tâches) à exécuter. Voici la structure fondamentale :

---
# Un playbook peut contenir plusieurs plays
- name: Premier play - Configuration des serveurs web
  hosts: webservers
  become: true
  gather_facts: true

  vars:
    http_port: 80

  tasks:
    - name: Première tâche
      ansible.builtin.debug:
        msg: "Configuration du serveur web en cours..."

    - name: Deuxième tâche
      ansible.builtin.apt:
        name: nginx
        state: present

  handlers:
    - name: Redémarrer Nginx
      ansible.builtin.service:
        name: nginx
        state: restarted

- name: Deuxième play - Configuration des bases de données
  hosts: databases
  become: true

  tasks:
    - name: Installer PostgreSQL
      ansible.builtin.apt:
        name: postgresql
        state: present

Anatomie d'un Play

Chaque play contient les éléments suivants :

  • name : description lisible du play (fortement recommandé)
  • hosts : les machines cibles (groupe d'inventaire, pattern ou all)
  • become : activer l'élévation de privilèges (sudo)
  • gather_facts : collecter les informations système (true par défaut)
  • vars : variables locales au play
  • tasks : liste ordonnée des tâches à exécuter
  • handlers : tâches déclenchées uniquement sur notification
  • pre_tasks : tâches exécutées avant les rôles
  • post_tasks : tâches exécutées après les rôles
  • roles : liste des rôles à appliquer

Anatomie d'une Task

Chaque tâche utilise un module Ansible et peut inclure des paramètres supplémentaires :

tasks:
  - name: Description claire de la tâche
    ansible.builtin.module_name:
      parametre1: valeur1
      parametre2: valeur2
    become: true              # Élévation de privilèges
    when: condition            # Condition d'exécution
    register: variable_result  # Stocker le résultat
    notify: Nom du handler     # Déclencher un handler
    tags:                      # Tags pour filtrage
      - configuration
      - nginx
    ignore_errors: true        # Ignorer les erreurs
    changed_when: false        # Contrôler le statut "changed"

Les modules essentiels d'Ansible

Ansible dispose de milliers de modules, mais certains sont utilisés quotidiennement par la majorité des administrateurs. Voici les modules incontournables que vous devez maîtriser.

Gestion des paquets : apt, yum, dnf et package

tasks:
  # Module apt (Debian/Ubuntu)
  - name: Installer un paquet avec apt
    ansible.builtin.apt:
      name: nginx
      state: present
      update_cache: true
      cache_valid_time: 3600

  - name: Installer plusieurs paquets
    ansible.builtin.apt:
      name:
        - nginx
        - certbot
        - python3-certbot-nginx
        - htop
        - vim
      state: present

  - name: Supprimer un paquet
    ansible.builtin.apt:
      name: apache2
      state: absent
      purge: true

  - name: Mettre à jour tous les paquets
    ansible.builtin.apt:
      upgrade: dist
      update_cache: true

  # Module yum (CentOS/RHEL 7)
  - name: Installer avec yum
    ansible.builtin.yum:
      name: httpd
      state: present

  # Module dnf (CentOS/RHEL 8+, Fedora)
  - name: Installer avec dnf
    ansible.builtin.dnf:
      name: httpd
      state: latest

  # Module package (multi-distribution)
  - name: Installer un paquet (indépendant de la distribution)
    ansible.builtin.package:
      name: git
      state: present

Gestion des fichiers : copy, template et file

tasks:
  # Module copy - Copier un fichier local vers la cible
  - name: Copier un fichier de configuration
    ansible.builtin.copy:
      src: files/nginx.conf
      dest: /etc/nginx/nginx.conf
      owner: root
      group: root
      mode: '0644'
      backup: true
    notify: Redémarrer Nginx

  # Module copy - Créer un fichier avec du contenu inline
  - name: Créer un fichier avec du contenu
    ansible.builtin.copy:
      content: |
        # Configuration générée par Ansible
        server_name {{ ansible_hostname }};
        listen {{ http_port }};
      dest: /etc/nginx/conf.d/default.conf
      owner: root
      group: root
      mode: '0644'

  # Module template - Utiliser un template Jinja2
  - name: Déployer un template de configuration
    ansible.builtin.template:
      src: templates/vhost.conf.j2
      dest: "/etc/nginx/sites-available/{{ site_name }}.conf"
      owner: root
      group: root
      mode: '0644'
      validate: nginx -t -c %s
    notify: Recharger Nginx

  # Module file - Gérer les fichiers et répertoires
  - name: Créer un répertoire
    ansible.builtin.file:
      path: /var/www/mon-site
      state: directory
      owner: www-data
      group: www-data
      mode: '0755'
      recurse: true

  - name: Créer un lien symbolique
    ansible.builtin.file:
      src: /etc/nginx/sites-available/mon-site.conf
      dest: /etc/nginx/sites-enabled/mon-site.conf
      state: link

  - name: Supprimer un fichier
    ansible.builtin.file:
      path: /etc/nginx/sites-enabled/default
      state: absent

Gestion des services : service et systemd

tasks:
  # Module service (compatible avec tous les systèmes d'init)
  - name: Démarrer et activer Nginx
    ansible.builtin.service:
      name: nginx
      state: started
      enabled: true

  - name: Redémarrer un service
    ansible.builtin.service:
      name: nginx
      state: restarted

  - name: Recharger la configuration sans redémarrer
    ansible.builtin.service:
      name: nginx
      state: reloaded

  # Module systemd (spécifique à systemd)
  - name: Recharger les unités systemd après modification
    ansible.builtin.systemd:
      daemon_reload: true

  - name: Activer et démarrer un service avec systemd
    ansible.builtin.systemd:
      name: nginx
      state: started
      enabled: true
      masked: false

Gestion des utilisateurs : user et group

tasks:
  - name: Créer un groupe
    ansible.builtin.group:
      name: deployers
      gid: 1500
      state: present

  - name: Créer un utilisateur
    ansible.builtin.user:
      name: deploy
      comment: "Utilisateur de déploiement"
      uid: 1500
      group: deployers
      groups:
        - sudo
        - www-data
      shell: /bin/bash
      home: /home/deploy
      create_home: true
      generate_ssh_key: true
      ssh_key_bits: 4096
      state: present

  - name: Ajouter une clé SSH autorisée
    ansible.posix.authorized_key:
      user: deploy
      key: "{{ lookup('file', '~/.ssh/deploy_key.pub') }}"
      state: present

Exécution de commandes : command, shell et raw

tasks:
  # Module command - Exécuter une commande (sans shell)
  - name: Vérifier la version de Node.js
    ansible.builtin.command: node --version
    register: node_version
    changed_when: false
    failed_when: false

  # Module shell - Exécuter via le shell (permet pipes, redirections)
  - name: Compter les processus Nginx
    ansible.builtin.shell: ps aux | grep nginx | grep -v grep | wc -l
    register: nginx_processes
    changed_when: false

  # Module raw - Exécuter sans Python (utile pour bootstrap)
  - name: Installer Python sur une machine vierge
    ansible.builtin.raw: apt-get install -y python3
    changed_when: false

Modification de fichiers : lineinfile et blockinfile

tasks:
  # Module lineinfile - Modifier une ligne dans un fichier
  - name: S'assurer que le port SSH est configuré
    ansible.builtin.lineinfile:
      path: /etc/ssh/sshd_config
      regexp: '^#?Port '
      line: 'Port 2222'
      state: present
      backup: true
    notify: Redémarrer SSH

  - name: Désactiver l'authentification par mot de passe SSH
    ansible.builtin.lineinfile:
      path: /etc/ssh/sshd_config
      regexp: '^#?PasswordAuthentication'
      line: 'PasswordAuthentication no'
      state: present
    notify: Redémarrer SSH

  - name: Ajouter une entrée dans /etc/hosts
    ansible.builtin.lineinfile:
      path: /etc/hosts
      line: '192.168.1.50 app.codeclan.local'
      state: present

  # Module blockinfile - Insérer un bloc de texte
  - name: Ajouter un bloc de configuration
    ansible.builtin.blockinfile:
      path: /etc/nginx/nginx.conf
      insertafter: 'http {'
      marker: "# {mark} ANSIBLE MANAGED - Rate limiting"
      block: |
        limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
        limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

Les variables dans Ansible

Les variables sont un élément fondamental des playbooks Ansible. Elles permettent de rendre vos playbooks dynamiques, réutilisables et adaptables à différents environnements.

Définir des variables dans le playbook (vars)

---
- name: Déploiement avec variables
  hosts: webservers
  become: true

  vars:
    app_name: mon-application
    app_version: "2.4.1"
    app_port: 8080
    app_user: appuser
    app_directory: "/opt/{{ app_name }}"
    allowed_ips:
      - 192.168.1.0/24
      - 10.0.0.0/8
    database:
      host: db.codeclan.fr
      port: 5432
      name: app_production
      user: app_db_user

  tasks:
    - name: Créer le répertoire de l'application
      ansible.builtin.file:
        path: "{{ app_directory }}"
        state: directory
        owner: "{{ app_user }}"
        mode: '0755'

    - name: Afficher la configuration de la base de données
      ansible.builtin.debug:
        msg: "Connexion à {{ database.host }}:{{ database.port }}/{{ database.name }}"

Variables dans des fichiers externes (vars_files)

---
- name: Playbook avec fichiers de variables
  hosts: webservers
  become: true

  vars_files:
    - vars/common.yml
    - vars/{{ env }}.yml
    - vars/secrets.yml  # Chiffré avec ansible-vault

  tasks:
    - name: Afficher l'environnement
      ansible.builtin.debug:
        msg: "Déploiement en {{ env }} - Version {{ app_version }}"

Exemple de fichier vars/production.yml :

# vars/production.yml
---
env: production
app_version: "2.4.1"
debug_mode: false
log_level: warn
replicas: 3
ssl_enabled: true
domain_name: app.codeclan.fr
monitoring_enabled: true

Register : capturer le résultat d'une tâche

Le mot-clé register permet de stocker le résultat d'une tâche dans une variable, utilisable ensuite dans les tâches suivantes :

tasks:
  - name: Vérifier si Nginx est installé
    ansible.builtin.command: which nginx
    register: nginx_check
    failed_when: false
    changed_when: false

  - name: Afficher le résultat
    ansible.builtin.debug:
      msg: "Nginx est {{ 'installé' if nginx_check.rc == 0 else 'absent' }}"

  - name: Afficher les détails complets du résultat
    ansible.builtin.debug:
      var: nginx_check

  - name: Installer Nginx seulement s'il n'est pas présent
    ansible.builtin.apt:
      name: nginx
      state: present
    when: nginx_check.rc != 0

  - name: Lire le contenu d'un fichier
    ansible.builtin.command: cat /etc/hostname
    register: hostname_content
    changed_when: false

  - name: Utiliser le contenu du fichier
    ansible.builtin.debug:
      msg: "Le nom d'hôte est : {{ hostname_content.stdout }}"

Les Facts Ansible

Les facts sont des variables collectées automatiquement par Ansible sur chaque machine cible. Ils fournissent une mine d'informations sur le système : distribution, version du noyau, adresses IP, mémoire, disques, interfaces réseau, etc.

Collecte et utilisation des facts

---
- name: Utilisation des facts
  hosts: all
  gather_facts: true  # Activé par défaut

  tasks:
    - name: Afficher les informations système
      ansible.builtin.debug:
        msg: |
          Hostname: {{ ansible_hostname }}
          FQDN: {{ ansible_fqdn }}
          Distribution: {{ ansible_distribution }} {{ ansible_distribution_version }}
          Famille OS: {{ ansible_os_family }}
          Noyau: {{ ansible_kernel }}
          Architecture: {{ ansible_architecture }}
          Processeurs: {{ ansible_processor_vcpus }}
          Mémoire totale: {{ ansible_memtotal_mb }} MB
          Adresse IPv4: {{ ansible_default_ipv4.address }}
          Interface réseau: {{ ansible_default_ipv4.interface }}

    - name: Actions conditionnelles basées sur les facts
      ansible.builtin.apt:
        update_cache: true
      when: ansible_os_family == "Debian"

    - name: Configuration spécifique à Ubuntu 22.04
      ansible.builtin.debug:
        msg: "Configuration spécifique à Ubuntu 22.04 Jammy"
      when:
        - ansible_distribution == "Ubuntu"
        - ansible_distribution_version == "22.04"

Désactiver la collecte des facts

La collecte des facts prend quelques secondes par hôte. Si vous n'en avez pas besoin, vous pouvez la désactiver pour accélérer l'exécution :

---
- name: Playbook rapide sans facts
  hosts: all
  gather_facts: false

  tasks:
    - name: Simple vérification
      ansible.builtin.ping:

Les conditionnels avec when

Le mot-clé when permet d'exécuter une tâche uniquement si une condition est remplie. Les conditions sont exprimées en Jinja2 et supportent les opérateurs Python.

tasks:
  # Condition simple
  - name: Installer Nginx sur Debian/Ubuntu
    ansible.builtin.apt:
      name: nginx
      state: present
    when: ansible_os_family == "Debian"

  # Condition avec ET (and)
  - name: Installer sur Ubuntu 22.04 uniquement
    ansible.builtin.apt:
      name: nginx
      state: present
    when:
      - ansible_distribution == "Ubuntu"
      - ansible_distribution_version == "22.04"
    # Équivalent à : when: ansible_distribution == "Ubuntu" and ansible_distribution_version == "22.04"

  # Condition avec OU (or)
  - name: Installer sur Debian ou Ubuntu
    ansible.builtin.apt:
      name: nginx
      state: present
    when: ansible_distribution == "Debian" or ansible_distribution == "Ubuntu"

  # Condition basée sur une variable
  - name: Activer SSL si demandé
    ansible.builtin.template:
      src: ssl.conf.j2
      dest: /etc/nginx/conf.d/ssl.conf
    when: ssl_enabled | default(false)

  # Condition basée sur le résultat d'une tâche précédente
  - name: Vérifier si le fichier de config existe
    ansible.builtin.stat:
      path: /etc/app/config.yml
    register: config_file

  - name: Créer la configuration par défaut si absente
    ansible.builtin.template:
      src: config.yml.j2
      dest: /etc/app/config.yml
    when: not config_file.stat.exists

  # Condition sur une liste
  - name: Installer uniquement sur les serveurs de production
    ansible.builtin.apt:
      name: monitoring-agent
      state: present
    when: inventory_hostname in groups['production']

  # Condition avec is defined
  - name: Configurer le proxy si défini
    ansible.builtin.lineinfile:
      path: /etc/environment
      line: "http_proxy={{ proxy_url }}"
    when: proxy_url is defined

  # Condition numérique
  - name: Alerte si mémoire insuffisante
    ansible.builtin.debug:
      msg: "ATTENTION : Mémoire faible ({{ ansible_memtotal_mb }} MB)"
    when: ansible_memtotal_mb < 2048

Les boucles : loop et with_items

Les boucles permettent de répéter une tâche pour chaque élément d'une liste, évitant ainsi la duplication de code.

La directive loop (recommandée)

tasks:
  # Boucle simple sur une liste
  - name: Installer plusieurs paquets
    ansible.builtin.apt:
      name: "{{ item }}"
      state: present
    loop:
      - nginx
      - certbot
      - htop
      - vim
      - curl
      - wget

  # Boucle sur une liste de dictionnaires
  - name: Créer plusieurs utilisateurs
    ansible.builtin.user:
      name: "{{ item.name }}"
      groups: "{{ item.groups }}"
      shell: "{{ item.shell | default('/bin/bash') }}"
      state: present
    loop:
      - { name: alice, groups: "sudo,developers" }
      - { name: bob, groups: "developers" }
      - { name: charlie, groups: "sudo,devops" }

  # Boucle sur une variable
  - name: Ouvrir les ports du pare-feu
    community.general.ufw:
      rule: allow
      port: "{{ item.port }}"
      proto: "{{ item.proto }}"
    loop: "{{ firewall_rules }}"
    # où firewall_rules est défini dans vars:
    # firewall_rules:
    #   - { port: 22, proto: tcp }
    #   - { port: 80, proto: tcp }
    #   - { port: 443, proto: tcp }

  # Boucle avec index
  - name: Créer des fichiers numérotés
    ansible.builtin.copy:
      content: "Fichier numéro {{ index }}"
      dest: "/tmp/fichier_{{ index }}.txt"
    loop: "{{ range(1, 6) | list }}"
    loop_control:
      loop_var: index

  # Boucle avec label personnalisé (pour une sortie propre)
  - name: Déployer les configurations
    ansible.builtin.template:
      src: "vhost.conf.j2"
      dest: "/etc/nginx/sites-available/{{ item.domain }}.conf"
    loop: "{{ virtual_hosts }}"
    loop_control:
      label: "{{ item.domain }}"

L'ancienne syntaxe with_items (encore supportée)

tasks:
  # with_items - équivalent de loop
  - name: Installer des paquets
    ansible.builtin.apt:
      name: "{{ item }}"
      state: present
    with_items:
      - nginx
      - postgresql

  # with_dict - itérer sur un dictionnaire
  - name: Configurer les variables d'environnement
    ansible.builtin.lineinfile:
      path: /etc/environment
      line: "{{ item.key }}={{ item.value }}"
    with_dict:
      APP_ENV: production
      APP_DEBUG: "false"
      APP_PORT: "8080"

  # with_fileglob - itérer sur des fichiers
  - name: Copier tous les fichiers de configuration
    ansible.builtin.copy:
      src: "{{ item }}"
      dest: /etc/app/conf.d/
    with_fileglob:
      - "files/conf.d/*.conf"

Les Handlers et Notify

Les handlers sont des tâches spéciales qui ne s'exécutent que lorsqu'elles sont notifiées par une autre tâche, et uniquement si cette tâche a effectué un changement (statut changed). Ils sont exécutés une seule fois à la fin du play, quel que soit le nombre de notifications reçues.

---
- name: Configuration complète de Nginx
  hosts: webservers
  become: true

  vars:
    nginx_port: 80

  tasks:
    - name: Installer Nginx
      ansible.builtin.apt:
        name: nginx
        state: present
      notify: Démarrer Nginx

    - name: Déployer la configuration principale
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: Recharger Nginx

    - name: Déployer la configuration du site
      ansible.builtin.template:
        src: vhost.conf.j2
        dest: /etc/nginx/sites-available/default
      notify: Recharger Nginx

    - name: Activer le site
      ansible.builtin.file:
        src: /etc/nginx/sites-available/default
        dest: /etc/nginx/sites-enabled/default
        state: link
      notify: Recharger Nginx

  handlers:
    - name: Démarrer Nginx
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

    - name: Recharger Nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

    - name: Redémarrer Nginx
      ansible.builtin.service:
        name: nginx
        state: restarted
Les handlers sont essentiels pour éviter les redémarrages inutiles de services. Par exemple, si vous modifiez 3 fichiers de configuration qui notifient tous le même handler, le service ne sera rechargé qu'une seule fois, à la fin du play. C'est à la fois plus efficace et plus sûr.

Pour forcer l'exécution des handlers avant la fin du play, vous pouvez utiliser le module meta :

tasks:
  - name: Modifier la configuration
    ansible.builtin.template:
      src: config.j2
      dest: /etc/app/config.yml
    notify: Redémarrer application

  - name: Forcer l'exécution des handlers maintenant
    ansible.builtin.meta: flush_handlers

  - name: Vérifier que l'application fonctionne
    ansible.builtin.uri:
      url: "http://localhost:8080/health"
      status_code: 200

Les Tags

Les tags permettent de marquer des tâches pour les exécuter ou les exclure sélectivement lors du lancement du playbook. C'est extrêmement utile pour les playbooks volumineux où vous ne souhaitez exécuter qu'une partie des tâches.

tasks:
  - name: Mettre à jour les paquets
    ansible.builtin.apt:
      upgrade: dist
    tags:
      - update
      - maintenance

  - name: Installer Nginx
    ansible.builtin.apt:
      name: nginx
      state: present
    tags:
      - install
      - nginx

  - name: Configurer Nginx
    ansible.builtin.template:
      src: nginx.conf.j2
      dest: /etc/nginx/nginx.conf
    tags:
      - configure
      - nginx

  - name: Déployer l'application
    ansible.builtin.git:
      repo: https://github.com/codeclan/app.git
      dest: /var/www/app
    tags:
      - deploy
      - app

Utilisation des tags en ligne de commande :

# Exécuter uniquement les tâches taguées "nginx"
ansible-playbook site.yml --tags nginx

# Exécuter plusieurs tags
ansible-playbook site.yml --tags "install,configure"

# Exclure des tags
ansible-playbook site.yml --skip-tags "update,maintenance"

# Lister tous les tags d'un playbook
ansible-playbook site.yml --list-tags

# Lister toutes les tâches qui seraient exécutées
ansible-playbook site.yml --list-tasks

Import et Include : organiser vos tâches

Pour les playbooks complexes, il est essentiel de diviser le code en fichiers séparés. Ansible offre deux mécanismes : import_tasks (statique) et include_tasks (dynamique).

import_tasks : inclusion statique

Les tâches importées sont intégrées au playbook lors de son analyse, avant l'exécution. Les tags et les conditions du play parent s'appliquent aux tâches importées.

# playbook principal : site.yml
---
- name: Configuration complète du serveur
  hosts: webservers
  become: true

  tasks:
    - name: Tâches communes
      ansible.builtin.import_tasks: tasks/common.yml

    - name: Installation de Nginx
      ansible.builtin.import_tasks: tasks/nginx.yml
      tags: nginx

    - name: Configuration SSL
      ansible.builtin.import_tasks: tasks/ssl.yml
      when: ssl_enabled | default(false)
      tags: ssl
# tasks/common.yml
---
- name: Mettre à jour le cache apt
  ansible.builtin.apt:
    update_cache: true
    cache_valid_time: 3600

- name: Installer les paquets de base
  ansible.builtin.apt:
    name:
      - vim
      - htop
      - curl
      - wget
      - unzip
      - net-tools
    state: present

- name: Configurer le fuseau horaire
  community.general.timezone:
    name: Europe/Paris

include_tasks : inclusion dynamique

Les tâches incluses dynamiquement sont chargées pendant l'exécution. Cela permet d'utiliser des variables dans le nom du fichier ou de conditionner l'inclusion.

tasks:
  # Inclusion conditionnelle basée sur l'OS
  - name: Inclure les tâches spécifiques à la distribution
    ansible.builtin.include_tasks: "tasks/{{ ansible_os_family | lower }}.yml"

  # Inclusion dans une boucle
  - name: Configurer chaque application
    ansible.builtin.include_tasks: tasks/deploy_app.yml
    loop: "{{ applications }}"
    loop_control:
      loop_var: app

  # Inclusion conditionnelle
  - name: Configurer le monitoring si activé
    ansible.builtin.include_tasks: tasks/monitoring.yml
    when: monitoring_enabled | default(false)

Les Templates Jinja2

Les templates Jinja2 sont l'un des outils les plus puissants d'Ansible. Ils permettent de générer des fichiers de configuration dynamiques en combinant un modèle (template) avec des variables.

Syntaxe Jinja2 de base

# templates/nginx-vhost.conf.j2

# Configuration générée par Ansible - NE PAS MODIFIER MANUELLEMENT
# Dernière mise à jour : {{ ansible_date_time.iso8601 }}

upstream {{ app_name }}_backend {
{% for server in backend_servers %}
    server {{ server.host }}:{{ server.port }} weight={{ server.weight | default(1) }};
{% endfor %}
}

server {
    listen {{ http_port | default(80) }};
    server_name {{ domain_name }};

{% if ssl_enabled | default(false) %}
    # Redirection HTTP vers HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name {{ domain_name }};

    ssl_certificate {{ ssl_cert_path }};
    ssl_certificate_key {{ ssl_key_path }};
    ssl_protocols TLSv1.2 TLSv1.3;
{% endif %}

    root {{ document_root }};
    index index.html;

    # Logs
    access_log /var/log/nginx/{{ app_name }}_access.log;
    error_log /var/log/nginx/{{ app_name }}_error.log {{ log_level | default('warn') }};

    location / {
        proxy_pass http://{{ app_name }}_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

{% if monitoring_enabled | default(false) %}
    # Endpoint de monitoring
    location /nginx_status {
        stub_status on;
        allow 127.0.0.1;
{% for ip in monitoring_ips | default([]) %}
        allow {{ ip }};
{% endfor %}
        deny all;
    }
{% endif %}

    # Pages d'erreur personnalisées
    error_page 404 /404.html;
    error_page 500 502 503 504 /50x.html;
}

Utilisation du template dans un playbook

---
- name: Déployer la configuration Nginx
  hosts: webservers
  become: true

  vars:
    app_name: codeclan
    domain_name: app.codeclan.fr
    http_port: 80
    ssl_enabled: true
    ssl_cert_path: /etc/ssl/certs/codeclan.pem
    ssl_key_path: /etc/ssl/private/codeclan.key
    document_root: /var/www/codeclan
    log_level: warn
    monitoring_enabled: true
    monitoring_ips:
      - 10.0.0.50
      - 10.0.0.51
    backend_servers:
      - { host: "127.0.0.1", port: 8001, weight: 3 }
      - { host: "127.0.0.1", port: 8002, weight: 2 }
      - { host: "127.0.0.1", port: 8003, weight: 1 }

  tasks:
    - name: Déployer le virtual host
      ansible.builtin.template:
        src: templates/nginx-vhost.conf.j2
        dest: "/etc/nginx/sites-available/{{ app_name }}.conf"
        owner: root
        group: root
        mode: '0644'
      notify: Recharger Nginx

  handlers:
    - name: Recharger Nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

Élévation de privilèges : become

La plupart des tâches d'administration système nécessitent des privilèges root. Ansible gère cela via le mécanisme become.

---
# Become au niveau du play (s'applique à toutes les tâches)
- name: Configuration système
  hosts: all
  become: true           # Activer sudo pour toutes les tâches
  become_user: root      # Utilisateur cible (root par défaut)
  become_method: sudo    # Méthode d'élévation (sudo par défaut)

  tasks:
    - name: Cette tâche s'exécute en tant que root
      ansible.builtin.apt:
        name: nginx
        state: present

    # Become au niveau de la tâche (override)
    - name: Exécuter en tant qu'utilisateur spécifique
      ansible.builtin.command: whoami
      become: true
      become_user: www-data
      register: current_user

    # Désactiver become pour une tâche spécifique
    - name: Cette tâche s'exécute en tant qu'utilisateur normal
      ansible.builtin.command: whoami
      become: false
# Options en ligne de commande
ansible-playbook site.yml -b              # Activer become
ansible-playbook site.yml -K              # Demander le mot de passe sudo
ansible-playbook site.yml --become-user=postgres  # Devenir un utilisateur spécifique

Gestion des erreurs

Une bonne gestion des erreurs est cruciale pour des playbooks robustes et fiables. Ansible offre plusieurs mécanismes pour gérer les situations d'erreur.

ignore_errors : continuer malgré les erreurs

tasks:
  - name: Tenter d'arrêter un service qui n'existe peut-être pas
    ansible.builtin.service:
      name: ancien-service
      state: stopped
    ignore_errors: true

  - name: Cette tâche s'exécutera même si la précédente a échoué
    ansible.builtin.debug:
      msg: "On continue le traitement"

failed_when : personnaliser les conditions d'échec

tasks:
  - name: Exécuter un script de vérification
    ansible.builtin.command: /opt/scripts/check_health.sh
    register: health_result
    failed_when:
      - health_result.rc != 0
      - "'CRITICAL' in health_result.stdout"

  - name: Vérifier la connexion à la base de données
    ansible.builtin.shell: pg_isready -h localhost
    register: pg_check
    failed_when: pg_check.rc != 0 and pg_check.rc != 1

block / rescue / always : try-catch-finally

La structure block/rescue/always est l'équivalent Ansible du try/catch/finally des langages de programmation. C'est le mécanisme le plus puissant pour la gestion des erreurs.

tasks:
  - name: Déploiement avec rollback automatique
    block:
      - name: Sauvegarder la version actuelle
        ansible.builtin.command: cp -r /var/www/app /var/www/app.backup
        args:
          creates: /var/www/app.backup

      - name: Déployer la nouvelle version
        ansible.builtin.git:
          repo: https://github.com/codeclan/app.git
          dest: /var/www/app
          version: "{{ app_version }}"

      - name: Installer les dépendances
        ansible.builtin.command: pip install -r requirements.txt
        args:
          chdir: /var/www/app

      - name: Exécuter les migrations de base de données
        ansible.builtin.command: python manage.py migrate
        args:
          chdir: /var/www/app

      - name: Redémarrer l'application
        ansible.builtin.service:
          name: app
          state: restarted

      - name: Vérifier que l'application répond
        ansible.builtin.uri:
          url: "http://localhost:8080/health"
          status_code: 200
        retries: 5
        delay: 3

    rescue:
      - name: ÉCHEC - Restaurer la version précédente
        ansible.builtin.command: cp -r /var/www/app.backup /var/www/app

      - name: Redémarrer avec l'ancienne version
        ansible.builtin.service:
          name: app
          state: restarted

      - name: Envoyer une alerte
        ansible.builtin.debug:
          msg: "ALERTE : Le déploiement de la version {{ app_version }} a échoué. Rollback effectué."

    always:
      - name: Nettoyer la sauvegarde
        ansible.builtin.file:
          path: /var/www/app.backup
          state: absent

      - name: Enregistrer le résultat du déploiement dans les logs
        ansible.builtin.lineinfile:
          path: /var/log/deployments.log
          line: "{{ ansible_date_time.iso8601 }} - Version {{ app_version }} - {{ 'SUCCÈS' if ansible_failed_task is not defined else 'ÉCHEC' }}"
          create: true

Playbook complet : configuration d'un serveur web de production

Pour conclure ce guide, voici un playbook complet et fonctionnel qui configure un serveur web de production avec Nginx, des certificats SSL, un pare-feu, du monitoring et un durcissement de la sécurité. Ce playbook illustre la plupart des concepts abordés dans cet article.

---
# configure_webserver.yml
# Playbook complet de configuration d'un serveur web de production
# Auteur : CodeClan.fr

- name: Configuration complète du serveur web de production
  hosts: webservers
  become: true
  gather_facts: true

  vars:
    # Configuration de l'application
    app_name: codeclan-app
    app_user: deploy
    app_group: www-data
    app_directory: "/var/www/{{ app_name }}"
    app_port: 8080

    # Configuration Nginx
    nginx_port: 80
    nginx_ssl_port: 443
    domain_name: app.codeclan.fr

    # Configuration sécurité
    ssh_port: 2222
    allowed_ssh_ips:
      - 10.0.0.0/24
      - 192.168.1.0/24

    # Paquets à installer
    system_packages:
      - nginx
      - ufw
      - fail2ban
      - htop
      - curl
      - wget
      - unzip
      - git
      - vim
      - logrotate
      - ntp

    # Configuration NTP
    ntp_servers:
      - 0.fr.pool.ntp.org
      - 1.fr.pool.ntp.org
      - 2.fr.pool.ntp.org

  tasks:
    # ===== PHASE 1 : Préparation du système =====
    - name: Mettre à jour le cache APT
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600
      tags: system

    - name: Installer les paquets système
      ansible.builtin.apt:
        name: "{{ system_packages }}"
        state: present
      tags: system

    - name: Configurer le fuseau horaire
      community.general.timezone:
        name: Europe/Paris
      tags: system

    - name: S'assurer que NTP est activé
      ansible.builtin.service:
        name: ntp
        state: started
        enabled: true
      tags: system

    # ===== PHASE 2 : Utilisateurs et permissions =====
    - name: Créer le groupe de l'application
      ansible.builtin.group:
        name: "{{ app_group }}"
        state: present
      tags: users

    - name: Créer l'utilisateur de déploiement
      ansible.builtin.user:
        name: "{{ app_user }}"
        group: "{{ app_group }}"
        groups: sudo
        shell: /bin/bash
        create_home: true
        state: present
      tags: users

    - name: Configurer sudo sans mot de passe pour l'utilisateur deploy
      ansible.builtin.lineinfile:
        path: /etc/sudoers.d/{{ app_user }}
        line: "{{ app_user }} ALL=(ALL) NOPASSWD:ALL"
        create: true
        mode: '0440'
        validate: 'visudo -cf %s'
      tags: users

    # ===== PHASE 3 : Sécurité SSH =====
    - name: Configurer le port SSH
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?Port '
        line: "Port {{ ssh_port }}"
      notify: Redémarrer SSH
      tags: security

    - name: Désactiver l'authentification root par SSH
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?PermitRootLogin'
        line: 'PermitRootLogin no'
      notify: Redémarrer SSH
      tags: security

    - name: Désactiver l'authentification par mot de passe
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?PasswordAuthentication'
        line: 'PasswordAuthentication no'
      notify: Redémarrer SSH
      tags: security

    # ===== PHASE 4 : Pare-feu =====
    - name: Configurer UFW - politique par défaut (entrée)
      community.general.ufw:
        direction: incoming
        default: deny
      tags: firewall

    - name: Configurer UFW - politique par défaut (sortie)
      community.general.ufw:
        direction: outgoing
        default: allow
      tags: firewall

    - name: Autoriser SSH depuis les IPs autorisées
      community.general.ufw:
        rule: allow
        port: "{{ ssh_port }}"
        proto: tcp
        src: "{{ item }}"
      loop: "{{ allowed_ssh_ips }}"
      tags: firewall

    - name: Autoriser HTTP et HTTPS
      community.general.ufw:
        rule: allow
        port: "{{ item }}"
        proto: tcp
      loop:
        - "80"
        - "443"
      tags: firewall

    - name: Activer UFW
      community.general.ufw:
        state: enabled
      tags: firewall

    # ===== PHASE 5 : Configuration Nginx =====
    - name: Créer le répertoire de l'application
      ansible.builtin.file:
        path: "{{ app_directory }}"
        state: directory
        owner: "{{ app_user }}"
        group: "{{ app_group }}"
        mode: '0755'
      tags: nginx

    - name: Créer les sous-répertoires
      ansible.builtin.file:
        path: "{{ app_directory }}/{{ item }}"
        state: directory
        owner: "{{ app_user }}"
        group: "{{ app_group }}"
        mode: '0755'
      loop:
        - public
        - logs
        - shared
        - releases
      tags: nginx

    - name: Déployer la page d'accueil par défaut
      ansible.builtin.copy:
        content: |
          
          
          
              
              
              {{ domain_name }}
              
                  body { font-family: Arial, sans-serif; text-align: center; padding: 50px; background: #f4f4f4; }
                  h1 { color: #333; }
                  p { color: #666; }
              
          
          
              Bienvenue sur {{ domain_name }}
              Le serveur est opérationnel. Déployé avec Ansible.
              Serveur : {{ ansible_hostname }} | IP : {{ ansible_default_ipv4.address }}
          
          
        dest: "{{ app_directory }}/public/index.html"
        owner: "{{ app_user }}"
        group: "{{ app_group }}"
        mode: '0644'
      tags: nginx

    - name: Supprimer la configuration par défaut de Nginx
      ansible.builtin.file:
        path: /etc/nginx/sites-enabled/default
        state: absent
      notify: Recharger Nginx
      tags: nginx

    - name: Démarrer et activer Nginx
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true
      tags: nginx

    # ===== PHASE 6 : Fail2ban =====
    - name: Configurer fail2ban pour SSH
      ansible.builtin.copy:
        content: |
          [sshd]
          enabled = true
          port = {{ ssh_port }}
          filter = sshd
          logpath = /var/log/auth.log
          maxretry = 3
          bantime = 3600
          findtime = 600

          [nginx-http-auth]
          enabled = true
          filter = nginx-http-auth
          logpath = /var/log/nginx/error.log
          maxretry = 5
          bantime = 3600
        dest: /etc/fail2ban/jail.local
        mode: '0644'
      notify: Redémarrer Fail2ban
      tags: security

    - name: Activer fail2ban
      ansible.builtin.service:
        name: fail2ban
        state: started
        enabled: true
      tags: security

    # ===== PHASE 7 : Configuration logrotate =====
    - name: Configurer la rotation des logs de l'application
      ansible.builtin.copy:
        content: |
          {{ app_directory }}/logs/*.log {
              daily
              missingok
              rotate 30
              compress
              delaycompress
              notifempty
              create 0640 {{ app_user }} {{ app_group }}
              sharedscripts
              postrotate
                  [ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
              endscript
          }
        dest: "/etc/logrotate.d/{{ app_name }}"
        mode: '0644'
      tags: logs

    # ===== PHASE 8 : Vérifications finales =====
    - name: Forcer l'exécution des handlers
      ansible.builtin.meta: flush_handlers

    - name: Vérifier que Nginx répond
      ansible.builtin.uri:
        url: "http://localhost:{{ nginx_port }}"
        status_code:
          - 200
          - 301
          - 302
      register: nginx_response
      retries: 3
      delay: 5
      until: nginx_response.status in [200, 301, 302]
      tags: verify

    - name: Vérifier que le pare-feu est actif
      ansible.builtin.command: ufw status
      register: ufw_status
      changed_when: false
      tags: verify

    - name: Résumé de la configuration
      ansible.builtin.debug:
        msg: |
          ====================================
          CONFIGURATION TERMINÉE AVEC SUCCÈS
          ====================================
          Serveur     : {{ ansible_hostname }}
          IP          : {{ ansible_default_ipv4.address }}
          OS          : {{ ansible_distribution }} {{ ansible_distribution_version }}
          Nginx       : Port {{ nginx_port }}
          SSH         : Port {{ ssh_port }}
          Application : {{ app_directory }}
          Utilisateur : {{ app_user }}
          Pare-feu    : Actif
          Fail2ban    : Actif
          ====================================
      tags: verify

  # ===== HANDLERS =====
  handlers:
    - name: Redémarrer SSH
      ansible.builtin.service:
        name: sshd
        state: restarted

    - name: Recharger Nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

    - name: Redémarrer Nginx
      ansible.builtin.service:
        name: nginx
        state: restarted

    - name: Redémarrer Fail2ban
      ansible.builtin.service:
        name: fail2ban
        state: restarted

Pour exécuter ce playbook :

# Exécution complète
ansible-playbook configure_webserver.yml -i inventaire/production/hosts.yml

# Mode simulation (dry-run)
ansible-playbook configure_webserver.yml -i inventaire/production/hosts.yml --check --diff

# Exécuter uniquement la partie sécurité
ansible-playbook configure_webserver.yml -i inventaire/production/hosts.yml --tags security

# Exécuter tout sauf le pare-feu
ansible-playbook configure_webserver.yml -i inventaire/production/hosts.yml --skip-tags firewall

# Limiter à un seul serveur
ansible-playbook configure_webserver.yml -i inventaire/production/hosts.yml --limit web1.codeclan.fr

# Mode verbose pour le débogage
ansible-playbook configure_webserver.yml -i inventaire/production/hosts.yml -vvv

Bonnes pratiques pour vos Playbooks

Pour conclure ce guide, voici un résumé des bonnes pratiques à suivre pour écrire des playbooks professionnels et maintenables.

  1. Toujours nommer vos tâches : des noms clairs et descriptifs facilitent le débogage et la lisibilité
  2. Utiliser les FQCN (Fully Qualified Collection Names) : préférez ansible.builtin.apt à apt
  3. Utiliser des variables plutôt que des valeurs en dur dans les tâches
  4. Séparer les variables sensibles dans des fichiers chiffrés avec ansible-vault
  5. Tester en mode check (--check --diff) avant d'appliquer en production
  6. Utiliser des tags pour permettre l'exécution partielle des playbooks
  7. Structurer en rôles pour la réutilisabilité et la maintenabilité
  8. Versionner dans Git tous vos playbooks, rôles et inventaires
  9. Documenter vos playbooks avec des commentaires YAML clairs
  10. Utiliser les handlers pour les redémarrages de services au lieu de les mettre dans les tâches
  11. Préférer les modules dédiés aux modules command et shell quand ils existent
  12. Gérer les erreurs avec block/rescue/always pour les opérations critiques
Un playbook bien écrit est comme une documentation vivante de votre infrastructure. Toute personne de votre équipe devrait pouvoir le lire et comprendre comment vos serveurs sont configurés. Investissez du temps dans la qualité de vos playbooks, c'est un investissement qui vous sera rendu au centuple.

Conclusion

Vous disposez maintenant de toutes les connaissances nécessaires pour écrire des playbooks Ansible professionnels. Depuis la structure de base avec les plays et les tasks, en passant par les modules essentiels, les variables, les conditionnels, les boucles, les handlers, les templates Jinja2 et la gestion des erreurs, vous avez couvert l'ensemble du spectre des fonctionnalités des playbooks.

Le playbook complet de configuration de serveur web que nous avons présenté peut servir de base solide pour vos propres projets. N'hésitez pas à l'adapter, à le découper en rôles et à l'enrichir en fonction de vos besoins spécifiques.

La clé pour maîtriser Ansible est la pratique. Commencez par automatiser les tâches simples et répétitives de votre quotidien, puis augmentez progressivement la complexité. Avec le temps, vous développerez des réflexes et des patterns qui rendront l'écriture de playbooks de plus en plus naturelle et efficace.

Bonne automatisation avec Ansible !

Vous vous êtes abonné avec succès à CodeClan
Parfait ! Ensuite, complétez le paiement pour obtenir un accès complet à tout le contenu premium.
Erreur ! Impossible de s'inscrire. Lien invalide.
Bienvenue ! Vous vous êtes connecté avec succès.
Erreur ! Impossible de se connecter. Veuillez réessayer.
Succès ! Votre compte est entièrement activé, vous avez maintenant accès à tout le contenu.
Erreur ! Le paiement Stripe a échoué.
Succès ! Vos informations de facturation sont mises à jour.
Erreur ! La mise à jour des informations de facturation a échoué.