Étude de cas 2 : Déploiement d'une application LAMP avec Ansible

Objectifs

  • Mettre en place une infrastructure LAMP (Linux, Apache, MySQL, PHP)
  • Utiliser Ansible pour automatiser le déploiement et la configuration
  • Gérer les fichiers de configuration et les services

Les quatres lettres LAMP signifient :

  • L pour Linux : le système d'exploitation
  • A pour Apache : le serveur web
  • M pour MySQL : le système de gestion de base de données
  • P pour PHP : le langage de programmation côté serveur

Cette architecture est couramment utilisée pour héberger des sites web dynamiques et des applications web.

Image: LAMP Stack Diagram

Prérequis

  • Avoir une machine de contrôle avec Ansible installé
  • Avoir une machine cible accessible via SSH

Vous pouvez utiliser les .ova fournis dans le cours de la semaine 10 (ansible-controller et node1 - nous n'avons pas besoin d'un deuxième noeud géré).

Étape 1 : Préparation de l'environnement

Structure à créer

lamp-project/
├── inventory.yml      # Liste des serveurs cibles
├── ansible.cfg        # Configuration locale d'Ansible
└── lamp_stack.yml     # Notre playbook (créé à l'étape suivante)

Créez ce dossier et les fichiers suivants :

Fichier : ansible.cfg

[defaults]
inventory = inventory.ini
host_key_checking = False

Explications :

  • inventory : indique où trouver la liste des serveurs
  • host_key_checking = False : évite la confirmation manuelle à la première connexion (pratique en lab, à éviter en prod)

Fichier : inventory.ini

Remplacez l'adresse IP par celle de votre machine cible.

[webservers]
web01 ansible_host=ADDRESS_WEB1

[all:vars]
ansible_user=ansible
ansible_ssh_pass=Ansible123!
ansible_become_pass=Ansible123!

Vérification

Testez la connectivité :

# Voir l'inventaire parsé
ansible-inventory --list

# Tester la connexion SSH
ansible webservers -m ping

# Vérifier qu'on peut devenir root
ansible webservers -m command -a "whoami" --become

Résultat attendu du ping :

web01 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

Étape 2 : Installation d'Apache

Le A de LAMP est pour Apache, le serveur web. Le serveur web est responsable de la gestion des requêtes HTTP des clients (navigateurs web) et de la livraison des pages web. Ainsi, lorsqu'une requête est faite pour une page web, Apache traite cette requête et renvoie le contenu approprié au client.

Nous allons créer un playbook Ansible pour installer et configurer Apache sur notre serveur web. Nous allons créer une application web que nous appelerons "monapp". Nous allons également utiliser le concept de VirtualHost, qui permet d'héberger plusieurs sites web sur un même serveur Apache en utilisant des configurations distinctes.

Fichier : lamp_stack.yml

---
- name: Déploiement stack LAMP
  hosts: webservers
  become: true
  
  vars:
    apache_document_root: "/var/www/monapp"

  tasks:
    # ==========================================
    # APACHE - Installation de base
    # ==========================================
    
    - name: Installer Apache
      ansible.builtin.apt:
        name: apache2
        state: present
        update_cache: true
        cache_valid_time: 3600

Explications :

- name: Déploiement stack LAMP

Nom du play. Un playbook peut contenir plusieurs plays.

  hosts: webservers

Cible le groupe défini dans l'inventaire.

  become: true

Exécute les tâches avec sudo (nécessaire pour apt).

  vars:
    apache_document_root: "/var/www/monapp"

Définit des variables réutilisables. Centralise les valeurs = maintenance facile.

    - name: Installer Apache

Nom de la tâche. Apparaît dans la sortie d'exécution. Soyez descriptif !

      ansible.builtin.apt:

