---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# Ansible — Gestion de configuration

```{code-cell} python
:tags: [hide-input]

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.patches as patches
import numpy as np
import pandas as pd
import seaborn as sns
import re
from collections import defaultdict

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
```

## Pourquoi la gestion de configuration

### Dérive de configuration

Lorsqu'une infrastructure grandit, les serveurs administrés manuellement divergent inévitablement.
Un administrateur applique un correctif sur trois machines mais en oublie une quatrième. Un
collègue modifie un paramètre noyau en urgence sans le documenter. Six mois plus tard,
personne ne sait pourquoi le serveur de production se comporte différemment des serveurs de
staging.

Ce phénomène s'appelle la **dérive de configuration** (*configuration drift*). Il se manifeste par :

- des différences subtiles de versions entre environnements,
- des fichiers de configuration locaux non tracés,
- des cron jobs ajoutés à la main et jamais supprimés,
- des paquets installés manuellement qui n'apparaissent dans aucun registre.

### Snowflake servers

Un serveur devenu unique par accumulation de modifications manuelles est appelé un **snowflake
server** (serveur flocon de neige). Chaque flocon est irremplaçable, fragile, et opaque. Personne
n'ose le toucher. S'il tombe, la restauration prend des jours.

L'antidote est le **phoenix server** : une machine que l'on peut détruire et reconstruire en
quelques minutes à partir de la définition en code.

### Infrastructure as Code

L'**Infrastructure as Code (IaC)** consiste à décrire l'état désiré de l'infrastructure dans des
fichiers versionnable et rejouables. Les avantages sont nombreux :

- **Reproductibilité** : le même code produit le même résultat sur n'importe quelle machine cible.
- **Traçabilité** : chaque changement est un commit git avec auteur et message.
- **Idempotence** : rejouer le code n'a pas d'effet de bord si l'état est déjà atteint.
- **Revue de code** : les changements d'infrastructure passent par une pull request.

Ansible est l'un des outils IaC les plus populaires. Il se distingue par sa simplicité : pas
d'agent à installer, pas de serveur maître complexe, un inventaire de fichiers texte et des
playbooks YAML lisibles.

## Architecture Ansible

### Control node et managed nodes

L'architecture Ansible repose sur deux rôles :

- **Control node** : la machine depuis laquelle Ansible est exécuté. Elle porte le code, les
  inventaires, les playbooks et les rôles. Ansible doit y être installé (Python ≥ 3.8).
- **Managed nodes** : les machines cibles. Elles n'ont besoin que d'un accès SSH et d'un
  interpréteur Python (généralement préinstallé).

### Agentless et SSH

La particularité d'Ansible est d'être **agentless** : aucun démon ne tourne sur les managed
nodes. Ansible se connecte en SSH, copie un module Python dans un répertoire temporaire,
l'exécute, récupère le résultat JSON et supprime le fichier temporaire. Cette simplicité facilite
l'adoption et élimine une surface d'attaque.

```{admonition} Prérequis SSH
:class: note
Les managed nodes doivent être accessibles par SSH sans mot de passe interactif (clé publique
déposée) ou via un bastion. L'utilisateur SSH doit disposer des droits `sudo` si les tâches
nécessitent une élévation de privilèges (`become: true`).
```

### Inventaire statique et dynamique

L'**inventaire** liste les hôtes gérés. En mode statique, c'est un fichier INI ou YAML. En mode
**dynamique**, Ansible interroge une API (AWS EC2, GCP, Azure, vSphere, Netbox…) pour
construire l'inventaire à la volée.

## Inventaire

### Format INI

```ini
# inventaire/hosts.ini

[webservers]
web01.example.com
web02.example.com ansible_user=deploy

[dbservers]
db01.example.com ansible_port=2222
db02.example.com

[prod:children]
webservers
dbservers

[prod:vars]
ansible_user=ansible
ansible_become=true
```

### Format YAML

```yaml
# inventaire/hosts.yml
all:
  children:
    webservers:
      hosts:
        web01.example.com:
        web02.example.com:
          ansible_user: deploy
    dbservers:
      hosts:
        db01.example.com:
          ansible_port: 2222
        db02.example.com:
      vars:
        ansible_user: ansible
        ansible_become: true
```

### Variables de groupe et d'hôte

Les variables peuvent être définies dans des répertoires dédiés :

