Étude de cas : Gestion d'une infrastructure d'application web

Contexte

Vous devez déployer et configurer une stack applicative comprenant :

  • Un serveur web (Nginx)
  • Une base de données (PostgreSQL)
  • Un serveur de cache (Redis)
  • Une application Python

Étape 0: Préparation de l'environnement

Pour cette étude de cas, vous aurez besoin d'une machine de contrôle Ansible avec Ansible installé, ainsi que de trois hôtes cibles pour simuler les serveurs web, bases de données et applications. Vous pouvez utiliser la machine de contrôle Ansible créée dans la partie pratique du cours de la semaine 10, ainsi que les 2 machines virtuelles supplémentaires pour les serveurs web et base de données.

Pour créer la machine virtuelle supplémentaire pour le serveur d'application, vous pouvez cloner l'une des machines existantes (node1 ou node2) en effectuant un "full clone". Changez ensuite son nom d'hôte en node3 en effectuant les commandes suivantes sur la machine clonée :

sudo hostnamectl set-hostname node3

Ensuite, modifiez le fichier /etc/hosts pour ajouter une entrée pour node3 :

sudo nano /etc/hosts

Finalement, redémarrez la machine pour que les changements prennent effet :

sudo reboot

Étape 1: Création des fichiers de configuration Ansible

Voici la structure des fichiers et le contenu nécessaire pour configurer votre infrastructure sur votre machine de contrôle Ansible.

lab_semaine11/
├── ansible.cfg
├── inventory/
│   └── hosts.ini
├── group_vars/
│   ├── all.yml
│   ├── webservers.yml
│   └── databases.yml
├── host_vars/
│   └── app1.yml
├── vars/
│   └── app_config.yml
└── deploy_stack.yml

Utilisez la commande suivante pour créer les répertoires nécessaires :

mkdir -p lab_semaine11/inventory lab_semaine11/group_vars lab_semaine11/host_vars lab_semaine11/vars

Et la commande suivante pour créer les fichiers vides :

touch lab_semaine11/inventory/hosts.ini lab_semaine11/group_vars/all.yml lab_semaine11/group_vars/webservers.yml lab_semaine11/group_vars/databases.yml lab_semaine11/host_vars/app-server.yml lab_semaine11/vars/app_config.yml lab_semaine11/deploy_stack.yml lab_semaine11/ansible.cfg

Fichier d'inventaire inventory/hosts.ini

Modifiez votre fichier d'inventaire pour définir les groupes d'hôtes et leurs adresses, en remplacant les adresses par celles de vos hôtes cibles.

[webservers]
web1 ansible_host=ADDRESS_WEB1

[databases]
db1 ansible_host=ADDRESS_DB1

[appservers]
app1 ansible_host=ADDRESS_APP1

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

Maintenant, modifiez le fichier de configuration Ansible ansible.cfg dans le répertoire lab_semaine11/ avec le contenu suivant :

[defaults]
inventory = inventory/hosts.ini
host_key_checking = False

Étape 2: Test des connexions

Avant de commencer le déploiement, assurez-vous que vous pouvez vous connecter à tous les hôtes cibles en utilisant Ansible :

ansible all -m ping

Étape 3 : Définition des variables

Nous allons définir les variables nécessaires pour chaque groupe d'hôtes et pour l'application.

Variables communes à tous les serveurs

Pour définir des variables communes à tous les serveurs, modifiez le fichier group_vars/all.yml comme suit :

---
# Variables communes à tous les serveurs
app_environment: "production"
company_name: "TechCorp"
admin_email: "admin@techcorp.com"

# Configuration de base
base_packages:
  - curl
  - wget
  - vim
  - htop

# Configuration de sécurité
firewall_enabled: true
selinux_enabled: false

Variables spécifiques aux groupes d'hôtes

Nous allons maintenant définir des variables spécifiques pour chaque groupe d'hôtes: le groupe des serveurs web et le groupe des bases de données.

Variables pour les serveurs web

Modifiez le fichier group_vars/webservers.yml comme suit :

---
# Configuration Nginx
nginx_config:
  worker_processes: 4
  worker_connections: 1024
  keepalive_timeout: 65

# Virtual hosts
virtual_hosts:
  - name: "app.techcorp.com"
    port: 80
    ssl_port: 443
    document_root: "/var/www/app"
    ssl_enabled: true
  
  - name: "api.techcorp.com"
    port: 80
    ssl_port: 443
    document_root: "/var/www/api"
    ssl_enabled: true