Module Ansible pour gérer les paquets Debian/Ubuntu. Ici, nous utilisons le FQCN (Fully Qualified Collection Name) pour plus de clarté :

  • ansible.builtin. = FQCN (Fully Qualified Collection Name)
  • Bonne pratique depuis Ansible 2.10+, bien que l'ancienne notation apt: fonctionne toujours.
        name: apache2
        state: present
  • state: present → installe si absent, ne fait rien si présent
  • state: absent → désinstalle
  • state: latest → installe ou met à jour vers la dernière version
        update_cache: true
        cache_valid_time: 3600
  • Équivalent de apt-get update avant l'installation
  • cache_valid_time: 3600 → ne rafraîchit que si le cache a plus d'1 heure

Exécution

# Lancer le playbook
ansible-playbook lamp_stack.yml

# Mode verbose pour voir les détails
ansible-playbook lamp_stack.yml -v

# Mode check (dry-run) - simule sans appliquer
ansible-playbook lamp_stack.yml --check

Sortie attendue (première exécution) :

TASK [Installer Apache] ************************************
changed: [web01]

PLAY RECAP *************************************************
web01  : ok=1  changed=1  unreachable=0  failed=0

Sortie attendue (deuxième exécution) :

TASK [Installer Apache] ************************************
ok: [web01]

PLAY RECAP *************************************************
web01  : ok=1  changed=0  unreachable=0  failed=0

Remarquez : changed=1 devient changed=0 → c'est l'idempotence !


Vérification sur le serveur

# Connexion SSH
ssh ansible@192.168.1.10 # Remplacez par l'IP de votre serveur

# Vérifier qu'Apache est installé
apache2 -v

# Vérifier que le service tourne
systemctl status apache2

Questions de compréhension

  1. Que signifie "changed" vs "ok" dans le résultat ?
  2. Quel est l'avantage d'utiliser cache_valid_time plutôt que update_cache: true seul ?
  3. Quel est l'avantage d'utiliser state: latest plutôt que state: present en production ?

Étape 3 : Configuration d'Apache


Ajoutez ces tâches à lamp_stack.yml

  tasks:
    # [...tâche "Installer Apache" de l'étape précédente...]

    # ==========================================
    # APACHE - Configuration
    # ==========================================
    
    - name: Activer les modules Apache nécessaires
      ansible.builtin.apache2_module:
        name: "{{ item }}"
        state: present
      loop:
        - rewrite
        - ssl
      notify: Redémarrer Apache

    - name: Créer le répertoire de l'application
      ansible.builtin.file:
        path: "{{ apache_document_root }}"
        state: directory
        owner: www-data
        group: www-data
        mode: '0755'

    - name: Configurer le VirtualHost
      ansible.builtin.copy:
        dest: /etc/apache2/sites-available/monapp.conf
        content: |
          <VirtualHost *:80>
              ServerName {{ ansible_fqdn }}
              DocumentRoot {{ apache_document_root }}
              
              <Directory {{ apache_document_root }}>
                  AllowOverride All
                  Require all granted
              </Directory>
              
              ErrorLog ${APACHE_LOG_DIR}/monapp_error.log
              CustomLog ${APACHE_LOG_DIR}/monapp_access.log combined
          </VirtualHost>
        owner: root
        group: root
        mode: '0644'
      notify: Redémarrer Apache

    - name: Activer le site monapp
      ansible.builtin.command:
        cmd: a2ensite monapp.conf
        creates: /etc/apache2/sites-enabled/monapp.conf
      notify: Redémarrer Apache

    - name: Désactiver le site par défaut
      ansible.builtin.file:
        path: /etc/apache2/sites-enabled/000-default.conf
        state: absent
      notify: Redémarrer Apache

    - name: S'assurer qu'Apache est démarré et activé
      ansible.builtin.service:
        name: apache2
        state: started
        enabled: true

  # ==========================================
  # HANDLERS - À ajouter À LA FIN du playbook
  # Au même niveau d'indentation que 'tasks:'
  # ==========================================
  handlers:
    - name: Redémarrer Apache
      ansible.builtin.service:
        name: apache2
        state: restarted