```
inventaire/
├── hosts.yml
├── group_vars/
│   ├── all.yml          # variables pour tous les hôtes
│   ├── webservers.yml   # variables pour le groupe webservers
│   └── dbservers.yml
└── host_vars/
    ├── web01.example.com.yml
    └── db01.example.com.yml
```

### Visualiser l'inventaire

```bash
ansible-inventory -i inventaire/hosts.yml --graph
```

Sortie :
```
@all:
  |--@prod:
  |  |--@webservers:
  |  |  |--web01.example.com
  |  |  |--web02.example.com
  |  |--@dbservers:
  |  |  |--db01.example.com
  |  |  |--db02.example.com
  |--@ungrouped:
```

```{code-cell} python
:tags: [hide-input]

# Visualisation d'un inventaire Ansible INI simulé → graphe de topologie

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, ax = plt.subplots(figsize=(10, 6))
ax.set_xlim(0, 10)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Topologie d'inventaire Ansible", fontsize=14, fontweight="bold", pad=15)

# Définition des noeuds
noeuds = {
    "all":         (5.0, 7.2),
    "prod":        (2.5, 5.8),
    "staging":     (7.5, 5.8),
    "webservers":  (1.2, 4.2),
    "dbservers":   (3.8, 4.2),
    "web01":       (0.4, 2.5),
    "web02":       (1.8, 2.5),
    "db01":        (3.2, 2.5),
    "db02":        (4.5, 2.5),
    "stg-web":     (6.8, 4.2),
    "stg-web01":   (6.4, 2.5),
    "stg-web02":   (7.8, 2.5),
}

couleurs = {
    "all": "#4C72B0",
    "prod": "#55A868", "staging": "#C44E52",
    "webservers": "#8172B2", "dbservers": "#8172B2", "stg-web": "#CCB974",
    "web01": "#64B5CD", "web02": "#64B5CD",
    "db01": "#64B5CD", "db02": "#64B5CD",
    "stg-web01": "#64B5CD", "stg-web02": "#64B5CD",
}

aretes = [
    ("all", "prod"), ("all", "staging"),
    ("prod", "webservers"), ("prod", "dbservers"),
    ("staging", "stg-web"),
    ("webservers", "web01"), ("webservers", "web02"),
    ("dbservers", "db01"), ("dbservers", "db02"),
    ("stg-web", "stg-web01"), ("stg-web", "stg-web02"),
]

for src, dst in aretes:
    x1, y1 = noeuds[src]
    x2, y2 = noeuds[dst]
    ax.plot([x1, x2], [y1, y2], color="#AAAAAA", linewidth=1.5, zorder=1)

for nom, (x, y) in noeuds.items():
    c = couleurs[nom]
    r = 0.35 if nom in ("all", "prod", "staging") else 0.28
    cercle = plt.Circle((x, y), r, color=c, zorder=2, ec="white", linewidth=1.5)
    ax.add_patch(cercle)
    ax.text(x, y, nom, ha="center", va="center", fontsize=7.5,
            color="white", fontweight="bold", zorder=3)

legende = [
    mpatches.Patch(color="#4C72B0", label="Groupe racine"),
    mpatches.Patch(color="#55A868", label="Groupe prod"),
    mpatches.Patch(color="#C44E52", label="Groupe staging"),
    mpatches.Patch(color="#8172B2", label="Sous-groupe"),
    mpatches.Patch(color="#64B5CD", label="Hôte"),
]
ax.legend(handles=legende, loc="lower right", fontsize=8)
plt.savefig("19_inventaire_topologie.png", dpi=100, bbox_inches="tight")
plt.show()
```

## Modules essentiels

Les modules sont les unités d'action d'Ansible. Chaque tâche appelle un module avec des
paramètres. Ansible en propose plus de 3 000 ; voici les incontournables.

### command et shell

```yaml
- name: Vérifier l'uptime
  ansible.builtin.command: uptime

- name: Lister les processus avec pipeline
  ansible.builtin.shell: ps aux | grep nginx | wc -l
  register: nb_nginx
```

`command` n'interprète pas le shell (pas de `|`, `>`, `&&`). `shell` exécute via `/bin/sh` et
supporte les redirections. Préférer `command` quand possible pour la sécurité.

### copy et template

```yaml
- name: Copier un fichier statique
  ansible.builtin.copy:
    src: files/nginx.conf
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: "0644"

- name: Déployer un template Jinja2
  ansible.builtin.template:
    src: templates/vhost.conf.j2
    dest: /etc/nginx/sites-available/{{ site_name }}.conf
    mode: "0644"
  notify: Recharger nginx
```

