Aller au contenu
CI/CD & Automatisation medium

Maintenance des runners self-hosted

17 min de lecture

Les runners self-hosted nécessitent une maintenance régulière pour rester performants et sécurisés. Ce guide couvre les opérations essentielles : mises à jour, monitoring, nettoyage et scaling.

  • Maintenir à jour le runner, l'OS et les outils
  • Superviser les runners avec des health checks et Prometheus
  • Nettoyer Docker, le répertoire de travail et les caches locaux
  • Faire évoluer le parc, manuellement ou par auto-scaling
  • Assurer la haute disponibilité pour éviter les points de défaillance

Trois éléments demandent une attention différente : le runner (géré par GitHub), l'OS et les outils de build (à votre charge).

Le runner se met à jour automatiquement quand GitHub déploie une nouvelle version. Lors d'un job, si une mise à jour est disponible, elle s'applique avant l'exécution.

Vérifier la version :

Fenêtre de terminal
./run.sh --version
# ou
cat /opt/actions-runner/.runner | jq '.agentVersion'

Le système de l'hôte, lui, n'est mis à jour par personne : c'est votre travail. Le risque n'est pas la mise à jour elle-même mais le redémarrage qu'elle peut exiger au milieu d'un job. unattended-upgrades applique les correctifs de sécurité automatiquement ; dpkg-reconfigure ouvre un dialogue interactif où vous choisissez si les mises à jour s'installent seules.

Fenêtre de terminal
# Ubuntu/Debian
sudo apt update && sudo apt upgrade -y
# Avec redémarrage planifié
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

Un runner hébergé par GitHub repart d'une image reconstruite régulièrement. Un runner self-hosted, lui, garde les versions installées le jour de sa mise en service : l'écart avec ce que supposent vos workflows se creuse en silence. Node.js, Docker et kubectl sont les trois outils qui décrochent le plus vite.