Explications des nouveaux concepts

La boucle loop

    - name: Activer les modules Apache nécessaires
      ansible.builtin.apache2_module:
        name: "{{ item }}"
        state: present
      loop:
        - rewrite
        - ssl
  • loop itère sur une liste
  • {{ item }} contient la valeur courante
  • Équivalent à écrire 2 tâches séparées, mais plus maintenable

Le module file

    - name: Créer le répertoire de l'application
      ansible.builtin.file:
        path: "{{ apache_document_root }}"
        state: directory    # directory, file, link, absent
        owner: www-data
        group: www-data
        mode: '0755'        # Toujours en string avec quotes !

Attention! : mode: 0755 (sans quotes) est interprété en octal → résultat inattendu !

Le module copy avec contenu inline

    - name: Configurer le VirtualHost
      ansible.builtin.copy:
        dest: /etc/apache2/sites-available/monapp.conf
        content: |
          <VirtualHost *:80>
          [...]
  • content: | permet d'écrire du contenu multi-lignes directement
  • Alternative : src: fichier.conf pour copier un fichier local
  • {{ ansible_fqdn }} → fact automatique (nom complet de la machine)

Le module command avec idempotence

    - name: Activer le site monapp
      ansible.builtin.command:
        cmd: a2ensite monapp.conf
        creates: /etc/apache2/sites-enabled/monapp.conf
  • command exécute une commande shell
  • Problème : pas idempotent par défaut
  • Solution : creates: → n'exécute que si le fichier n'existe pas
  • Alternative : removes: → n'exécute que si le fichier existe

Le handler

  handlers:
    - name: Redémarrer Apache
      ansible.builtin.service:
        name: apache2
        state: restarted
  • Déclenché par notify: Redémarrer Apache
  • S'exécute une seule fois à la fin, même si notifié 4 fois
  • Le nom doit correspondre exactement au notify

Ordre d'exécution

1. Tâche "Activer modules"      → changed → notify handler
2. Tâche "Créer répertoire"     → changed (pas de notify)
3. Tâche "Configurer VHost"     → changed → notify handler
4. Tâche "Activer site"         → changed → notify handler
5. Tâche "Désactiver default"   → changed → notify handler
6. Tâche "Service started"      → ok
7. HANDLER "Redémarrer Apache"  → s'exécute UNE SEULE FOIS

Vérification

# Exécuter le playbook
ansible-playbook lamp_stack.yml

# Vérifier sur le serveur
curl http://192.168.1.10 # Remplacez par l'IP de votre serveur

# Vérifier la config Apache
ssh ansible@192.168.1.10 "apache2ctl -S" # Remplacez par l'IP de votre serveur

Questions de compréhension

  1. Pourquoi utiliser un handler plutôt que state: restarted directement ?
  2. Comment forcer l'exécution des handlers immédiatement (avant la fin) ?

Étape 4 : Installation et sécurisation de MariaDB

Le M de LAMP est pour MySQL/MariaDB, le système de gestion de base de données relationnelle. MariaDB est un fork de MySQL, souvent utilisé comme alternative open-source. Le rôle de la base de donnée dans la structure LAMP est de stocker, gérer et récupérer les données nécessaires pour les applications web. Les applications web interagissent avec la base de données pour effectuer des opérations telles que la création, la lecture, la mise à jour et la suppression de données.


Ici, nous allons utiliser la collection Ansible community.mysql pour gérer MariaDB/MySQL. Nous verrons les collections plus en détail dans les semaines suivantes, mais pour l'instant, on peut comprendre que les collections sont des ensembles de modules et plugins Ansible supplémentaires, souvent maintenus par la communauté. On peut les installer via la commande ansible-galaxy collection install.

Prérequis : Installer la collection MySQL

# Installer la collection community.mysql
ansible-galaxy collection install community.mysql