### file

```yaml
- name: Créer un répertoire
  ansible.builtin.file:
    path: /var/www/{{ site_name }}
    state: directory
    owner: www-data
    group: www-data
    mode: "0755"

- name: Supprimer un fichier
  ansible.builtin.file:
    path: /tmp/ancien_fichier
    state: absent
```

### user

```yaml
- name: Créer un utilisateur applicatif
  ansible.builtin.user:
    name: appuser
    shell: /bin/bash
    groups: sudo
    append: true
    create_home: true
```

### service et package

```yaml
- name: Installer nginx
  ansible.builtin.package:
    name: nginx
    state: present

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

`package` est un module générique qui délègue à `apt`, `yum`, `dnf` selon la distribution
détectée. On peut aussi appeler `ansible.builtin.apt` directement pour bénéficier de paramètres
spécifiques comme `update_cache: true`.

```{admonition} État vs action
:class: tip
Les modules Ansible expriment un **état désiré**, pas une action. `state: started` signifie
"assure-toi que le service est démarré", pas "démarre-le". Si le service tourne déjà, Ansible ne
fait rien et rapporte `ok` au lieu de `changed`.
```

## Playbooks

### Structure YAML

```yaml
---
- name: Configurer les serveurs web
  hosts: webservers
  become: true
  vars:
    site_name: monapp
    http_port: 80

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

  tasks:
    - name: Installer nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true

    - name: Déployer la configuration
      ansible.builtin.template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/{{ site_name }}.conf
      notify: Recharger nginx

    - name: Activer le site
      ansible.builtin.file:
        src: /etc/nginx/sites-available/{{ site_name }}.conf
        dest: /etc/nginx/sites-enabled/{{ site_name }}.conf
        state: link
      notify: Recharger nginx
```

### Conditions et boucles

```yaml
- name: Installer des paquets supplémentaires (Debian uniquement)
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop:
    - vim
    - htop
    - curl
  when: ansible_os_family == "Debian"

- name: Créer plusieurs utilisateurs
  ansible.builtin.user:
    name: "{{ item.name }}"
    groups: "{{ item.groups }}"
  loop:
    - { name: alice, groups: sudo }
    - { name: bob,   groups: dev  }
```

### register et debug

```yaml
- name: Lire la version de Python
  ansible.builtin.command: python3 --version
  register: py_version

- name: Afficher la version
  ansible.builtin.debug:
    msg: "Python détecté : {{ py_version.stdout }}"
```

### Exécuter un playbook

```bash
ansible-playbook -i inventaire/hosts.yml site.yml
ansible-playbook -i inventaire/hosts.yml site.yml --limit webservers
ansible-playbook -i inventaire/hosts.yml site.yml --tags configuration
```

```{code-cell} python
:tags: [hide-input]

# Heatmap d'un run Ansible simulé : changed/ok/failed par hôte

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

hotes = ["web01", "web02", "web03", "db01", "db02", "cache01"]
etats = ["ok", "changed", "unreachable", "failed", "skipped"]

np.random.seed(42)
data = np.array([
    [12, 4, 0, 0, 2],
    [12, 3, 0, 0, 2],
    [12, 5, 0, 0, 2],
    [8,  2, 0, 0, 1],
    [8,  2, 0, 1, 1],
    [5,  0, 1, 0, 3],
])

df = pd.DataFrame(data, index=hotes, columns=etats)

cmap = sns.diverging_palette(10, 130, s=75, l=50, n=9, as_cmap=True)

fig, ax = plt.subplots(figsize=(9, 5))
sns.heatmap(
    df, annot=True, fmt="d", cmap="YlOrRd_r",
    linewidths=0.5, linecolor="white",
    cbar_kws={"label": "Nombre de tâches"},
    ax=ax
)
ax.set_title("Résultat d'un run Ansible par hôte", fontsize=13, fontweight="bold", pad=12)
ax.set_xlabel("État des tâches", fontsize=11)
ax.set_ylabel("Hôte", fontsize=11)
ax.tick_params(axis="x", rotation=0)
ax.tick_params(axis="y", rotation=0)
plt.savefig("19_run_heatmap.png", dpi=100, bbox_inches="tight")
plt.show()
```

## Variables et templates Jinja2

### Hiérarchie de priorité des variables

Ansible applique une hiérarchie stricte de priorité (du plus faible au plus fort) :
`defaults` → `group_vars/all` → `group_vars/<groupe>` → `host_vars/<hôte>` → `vars` dans le
play → `set_fact` → options `-e` en ligne de commande.

### group_vars et host_vars

```yaml
# group_vars/webservers.yml
nginx_worker_processes: 4
nginx_keepalive_timeout: 65
site_root: /var/www/html

