Fiche technique

Mettre à jour un runner CI Mac cloud avec retour arrière rapide

Mettre à jour un runner CI Mac cloud avec retour arrière rapide

Les équipes à distance considèrent souvent les runners CI hébergés sur des Mac cloud comme une infrastructure destinée à rester disponible en permanence. Pourtant, ces runners doivent eux aussi être mis à jour. La méthode la plus risquée consiste à écraser directement le répertoire du programme et à redémarrer alors qu’une tâche est encore en cours : le processus de compilation peut continuer à utiliser d’anciens fichiers tandis que le processus d’orchestration a déjà chargé de nouveaux composants, créant ainsi un état hybride difficile à reproduire. Une approche plus sûre consiste à drainer les tâches, puis à basculer vers un répertoire de version immuable au moyen d’un lien symbolique remplacé atomiquement.

Définir d’abord le périmètre de sécurité de la mise à jour

Une mise à jour maîtrisée doit remplir au moins quatre conditions : ne plus accepter de nouvelles tâches, laisser la tâche en cours se terminer normalement, permettre la coexistence de l’ancienne et de la nouvelle version, et autoriser un rétablissement immédiat en cas d’échec du basculement. Ici, « drainer » ne signifie ni vider l’espace de travail ni interrompre la compilation, mais suspendre le runner entre deux tâches.

Pendant la mise à jour, l’état le plus important n’est pas de savoir si le service est démarré, mais si l’ancienne tâche est terminée et si aucune nouvelle tâche n’a encore commencé.

Avant de commencer, consignez la version actuelle, le PID du processus runner, l’identifiant de la tâche en cours et son répertoire de travail. Si la tâche comprend une compilation Xcode, vérifiez également la présence éventuelle de xcodebuild, de processus de test ou de sous-processus d’exportation d’archive. Ne vous contentez pas de vérifier si le runner principal est inactif : même après son arrêt, un script défectueux peut avoir laissé des sous-processus s’exécuter en arrière-plan.

Il est recommandé de formaliser les critères de mise à jour : durée maximale autorisée pour la tâche en cours, délai avant de passer à une vérification manuelle, et tests de fumée à réussir avant de recommencer à accepter des tâches. Ces règles doivent être définies par l’équipe en fonction de la durée habituelle des projets, plutôt qu’improvisées au moment d’un incident.

Séparer le programme, la configuration et l’espace de travail

Le programme du runner doit être installé dans un répertoire distinct pour chaque version, tandis que la configuration persistante et l’espace de travail de compilation doivent rester en dehors des répertoires versionnés. Voici un exemple d’arborescence simple :

/opt/ci-runner/
├── current -> releases/runner-current
├── previous-target
├── drain
├── active.pid
├── config/
├── work/
└── releases/
    ├── runner-old/
    └── runner-current/

config contient les informations d’enregistrement et les paramètres non publics ; ses autorisations doivent être limitées à l’utilisateur du runner. work contient les répertoires extraits et les fichiers temporaires de compilation. releases ne doit contenir que le programme. Ne copiez pas les jetons dans chaque répertoire de version et ne laissez pas le paquet de mise à jour écraser l’espace de travail.

Avec cette séparation, la mise à niveau ne modifie que la cible de current. L’ancienne version reste intacte et le retour arrière ne dépend ni d’un nouveau téléchargement ni d’une réinstallation. Le nettoyage des anciennes versions doit également attendre la fin de la période d’observation, et au moins la dernière version validée doit être conservée.

Drainer les tâches à leur frontière avec un wrapper

Le modèle le plus simple à contrôler consiste à faire exécuter une seule tâche à la fois au runner, puis à laisser un wrapper décider, après sa sortie, s’il peut en accepter une autre. Les options permettant de quitter après une tâche varient selon les runners. Commencez par encapsuler la commande de démarrage réelle dans $RUNNER_CMD, puis utilisez une boucle commune :

#!/bin/zsh
set -u

root=/opt/ci-runner
lock="$root/wrapper.lock"

mkdir "$lock" 2>/dev/null || exit 1

cleanup() {
  rm -f "$root/active.pid"
  rmdir "$lock" 2>/dev/null
}

trap cleanup EXIT INT TERM

while [[ ! -e "$root/drain" ]]; do
  "$root/current/bin/$RUNNER_CMD" run --once &
  child=$!
  print -r -- "$child" > "$root/active.pid"

  if wait "$child"; then
    status=0
  else
    status=$?
  fi

  rm -f "$root/active.pid"

  if (( status != 0 )); then
    sleep 10
  fi
done

Pour préparer la mise à jour, exécutez touch /opt/ci-runner/drain. Le wrapper laissera le sous-processus en cours se terminer, mais ne lancera pas de nouvelle itération. Lisez ensuite active.pid et utilisez kill -0 PID pour vérifier si le processus existe toujours. L’absence de PID dans le fichier ne garantit pas que la machine soit inactive : contrôlez également les sous-processus de compilation et de test associés à la tâche.

