Zfs supervision snapshots
Supervision ZFS et Automatisation des Clichés Sanoid
Gestion du stockage ZFS en environnement de production
Lorsqu'un serveur Linux héberge l'ensemble des données volumineuses et persistantes des conteneurs applicatifs (Nextcloud, bibliothèque photos Immich, téléchargements, bases de données PostgreSQL) sur un pool ZFS (ex: data-1).
ZFS intègre des mécanismes d'intégrité natifs (checksums systématiques, détection du bit rot). En production, trois réglages sont utiles : 1. Régulation de la mémoire vive (ZFS ARC) : ZFS a tendance par défaut à occuper jusqu'à 50% de la RAM disponible pour son cache de lecture, ce qui peut provoquer des évictions (OOM) de conteneurs gourmands en mémoire comme Ollama ou Immich en fonction des config. 2. Politique de clichés instantanés (snapshots) : Permet de restaurer des données en cas de suppression accidentelle ou d'anomalie sans impacter les performances d'écriture (Copy-on-Write). 3. Surveillance automatisée de la santé du pool : Pour être alerté dès le premier secteur défectueux ou la première erreur de checksum.
1. Régulation du Cache Mémoire ZFS ARC (zfs_arc_max)
Sur les machines disposant de moins de 64 Go de RAM, il est recommandé d'appliquer une limitation stricte du cache ARC à 2 Go (2147483648 octets).
Le paramètre est inscrit dans /etc/modprobe.d/zfs.conf et l'initramfs est reconstruit :
# /etc/modprobe.d/zfs.conf
# Limite l'Adaptive Replacement Cache (ARC) à 2 Go pour préserver la RAM des conteneurs
options zfs zfs_arc_max=2147483648
Cette limite garantit que le système d'exploitation et la couche applicative Docker conservent toujours une marge confortable de mémoire vive sans dépendre excessivement du swap.
2. Automatisation des Clichés avec Sanoid
Pour automatiser la création et la purge des instantanés sans recourir à des scripts crontab personnalisés fragiles, nous utilisons l'outil standard Sanoid.
Configuration : /etc/sanoid/sanoid.conf
La politique de production s'applique récursivement sur tout le pool cible (ex: data-1) :
# Configuration Sanoid pour le pool de données
[data-1]
use_template = production
recursive = yes
[template_production]
# Pas de snapshots à la minute
frequently = 0
# Rétention de 8 snapshots horaires (1 cliché par heure pendant 8h)
hourly = 8
# Pas de snapshots quotidiens redondants
daily = 0
# Rétention d'1 snapshot hebdomadaire (pour l'historique moyen terme)
weekly = 1
# Pas de rétention mensuelle ou annuelle sur ce pool
monthly = 0
yearly = 0
# Activation de la création automatique et du nettoyage des clichés expirés
autosnap = yes
autoprune = yes
Ordonnancement Systemd
Sanoid est orchestré par son timer natif /lib/systemd/system/sanoid.timer :
* S'exécute automatiquement toutes les heures.
* Crée le nouvel instantané sous la nomenclature standard data-1@autosnap_AAAA-MM-JJ_HH:MM:SS_hourly.
* Supprime automatiquement les snapshots plus anciens que la fenêtre de rétention définie (autoprune = yes).
3. Contrôle d'Intégrité Automatisé : ansible-script-check_snapshot.sh
Afin de vérifier que le mécanisme de clichés ne s'est pas bloqué ou arrêté silencieusement, un script d'audit dédié valide périodiquement deux critères stricts :
- Le volume minimal de clichés : Au moins 9 snapshots récents doivent être présents (
COUNT >= 9). - L'âge maximal autorisé : Aucun snapshot conservé ne doit être orphelin ou âgé de plus de 7 jours (
MAX_AGE_DAYS=7).
Algorithme de vérification du script :
# 1. Extraction et comptage des clichés autosnap
SNAPSHOTS=$(zfs list -H -t snapshot -o name 2>/dev/null | grep "^${DATASET}@" | grep "autosnap" || true)
COUNT=$(echo "$SNAPSHOTS" | grep -v '^$' | wc -l)
if [ "$COUNT" -lt "$EXPECTED_COUNT" ]; then
echo "✗ ERREUR : Nombre de snapshots insuffisant ($COUNT trouvé(s), $EXPECTED_COUNT attendu(s))."
exit 1
fi
# 2. Analyse de l'horodatage de chaque instantané
NOW=$(date +%s)
MAX_AGE_SECONDS=$(( MAX_AGE_DAYS * 86400 ))
THRESHOLD=$(( NOW - MAX_AGE_SECONDS ))
while read -r snap; do
datetime_str=$(echo "$snap" | sed -E 's/.*autosnap_([0-9]{4}-[0-9]{2}-[0-9]{2})_([0-9]{2}:[0-9]{2}:[0-9]{2}).*/\1 \2/')
snap_epoch=$(date -d "$datetime_str" +%s 2>/dev/null)
if [ "$snap_epoch" -lt "$THRESHOLD" ]; then
echo "✗ ERREUR : Le snapshot '$snap' est trop ancien (> $MAX_AGE_DAYS jours)."
exit 1
fi
done <<< "$SNAPSHOTS"
Si le script détecte un écart de conformité, Ansible remonte immédiatement une erreur critique qui déclenche l'envoi d'un courriel d'alerte par le plugin de notification SMTP (script-notify_failure.py).
4. Surveillance de la Santé du Pool (zpool status)
La surveillance matérielle du stockage peut être exécutée de manière programmée via Ansible ou un script de supervision :
- Exécution de
zpool status data-1. - Vérification que l'état global est strictement
ONLINE. - Recherche d'états critiques de dégradation (
DEGRADED,FAULTED,OFFLINE,UNAVAIL). - Parsing des compteurs d'erreurs d'E/S physiques :
- Erreurs de lecture (
READ) - Erreurs d'écriture (
WRITE) - Erreurs de checksum (
CKSUM) - En cas de compteur supérieur à zéro ou d'anomalie signalée sur un disque, le playbook capture le rapport complet et transmet immédiatement une alerte d'urgence par SMTP à l'administrateur.
5. Commandes Sysadmin pour la Restauration
Grâce aux snapshots ZFS Copy-on-Write, restaurer des données corrompues ou supprimées par erreur est quasi instantané :
# 1. Parcourir les fichiers d'un snapshot sans rien démonter (accès en lecture seule)
ls -la /mnt/data/.zfs/snapshot/autosnap_2026-09-26_14:00:00_hourly/Nextcloud/
# 2. Restaurer un fichier spécifique
cp /mnt/data/.zfs/snapshot/autosnap_2026-09-26_14:00:00_hourly/Nextcloud/document_perdu.pdf /mnt/data/Nextcloud/
# 3. Rollback complet d'un dataset (attention : écrase les modifications postérieures)
zfs rollback data-1@autosnap_2026-09-26_14:00:00_hourly
6. Playbook Ansible Prêt à l'Emploi (playbook-zfs-sanoid.yaml)
Ce playbook autonome installe Sanoid, configure la limitation du cache mémoire ARC, déploie la politique de snapshots horaires/hebdomadaires, active le timer systemd et déploie le script de contrôle d'intégrité :
---
- name: Gestion du Stockage ZFS - Régulation ARC, Sanoid et Contrôle de Santé
hosts: all
become: true
vars:
# Nom du pool ou dataset ZFS à protéger
zfs_dataset_name: "data-1"
# Limitation du cache ARC en octets (ex: 2 Go pour laisser la RAM aux conteneurs)
zfs_arc_limit_bytes: 2147483648
# Politique de rétention des clichés
sanoid_hourly: 8
sanoid_weekly: 1
# Nombre minimal de snapshots attendus pour validation
expected_snapshots_min: 9
# Âge maximum autorisé des clichés en jours
max_snapshot_age_days: 7
tasks:
# -------------------------------------------------------------------------
# 1. VÉRIFICATION DU MODULE ZFS ET RÉGULATION MÉMOIRE ARC
# -------------------------------------------------------------------------
- name: Vérifier la présence de la commande zfs
ansible.builtin.command: which zfs
register: zfs_bin_check
changed_when: false
failed_when: false
- name: Interrompre si ZFS n'est pas installé sur la machine
ansible.builtin.meta: end_host
when: zfs_bin_check.rc != 0
- name: Configurer la limite ZFS ARC dans /etc/modprobe.d/zfs.conf
ansible.builtin.copy:
dest: /etc/modprobe.d/zfs.conf
mode: '0644'
owner: root
group: root
content: |
# Limite maximale de l'Adaptive Replacement Cache (ARC)
options zfs zfs_arc_max={{ zfs_arc_limit_bytes }}
register: zfs_arc_conf
- name: Mettre à jour l'initramfs si la configuration ARC a changé
ansible.builtin.command: update-initramfs -u
when: zfs_arc_conf.changed
# -------------------------------------------------------------------------
# 2. INSTALLATION ET CONFIGURATION DE SANOID
# -------------------------------------------------------------------------
- name: Installer le paquet Sanoid
ansible.builtin.apt:
name: sanoid
state: present
update_cache: true
- name: S'assurer de la présence du dossier /etc/sanoid
ansible.builtin.file:
path: /etc/sanoid
state: directory
mode: '0755'
owner: root
group: root
- name: Déployer la configuration de politique Sanoid (/etc/sanoid/sanoid.conf)
ansible.builtin.copy:
dest: /etc/sanoid/sanoid.conf
mode: '0644'
owner: root
group: root
content: |
# Configuration Sanoid générée par Ansible
[{{ zfs_dataset_name }}]
use_template = production
recursive = yes
[template_production]
frequently = 0
hourly = {{ sanoid_hourly }}
daily = 0
weekly = {{ sanoid_weekly }}
monthly = 0
yearly = 0
autosnap = yes
autoprune = yes
- name: Activer et démarrer le timer systemd Sanoid
ansible.builtin.systemd:
name: sanoid.timer
enabled: true
state: started
# -------------------------------------------------------------------------
# 3. DÉPLOIEMENT DU SCRIPT D'AUDIT ET DE VALIDATION D'INTÉGRITÉ
# -------------------------------------------------------------------------
- name: Déployer le script d'audit des snapshots dans /usr/local/bin
ansible.builtin.copy:
dest: /usr/local/bin/check_zfs_snapshots.sh
mode: '0755'
owner: root
group: root
content: |
#!/bin/bash
# Audit automatisé des snapshots Sanoid
set -euo pipefail
DATASET="${1:-{{ zfs_dataset_name }}}"
EXPECTED_COUNT="${2:-{{ expected_snapshots_min }}}"
MAX_AGE_DAYS="${3:-{{ max_snapshot_age_days }}}"
SNAPSHOTS=$(zfs list -H -t snapshot -o name 2>/dev/null | grep "^${DATASET}@" | grep "autosnap" || true)
COUNT=$(echo "$SNAPSHOTS" | grep -v '^$' | wc -l)
echo "[ZFS Audit] Snapshots trouvés pour $DATASET : $COUNT (Attendu: >= $EXPECTED_COUNT)"
if [ "$COUNT" -lt "$EXPECTED_COUNT" ]; then
echo "[ZFS Audit] Nombre de snapshots insuffisant pour $DATASET ($COUNT < $EXPECTED_COUNT)."
exit 1
fi
NOW=$(date +%s)
MAX_AGE_SECONDS=$(( MAX_AGE_DAYS * 86400 ))
THRESHOLD=$(( NOW - MAX_AGE_SECONDS ))
while read -r snap; do
[ -z "$snap" ] && continue
datetime_str=$(echo "$snap" | sed -E 's/.*autosnap_([0-9]{4}-[0-9]{2}-[0-9]{2})_([0-9]{2}:[0-9]{2}:[0-9]{2}).*/\1 \2/')
snap_epoch=$(date -d "$datetime_str" +%s 2>/dev/null || true)
if [ -n "$snap_epoch" ] && [ "$snap_epoch" -lt "$THRESHOLD" ]; then
echo "[ZFS Audit] Snapshot trop ancien détecté : $snap ($datetime_str)"
exit 1
fi
done <<< "$SNAPSHOTS"
echo "[ZFS Audit] Tous les snapshots de $DATASET sont conformes et récents."
exit 0
- name: Effectuer une vérification immédiate des snapshots
ansible.builtin.command: >
/usr/local/bin/check_zfs_snapshots.sh {{ zfs_dataset_name }} {{ expected_snapshots_min }} {{ max_snapshot_age_days }}
register: audit_snapshots
changed_when: false
failed_when: false
- name: Afficher le résultat de l'audit
ansible.builtin.debug:
msg: "{{ audit_snapshots.stdout_lines }}"
# -------------------------------------------------------------------------
# 4. SURVEILLANCE DE L'ÉTAT DU POOL (ZPOOL STATUS)
# -------------------------------------------------------------------------
- name: Interroger la santé matérielle du pool ZFS
ansible.builtin.command: zpool status {{ zfs_dataset_name }}
register: zpool_status_cmd
changed_when: false
- name: Valider que le statut est ONLINE et exempt d'erreurs
ansible.builtin.assert:
that:
- "'ONLINE' in zpool_status_cmd.stdout"
- "'DEGRADED' not in zpool_status_cmd.stdout"
- "'FAULTED' not in zpool_status_cmd.stdout"
- "'errors: No known data errors' in zpool_status_cmd.stdout"
fail_msg: "Attention : Le pool ZFS {{ zfs_dataset_name }} présente des dégradations ou des erreurs."
success_msg: "Le pool ZFS {{ zfs_dataset_name }} est sain (ONLINE) et sans erreur."