# Vérifier l'installation
ansible-galaxy collection list | grep mysql

Ajoutez ces variables dans la section vars:

  vars:
    apache_document_root: "/var/www/monapp"
    # Nouvelles variables pour MySQL
    mysql_root_password: "ChangezMoiEnProd123!"
    app_db_name: "monapp"
    app_db_user: "appuser"
    app_db_password: "AppPassword456!"

En production : ne jamais mettre de mots de passe en clair ! Nous pouvons utiliser Ansible Vault ou des solutions de gestion de secrets.


Ajoutez ces tâches après la section Apache

    # ==========================================
    # MARIADB - Installation
    # ==========================================
    
    - name: Installer MariaDB et les dépendances Python
      ansible.builtin.apt:
        name:
          - mariadb-server
          - mariadb-client
          - python3-mysqldb
        state: present

    - name: S'assurer que MariaDB est démarré et activé
      ansible.builtin.service:
        name: mariadb
        state: started
        enabled: true

    # ==========================================
    # MARIADB - Sécurisation (équivalent mysql_secure_installation)
    # ==========================================

    - name: Définir le mot de passe root MySQL
      community.mysql.mysql_user:
        name: root
        password: "{{ mysql_root_password }}"
        host: localhost
        login_unix_socket: /var/run/mysqld/mysqld.sock
        state: present

    - name: Créer le fichier .my.cnf pour root
      ansible.builtin.copy:
        dest: /root/.my.cnf
        content: |
          [client]
          user=root
          password={{ mysql_root_password }}
        owner: root
        group: root
        mode: '0600'

    - name: Supprimer les utilisateurs anonymes
      community.mysql.mysql_user:
        name: ''
        host_all: true
        state: absent

    - name: Supprimer la base de données test
      community.mysql.mysql_db:
        name: test
        state: absent

    # ==========================================
    # MARIADB - Création de la base applicative
    # ==========================================

    - name: Créer la base de données de l'application
      community.mysql.mysql_db:
        name: "{{ app_db_name }}"
        encoding: utf8mb4
        collation: utf8mb4_unicode_ci
        state: present

    - name: Créer l'utilisateur de l'application
      community.mysql.mysql_user:
        name: "{{ app_db_user }}"
        password: "{{ app_db_password }}"
        priv: "{{ app_db_name }}.*:ALL"
        host: localhost
        state: present

Explications des nouveaux concepts

Installation de plusieurs paquets

    - name: Installer MariaDB et les dépendances Python
      ansible.builtin.apt:
        name:
          - mariadb-server
          - mariadb-client
          - python3-mysqldb    # Nécessaire pour les modules mysql_*
        state: present
  • On peut passer une liste à name:
  • python3-mysqldb est obligatoire sur la machine cible pour que les modules community.mysql.* fonctionnent

Module d'une collection externe

    - name: Définir le mot de passe root MySQL
      community.mysql.mysql_user:
  • community.mysql → nom de la collection
  • mysql_user → nom du module
  • FQCN complet = community.mysql.mysql_user
  • Ce module gère les utilisateurs MySQL/MariaDB. Ici, nous définissons le mot de passe root.
        name: root
        password: "{{ mysql_root_password }}"
        host: localhost
        login_unix_socket: /var/run/mysqld/mysqld.sock
        state: present
  • Le paramètre name spécifie l'utilisateur (ici root)
  • Le paramètre password définit le nouveau mot de passe pour l'utilisateur spécifié (root).
  • Le paramètre host indique l'hôte (ici localhost) à partir duquel cet utilisateur est autorisé à se connecter. Ici, nous limitons l'accès à root uniquement depuis localhost, soit la machine locale.
  • Le paramètre login_unix_socket spécifie le socket Unix à utiliser pour l'authentification. C'est le point clé qui rend cette tâche possible juste après l'installation de MariaDB, avant même que le mot de passe root soit défini. Ce paramètre indique au module Ansible de se connecter à la base de données en utilisant le socket Unix local (et non via le réseau avec port TCP/IP). Ce socket n'est accessible que par les utilisateurs locaux, ce qui permet à Ansible de s'authentifier en tant que root sans avoir besoin du mot de passe.
  • Le paramètre state: present indique que l'utilisateur doit exister avec les paramètres spécifiés. Si l'utilisateur n'existe pas, il sera créé ; s'il existe déjà, ses paramètres seront mis à jour.