Si le runner ne prend pas en charge un mode de sortie après une seule tâche, utilisez sa fonction native de suspension de la prise en charge des tâches et vérifiez qu’aucune tâche n’est préchargée après la suspension. En cas de doute, simulez d’abord une tâche longue dans un environnement isolé, puis observez le comportement du drainage.

Vérifier le paquet de mise à jour et basculer atomiquement

Décompressez la nouvelle version dans un répertoire temporaire, effectuez les vérifications, puis renommez le répertoire pour le placer dans releases. La somme de contrôle du paquet téléchargé doit provenir d’un registre de publication approuvé par l’équipe ; elle ne doit pas être générée à la volée à côté de la même archive non vérifiée.

root=/opt/ci-runner
release="$root/releases/runner-next"
archive=/tmp/runner-next.tar.gz
expected="$EXPECTED_SHA256"

actual="$(shasum -a 256 "$archive" | awk '{print $1}')"
[[ "$actual" = "$expected" ]] || exit 2

mkdir -p "$release"
tar -xzf "$archive" -C "$release"
"$release/bin/$RUNNER_CMD" --version || exit 3

old="$(readlink "$root/current")"
print -r -- "$old" > "$root/previous-target"

ln -sfn "$release" "$root/current.next"
mv -fh "$root/current.next" "$root/current"

Avant de remplacer le lien symbolique, vérifiez que le programme du nouveau répertoire est exécutable, qu’il correspond à la bonne architecture et qu’il peut lire la configuration externe. Le basculement lui-même doit rester très bref : n’incluez pas la décompression, l’installation de dépendances ou les téléchargements réseau dans la section critique. Si la mise à jour nécessite également une migration de configuration, copiez d’abord celle-ci dans un fichier temporaire, validez son format, puis remplacez-la atomiquement.

Éviter la dérive des autorisations après extraction

Une archive peut contenir des propriétaires ou des autorisations différents. Avant le basculement, vérifiez que les fichiers du programme ne peuvent pas être modifiés par un utilisateur autre que celui du runner. Les fichiers de configuration sensibles doivent généralement avoir les autorisations 600, et le répertoire de configuration 700. Vérifiez également que le répertoire de travail appartient toujours à l’utilisateur d’origine du runner, afin de ne pas découvrir un échec d’écriture uniquement lors de la première exécution de la nouvelle version.

Tests de fumée et retour arrière rapide

Après le basculement, ne supprimez pas encore drain. Lancez manuellement une tâche de test contrôlée et vérifiez au minimum que le runner peut lire la configuration, créer un espace de travail temporaire, appeler les outils de développement, terminer une compilation minimale, enregistrer les artefacts, puis effectuer correctement son nettoyage avant de quitter.

Lors de l’examen des journaux, accordez une attention particulière aux chemins. Le nouveau processus doit démarrer depuis le nouveau répertoire ciblé par current, tandis que les caches, la configuration et l’espace de travail doivent continuer à utiliser leurs chemins fixes. Si les journaux mélangent des chemins de l’ancienne et de la nouvelle version, cela signifie qu’un ancien processus est toujours actif ; il ne faut alors pas recommencer à accepter des tâches.

En cas d’échec, lisez l’ancienne cible du lien dans previous-target, revenez-y au moyen du même mécanisme de lien temporaire, puis relancez la tâche minimale. N’essayez pas de corriger et de retester directement dans le répertoire de la nouvelle version, car cela compromettrait le principe d’une publication immuable. Après avoir confirmé le rétablissement de l’ancienne version, supprimez la version défaillante et recréez le paquet de publication.

Une fois la validation terminée avec succès, exécutez rm -f /opt/ci-runner/drain, puis laissez le gestionnaire de processus redémarrer le wrapper. Observez enfin au moins une tâche réelle, depuis sa prise en charge jusqu’à son état de sortie, en passant par la compilation et l’archivage des artefacts. Consignez dans le journal de mise à jour le numéro de version, le résultat du basculement et la cible de retour arrière. La mise à jour n’est terminée qu’à ce stade, et non dès que le runner apparaît en ligne.

Questions fréquentes

Pourquoi éviter de mettre à jour un runner CI dans son répertoire actuel ?

Une tâche active pourrait lire un mélange de fichiers anciens et nouveaux, tandis que la version fonctionnelle ne serait plus disponible pour un retour immédiat. Des répertoires séparés évitent ce risque.

Le drainage impose-t-il d’arrêter la compilation en cours ?

Non. Il bloque seulement la prise de la tâche suivante et laisse le processus enfant actif se terminer normalement. Les diagnostics doivent être conservés avant tout arrêt forcé.

Quels contrôles effectuer après la mise à jour ?

Vérifiez la version, l’enregistrement, l’écriture dans l’espace de travail, une compilation minimale, la collecte d’un artefact et les droits des fichiers sensibles.

MacVPSGo Mac dans le cloud

Besoin d’une machine de build Apple Silicon dédiée ?

Louez un nœud physique dédié à la journée, à la semaine, au mois ou au trimestre pour vos builds Xcode, votre CI iOS, votre développement à distance et vos tâches d’automatisation.

Choisir une configuration et commander