# host_vars/web01.example.com.yml
nginx_worker_processes: 8   # override pour cette machine plus puissante
```

### Templates Jinja2

Les templates `.j2` utilisent la syntaxe Jinja2 : `{{ variable }}` pour l'interpolation,
`{% if %}` / `{% for %}` pour la logique.

```jinja2
# templates/nginx.conf.j2
worker_processes {{ nginx_worker_processes }};

events {
    worker_connections 1024;
}

http {
    keepalive_timeout {{ nginx_keepalive_timeout }};

    {% for vhost in virtual_hosts %}
    server {
        listen 80;
        server_name {{ vhost.name }};
        root {{ vhost.root | default(site_root) }};
    }
    {% endfor %}
}
```

### Filtres Jinja2 courants

```yaml
- name: Mettre en majuscule
  debug:
    msg: "{{ 'hello' | upper }}"          # HELLO

- name: Valeur par défaut
  debug:
    msg: "{{ ma_var | default('none') }}"

- name: Joindre une liste
  debug:
    msg: "{{ ['a','b','c'] | join(',') }}" # a,b,c
```

## Rôles

### Structure d'un rôle

Un rôle est une unité de réutilisation qui regroupe tâches, handlers, templates, fichiers et
variables dans une structure conventionnelle.

```
roles/nginx/
├── defaults/
│   └── main.yml        # variables par défaut (priorité minimale)
├── handlers/
│   └── main.yml        # handlers déclenchés par notify
├── tasks/
│   └── main.yml        # tâches principales
├── templates/
│   └── nginx.conf.j2   # templates Jinja2
├── files/
│   └── index.html      # fichiers statiques
├── vars/
│   └── main.yml        # variables internes (priorité haute)
└── meta/
    └── main.yml        # dépendances, metadata galaxy
```

### Utiliser un rôle dans un playbook

```yaml
---
- name: Configurer les serveurs web
  hosts: webservers
  become: true
  roles:
    - nginx
    - { role: certbot, site_name: monapp.fr }
```

### ansible-galaxy

```bash
# Installer un rôle depuis Ansible Galaxy
ansible-galaxy install geerlingguy.nginx

# Initialiser la structure d'un nouveau rôle
ansible-galaxy role init mon_role

# Installer depuis un fichier requirements.yml
ansible-galaxy install -r requirements.yml
```

```yaml
# requirements.yml
roles:
  - name: geerlingguy.nginx
    version: "3.2.0"
  - name: geerlingguy.postgresql
collections:
  - name: community.postgresql
    version: ">=2.0.0"
```

## Idempotence

### Principe

L'idempotence est la propriété fondamentale d'un playbook Ansible : **rejouer le playbook
plusieurs fois produit exactement le même résultat** que de l'exécuter une seule fois. Si l'état
désiré est déjà atteint, Ansible ne modifie rien et rapporte `ok`.

Cette propriété est garantie par les modules bien écrits. Le module `apt` vérifie si le paquet
est installé avant de l'installer. Le module `file` vérifie les permissions avant de les modifier.

```{admonition} Idempotence et commandes brutes
:class: warning
Les modules `command` et `shell` ne sont **pas idempotents** par nature. Il faut les combiner
avec `creates:` ou `when:` pour éviter les exécutions répétées.
```

```yaml
# Non idempotent — crée le répertoire même s'il existe
- ansible.builtin.command: mkdir /opt/app

# Idempotent — crée uniquement si absent
- ansible.builtin.command:
    cmd: mkdir /opt/app
    creates: /opt/app
```

### Mode --check et --diff

```bash
# Simulation sans modification (dry-run)
ansible-playbook site.yml --check

# Afficher les différences de fichiers modifiés
ansible-playbook site.yml --check --diff
```

La sortie `--diff` montre les deltas de fichiers comme un `diff -u` :

```diff
--- before: /etc/nginx/nginx.conf
+++ after: /etc/nginx/nginx.conf
@@ -1,3 +1,3 @@
 worker_processes 4;