Le fichier .my.cnf

    - name: Créer le fichier .my.cnf pour root
      ansible.builtin.copy:
        dest: /root/.my.cnf
        content: |
          [client]
          user=root
          password={{ mysql_root_password }}
        mode: '0600'    # Lecture/écriture uniquement pour root !
  • MySQL/MariaDB lit automatiquement ce fichier pour les credentials
  • Permet aux tâches suivantes de s'authentifier sans spécifier le mot de passe
  • mode '0600' est critique pour la sécurité !

Gestion des privilèges

        priv: "{{ app_db_name }}.*:ALL"
  • Format : base.table:PRIVILEGES
  • monapp.*:ALL → tous les privilèges sur toutes les tables de monapp
  • Exemples : *.*:ALL (super admin), monapp.users:SELECT,INSERT

Vérification

# Exécuter le playbook
ansible-playbook lamp_stack.yml

# Tester la connexion MySQL sur le serveur
ssh ansible@192.168.1.10

# En tant que root (utilise .my.cnf)
sudo mysql -e "SHOW DATABASES;"

# En tant qu'utilisateur applicatif
mysql -u appuser -p'AppPassword456!' -e "SHOW DATABASES;"

Résultat attendu :

+--------------------+
| Database           |
+--------------------+
| information_schema |
| monapp             |
+--------------------+

Questions de compréhension

  1. À quoi sert la collection community.mysql que nous avons installée ?
  2. Que se passe-t-il si on relance le playbook ? Le mot de passe root est-il réinitialisé ?
  3. Pourquoi le fichier .my.cnf doit avoir les permissions 0600 ?

Étape 5 : Installation et configuration de PHP

Finalement, le P de LAMP est pour PHP, le langage de programmation côté serveur. PHP est utilisé pour créer des pages web dynamiques qui interagissent avec la base de données et génèrent du contenu en fonction des requêtes des utilisateurs. Nous allons installer PHP et les extensions nécessaires pour que notre application web puisse fonctionner correctement avec Apache et MariaDB. D'autres options du P seraient Python (LAMP devient LAMPy) ou Perl (LAMP devient LAMPe), mais PHP reste le plus courant.


Ajoutez cette variable dans la section vars:

  vars:
    # [...variables précédentes...]
    php_packages:
      - php
      - php-mysql
      - php-cli
      - php-curl
      - php-gd
      - php-mbstring
      - php-xml
      - libapache2-mod-php

Ajoutez ces tâches après la section MariaDB

    # ==========================================
    # PHP - Installation
    # ==========================================
    
    - name: Installer PHP et les extensions
      ansible.builtin.apt:
        name: "{{ php_packages }}"
        state: present
      notify: Redémarrer Apache

    # ==========================================
    # PHP - Configuration pour la production
    # ==========================================

    - name: Configurer PHP pour la production
      ansible.builtin.lineinfile:
        path: /etc/php/8.3/apache2/php.ini
        regexp: "{{ item.regexp }}"
        line: "{{ item.line }}"
        backup: true
      loop:
        - regexp: '^;?display_errors'
          line: 'display_errors = Off'
        - regexp: '^;?expose_php'
          line: 'expose_php = Off'
        - regexp: '^;?upload_max_filesize'
          line: 'upload_max_filesize = 64M'
        - regexp: '^;?post_max_size'
          line: 'post_max_size = 64M'
        - regexp: '^;?max_execution_time'
          line: 'max_execution_time = 300'
        - regexp: '^;?date.timezone'
          line: 'date.timezone = America/Montreal'
      notify: Redémarrer Apache

