LinuxBashDébutantAdministration systèmeHealth checks

Administration Linux et automatisation Bash

Jerry RajaonarivoMis à jour le 30 sept. 20268 min de lecture

Ceci couvre les habitudes défensives en bash, set -euo pipefail, la rotation des logs, les health checks, et l'interception d'erreurs, qui empêchent un script ou une tâche planifiée sans supervision d'échouer silencieusement.

set -euo pipefail, et pourquoi ce n'est pas optionnel

Chaque script Bash que j'écris commence par cette ligne, juste après le shebang :

bash
#!/usr/bin/env bash
set -euo pipefail

Chaque option ferme une vraie faille :

  • -e arrête le script dès qu'une commande échoue. Sans cette option, un cd /app/releases/current qui échoue laisse quand même la ligne suivante s'exécuter dans le mauvais répertoire.
  • -u traite une variable non définie comme une erreur. Cela détecte la faute de frappe classique ($RELEASE_DIR mal orthographiée en $RELEASE_DR) avant qu'elle ne se transforme silencieusement en chaîne vide et ne change rm -rf "$RELEASE_DR/old" en rm -rf /old.
  • pipefail fait échouer un pipeline dès qu'une de ses commandes échoue, au lieu de ne regarder que le code de sortie de la dernière. Sans cette option, cat fichier_absent.txt | sort > trie.txt rapporte un succès. cat échoue, sort trie une entrée vide et renvoie 0, et le script continue avec un fichier vide.

set -e ne vous protège pas de toutes les erreurs. Cette option ne se déclenche pas dans une condition if, dans une chaîne &&/||, ni dans une fonction appelée depuis un de ces contextes. Testez vos véritables chemins d'échec.

Dès qu'un script utilise un trap ERR (on y revient à la fin), je passe à set -Eeuo pipefail. Le -E supplémentaire (errtrace) transmet le trap aux fonctions, aux substitutions de commande et aux sous-shells. Sans lui, un échec dans une fonction arrête bien le script, mais votre trap ne s'exécute jamais.

Un script de rotation de logs qui n'emporte pas le disque avec lui

L'autre incident que je croise souvent, c'est une application (ou un script) qui écrit dans un fichier plat sans jamais s'arrêter, jusqu'au jour où /var/log, ou pire /, atteint 100 % et fait tomber la machine. logrotate gère ça pour la plupart des services système, mais beaucoup de logs applicatifs, de sorties de cron et de logs de jobs maison n'y sont jamais raccordés. Pour les applications qui écrivent un nouveau fichier de log par jour ou par exécution, j'installe ce script en attendant mieux :

bash
#!/usr/bin/env bash
set -euo pipefail
 
LOG_DIR="/var/log/myapp"
RETENTION_DAYS=14
 
if [[ ! -d "$LOG_DIR" ]]; then
  echo "ERREUR : le répertoire de logs $LOG_DIR n'existe pas" >&2
  exit 1
fi
 
# Compresse les logs non modifiés depuis 24 heures (pas encore compressés)
find "$LOG_DIR" -maxdepth 1 -type f -name "*.log" -mmin +1440 -exec gzip {} \;
 
# Supprime les logs compressés au-delà de la rétention (-mtime +14 = 15 jours ou plus)
find "$LOG_DIR" -maxdepth 1 -type f -name "*.log.gz" -mtime "+${RETENTION_DAYS}" -print -delete
 
echo "Nettoyage des logs terminé : $(date -Iseconds)"

Attention aux unités de temps de find. -mtime +1 ne veut pas dire « plus d'un jour ». find arrondit l'âge au nombre de jours entiers inférieur, donc +1 ne correspond qu'aux fichiers vieux d'au moins 48 heures. Pour 24 heures, utilisez -mmin +1440.

Ce script ne couvre pas le cas du début, celui d'un unique fichier de log que l'application garde ouvert et alimente sans fin. Sa date de modification est toujours récente, donc find -mtime ne le trouve jamais. Pour ce fichier-là, ajoutez une règle logrotate basée sur la taille avec copytruncate. Elle copie le fichier puis le tronque sur place, et l'application continue d'écrire dans le même descripteur de fichier :

bash
# /etc/logrotate.d/myapp
/var/log/myapp/app.log {
    size 100M
    rotate 7
    compress
    missingok
    notifempty
    copytruncate
}

logrotate ne tourne normalement qu'une fois par jour, donc une règle size n'est vérifiée qu'une fois par jour. Si le fichier peut grossir plus vite, lancez logrotate /etc/logrotate.d/myapp depuis un timer plus fréquent. copytruncate peut perdre les quelques lignes écrites entre la copie et la troncature. C'est un compromis acceptable pour une application à laquelle on ne peut pas demander de rouvrir son log.

Mettez toujours vos variables entre guillemets : "$LOG_DIR", jamais $LOG_DIR seul. Une variable non protégée qui contient un espace, ou qui s'étend en chaîne vide, se fait découper en mots par Bash, et c'est exactement ainsi qu'une combinaison find/rm construite sur un chemin vide ou erroné finit par supprimer des choses qu'on ne voulait pas supprimer. Combinez cette discipline avec -maxdepth 1 et une variable de chemin explicite plutôt que quoi que ce soit dérivé d'une entrée utilisateur.

Un script de health-check que vous pouvez réellement planifier

Pour tout ce qui touche au client final, je veux quelque chose qui tourne sur un planning et qui interroge le vrai endpoint, plutôt que de se contenter d'un systemctl status :