# Modules Nginx à activer
nginx_modules:
  - ssl
  - gzip
  - headers

Variables pour les bases de données

Modifiez le fichier group_vars/databases.yml comme suit :

---
# Configuration PostgreSQL
postgresql_version: "14"
postgresql_port: 5432
postgresql_max_connections: 100
postgresql_shared_buffers: "256MB"

# Bases de données à créer
databases:
  - name: "app_production"
    owner: "app_user"
    encoding: "UTF8"
  
  - name: "analytics"
    owner: "analytics_user"
    encoding: "UTF8"

# Configuration Redis
redis_port: 6379
redis_maxmemory: "512mb"
redis_maxmemory_policy: "allkeys-lru"

Note: Pour simplifier, la base de données PostgreSQL et Redis seront installés sur le même serveur (db1).

Variables spécifiques à un hôte

Finalement, nous allons définir des variables spécifiques pour un hôte particulier, soit le serveur d'application, dans le fichier host_vars/app-server.yml.

---
# Surcharge pour le serveur d'application principal
nginx_config:
  worker_processes: 8  # Plus de workers pour le serveur principal
  worker_connections: 2048

# Configuration spécifique de l'application
app_settings:
  debug_mode: false
  log_level: "INFO"
  max_upload_size: "50M"
  session_timeout: 3600

Variables de l'application

Finalement, nous allons définir les variables qui seront utilisés dans le playbook dans le fichier vars/app_config.yml.

---
# Configuration de l'application Python
python_app:
  name: "TechCorp WebApp"
  version: "2.1.0"
  port: 8000
  workers: 4
  
  dependencies:
    - django==4.2.0
    - psycopg2-binary==2.9.6
    - redis==4.5.5
    - celery==5.2.7
    - gunicorn==20.1.0
  
  environment_vars:
    DATABASE_URL: "postgresql://app_user@db1:5432/app_production"
    REDIS_URL: "redis://localhost:6379/0"
    SECRET_KEY: "change-me-in-production"
    ALLOWED_HOSTS: "app.techcorp.com,api.techcorp.com"

# Configuration de monitoring
monitoring:
  enabled: true
  check_interval: 60
  alert_email: "{{ admin_email }}"
  services_to_monitor:
    - nginx
    - postgresql
    - redis
    - gunicorn

Étape 4: Le déploiement avec le playbook principal

Maintenant que toutes les variables sont définies, nous allons créer le playbook principal deploy_stack.yml pour déployer et configurer l'infrastructure complète.

---
# Play 1 : Préparation commune sur tous les serveurs
- name: Préparation commune des serveurs
  hosts: all
  become: true
  gather_facts: true
  vars_files:
    - vars/app_config.yml

  tasks:
    - name: Afficher le contexte du serveur
      debug:
        msg: "Serveur {{ inventory_hostname }} dans l'environnement {{ app_environment }} (company={{ company_name }})"

    - name: Installer les paquets de base (Debian/Ubuntu seulement)
      apt:
        name: "{{ item }}"
        state: present
        update_cache: yes
      loop: "{{ base_packages }}"
      when: ansible_facts['os_family'] == "Debian"

    - name: Simulation de configuration du pare-feu
      debug:
        msg: "Le pare-feu serait configuré ici."
      when: firewall_enabled

    - name: Vérifier l'état attendu de SELinux (simulation)
      debug:
        msg: "SELinux doit être désactivé (selinux_enabled={{ selinux_enabled }})"

    - name: Exemple de condition basée sur une variable
      debug:
        msg: "Configuration spécifique pour la production"
      when: app_environment == "production"


# Play 2 : Configuration des serveurs web (Nginx)
- name: Configuration des serveurs web (Nginx)
  hosts: webservers
  become: true
  gather_facts: true
  vars_files:
    - vars/app_config.yml

  tasks:
    - name: Installer Nginx (simulation)
      debug:
        msg: "Installation de Nginx sur {{ inventory_hostname }}"
      changed_when: true
      notify: manage_nginx

    - name: Afficher la configuration Nginx utilisée
      debug:
        msg: "Nginx config: {{ nginx_config }}"

    - name: Afficher les modules Nginx à activer
      debug:
        msg: "Modules Nginx à activer: {{ nginx_modules | join(', ') }}"

    - name: Générer les virtual hosts (simulation)
      debug:
        msg: "VHost {{ item.name }} -> root {{ item.document_root }} (SSL={{ item.ssl_enabled }})"
      loop: "{{ virtual_hosts }}"
      changed_when: true
      notify: manage_nginx

    - name: N'appliquer une action que si le port applicatif est supérieur à 8000
      debug:
        msg: "Le port applicatif {{ python_app.port }} est supérieur à 8000."
      when: python_app.port > 8000

    - name: Forcer l'exécution des handlers tout de suite
      meta: flush_handlers

    - name: Tâche qui s'exécute après le flush des handlers
      debug:
        msg: "Cette tâche s'exécute après le redémarrage / rechargement de Nginx."

  handlers:
    - name: Redémarrer Nginx
      debug:
        msg: ">>> Redémarrage complet de Nginx sur {{ inventory_hostname }} <<<"
      listen: manage_nginx

    - name: Recharger la configuration Nginx
      debug:
        msg: ">>> Rechargement de la configuration Nginx sur {{ inventory_hostname }} <<<"
      listen: manage_nginx


# Play 3 : Configuration de la base de données et de Redis
- name: Configuration des bases de données et du cache
  hosts: databases
  become: true
  gather_facts: true
  vars_files:
    - vars/app_config.yml

  tasks:
    - name: Afficher la configuration PostgreSQL
      debug:
        msg: "PostgreSQL {{ postgresql_version }} sur le port {{ postgresql_port }} (max_connections={{ postgresql_max_connections }})"

    - name: Afficher la configuration Redis
      debug:
        msg: "Redis sur le port {{ redis_port }} (maxmemory={{ redis_maxmemory }}, policy={{ redis_maxmemory_policy }})"

    - name: Créer les bases de données (simulation)
      debug:
        msg: "Création de la base {{ item.name }} pour l'utilisateur {{ item.owner }} (encoding={{ item.encoding }})"
      loop: "{{ databases }}"
      changed_when: true
      notify: restart_db_services

    - name: Vérifier la disponibilité de PostgreSQL (simulation)
      command: "echo 'pg_isready sur le port {{ postgresql_port }}'"
      register: pg_check
      changed_when: false

    - name: Afficher le résultat du check PostgreSQL
      debug:
        msg: "Check PostgreSQL rc={{ pg_check.rc }} stdout={{ pg_check.stdout }}"

    - name: Exemple de failed_when personnalisé
      command: "/bin/false"
      register: pg_fake
      failed_when: "'CRITICAL' in pg_fake.stderr"
      ignore_errors: true

    - name: Réagir à l'échec simulé
      debug:
        msg: "Commande en erreur (rc={{ pg_fake.rc }}), mais playbook non interrompu."
      when: pg_fake is defined and pg_fake.rc != 0

  handlers:
    - name: Redémarrer les services de base de données
      debug:
        msg: ">>> Redémarrage de PostgreSQL et Redis sur {{ inventory_hostname }} <<<"
      listen: restart_db_services


# Play 4 : Déploiement de l'application Python
- name: Déploiement de l'application Python
  hosts: appservers
  become: true
  gather_facts: true
  vars_files:
    - vars/app_config.yml

  tasks:
    - name: Afficher la configuration de l'application
      debug:
        msg: "Déploiement de {{ python_app.name }} v{{ python_app.version }} sur le port {{ python_app.port }}"

    - name: Déterminer dynamiquement le nombre de workers
      set_fact:
        effective_workers: "{{ python_app.workers if app_environment == 'production' else 1 }}"

    - name: Afficher le nombre de workers effectif
      debug:
        msg: "Workers utilisés pour {{ python_app.name }} : {{ effective_workers }}"

    - name: Afficher les variables d'environnement de l'application
      debug:
        msg: "Env vars: {{ python_app.environment_vars }}"

    - name: Installer les dépendances Python (simulation)
      debug:
        msg: "Installation de la dépendance Python {{ item }}."
      loop: "{{ python_app.dependencies }}"
      changed_when: true

    - name: Simulation de création du service Gunicorn
      debug:
        msg: "Création / mise à jour du service Gunicorn pour {{ python_app.name }} (port {{ python_app.port }})"
      changed_when: true
      notify: restart_app

    - name: Exemple de commande avec changed_when = false
      command: "echo 'Healthcheck applicatif'"
      register: healthcheck
      changed_when: false

    - name: Afficher le résultat du healthcheck
      debug:
        msg: "Healthcheck rc={{ healthcheck.rc }} stdout={{ healthcheck.stdout }}"

    - name: Configurer le monitoring si activé
      debug:
        msg: "Configuration du monitoring (intervalle={{ monitoring.check_interval }}s) pour services: {{ monitoring.services_to_monitor | join(', ') }}"
      when: monitoring.enabled

  handlers:
    - name: Redémarrer l'application
      debug:
        msg: ">>> Redémarrage de l'application {{ python_app.name }} sur {{ inventory_hostname }} <<<"
      listen: restart_app