-keepalive_timeout 65;
+keepalive_timeout 75;
```

## Vault

### Chiffrement de secrets

Ansible Vault permet de chiffrer des fichiers de variables ou des chaînes individuelles
contenant des mots de passe, clés API, certificats, etc.

```bash
# Chiffrer un fichier entier
ansible-vault encrypt group_vars/all/secrets.yml

# Déchiffrer pour édition
ansible-vault edit group_vars/all/secrets.yml

# Chiffrer une valeur inline
ansible-vault encrypt_string 'MonMotDePasse!' --name 'db_password'
```

La commande `encrypt_string` produit un bloc YAML utilisable directement :

```yaml
db_password: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  62613833623333366565393431356166303366636662313564373630613531
  ...
```

### Utilisation dans les playbooks

```bash
# Fournir le mot de passe vault interactivement
ansible-playbook site.yml --ask-vault-pass

# Fournir via un fichier (pour CI/CD)
ansible-playbook site.yml --vault-password-file ~/.vault_pass

# Fichier .vault_pass sécurisé
chmod 600 ~/.vault_pass
echo 'MotDePasseVault' > ~/.vault_pass
```

```{admonition} Vault en production
:class: important
Ne jamais stocker `~/.vault_pass` dans git. Dans un pipeline CI/CD, injecter le mot de passe
vault comme variable secrète (GitLab CI, GitHub Actions Secrets, Jenkins credentials). Ansible
Vault n'est pas un gestionnaire de secrets complet ; pour des besoins avancés, intégrer
HashiCorp Vault via le plugin `community.hashi_vault`.
```

```{code-cell} python
:tags: [hide-input]

# Diagramme de flux d'un playbook Ansible (flowchart matplotlib)

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, ax = plt.subplots(figsize=(8, 11))
ax.set_xlim(0, 8)
ax.set_ylim(0, 12)
ax.axis("off")
ax.set_title("Flux d'exécution d'un playbook Ansible", fontsize=13,
             fontweight="bold", pad=12)

def boite(ax, x, y, w, h, texte, couleur="#4C72B0", fontsize=9):
    rect = patches.FancyBboxPatch(
        (x - w/2, y - h/2), w, h,
        boxstyle="round,pad=0.08", linewidth=1.2,
        edgecolor="#333333", facecolor=couleur
    )
    ax.add_patch(rect)
    ax.text(x, y, texte, ha="center", va="center", fontsize=fontsize,
            color="white", fontweight="bold", wrap=True,
            multialignment="center")

def losange(ax, x, y, w, h, texte, couleur="#C44E52"):
    dx, dy = w/2, h/2
    poly = plt.Polygon(
        [(x, y+dy), (x+dx, y), (x, y-dy), (x-dx, y)],
        closed=True, facecolor=couleur, edgecolor="#333333", linewidth=1.2
    )
    ax.add_patch(poly)
    ax.text(x, y, texte, ha="center", va="center", fontsize=8.5,
            color="white", fontweight="bold", multialignment="center")

def fleche(ax, x1, y1, x2, y2):
    ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="->", color="#555555", lw=1.5))

# Noeuds
boite(ax, 4, 11.2, 3.2, 0.7, "ansible-playbook site.yml", couleur="#2d6a9f")
fleche(ax, 4, 10.85, 4, 10.35)
boite(ax, 4, 10.0, 3.2, 0.6, "Lecture de l'inventaire", couleur="#4C72B0")
fleche(ax, 4, 9.7, 4, 9.2)
boite(ax, 4, 8.9, 3.2, 0.6, "Collecte des facts (setup)", couleur="#4C72B0")
fleche(ax, 4, 8.6, 4, 8.1)
losange(ax, 4, 7.7, 3.2, 0.7, "when: condition ?")
# Branche non
ax.annotate("", xy=(6.2, 7.7), xytext=(5.6, 7.7),
            arrowprops=dict(arrowstyle="->", color="#555555", lw=1.5))
ax.text(5.9, 7.85, "Non", fontsize=8, color="#C44E52")
boite(ax, 6.9, 7.7, 1.4, 0.55, "Skipped", couleur="#8d8d8d")
# Branche oui
fleche(ax, 4, 7.35, 4, 6.85)
ax.text(4.12, 7.1, "Oui", fontsize=8, color="#55A868")
boite(ax, 4, 6.5, 3.2, 0.6, "Exécuter le module", couleur="#55A868")
fleche(ax, 4, 6.2, 4, 5.65)
losange(ax, 4, 5.25, 3.0, 0.7, "Changement\neffectué ?")
# Branche changed
ax.annotate("", xy=(6.2, 5.25), xytext=(5.5, 5.25),
            arrowprops=dict(arrowstyle="->", color="#555555", lw=1.5))