bash
#!/usr/bin/env bash
set -euo pipefail
 
URL="https://api.example.com/healthz"
TIMEOUT=5
EXPECTED_CODE=200
 
status_code=$(curl --silent --output /dev/null --write-out "%{http_code}" \
  --max-time "$TIMEOUT" "$URL" || true)
 
if [[ "$status_code" -ne "$EXPECTED_CODE" ]]; then
  echo "ÉCHEC DU HEALTH CHECK : $URL a renvoyé $status_code" >&2
  exit 1
fi
 
echo "OK : $URL a renvoyé $status_code"
exit 0

Le || true empêche set -e de tuer le script quand curl lui-même échoue (erreur DNS, timeout, connexion refusée). Dans ce cas, curl affiche déjà 000 comme code de statut. N'ajoutez donc pas de valeur de repli, sinon vous obtiendrez 000000.

Ce qui compte ici, c'est le code de sortie : c'est lui qui permet à cron, à systemd, ou à un agent de supervision (Nagios, le blackbox exporter de Prometheus) de décider s'il faut alerter quelqu'un.

Cron ou timers systemd

Pour des plannings simples sur une seule machine, cron reste tout à fait valable :

bash
*/5 * * * * /usr/local/bin/healthcheck.sh >> /var/log/healthcheck.log 2>&1

Je vois encore souvent cette ligne partir sans le >> ... 2>&1, ce qui fait disparaître toute sortie, y compris le message d'erreur dont vous auriez besoin.

Mais dès qu'un job a besoin d'un ordre de dépendances, de tentatives de reprise ou de limites de ressources, je passe à un timer systemd. Pareil si vous voulez simplement retrouver ses logs dans journalctl plutôt que dans un fichier plat que vous devrez faire tourner vous-même :

bash
# /etc/systemd/system/healthcheck.service
[Unit]
Description=Health check de l'API
 
[Service]
Type=oneshot
ExecStart=/usr/local/bin/healthcheck.sh
bash
# /etc/systemd/system/healthcheck.timer
[Unit]
Description=Exécute healthcheck.service toutes les 5 minutes
 
[Timer]
OnCalendar=*:0/5
Persistent=true
 
[Install]
WantedBy=timers.target

OnCalendar=*:0/5 se déclenche toutes les cinq minutes selon l'horloge (:00, :05, :10...). Avec Persistent=true, si la machine était éteinte au moment prévu, systemd lance l'exécution manquée juste après le démarrage. Persistent= ne fonctionne qu'avec les timers OnCalendar= et il est ignoré sans avertissement sur les timers monotones comme OnUnitActiveSec=. Vous pouvez vérifier l'expression avec systemd-analyze calendar '*:0/5'.

Activez-le avec systemctl enable --now healthcheck.timer. À partir de là, journalctl -u healthcheck.service vous donne chaque exécution, sa sortie et son code de retour, et il survit à un redémarrage sans entrée cron @reboot isolée.

Journalisation et interception d'erreurs pour que les échecs ne restent pas silencieux

Cette dernière habitude, c'est ce qui empêche un échec de rester silencieux pendant des semaines : rendre l'échec bruyant, volontairement.

bash
#!/usr/bin/env bash
set -Eeuo pipefail
 
LOG_FILE="/var/log/myapp/etl.log"
 
log() {
  echo "$(date -Iseconds) $*" | tee -a "$LOG_FILE"
}
 
on_error() {
  local exit_code=$?
  local line_no=$1
  log "ERREUR : le script a échoué à la ligne $line_no avec le code de sortie $exit_code"
  # envoyez une alerte Slack/PagerDuty ici, plutôt que d'espérer que quelqu'un lise le log
  exit "$exit_code"
}
 
trap 'on_error $LINENO' ERR
 
log "Démarrage du run ETL"
# ... le vrai travail se passe ici ...
log "Run ETL terminé avec succès"

trap ... ERR se déclenche dès qu'une commande échoue (avec -e actif), et $LINENO vous indique où. Le -E de set -Eeuo pipefail le fait aussi se déclencher pour les échecs à l'intérieur des fonctions. Avec un simple set -euo pipefail, une commande qui échoue dans une fonction arrête le script sans appeler on_error, et on retombe sur l'échec silencieux que cette section cherche à éviter. Avec le trap, vous ne découvrez plus des semaines plus tard que le job s'est arrêté quelque part. Vous recevez une alerte au moment de l'échec, qui indique qu'il s'est arrêté à la ligne 42 à 3h14.

Ce sont six habitudes, appliquées à chaque fois. La différence, c'est d'apprendre une panne par une alerte de supervision quelques minutes après qu'elle survient, ou par quelqu'un d'autre plusieurs semaines plus tard.

ALLER PLUS LOIN

Envie de faire tourner ça en production ?

Ce tutoriel couvre les concepts et l'architecture. Si vous voulez l'implémenter dans votre propre infrastructure, ou monter en compétence pour posséder ce sujet durablement, je propose du mentorat individuel construit autour de votre environnement réel, pas une formation générique.

Ce tutoriel

  • Concepts clés et architecture principale
  • Extraits de code illustratifs
  • Le raisonnement derrière chaque décision

Mentorat individuel

  • Des sessions de travail sur votre propre environnement
  • Des réponses directes aux cas particuliers que vous rencontrez
  • Un retour sur votre implémentation réelle
  • Un accompagnement continu pendant que vous la construisez
Me contacterRéponse sous 1 jour ouvré.

Tutoriels similaires