Lorsqu’un même Mac dans le cloud exécute plusieurs tâches de compilation à la suite, les problèmes les plus difficiles à détecter ne sont souvent pas les échecs de compilation, mais le code source, les certificats temporaires, les données de test ou les caches laissés par une tâche et lus par la suivante. Exécuter simplement rm -rf après chaque tâche n’est pas assez fiable : des processus peuvent encore utiliser certains fichiers, les répertoires cachés sont faciles à oublier et un arrêt anormal peut empêcher l’étape de nettoyage de s’exécuter. Une frontière plus explicite consiste à monter une image clairsemée chiffrée distincte pour chaque tâche, puis à confiner l’extraction du code, la compilation et les fichiers temporaires dans ce système de fichiers.
Pourquoi isoler les tâches avec des images clairsemées ?
Une image clairsemée peut disposer d’une capacité logique importante tout en n’occupant sur le disque hôte que l’espace réellement écrit. Elle reste un fichier ordinaire, ce qui permet de la localiser, d’en mesurer la taille et de la supprimer facilement à partir de l’identifiant de la tâche. Une fois montée, elle se comporte comme un volume APFS indépendant ; les scripts de compilation existants n’ont donc généralement besoin que d’un changement de répertoire de travail.
Le chiffrement protège les données lorsque l’image n’est pas montée, tandis que le point de montage dédié établit une frontière de chemin entre les tâches. Aucun des deux ne remplace l’isolation des comptes d’exécution ni le principe du moindre privilège, mais cette approche est plus facile à auditer qu’un répertoire de travail partagé et conservé durablement.
Ne considérez pas qu’une image chiffrée est inaccessible pendant l’exécution d’une tâche. Une fois l’image montée, les processus disposant des autorisations nécessaires peuvent toujours en lire le contenu. Le compte du runner, la journalisation et le mode d’injection des secrets doivent donc être sécurisés dans le cadre du même dispositif.
| Approche | Résidus après un arrêt anormal | Frontière entre les tâches | Cas d’usage |
|---|---|---|---|
| Suppression d’un répertoire partagé après usage | Les fichiers encore utilisés et les répertoires cachés sont faciles à oublier | Dépend de la fiabilité du script | Tâches courtes sans données sensibles |
| Un répertoire ordinaire par tâche | Le répertoire reste sur le volume hôte | Chemins séparés, mais système de fichiers commun | Compilations simultanées à faible risque |
| Image clairsemée chiffrée | Les montages résiduels et les fichiers image peuvent être identifiés | Système de fichiers indépendant | CI nécessitant une frontière de nettoyage explicite |
Créer un espace de travail APFS nommé d’après la tâche
L’identifiant de la tâche doit d’abord être limité à un jeu de caractères autorisés afin d’empêcher l’insertion de barres obliques, d’espaces ou de substitutions de commande dans le chemin. Le répertoire des images doit se trouver dans un emplacement accessible en lecture et en écriture uniquement par le compte du runner, et chaque tâche doit disposer de son propre point de montage.
set -euo pipefail
set +x
umask 077
JOB_KEY="$(printf '%s' "${CI_JOB_ID:?}" | tr -cd 'A-Za-z0-9._-')"
IMAGE_ROOT="$HOME/ci-images"
IMAGE_PATH="$IMAGE_ROOT/$JOB_KEY.sparsebundle"
MOUNT_PATH="/Volumes/ci-$JOB_KEY"
mkdir -p "$IMAGE_ROOT" "$MOUNT_PATH"
chmod 700 "$IMAGE_ROOT"
printf '%s' "${CI_VOLUME_PASSWORD:?}" |
hdiutil create \
-size 80g \
-type SPARSEBUNDLE \
-fs APFS \
-volname "ci-$JOB_KEY" \
-encryption AES-256 \
-stdinpass \
"$IMAGE_PATH"
80g est une limite logique et ne signifie pas que 80 Go sont immédiatement occupés. Cette limite doit couvrir le code source, les dépendances, les données dérivées et le pic de volume des archives, tout en prévoyant une marge pour les journaux d’échec. N’inscrivez jamais le mot de passe dans les arguments de commande, les noms de fichiers ou les journaux de compilation. Désactivez d’abord l’affichage des commandes, puis transmettez le secret sur l’entrée standard depuis une variable protégée de la CI.
Vérifier le montage immédiatement
La création de l’image ne garantit pas qu’elle soit montée au bon emplacement. Le script doit vérifier que le chemin cible correspond bien à un volume monté et confirmer le type de système de fichiers.
printf '%s' "$CI_VOLUME_PASSWORD" |
hdiutil attach \
-stdinpass \
-nobrowse \
-mountpoint "$MOUNT_PATH" \
"$IMAGE_PATH"
mount | grep -F "on $MOUNT_PATH "
diskutil info "$MOUNT_PATH" | grep -E 'File System Personality|Volume Name'
mkdir -p "$MOUNT_PATH/src" "$MOUNT_PATH/output" "$MOUNT_PATH/tmp"
Dirigez ensuite vers ce volume le répertoire d’extraction du code source, les sorties de compilation et le répertoire temporaire propre à la tâche. Les caches partagés en lecture seule des gestionnaires de paquets peuvent rester en dehors du volume. En revanche, tout cache susceptible d’être modifié par une tâche doit être copié dans le volume afin d’éviter toute contamination due aux écritures simultanées.
Intégrer le démontage au cycle de vie plutôt qu’à la fin du script
Le nettoyage ne peut pas se limiter à la dernière ligne du script, car une erreur de compilation, un dépassement de délai ou un signal d’arrêt peut interrompre la tâche prématurément. Utilisez un gestionnaire de sortie pour exécuter systématiquement la synchronisation, la recherche des fichiers utilisés et le démontage, tout en conservant suffisamment d’informations de diagnostic en cas d’échec.
cleanup_workspace() {
set +e
sync
if mount | grep -Fq "on $MOUNT_PATH "; then
lsof +D "$MOUNT_PATH" > "$IMAGE_ROOT/$JOB_KEY.lsof.txt" 2>/dev/null
hdiutil detach "$MOUNT_PATH"
fi
rmdir "$MOUNT_PATH" 2>/dev/null
}
trap cleanup_workspace EXIT INT TERM
export TMPDIR="$MOUNT_PATH/tmp"
cd "$MOUNT_PATH/src"
lsof +D peut être lent sur un répertoire volumineux. Vous pouvez commencer par vérifier les processus connus de compilation, de test et de création de paquets, puis lancer une analyse complète uniquement si le démontage échoue. N’utilisez pas d’emblée le démontage forcé : il pourrait masquer des processus encore en train d’écrire et laisser des fichiers incomplets.
Gérer la simultanéité, la capacité et les résidus d’échec
Lorsqu’une même tâche est relancée, son ancienne image peut encore exister. La méthode sûre ne consiste pas à l’écraser immédiatement, mais à vérifier d’abord si elle est montée. Si c’est le cas, bloquez la nouvelle tâche et consignez le conflit. Sinon, archivez ou supprimez l’image conformément à la politique de conservation. L’identifiant de la tâche doit également inclure le numéro de l’exécution en cours afin que deux exécutions ne pointent jamais vers la même image.
Mettre en place trois contrôles
Premièrement, consultez hdiutil info avant le démarrage de la tâche pour vérifier qu’aucun volume du même nom n’existe. Deuxièmement, surveillez pendant la compilation l’espace disponible sur le volume hôte et sur le volume monté. Une image clairsemée s’agrandit au fil des écritures : la présence d’espace libre dans le volume logique ne garantit donc pas qu’il en reste sur l’hôte. Troisièmement, analysez le répertoire des images à la fin de la tâche et ne conservez que les échantillons d’échec explicitement marqués à des fins de diagnostic.
Les commandes suivantes permettent de distinguer ces deux niveaux de capacité :
df -h "$MOUNT_PATH"
df -h "$IMAGE_ROOT"
du -sh "$IMAGE_PATH"
hdiutil info
Si une catégorie de tâches atteint régulièrement la limite, commencez par séparer les fichiers intermédiaires qui n’ont pas besoin d’être conservés durablement, puis ajustez la capacité logique. Augmenter aveuglément la taille de l’image ne fait que repousser le moment où le disque hôte sera saturé.
Périmètre de sécurité et liste de contrôle avant déploiement
Le secret ne doit être transmis au processus que pendant les phases de création et de montage, puis être immédiatement retiré des variables exportées dans le shell courant. Les scripts de compilation ne doivent pas afficher les variables d’environnement ni copier le mot de passe dans le volume. Conservez des autorisations de 600 ou plus strictes pour le fichier image, et de 700 pour le répertoire racine des images.
Avant la mise en production, vérifiez chaque point :
- Le runner utilise un compte non administrateur dédié ;
- L’identifiant de la tâche est filtré à l’aide d’une liste blanche afin d’empêcher toute traversée de chemin ;
- La création, le montage, la compilation et le démontage peuvent échouer indépendamment tout en renvoyant un état explicite ;
EXIT,INTetTERMdéclenchent tous la même fonction de nettoyage ;- En cas d’échec du démontage, les processus utilisant le volume sont consignés avant toute action forcée ;
- La capacité du volume APFS, du fichier d’image clairsemée et du volume hôte est surveillée simultanément ;
- Une durée de conservation est définie pour les échantillons d’échec, après quoi l’image entière est supprimée ;
- Les journaux ne contiennent aucun mot de passe, aucune clé privée, aucun identifiant complet ni aucun extrait sensible du code source.
Commencez par un petit projet dépourvu de données sensibles et testez quatre scénarios : exécution normale, échec de compilation, arrêt manuel et disque proche de sa capacité maximale. Ce dispositif d’isolation ne devient réellement exploitable que si, dans les quatre cas, il est possible de retrouver l’image, d’expliquer son état et de récupérer les ressources associées.
Questions fréquentes
Une image chiffrée remplace-t-elle l’isolation des permissions système ?
Non. Elle protège surtout les données du répertoire de travail au repos. Il faut aussi un compte d’exécution dédié, des permissions minimales, une injection contrôlée du secret et un démontage systématique.
Comment traiter une image encore montée après l’échec d’une tâche ?
Identifiez les processus qui utilisent le point de montage avec lsof, arrêtez uniquement ceux de la tâche, exécutez sync, puis tentez un démontage normal avant toute option forcée.
Choisissez un serveur physique dédié pour le développement, les builds et le bureau à distance
Comparez trois configurations Apple Silicon et choisissez le nœud, la durée de location et les options de stockage lors de la commande.