À remarquer::

  • Pour le play 1:
    • Utilisation de variables communes avec vars/app_config.yml.
    • Le play s'applique à tous les hôtes.
    • Utilisation implicite des variables définies dans group_vars/all.yml.
    • Installation d'une liste de paquets avec une boucle loop. La liste de paquets est définie dans les variables communes (variable base_packages du fichier group_vars/all.yml).
    • Utilisation de conditions when pour exécuter des tâches spécifiques selon les variables définies.
      • ansible_facts est une variable prédéfinie par Ansible qui contient des informations sur le système cible.
      • firewall_enabled, selinux_enabled et app_environment sont des variables définies dans group_vars/all.yml.
    • Les tâches de configuration du pare-feu et de SELinux sont simulées avec des messages de débogage afin de simplifier l'exemple.
  • Pour le play 2:
    • Le play ne s'applique que sur les hôtes du groupe webservers.
    • Utilisation de variables spécifiques aux serveurs web définies dans group_vars/webservers.yml (de façon implicite via l'inventaire).
    • Utilisation de handlers pour redémarrer ou recharger Nginx lorsque des changements sont effectués. Les deux handlers sont liés à la même notification manage_nginx.
    • La mention changed_when: true dans les tâches simulant l'installation de Nginx et la génération des virtual hosts force Ansible à considérer ces tâches comme ayant effectué des changements, déclenchant ainsi les handlers associés.
    • Utilisation d'une boucle loop pour créer plusieurs virtual hosts définis dans la variable virtual_hosts du fichier group_vars/webservers.yml.
    • Utilisation de la variable python_app.port définie dans vars/app_config.yml pour une condition when.
    • Utilisation de la commande meta: flush_handlers pour forcer l'exécution immédiate des handlers.
  • Pour le play 3:
    • Le play ne s'applique que sur les hôtes du groupe databases.
    • Utilisation de variables spécifiques aux bases de données définies dans group_vars/databases.yml (de façon implicite via l'inventaire).
    • Utilisation d'une boucle loop pour créer plusieurs bases de données définies dans la variable databases du fichier group_vars/databases.yml.
    • Simulation de la vérification de la disponibilité de PostgreSQL avec la commande pg_isready et affichage du résultat avec register suivi d'un debug qui affichera le code de retour (.rc) et la sortie standard (.stdout).
    • Utilisation d'une condition failed_when personnalisée pour simuler une gestion d'erreur avancée: la tâche échouera uniquement si le mot "CRITICAL" est présent dans la sortie d'erreur standard de la variable enregistrée pg_fake, qui représente le résultat d'une commande simulée qui échoue toujours (/bin/false). L'option ignore_errors: true permet de continuer l'exécution du playbook même si cette tâche échoue.
    • Utilisation d'une condition when pour réagir à l'échec simulé en affichant un message de débogage.
  • Pour le play 4:
    • Le play ne s'applique que sur les hôtes du groupe appservers.
    • Utilisation de variables spécifiques au serveur d'application définies dans host_vars/app1.yml (de façon implicite via l'inventaire).
    • Utilisation de la commande set_fact pour définir dynamiquement une variable effective_workers basée sur la variable app_environment définie dans group_vars/all.yml.
    • Utilisation de la variable effective_workers dans une tâche de débogage.
    • Utilisation d'une boucle loop pour installer plusieurs dépendances Python définies dans la variable python_app.dependencies du fichier vars/app_config.yml.
    • Simulation de la création ou mise à jour du service Gunicorn avec une tâche de débogage qui notifie un handler restart_app en cas de changement... qui sera toujours le cas grâce à changed_when: true.
    • Utilisation de la commande echo pour simuler un healthcheck applicatif, avec changed_when: false pour indiquer qu'aucun changement n'est effectué par cette tâche.
    • Utilisation d'une condition when pour configurer le monitoring uniquement si la variable monitoring.enabled est définie sur true (définie dans vars/app_config.yml grâce à un dictionnaire).