Explications détaillées

Variable de type liste

    php_packages:
      - php
      - php-mysql
      [...]

Définir la liste dans vars: plutôt qu'inline permet :

  • De réutiliser la liste ailleurs
  • De la surcharger facilement via l'inventaire ou la ligne de commande
  • Une meilleure lisibilité

Utilisation d'une variable liste

        name: "{{ php_packages }}"

Le module apt accepte une liste pour name:. Ansible installe tous les paquets en une seule transaction APT (plus efficace).

Le module lineinfile

    - name: Configurer PHP pour la production
      ansible.builtin.lineinfile:
        path: /etc/php/8.3/apache2/php.ini
        regexp: "{{ item.regexp }}"
        line: "{{ item.line }}"
        backup: true

Paramètres clés :

Paramètre Description
path Fichier à modifier
regexp Expression régulière pour trouver la ligne
line Nouvelle ligne à écrire
backup Crée une copie .bak avant modification

Comportement :

  1. Cherche une ligne correspondant à regexp
  2. Si trouvée → la remplace par line
  3. Si non trouvée → ajoute line à la fin du fichier

L'expression régulière expliquée

        regexp: '^;?display_errors'
  • ^ → début de ligne
  • ;? → point-virgule optionnel (0 ou 1 fois)
  • display_errors → texte littéral

Pourquoi ;? : Dans php.ini, les options commentées commencent par ;

;display_errors = On    ← Option commentée (désactivée)
display_errors = Off    ← Option active

Notre regexp matche les deux cas !

Boucle avec dictionnaires

      loop:
        - regexp: '^;?display_errors'
          line: 'display_errors = Off'
        - regexp: '^;?expose_php'
          line: 'expose_php = Off'
  • Chaque élément est un dictionnaire avec clés regexp et line
  • Accès via {{ item.regexp }} et {{ item.line }}

Pourquoi ces paramètres PHP ?

Paramètre Valeur Raison
display_errors = Off Sécurité Ne pas exposer les erreurs aux utilisateurs
expose_php = Off Sécurité Cache la version PHP dans les headers HTTP
upload_max_filesize = 64M Fonctionnel Permet l'upload de fichiers volumineux
post_max_size = 64M Fonctionnel Doit être ≥ upload_max_filesize
max_execution_time = 300 Fonctionnel Scripts longs (imports, exports)
date.timezone Fonctionnel Évite les warnings sur les fonctions date

Attention à la version PHP

        path: /etc/php/8.3/apache2/php.ini

Le chemin contient la version PHP (8.3). Si vous obtenez une erreur "Fichier non trouvé", c'est probablement dû à une différence de version.

Sur votre système, vérifiez :

# Trouver la version PHP installée
php -v

# Lister les php.ini disponibles
ls /etc/php/*/apache2/php.ini

Adaptez le chemin dans le playbook si nécessaire.


Vérification

# Exécuter le playbook
ansible-playbook lamp_stack.yml

# Vérifier PHP
ssh ansible@192.168.1.10 "php -v"

# Vérifier les modules PHP
ssh ansible@192.168.1.10 "php -m"

# Vérifier une config modifiée
ssh ansible@192.168.1.10 "grep display_errors /etc/php/8.3/apache2/php.ini"

# Vérifier le backup
ssh ansible@192.168.1.10 "ls -la /etc/php/8.3/apache2/php.ini*"

Étape 6 : Déploiement d'une application de test

Nous allons créer une simple page PHP qui affiche des informations sur le serveur, la version de PHP, les modules chargés, et teste la connexion à la base de données MariaDB. Cela nous permettra de vérifier que toute la stack LAMP fonctionne correctement.


Ajoutez cette tâche après la section PHP

    # ==========================================
    # APPLICATION - Page de test
    # ==========================================
    
    - name: Déployer une page PHP de test
      ansible.builtin.copy:
        dest: "{{ apache_document_root }}/index.php"
        content: |
          <?php
          $host = 'localhost';
          $dbname = '{{ app_db_name }}';
          $user = '{{ app_db_user }}';
          $pass = '{{ app_db_password }}';
          
          echo "<h1>Stack LAMP déployée avec Ansible pour le cours 5R3!</h1>";
          echo "<h2>Informations système</h2>";
          echo "<p><strong>Serveur:</strong> " . gethostname() . "</p>";
          echo "<p><strong>PHP Version:</strong> " . phpversion() . "</p>";
          echo "<p><strong>Système:</strong> {{ ansible_distribution }} {{ ansible_distribution_version }}</p>";
          echo "<p><strong>Adresse IP:</strong> {{ ansible_default_ipv4.address }}</p>";
          
          echo "<h2>Test de connexion MySQL</h2>";
          try {
              $pdo = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass);
              $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
              echo "<p style='color:green'>&#10004; Connexion à la base de données réussie!</p>";
              
              $stmt = $pdo->query("SELECT VERSION() as version");
              $row = $stmt->fetch();
              echo "<p><strong>Version MariaDB:</strong> " . $row['version'] . "</p>";
          } catch(PDOException $e) {
              echo "<p style='color:red'>&#10008; Erreur: " . htmlspecialchars($e->getMessage()) . "</p>";
          }
          
          echo "<h2>Modules PHP chargés</h2>";
          $modules = get_loaded_extensions();
          sort($modules);
          echo "<p>" . implode(", ", $modules) . "</p>";
          ?>
        owner: www-data
        group: www-data
        mode: '0644'

Les Facts Ansible utilisés

Dans le code PHP, nous utilisons des facts collectés automatiquement :

echo "<p>Système: {{ ansible_distribution }} {{ ansible_distribution_version }}</p>";
echo "<p>Adresse IP: {{ ansible_default_ipv4.address }}</p>";

Facts courants et utiles

Fact Description Exemple de valeur
ansible_hostname Nom court de la machine web01
ansible_fqdn Nom complet (FQDN) web01.example.com
ansible_distribution Distribution Linux Ubuntu, Debian
ansible_distribution_version Version de la distro 22.04, 12
ansible_os_family Famille d'OS Debian, RedHat
ansible_default_ipv4.address IP principale 192.168.1.10
ansible_processor_vcpus Nombre de vCPUs 4
ansible_memtotal_mb RAM totale en Mo 8192

Explorer tous les facts

# Voir tous les facts d'une machine
ansible web01 -m ansible.builtin.setup

# Filtrer les facts
ansible web01 -m ansible.builtin.setup -a "filter=ansible_distribution*"

# Sauvegarder dans un fichier
ansible web01 -m ansible.builtin.setup > facts_web01.json

Comment Ansible remplace les variables dans le fichier

Attention : le fichier index.php contient un mélange de :

  1. Variables Ansible (remplacées au déploiement) :

    $dbname = '{{ app_db_name }}';        // Devient: $dbname = 'monapp';
    echo "{{ ansible_distribution }}";    // Devient: echo "Ubuntu";
    
  2. Code PHP (exécuté par le serveur web) :

    echo phpversion();                    // Exécuté à chaque requête HTTP
    echo gethostname();                   // Exécuté à chaque requête HTTP
    

Résultat sur le serveur après déploiement :

$dbname = 'monapp';  // Variable Ansible → valeur fixe
echo phpversion();   // PHP → valeur dynamique

Vérification complète de la stack

# Exécuter le playbook
ansible-playbook lamp_stack.yml

# Tester dans le navigateur
curl http://192.168.1.10

# Ou ouvrir dans un navigateur :
# http://192.168.1.10

Résultat attendu :

Stack LAMP déployée avec Ansible pour le cours 5R3!

Informations système
Serveur: web01
PHP Version: 8.3.x
Système: Ubuntu 22.04
Adresse IP: 192.168.1.10

Test de connexion MySQL
✔ Connexion à la base de données réussie!
Version MariaDB: 10.x.x-MariaDB

Modules PHP chargés
Core, curl, date, gd, mbstring, mysql, mysqli, ...

Questions de compréhension

  1. Quelle est la différence entre {{ ansible_hostname }} dans le playbook et gethostname() en PHP ?

Étape 7 : Configuration du Firewall (UFW)


Prérequis : Installer la collection

ansible-galaxy collection install community.general

Ajoutez ces tâches à la fin (avant les handlers)

    # ==========================================
    # FIREWALL - Sécurisation
    # ==========================================
    
    - name: Installer UFW
      ansible.builtin.apt:
        name: ufw
        state: present

    - name: Autoriser SSH (TOUJOURS EN PREMIER !)
      community.general.ufw:
        rule: allow
        port: '22'
        proto: tcp

    - name: Autoriser HTTP
      community.general.ufw:
        rule: allow
        port: '80'
        proto: tcp

    - name: Autoriser HTTPS
      community.general.ufw:
        rule: allow
        port: '443'
        proto: tcp

    - name: Activer UFW avec politique deny par défaut
      community.general.ufw:
        state: enabled
        default: deny
        direction: incoming

Attention à l'ordre des tâches !

    - name: Autoriser SSH (TOUJOURS EN PREMIER !)
      community.general.ufw:
        rule: allow
        port: '22'

Cette tâche DOIT être exécutée AVANT l'activation du firewall !

Si vous activez UFW avant d'autoriser SSH :

  1. Le firewall bloque tout le trafic entrant
  2. Y compris votre connexion SSH
  3. Vous perdez l'accès au serveur car Ansible se connecte via SSH

Ansible exécute les tâches dans l'ordre → c'est pourquoi notre playbook est sûr.


Explications du module UFW

Autoriser un port

    - name: Autoriser HTTP
      community.general.ufw:
        rule: allow          # allow, deny, reject, limit
        port: '80'           # Numéro de port (en string)
        proto: tcp           # tcp, udp, any

Politique par défaut et activation

    - name: Activer UFW avec politique deny par défaut
      community.general.ufw:
        state: enabled       # enabled, disabled, reset
        default: deny        # allow, deny, reject
        direction: incoming  # incoming, outgoing, routed

Politique deny sur incoming signifie :

  • Tout trafic entrant est bloqué par défaut
  • Seuls les ports explicitement autorisés sont ouverts
  • Le trafic sortant reste autorisé

Vérification

# Exécuter le playbook
ansible-playbook lamp_stack.yml

# Vérifier le statut UFW
ssh ansible@192.168.1.10 "sudo ufw status verbose"

Résultat attendu :

Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    Anywhere
80/tcp                     ALLOW IN    Anywhere
443/tcp                    ALLOW IN    Anywhere
22/tcp (v6)                ALLOW IN    Anywhere (v6)
80/tcp (v6)                ALLOW IN    Anywhere (v6)
443/tcp (v6)               ALLOW IN    Anywhere (v6)

Playbook complet - Récapitulatif

À ce stade, votre playbook contient :

  1. Installation Apache
  2. Configuration VirtualHost
  3. Installation MariaDB
  4. Sécurisation MySQL
  5. Création base de données
  6. Installation PHP
  7. Configuration php.ini
  8. Application de test
  9. Firewall UFW

Nombre de tâches : environ 20 Lignes de code : environ 200

Prochaine étape : Refactoring !

Votre playbook fonctionne, mais il a des problèmes :

  • Trop long (200+ lignes dans un seul fichier)
  • Difficile à réutiliser sur d'autres projets

Solution : transformer ce playbook en utilisant :

  • import_tasks et include_tasks
  • Les rôles Ansible