ax.text(5.75, 5.42, "Oui", fontsize=8, color="#C44E52")
boite(ax, 6.9, 5.25, 1.4, 0.55, "notify\nhandler", couleur="#C44E52", fontsize=8)
# Branche ok
fleche(ax, 4, 4.9, 4, 4.35)
ax.text(4.12, 4.6, "Non", fontsize=8, color="#55A868")
losange(ax, 4, 3.95, 3.0, 0.7, "Tâches\nsuivantes ?")
fleche(ax, 4, 3.6, 4, 3.05)
ax.text(4.12, 3.3, "Non", fontsize=8)
boite(ax, 4, 2.7, 3.2, 0.6, "Exécuter les handlers notifiés", couleur="#8172B2")
fleche(ax, 4, 2.4, 4, 1.85)
boite(ax, 4, 1.5, 3.2, 0.65, "Résumé du run\n(ok / changed / failed)", couleur="#2d6a9f", fontsize=8.5)

# Boucle "tâches suivantes"
ax.annotate("", xy=(1.8, 6.5), xytext=(2.5, 3.95),
            arrowprops=dict(arrowstyle="->", color="#aaaaaa", lw=1.2,
                            connectionstyle="arc3,rad=-0.4"))
ax.text(1.1, 5.1, "Tâche\nsuivante", fontsize=7.5, color="#888888")

plt.savefig("19_playbook_flowchart.png", dpi=100, bbox_inches="tight")
plt.show()
```

```{code-cell} python
:tags: [hide-input]

# Comparaison Ansible vs Chef vs Puppet vs Salt

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

criteres = ["Courbe\nd'apprentissage", "Agentless", "Langage\nconfig",
            "Performance\n(nb hosts)", "Idempotence\nnative", "Communauté"]

# Scores de 1 à 5
donnees = {
    "Ansible": [5, 5, 4, 3, 4, 5],
    "Chef":    [2, 1, 3, 4, 4, 4],
    "Puppet":  [2, 1, 4, 4, 5, 4],
    "Salt":    [3, 3, 3, 5, 4, 3],
}

x = np.arange(len(criteres))
width = 0.2
fig, ax = plt.subplots(figsize=(11, 5))

palette = sns.color_palette("muted", 4)
for i, (outil, scores) in enumerate(donnees.items()):
    ax.bar(x + i * width, scores, width, label=outil, color=palette[i],
           edgecolor="white", linewidth=0.8)

ax.set_xticks(x + width * 1.5)
ax.set_xticklabels(criteres, fontsize=9.5)
ax.set_yticks(range(1, 6))
ax.set_yticklabels(["1\n(faible)", "2", "3", "4", "5\n(élevé)"], fontsize=9)
ax.set_ylabel("Score (1 = faible, 5 = élevé)", fontsize=10)
ax.set_title("Comparaison des outils de gestion de configuration", fontsize=13,
             fontweight="bold", pad=12)
ax.legend(fontsize=10)
ax.set_ylim(0, 6)

plt.savefig("19_comparaison_outils.png", dpi=100, bbox_inches="tight")
plt.show()
```

## Résumé

Ansible rend l'administration de parcs hétérogènes reproductible et auditable. Les concepts
clés à retenir :

| Concept | Rôle |
|---|---|
| **Inventaire** | Liste structurée des hôtes et de leurs variables |
| **Module** | Unité d'action idempotente (apt, copy, template…) |
| **Playbook** | Orchestration de tâches sur un ou plusieurs groupes |
| **Rôle** | Unité de réutilisation packagée |
| **Handler** | Action déclenchée une seule fois sur notification |
| **Vault** | Chiffrement symétrique des secrets |
| **--check** | Simulation sans effet de bord (dry-run) |

La philosophie d'Ansible peut se résumer ainsi : décrire l'**état désiré** du système, pas la
séquence d'opérations pour y parvenir. Cette distinction entre approche déclarative et
impérative est fondamentale en IaC.

```{admonition} Prochaine étape
:class: tip
Le chapitre suivant aborde la virtualisation et les conteneurs : KVM/libvirt pour les VMs
complètes, LXC/LXD pour les conteneurs système, et la base technologique (namespaces, cgroups)
sur laquelle repose Docker.
```