Fenêtre de terminal
# Node.js via nvm
nvm install --lts
nvm alias default lts/*
# Docker
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io
# kubectl : télécharger le binaire ET sa somme de contrôle officielle
KUBECTL_VERSION="$(curl -L -s https://dl.k8s.io/release/stable.txt)"
curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl.sha256"
# Refuser l'installation si l'empreinte ne correspond pas
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check
sudo install kubectl /usr/local/bin/

La vérification sha256sum --check affiche kubectl: OK quand l'empreinte correspond. Si elle affiche FAILED, le binaire téléchargé n'est pas celui publié par le projet : ne l'installez pas.

Un runner qui tombe sans alerte bloque toute la CI silencieusement. Le monitoring rend visibles l'état de la machine et la disponibilité du runner.

Ces commandes donnent l'état de la machine à l'instant présent. La dernière est la plus révélatrice : Runner.Listener est le processus qui maintient la connexion avec GitHub et attend les jobs. S'il n'apparaît pas, le runner est affiché hors ligne dans l'interface, même si la machine répond parfaitement au reste.

Fenêtre de terminal
# CPU, mémoire, disque
top -bn1 | head -20
free -h
df -h
# Processus du runner
ps aux | grep Runner.Listener

Ce script contrôle les trois causes de panne les plus fréquentes : le processus arrêté, le disque plein et la connexion à l'API GitHub coupée. Ses codes de sortie suivent la convention des sondes de supervision (0 pour sain, 1 pour avertissement, 2 pour critique), ce qui permet de le brancher tel quel sur la plupart des superviseurs.

healthcheck.sh
#!/bin/bash
RUNNER_DIR="/opt/actions-runner"
# Vérifier que le runner tourne
if ! pgrep -f "Runner.Listener" > /dev/null; then
echo "CRITICAL: Runner not running"
exit 2
fi
# Vérifier l'espace disque
DISK_USAGE=$(df -h / | awk 'NR==2 {print $5}' | sed 's/%//')
if [ "$DISK_USAGE" -gt 90 ]; then
echo "WARNING: Disk usage at ${DISK_USAGE}%"
exit 1
fi
# Vérifier la connexion à GitHub
if ! curl -s --connect-timeout 5 https://api.github.com > /dev/null; then
echo "CRITICAL: Cannot reach GitHub API"
exit 2
fi
echo "OK: Runner healthy"
exit 0

Le health check répond par oui ou non ; Prometheus conserve l'historique et permet de voir arriver une saturation avant qu'elle ne bloque un job. node_exporter est l'agent qui expose les métriques système de la machine sur le port 9100, que Prometheus vient ensuite collecter. Lancé en conteneur, il ne voit par défaut que le conteneur : les répertoires /proc et /sys de l'hôte sont donc montés en lecture seule, et les options --path.procfs et --path.sysfs lui indiquent où les lire.

docker-compose.yml
services:
node-exporter:
image: prom/node-exporter:v1.12.1@sha256:1b4e4438faca4dd7e001dd445d161a4a2091b0fededa84093b3a8dfeae1f1be0
ports:
- "9100:9100"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
command:
- "--path.procfs=/host/proc"
- "--path.sysfs=/host/sys"

Métriques utiles à surveiller :

  • node_cpu_seconds_total : utilisation CPU
  • node_memory_MemAvailable_bytes : mémoire disponible
  • node_filesystem_avail_bytes : espace disque
  • Métriques custom sur les jobs exécutés

Deux alertes couvrent l'essentiel : le runner injoignable et le disque proche de la saturation. Le for: 5m évite de réveiller quelqu'un pour un simple redémarrage du service, et le seuil 5e9 correspond à 5 Go d'espace libre restant, soit environ un gros build d'avance.

# alertmanager rules
groups:
- name: runner-alerts
rules:
- alert: RunnerDown
expr: up{job="runner"} == 0
for: 5m
labels:
severity: critical
annotations:
summary: "Runner {{ $labels.instance }} is down"
- alert: RunnerDiskFull
expr: node_filesystem_avail_bytes{mountpoint="/"} < 5e9
for: 10m
labels:
severity: warning
annotations:
summary: "Runner disk space low"

Sur un runner persistant, builds et images s'accumulent jusqu'à saturer le disque. Un nettoyage planifié maintient la machine saine.

Chaque prune vise une ressource différente et aucun n'est réversible. Le filtre until=24h épargne les images créées dans les dernières 24 heures, donc le cache de build de la journée. La dernière ligne est volontairement laissée en commentaire : docker system prune -af --volumes supprime en plus tous les volumes inutilisés, y compris les volumes nommés, et donc les données des services de test qui tournent sur la machine.

docker-cleanup.sh
#!/bin/bash
# Images non utilisées
docker image prune -af --filter "until=24h"
# Containers arrêtés
docker container prune -f
# Volumes orphelins
docker volume prune -f
# Networks inutilisés
docker network prune -f
# Tout d'un coup (agressif)
# docker system prune -af --volumes

Planifier avec cron :

/etc/cron.d/docker-cleanup
0 3 * * * root /opt/scripts/docker-cleanup.sh >> /var/log/docker-cleanup.log 2>&1

Le répertoire _work contient un sous-répertoire par dépôt cloné et n'est jamais purgé par le runner. _diag accumule les journaux d'exécution, un fichier par job. Programmez ce nettoyage à une heure creuse : supprimer _work pendant qu'un job tourne fait échouer ce job.

cleanup-workdir.sh
#!/bin/bash
RUNNER_DIR="/opt/actions-runner"
WORK_DIR="$RUNNER_DIR/_work"
# Supprimer les répertoires de travail vieux de plus de 7 jours
find "$WORK_DIR" -type d -mtime +7 -exec rm -rf {} \;
# Supprimer les logs anciens
find "$RUNNER_DIR/_diag" -name "*.log" -mtime +30 -delete

Attention à ne pas confondre deux choses. Le cache géré par l'action actions/cache est stocké chez GitHub, pas sur votre machine, et se purge depuis l'interface du dépôt. Les répertoires ci-dessous sont les caches locaux des gestionnaires de paquets npm, pip et Maven : les effacer ne casse rien, le prochain build les reconstruit au prix d'un téléchargement complet.

Fenêtre de terminal
# Le cache actions/cache est géré par GitHub
# Mais le cache local (npm, pip, etc.) peut s'accumuler
rm -rf ~/.npm/_cacache
rm -rf ~/.cache/pip
rm -rf ~/.m2/repository

Quand la file d'attente s'allonge, il faut ajouter des runners, à la main pour un petit parc, automatiquement dès que le volume grandit.

Un runner supplémentaire se configure exactement comme le premier. Deux détails comptent : le token d'enregistrement récupéré dans les paramètres du dépôt ou de l'organisation expire au bout d'une heure, et les labels décident seuls des jobs que la machine acceptera.

Fenêtre de terminal
# Sur une nouvelle machine
./config.sh --url https://github.com/ORG/REPO \
--token TOKEN \
--labels linux,x64,docker \
--name runner-$(hostname)
./run.sh

Actions Runner Controller (ARC) est un opérateur Kubernetes qui crée et détruit des pods runners à la demande. Le HorizontalRunnerAutoscaler réagit aux événements workflowJob envoyés par GitHub : un job entre en file, un runner apparaît. Le scaleDownDelaySecondsAfterScaleOut impose 300 secondes avant toute réduction, ce qui évite de détruire une machine qui vient d'être créée. L'exemple s'appuie sur l'API actions.summerwind.dev, celle du mode historique d'ARC.

horizontal-runner-autoscaler.yaml
apiVersion: actions.summerwind.dev/v1alpha1
kind: HorizontalRunnerAutoscaler
metadata:
name: runner-autoscaler
spec:
scaleTargetRef:
name: runner-deployment
minReplicas: 2
maxReplicas: 20
scaleUpTriggers:
- githubEvent:
workflowJob: {}
duration: "30m"
scaleDownDelaySecondsAfterScaleOut: 300

Sans Kubernetes, le parc devient un groupe d'autoscaling classique. Tout repose sur le launch_template : c'est lui qui doit contenir l'enregistrement du runner au démarrage, sinon l'instance créée ne s'annoncera jamais à GitHub et restera facturée pour rien. Le cooldown de 300 secondes empêche la politique de rajouter deux instances coup sur coup avant d'avoir vu l'effet de la première.

terraform/autoscaling.tf
resource "aws_autoscaling_group" "runners" {
name = "github-runners"
desired_capacity = 2
min_size = 1
max_size = 10
vpc_zone_identifier = var.subnet_ids
launch_template {
id = aws_launch_template.runner.id
version = "$Latest"
}
tag {
key = "Name"
value = "github-runner"
propagate_at_launch = true
}
}
resource "aws_autoscaling_policy" "scale_up" {
name = "scale-up"
scaling_adjustment = 2
adjustment_type = "ChangeInCapacity"
cooldown = 300
autoscaling_group_name = aws_autoscaling_group.runners.name
}

Un runner unique est un point de défaillance : s'il tombe, la CI s'arrête. Plusieurs réflexes éliminent ce risque.

GitHub attribue un job au premier runner libre dont les labels couvrent ceux demandés. En désignant un jeu de labels plutôt qu'une machine précise, vous laissez le service arbitrer : la perte d'un runner ralentit la file, elle ne la bloque plus.

jobs:
build:
runs-on: [self-hosted, linux]
# GitHub choisira un runner disponible parmi ceux qui matchent

Des labels de région permettent de rapprocher un job des ressources qu'il manipule : registre d'images, cluster cible ou base de données. Gardez en tête qu'un label est une simple chaîne déclarée à l'enregistrement du runner : GitHub ne vérifie pas qu'elle corresponde à une zone réelle.

jobs:
build-eu:
runs-on: [self-hosted, linux, eu-west-1]
build-us:
runs-on: [self-hosted, linux, us-east-1]

runs-on accepte une expression, ce qui autorise une bascule sans toucher au fichier de workflow. Il suffit de basculer la variable USE_SELF_HOSTED dans les paramètres du dépôt : le temps d'une maintenance du parc, les jobs repartent sur les runners hébergés par GitHub, facturés à la minute.

jobs:
build:
# Essayer self-hosted d'abord, fallback sur GitHub-hosted
runs-on: ${{ vars.USE_SELF_HOSTED == 'true' && 'self-hosted' || 'ubuntu-24.04' }}

Pour ne rien oublier, voici les opérations à mener, réparties selon leur fréquence, du quotidien automatisé à la revue trimestrielle.

Aucune de ces trois tâches ne doit dépendre d'une présence humaine : elles tournent en cron ou en timer systemd, sinon elles sautent le jour où vous êtes en congés.

  • Health checks toutes les 5 minutes
  • Nettoyage Docker la nuit
  • Rotation des logs

Une revue de dix minutes suffit à repérer une dérive avant qu'elle ne devienne une panne : un disque qui se remplit régulièrement ou une alerte qui se répète méritent une correction, pas un acquittement.

  • Vérifier les mises à jour de l'OS
  • Vérifier l'espace disque
  • Revoir les alertes de la semaine

Le rendez-vous mensuel traite ce qui bouge lentement : versions d'outils, droits d'accès et durée des builds. Un temps de build qui double sur trois mois passe inaperçu au quotidien mais saute aux yeux dans une comparaison mensuelle.

  • Mises à jour des outils (Node, Python, etc.)
  • Audit des accès aux runners
  • Revue des performances (temps de build)
  • Vérifier la rotation des tokens

À ce rythme, il ne s'agit plus de corriger mais de décider : l'architecture actuelle tient-elle encore la charge, et le coût du parc reste-t-il justifié face aux runners hébergés ? C'est aussi le moment de vérifier qu'une remise en service complète fonctionne réellement.

  • Mise à jour majeure de l'OS si nécessaire
  • Revue de l'architecture des runners
  • Test de disaster recovery
  • Optimisation des coûts

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  • Le runner se met à jour seul ; à votre charge : l'OS et les outils utilisés par vos workflows.
  • Un health check régulier (processus, disque, connexion GitHub) détecte les pannes avant qu'elles ne bloquent la CI.
  • Le nettoyage Docker et du _work est indispensable : sans lui, le disque sature en quelques jours.
  • L'auto-scaling (ARC ou cloud) ajuste le parc à la charge, pas de runners inactifs facturés pour rien.
  • Plusieurs runners par label évitent le point de défaillance unique ; un fallback GitHub-hosted sécurise les pics.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn