Aller au contenu
Runners auto-hébergés et exploitation

Runners auto-hébergés et exploitation

À la fin, vous saurez

  • Décider s'il faut des runners auto-hébergés, en connaissant leurs coûts et leurs risques
  • Expliquer l'enregistrement d'un runner et préférer les runners éphémères
  • Configurer Actions Runner Controller pour des runners éphémères sur Kubernetes
  • Diagnostiquer un run avec les journaux de débogage et les outils de la ligne de commande
  • Mesurer les durées et les coûts, et tenir le calendrier de maintenance de la plateforme

Prérequis

Testé avec actions/runner 2.337.0 gh 2.45.0 gha-runner-scale-set 0.15.0 helm 3.16.3 kubectl 1.36.0 , vérifié le 1 octobre 2026

Pourquoi

Les runners hébergés par GitHub couvrent la grande majorité des besoins, et ce cours s'en est contenté jusqu'ici. Il y a pourtant des situations où ils ne conviennent pas :

  • L'accès à un réseau privé. Un job qui doit joindre une base PostgreSQL privée pour une migration, un registre interne, ou l'API d'un cluster Kapsule restreinte à un réseau privé, ne peut pas le faire depuis une VM hébergée sur Internet, sauf à ouvrir des accès entrants qu'on préfère éviter.
  • Le matériel. Des processeurs graphiques, beaucoup de mémoire, un système particulier.
  • Le volume. À plusieurs dizaines de milliers de minutes par mois sur des dépôts privés, des machines que l'on possède déjà peuvent coûter moins cher.
  • La localisation des données. Certains clients exigent que leur code source et leurs artefacts ne soient traités que sur une infrastructure qu'ils connaissent, en France ou en Europe ; les runners hébergés ne laissent pas choisir la région.
  • La durée. Un job hébergé ne dépasse pas 6 heures ; un job auto-hébergé, 5 jours.

Faire tourner ses propres runners, c'est reprendre une partie du travail que GitHub faisait : fournir une machine propre à chaque job, la mettre à jour, l'isoler. Mal fait, c'est aussi la façon la plus sûre de transformer une demande de fusion en accès à son réseau interne. La première moitié de cette leçon montre comment le faire correctement ; la seconde traite de l'exploitation de GitHub Actions au quotidien, avec ou sans runners propres.

Les concepts

L'agent

Le runner auto-hébergé est le même programme que celui des machines de GitHub, actions/runner, publié sous forme d'archive pour Linux, Windows et macOS, en x64 et Arm64. Il ouvre une connexion sortante en HTTPS vers GitHub et attend qu'on lui attribue un job : aucune connexion entrante n'est nécessaire, ce qui permet de le placer dans un réseau privé avec seulement un accès sortant.

Un runner se rattache à un dépôt, à une organisation ou à une entreprise, avec un jeton d'enregistrement valable une heure, et porte des étiquettes (self-hosted, Linux, X64 par défaut, plus les vôtres). Un job y est envoyé quand toutes les étiquettes de son runs-on correspondent. Au niveau d'une organisation, les runners sont regroupés en groupes, qui limitent les dépôts autorisés à les utiliser ; sur le plan gratuit, seul le groupe par défaut existe, et tous les dépôts de l'organisation peuvent utiliser tous ses runners. Les groupes demandent le plan Team.

Persistant ou éphémère

Un runner persistant prend un job, puis le suivant, sur la même machine. Tout ce qu'un job laisse (fichiers dans l'espace de travail ou ailleurs, processus en arrière-plan, images Docker, configuration modifiée) est là pour le job suivant, qui peut appartenir à un autre dépôt. C'est un risque de sécurité et une source de résultats non reproductibles.

Un runner éphémère (--ephemeral) ne prend qu'un job, puis se désenregistre. Associé à une machine ou un conteneur détruit ensuite, il redonne la garantie des runners hébergés : un environnement neuf par job. GitHub recommande de ne construire de mise à l'échelle automatique qu'avec des runners éphémères. Variante plus récente, les runners juste-à-temps (JIT) reçoivent par l'API une configuration à usage unique, sans étape d'enregistrement.

Actions Runner Controller

Sur Kubernetes, l'outil officiel est Actions Runner Controller (ARC). Il se compose d'un contrôleur, installé une fois par cluster, et de scale sets, un par ensemble de runners. Pour chaque scale set, un écouteur interroge GitHub sur le nombre de jobs en attente qui le visent, et le contrôleur crée autant de pods de runner éphémères, chacun configuré juste-à-temps, détruit à la fin de son job. Avec minRunners: 0, il n'y a aucun pod quand il n'y a rien à faire, et l'autoscaler du cluster peut libérer les nœuds.

Version, mises à jour et coût

Un runner auto-hébergé se met à jour seul quand une nouvelle version sort. Si on désactive ce mécanisme (--disableupdate, courant avec des images de conteneur figées), il faut mettre à jour soi-même dans les 30 jours suivant chaque nouvelle version, faute de quoi GitHub cesse de lui envoyer des jobs. De plus, GitHub impose une version minimale, 2.329.0, appliquée sur github.com depuis le 29 septembre 2026.

L'usage des runners auto-hébergés est gratuit. GitHub avait annoncé en décembre 2025 des frais de plateforme de 0,002 dollar par minute à partir de mars 2026, puis les a reportés quelques jours plus tard pour revoir son approche, selon ses propres termes, sans nouvelle date au 1er octobre 2026. Le vrai coût est celui des machines, et du temps passé à les exploiter.

En pratique

Télécharger et vérifier l'agent

L'archive de l'agent est publiée avec son empreinte SHA-256 dans les notes de version. On la vérifie avant tout usage, comme n'importe quel binaire qui exécutera du code avec des accès privilégiés :

$ V=2.337.0
$ curl -sSLO "https://github.com/actions/runner/releases/download/v$V/actions-runner-linux-x64-$V.tar.gz"
$ ATTENDU=$(gh release view v$V -R actions/runner --json body --jq .body \
    | grep -A1 -i "actions-runner-linux-x64-$V.tar.gz" | grep -oE '[0-9a-f]{64}' | head -1)
$ echo "$ATTENDU"
70920811a4f8ad4328818682bca5c6469c1c942fab52448868071d0063816613
$ echo "$ATTENDU  actions-runner-linux-x64-$V.tar.gz" | sha256sum -c
actions-runner-linux-x64-2.337.0.tar.gz: OK
$ tar xzf actions-runner-linux-x64-$V.tar.gz && ls
actions-runner-linux-x64-2.337.0.tar.gz  bin  config.sh  env.sh  externals  run-helper.cmd.template  run-helper.sh.template  run.sh  safe_sleep.sh
$ ls bin | grep -E '^Runner\.(Listener|Worker|PluginHost)$'
Runner.Listener
Runner.PluginHost
Runner.Worker
$ ls externals
node20  node20_alpine  node24  node24_alpine
$ ./externals/node24/bin/node --version
v24.19.0

On retrouve ce que les leçons précédentes ont décrit : les deux processus Runner.Listener et Runner.Worker (leçon 1), et le Node.js 24 embarqué qui exécute les actions JavaScript (leçon 8). L'archive contient encore des répertoires node20, bien que GitHub n'exécute plus aucune action sous Node 20 depuis le 23 septembre 2026. L'archive pèse 226 Mo.

Enregistrer un runner

Les options d'enregistrement, affichées par l'agent lui-même :

$ ./config.sh --help
...
Config Options:
 --unattended           Disable interactive prompts for missing arguments. Defaults will be used for missing options
 --url string           Repository to add the runner to. Required if unattended
 --token string         Registration token. Required if unattended
 --name string          Name of the runner to configure (default <nom de la machine>)
 --runnergroup string   Name of the runner group to add this runner to (defaults to the default runner group)
 --labels string        Custom labels that will be added to the runner. This option is mandatory if --no-default-labels is used.
 --no-default-labels    Disables adding the default labels: 'self-hosted,Linux,X64'
 --local                Removes the runner config files from your local machine. Used as an option to the remove command
 --work string          Relative runner work directory (default _work)
 --replace              Replace any existing runner with the same name (default false)
 --pat                  GitHub personal access token with repo scope. Used for checking network connectivity when executing `./run.sh --check`
 --disableupdate        Disable self-hosted runner automatic update to the latest released version`
 --ephemeral            Configure the runner to only take one job and then let the service un-configure the runner after the job finishes (default false)

Un runner éphémère pour l'organisation s'enregistre ainsi (commandes non exécutées dans ce cours, qui n'enregistre aucun runner) :

$ JETON=$(gh api -X POST orgs/lyneko-team/actions/runners/registration-token --jq .token)
$ ./config.sh --unattended --url https://github.com/lyneko-team --token "$JETON" \
    --name runner-ci-01 --labels kapsule,fr-par --ephemeral
$ ./run.sh

Le jeton d'enregistrement donne le droit de rattacher une machine à l'organisation : quiconque l'obtient peut y inscrire sa machine et recevoir des jobs, avec leurs secrets. C'est exactement ce que faisait le ver Shai-Hulud 2.0 en novembre 2025 : il enregistrait les machines infectées comme runners auto-hébergés, sous le nom SHA1HULUD. La liste des runners de l'organisation (gh api orgs/lyneko-team/actions/runners) mérite une surveillance.

Des runners éphémères sur Kapsule avec ARC

ARC s'installe en deux charts Helm, publiés comme artefacts OCI sur GHCR : le contrôleur (gha-runner-scale-set-controller, dans un espace de noms arc-systems), puis un scale set par usage (gha-runner-scale-set). Les valeurs du scale set pour un pool de nœuds Kapsule dédié aux runners :

# Runners éphémères pour l'organisation, sur un pool de nœuds Kapsule dédié.
githubConfigUrl: https://github.com/lyneko-team
githubConfigSecret: arc-github-app        # identifiants d'une GitHub App, créés à part
runnerScaleSetName: lyneko-kapsule        # le libellé à utiliser dans runs-on
minRunners: 0
maxRunners: 10
template:
  spec:
    nodeSelector:
      k8s.scaleway.com/pool-name: runners-ci
    securityContext:
      runAsNonRoot: true
      runAsUser: 1001
      seccompProfile:
        type: RuntimeDefault
    containers:
      - name: runner
        image: ghcr.io/actions/actions-runner:2.337.0
        command: ["/home/runner/run.sh"]
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop: ["ALL"]
        resources:
          requests: {cpu: "1", memory: 2Gi}
          limits: {cpu: "2", memory: 4Gi}
controllerServiceAccount:                 # le contrôleur, installé à part dans arc-systems
  namespace: arc-systems
  name: arc-gha-rs-controller
  • githubConfigSecret désigne un secret Kubernetes contenant les identifiants d'une GitHub App de l'organisation (identifiant, installation, clé privée). Une application plutôt qu'un jeton personnel : elle n'appartient à personne, ses droits sont précis, ses jetons courts.

  • runnerScaleSetName devient l'étiquette de runs-on : un job qui veut ces runners écrit runs-on: lyneko-kapsule.

  • minRunners: 0, maxRunners: 10 : aucun pod au repos, dix jobs simultanés au plus.

  • nodeSelector place les runners sur un pool dédié. L'étiquette k8s.scaleway.com/pool-name est bien celle que porte chaque nœud Kapsule :

    $ kubectl --context <contexte du cluster> get nodes -o json | jq -r '.items[0].metadata.labels | keys[] | select(test("scaleway"))'
    k8s.scaleway.com/kapsule
    k8s.scaleway.com/managed
    k8s.scaleway.com/node
    k8s.scaleway.com/pool
    k8s.scaleway.com/pool-name
    k8s.scaleway.com/runtime
    topology.csi.scaleway.com/zone
    

    Un pool dédié sépare les jobs de CI des applications : un job qui consomme toute la mémoire ou qui serait compromis ne partage pas la machine d'un service de production.

  • Le contexte de sécurité : utilisateur non privilégié, profil seccomp par défaut, aucune capacité, pas d'élévation de privilèges.

  • controllerServiceAccount indique où trouver le contrôleur. Sans cette valeur, le chart cherche le contrôleur dans le cluster au moment du rendu, et échoue hors cluster.

Le rendu du chart, en local, montre ce qui serait créé. Le chart est un artefact OCI ; on peut le télécharger avec helm pull oci://..., ou, comme ici, directement par l'API du registre, avec un jeton anonyme, en vérifiant l'empreinte annoncée par le manifeste :

$ R=actions/actions-runner-controller-charts/gha-runner-scale-set
$ T=$(curl -s "https://ghcr.io/token?scope=repository:$R:pull&service=ghcr.io" | jq -r .token)
$ M=$(curl -s -H "Authorization: Bearer $T" -H "Accept: application/vnd.oci.image.manifest.v1+json" \
    "https://ghcr.io/v2/$R/manifests/0.15.0")
$ echo "$M" | jq -r '.layers[] | "\(.mediaType) \(.digest) \(.size)"'
application/vnd.cncf.helm.chart.content.v1.tar+gzip sha256:bab4a9ca41730b52cc4aec46dd5623b3d148ca24b4766b9bc25bf5a91f7bfdfe 13565
$ D=$(echo "$M" | jq -r '.layers[] | select(.mediaType | test("chart.content")).digest')
$ curl -sL -H "Authorization: Bearer $T" "https://ghcr.io/v2/$R/blobs/$D" -o gha-runner-scale-set-0.15.0.tgz
$ echo "${D#sha256:}  gha-runner-scale-set-0.15.0.tgz" | sha256sum -c
gha-runner-scale-set-0.15.0.tgz: OK

Puis le rendu :

$ helm template arc-runners ./gha-runner-scale-set-0.15.0.tgz --namespace arc-runners -f valeurs-arc.yaml > rendu.yaml
$ grep -E "^kind:" rendu.yaml | sort | uniq -c
      1 kind: AutoscalingRunnerSet
      1 kind: Role
      1 kind: RoleBinding
      1 kind: ServiceAccount
$ grep -n "serviceAccountName\|restartPolicy" rendu.yaml
170:      restartPolicy: Never
171:      serviceAccountName: lyneko-kapsule-gha-rs-no-permission

Deux détails du rendu méritent l'attention. Les pods de runner reçoivent un compte de service sans aucune permission Kubernetes (...-no-permission) : un job ne peut pas se servir du jeton monté dans son pod pour agir sur le cluster. Et le rôle du gestionnaire peut créer et lire des secrets dans l'espace de noms des runners : c'est là que le contrôleur range la configuration juste-à-temps de chaque runner, qui contient ses identifiants. L'espace de noms arc-runners ne doit donc héberger rien d'autre.

Ce qu'un runner Kubernetes ne fait pas tout seul

Les workflows du cours supposent Docker sur le runner : services de la leçon 4, construction d'images de la leçon 6. Un pod de runner n'a pas de démon Docker. ARC propose deux modes pour y remédier : Docker dans Docker (un conteneur Docker privilégié à côté du runner, simple mais qui annule une bonne partie de l'isolation) et le mode Kubernetes (le runner crée les conteneurs de job et de service comme des pods, via des crochets, sans privilège, mais avec des différences de comportement). Pour des jobs qui construisent des images, BuildKit sans démon privilégié (évoqué dans le cours sur les images) est l'option la plus propre. Ce choix se fait avant l'installation : il conditionne quels workflows pourront tourner sur ces runners.

Sous le capot

L'écouteur d'un runner ouvre une session auprès du service d'Actions et l'interroge en longue attente : la connexion reste ouverte jusqu'à ce qu'un job arrive ou qu'un délai expire, puis il recommence. Quand un job est attribué, il reçoit le message de job (étapes, jeton, secrets), lance un Runner.Worker, et rend compte de l'avancement. Un runner éphémère, après son unique job, est désenregistré par le service ; une configuration juste-à-temps est simplement inutilisable une seconde fois.

ARC repose sur la même mécanique, à l'échelle : l'écouteur du scale set reçoit de GitHub des statistiques (jobs en attente, en cours) et des événements d'attribution ; le contrôleur ajuste en conséquence le nombre de ressources EphemeralRunner, chacune devenant un pod qui démarre avec sa configuration juste-à-temps, rangée dans un secret, exécute son job, et s'arrête (restartPolicy: Never).

Le cache et les artefacts des runners auto-hébergés restent stockés chez GitHub (sauf avec GitHub Enterprise Server) : un runner dans un réseau privé doit pouvoir joindre ces services en sortie.

Pièges courants

Le job attend indéfiniment Waiting for a runner to pick up this job. Aucun runner en ligne ne porte toutes les étiquettes de runs-on ; ou le runner appartient à un groupe auquel le dépôt n'a pas accès ; ou ARC a atteint maxRunners. La page Runners de l'organisation montre l'état de chacun.

Le runner ne reçoit plus de job après des semaines de bon fonctionnement. Mise à jour automatique désactivée et version trop ancienne : plus de 30 jours après une nouvelle version, ou sous la version minimale imposée.

Le disque d'un runner persistant se remplit. Espaces de travail, images Docker, caches d'outils s'accumulent. Les runners éphémères règlent le problème ; sinon, un nettoyage planifié s'impose.

Un job réussit sur un runner hébergé et échoue sur le vôtre. L'image du runner hébergé contient des centaines d'outils ; la vôtre, seulement ce que vous y avez mis. Les actions setup-* comblent une partie de l'écart ; pour le reste, l'image de vos runners est un produit à maintenir.

Les pods ARC restent en attente. Le pool de nœuds est plein et l'autoscaler du cluster met plusieurs minutes à créer un nœud : le premier job de la journée attend. Un minRunners à 1 aux heures ouvrées, ou un nœud toujours présent dans le pool, évite cette attente au prix d'une machine allumée.

services échoue sur un runner ARC. Pas de Docker dans le pod : choisissez le mode Docker dans Docker ou le mode Kubernetes, ou gardez ces jobs sur des runners hébergés.

Sécurité

Jamais sur un dépôt public. La documentation de GitHub est formelle : les runners auto-hébergés ne devraient presque jamais servir à des dépôts publics, car n'importe qui peut y proposer une demande de fusion dont le workflow s'exécutera sur votre machine, dans votre réseau. Les approbations pour les contributeurs externes réduisent le risque sans le supprimer.

Éphémère, toujours. Un runner persistant partagé entre dépôts permet à un job de laisser derrière lui un processus, un fichier .bashrc ou un binaire modifié qui attendra le job suivant, peut-être celui qui déploie en production.

Séparer les usages. Les runners qui ont accès au réseau de production ne doivent servir qu'aux jobs de déploiement : un scale set à part, un groupe de runners limité à quelques dépôts (plan Team), et des environnements protégés (leçon 9) côté workflow.

Contrôler les sorties réseau. Un runner dans un réseau privé n'a besoin de joindre que GitHub et les services que ses jobs utilisent. Une politique réseau sortante (une NetworkPolicy Kubernetes avec un CNI qui l'applique, ou un pare-feu de sortie) limite ce qu'un job compromis peut exfiltrer et contacter.

Surveiller les enregistrements. Un runner inconnu dans la liste de l'organisation est un signal d'alerte. Les jetons d'enregistrement, les configurations juste-à-temps et les identifiants de la GitHub App d'ARC sont des secrets de premier rang.

Exploiter GitHub Actions au quotidien

Diagnostiquer un run

Les outils, du plus léger au plus lourd :

  1. gh run view --log-failed : seulement le journal des étapes en échec, dans le terminal.
  2. Le journal de condition d'un job ignoré (leçon 3) : l'expression if: et la valeur de chaque contexte au moment de l'évaluation.
  3. Relancer avec les journaux de débogage : gh run rerun <numéro> --failed --debug relance les jobs en échec avec le niveau de détail maximal, sans rien changer à la configuration du dépôt. Toute personne qui peut lancer le workflow peut le faire.
  4. Les journaux de débogage permanents : une variable (ou un secret) ACTIONS_STEP_DEBUG à true dans le dépôt affiche les messages de débogage des étapes ; ACTIONS_RUNNER_DEBUG à true ajoute au fichier de journaux téléchargeable les journaux internes de l'écouteur et de l'exécutant (runner-diagnostic-logs). Une étape peut tester runner.debug pour n'afficher des informations supplémentaires qu'en mode débogage.
$ gh run rerun --help
...
  -d, --debug        Rerun with debug logging
      --failed       Rerun only failed jobs, including dependencies
  -j, --job string   Rerun a specific job from a run, including dependencies

Méfiez-vous des actions qui ouvrent une session interactive sur le runner en cas d'échec : elles exposent une machine qui détient le jeton et les secrets du job à quiconque obtient l'adresse de la session.

Mesurer les durées et les coûts

Les durées se mesurent avec l'API, sans outil supplémentaire. Pour le workflow de déploiement de ce site, au 1er octobre 2026 :

"""Durées des runs lues sur l'entrée standard (début et fin séparés par une tabulation)."""
import statistics
import sys
from datetime import datetime

def lire(horodatage):
    return datetime.fromisoformat(horodatage.strip().replace("Z", "+00:00"))

durees = []
for ligne in sys.stdin:
    debut, fin = ligne.split("\t")
    durees.append((lire(fin) - lire(debut)).total_seconds())
print(f"runs : {len(durees)} | médiane : {statistics.median(durees):.0f} s"
      f" | min : {min(durees):.0f} s | max : {max(durees):.0f} s")
$ gh run list --workflow deploy.yml --limit 20 --json startedAt,updatedAt \
    --jq '.[] | [.startedAt, .updatedAt] | @tsv' | python3 durees.py
runs : 6 | médiane : 86 s | min : 62 s | max : 508 s

(updatedAt est la dernière mise à jour du run, une approximation de sa fin : une relance la déplace. Pour des mesures exactes, on additionne les durées des jobs.)

Et, pour le dernier run, la durée de chaque job, calculée de la même façon à partir des champs startedAt et completedAt de gh run view <numéro> --json jobs :

lint                   success      9 s
build-and-push         success     60 s
record deploy in git   success      9 s

Le dernier run a cumulé 78 secondes de jobs, réparties en trois jobs de 9, 60 et 9 secondes. Chaque job étant facturé à la minute entamée, il compte pour trois minutes sur un dépôt privé. Regrouper seulement lint avec build-and-push n'économiserait rien : 69 secondes font déjà deux minutes. Il faudrait un job unique de 78 secondes, facturé deux minutes, et ce job aurait alors la permission contents: write du déploiement pendant toute la construction : le découpage en jobs a aussi une raison de sécurité. C'est l'arbitrage de la leçon 1, chiffré sur un cas réel, et il se tranche ici en faveur des trois jobs.

Faire le ménage

Le cache et les artefacts consomment des quotas (leçon 5). L'état du cache d'un dépôt se lit avec gh :

$ gh cache list --limit 5
8374281131	index-buildkit-1-f921bd05#6	9.07 KiB	2026-10-01T16:00:24Z	2026-10-01T16:00:24Z
8369454072	index-buildkit-1-f921bd05#5	9.07 KiB	2026-10-01T14:23:25Z	2026-10-01T16:00:23Z
8374279613	buildkit-blob-1-sha256:f6e788edfff3a9b5214ddc4af85187e572013e35503d190c9500740c4679a4db	7.13 MiB	2026-10-01T16:00:23Z	2026-10-01T16:00:23Z
8356348698	buildkit-blob-1-sha256:ef1dbfdbb5d59bdeac07b1398a9f0808b453513bf6a76eb3ffd509a284483095	67.78 MiB	2026-10-01T09:29:02Z	2026-10-01T16:00:21Z
8356348357	buildkit-blob-1-sha256:edd0d77c90a0ba63a89c2c34554f2ee51c5e7a398faaf550b3bb009f5216f547	3.73 MiB	2026-10-01T09:28:57Z	2026-10-01T16:00:21Z
$ gh api repos/lyneko-team/apprendre/actions/cache/usage
{"full_name":"lyneko-team/apprendre","active_caches_size_in_bytes":226270072,"active_caches_count":45}

Le cache de construction type=gha (leçon 6) range une entrée par couche de l'image (buildkit-blob-...) et une entrée d'index par construction : 45 entrées et 226 Mo pour ce site, bien en dessous des 10 Go. gh cache delete supprime une entrée ou tout le cache d'une branche ; la rétention des artefacts et des journaux se règle dans les réglages Actions du dépôt.

Le calendrier de maintenance

GitHub Actions bouge sans qu'on modifie un seul fichier. Une équipe qui en dépend tient un calendrier, alimenté par le journal des changements de GitHub. Au 1er octobre 2026, il contenait par exemple :

ÉchéanceChangementAction à prévoir
23 septembre 2026 (passé)Node 20 retiré des runnersactions JavaScript maison en node24 (leçon 8)
29 septembre 2026 (passé)version minimale 2.329.0 des runners auto-hébergésimages de runners à jour
19 octobre au 19 novembre 2026ubuntu-latest passe d'Ubuntu 24.04 à 26.04épingler ubuntu-24.04, tester ubuntu-26.04 sur une branche (leçon 1)
2 novembre 2026pull_request_target désactivé par défaut sur les dépôts publics sans règlerègles d'exécution explicites (leçon 11)
à date inconnuefrais de plateforme des runners auto-hébergés, reportéssuivre le journal des changements

Les mises à jour d'actions, elles, arrivent par Dependabot (leçon 11) ; celles des outils (actionlint, zizmor) aussi, si on les installe par un fichier de dépendances.

En production

Pour Lyneko, les runners hébergés suffisent aujourd'hui : les déploiements suivent le modèle GitOps et n'ont pas besoin d'accéder au réseau des clusters, et les volumes restent modestes. Le jour où un pipeline devra joindre une ressource privée (une migration de base de données exécutée avant le déploiement, par exemple), la cible est ARC sur un pool Kapsule dédié, minRunners: 0, runners éphémères non privilégiés, réservés aux jobs qui en ont besoin, et le passage au plan Team pour limiter ces runners à quelques dépôts.

Le coût réel. Un pool Kapsule de runners est facturé à l'heure de nœud allumé. Avec l'autoscaler du cluster et minRunners: 0, il ne coûte presque rien au repos ; avec un nœud toujours présent pour éviter l'attente du matin, il coûte une instance permanente. Comparez avec le prix des minutes hébergées pour votre volume réel, mesuré avec l'API comme plus haut, avant de décider.

L'exploitation, c'est un rôle. Mises à jour des runners et de leur image, surveillance des enregistrements, calendrier des changements de la plateforme, ménage des caches, budgets : sur une organisation de taille moyenne, quelqu'un doit en être responsable. C'est l'un des sujets du platform engineering.

Exercices

1. Pour chacune de ces situations, runners hébergés ou auto-hébergés ? (a) Un dépôt public de bibliothèque libre ; (b) une migration de schéma sur un PostgreSQL privé de Scaleway, avant chaque déploiement ; (c) une organisation qui consomme 1 500 minutes par mois sur des dépôts privés.

Solution

(a) Hébergés, sans hésitation : minutes gratuites, et un runner auto-hébergé sur un dépôt public exposerait votre infrastructure aux demandes de fusion de n'importe qui. (b) Auto-hébergé dans le réseau privé (ARC sur un pool dédié), éphémère, réservé à ce job et protégé par un environnement ; ou, alternative à étudier, une migration exécutée par un job Kubernetes déclenché par Argo CD, qui évite tout runner. (c) Hébergés : 1 500 minutes tiennent dans les 2 000 incluses du plan gratuit.

2. Un collègue a installé un runner persistant sur une VM de l'équipe, utilisé par tous les dépôts de l'organisation, dont un dépôt public de documentation. Listez les risques, du plus grave au moins grave.

Solution
  1. Le dépôt public : n'importe qui peut ouvrir une demande de fusion qui modifie le workflow et fait exécuter du code sur la VM, dans le réseau de l'équipe. 2. La persistance : un job peut laisser un processus ou un fichier modifié qui attendra les jobs suivants, y compris ceux qui manipulent des secrets de déploiement. 3. Le partage entre tous les dépôts : un dépôt peu sensible et un dépôt critique partagent la même machine. 4. La maintenance : mises à jour du système et de l'agent, espace disque, outils installés à la main, qui dérivent. Correction : retirer le dépôt public, passer à des runners éphémères, séparer les usages.

3. Modifiez les valeurs ARC de cette leçon pour qu'au moins un runner soit prêt du lundi au vendredi, sans en garder la nuit. Est-ce possible avec les seules valeurs du chart ?

Solution

Non : minRunners est une valeur fixe. Il faut la modifier selon l'heure, par exemple avec deux CronJob Kubernetes qui modifient la ressource AutoscalingRunnerSet (le champ spec.minRunners à 1 le matin, à 0 le soir), ou, dans un modèle GitOps, un commit planifié (un workflow schedule) qui modifie la valeur dans Git, qu'Argo CD applique ensuite. Dans les deux cas, le nœud qui héberge ce runner doit exister : c'est l'autoscaler du cluster qui le crée à la demande du pod.

4. Un job est bloqué sur Waiting for a runner to pick up this job depuis vingt minutes, avec runs-on: [self-hosted, kapsule]. Décrivez votre diagnostic, dans l'ordre.

Solution
  1. La page Runners de l'organisation : existe-t-il un runner en ligne qui porte toutes ces étiquettes ? Avec ARC, l'étiquette est le nom du scale set (lyneko-kapsule), et kapsule seule ne correspond à rien. 2. Si un runner correspond : le dépôt a-t-il accès à son groupe ? 3. Avec ARC : l'écouteur tourne-t-il (kubectl get pods -n arc-systems), des EphemeralRunner sont-ils créés, les pods sont-ils en attente faute de nœud (kubectl describe pod), maxRunners est-il atteint ? 4. La version de l'agent est-elle au-dessus du minimum imposé ?

5. À partir de la mesure des durées de cette leçon, estimez le coût mensuel du workflow de déploiement de ce site s'il passait de 6 à 300 runs par mois, sur un dépôt privé, une fois les 2 000 minutes incluses consommées par ailleurs. Existe-t-il une modification qui réduit ce coût d'un tiers ? Faut-il la faire ?

Solution

Trois jobs facturés une minute chacun (aucun ne dépasse 60 secondes, à condition que build-and-push reste à la minute) : 3 minutes par run, 900 minutes pour 300 runs, à 0,006 dollar la minute, soit 5,40 dollars par mois. Regrouper lint avec build-and-push ne change rien (69 secondes, deux minutes). Seul un job unique de 78 secondes descend à deux minutes par run : 600 minutes, 3,60 dollars, un tiers de moins. Mais ce job unique porterait contents: write pendant toute la construction : pour 1,80 dollar par mois, on affaiblirait la séparation des privilèges. Le bon choix est de garder les trois jobs ; le calcul sert à savoir ce que coûte ce choix.

Récapitulatif

  • Les runners auto-hébergés servent l'accès aux réseaux privés, le matériel particulier, les gros volumes et les exigences de localisation ; ils transfèrent à l'équipe l'isolation, les mises à jour et la sécurité.
  • L'agent se connecte en sortie ; il s'enregistre avec un jeton d'une heure, porte des étiquettes, appartient à un groupe (plan Team pour les groupes).
  • Éphémère, toujours : un job par runner, un environnement neuf par job. Les configurations juste-à-temps évitent l'étape d'enregistrement.
  • ARC fait tourner des runners éphémères sur Kubernetes : contrôleur, écouteur, un pod par job, sans permission Kubernetes ; prévoir le mode Docker pour les services et les constructions.
  • Jamais sur un dépôt public ; usages séparés ; sorties réseau contrôlées ; enregistrements surveillés.
  • Exploiter, c'est diagnostiquer (--log-failed, rerun --debug), mesurer (durées et minutes facturées par job), faire le ménage, et tenir le calendrier des changements de la plateforme.

Pour aller plus loin

Voir ma constellation →

Sources