Héberger un GitHub Action Runner sur Azure Container Apps

Introduction
Après avoir abordé le sujet Azure Function sur Azure Container Apps, l’article se penche sur un autre scénario visant à héberger un GitHub Action Runner sur un conteneur. L’objectif de cet article est de documenter les étapes de la mise en œuvre, mais aussi de fournir un exemple d’implémentation fonctionnel.
Utiliser Azure Container Apps pour héberger un GitHub Action Runner éphémère est une approche très intéressante à plusieurs titres :
- Pas de machine virtuelle à maintenir, car nous utilisons une infrastructure managée par Microsoft
- Limiter la maintenance au seul conteneur que nous allons construire de manière totalement industrielle
- Conteneur instancié uniquement quand on en a besoin, donc une facturation limitée à la durée de vie du conteneur qui va exécuter notre GitHub Action workflow
- L’infrastructure utilisée est directement connectée à notre Virtual Network, donc prête pour des déploiements en environnement privatisé
Ce sujet est déjà documenté chez Microsoft : Deploy to Azure Container Apps with GitHub Actions. Cela fonctionne très bien, mais pour partir en production, on a besoin de travailler quelques sujets comme :
- La construction de l’image de notre GitHub Action Runner
- L’utilisation de l’authentification GitHub Apps en lieu et place d’un Personal Access Token (PAT)
- Comment industrialiser ce processus
La big picture
Commençons par poser une vue d’ensemble. L’environnement Azure repose sur Azure Container Apps Environment. Celui-ci disposera d’une User-Assigned Managed Identity lui permettant d’extraire un secret du Key Vault et d’instancier une image préalablement construite avec Azure Container Registry (ACR). L’Azure Container Apps Job instancié sera configuré pour exploiter le Scaler GitHub de Keda (Kubernetes Event–driven Autoscaling). L’objectif est de pouvoir détecter qu’un GitHub Action Workflow est dans l’attente d’un GitHub Action Runner éphémère. C’est ce qui déclenchera l’instanciation du Azure Container Apps Jobs.

Voila pour les grandes lignes. Maintenant rentrons dans le détail avec la mise en œuvre de l’infrastructure. Pour simplifier la mise en œuvre, tout est disponible sur ce repo GitHub : ContainerAppsJobGithubRunner
Etape n°1 : Mise en œuvre de l’infrastructure
L’infrastructure mise en œuvre est relativement simple. Elle se compose :
- D’un Virtual Network dans lequel nous allons dédier un sous-réseau pour notre futur Azure Container Apps Environment
- D’une User-Assigned identity qui sera utilisée par notre futur Azure Container Apps Environment pour « puller » l’image de notre GitHub Action Runner éphémère
- D’une instance Azure Container Registry pour stocker l’image de notre conteneur
- Une instance Azure Log Analytics Workspace pour collecter la télémétrie de notre Container Apps Environment
- Une instance Azure Container Apps Environment qui va porter notre futur job
- Une instance de Key Vault pour stocker les secrets que nous allons manipuler
- Quelques rôles assignments afin que notre Azure Container Apps Environment puisse déployer notre job mais aussi lui mettre à disposition un secret issus de notre instance de Key Vault
Pour simplifier la mise en œuvre, l’intégralité de ce déploiement est mise à disposition sous la forme d’un déploiement Bicep solution.bicep avec son fichier de réponse solution.json. Ne reste qu’à le déployer à l’aide de la commande New-AzSubscriptionDeployment
New-AzSubscriptionDeployment -TemplateFile solution.bicep -TemplateParameterFile solution.json -location WestEurope

Au terme de cette étape, tous les composants sont en place côté Azure, à l’exception de l’Azure Container Apps Jobs. Prochaine étape : la mise en place de l’authentification avec une GitHub App.
Etape n°2 : Mise en place de l’authentification GitHub App
À la première lecture de la documentation GitHub sur l’authentification GitHub App, on peut trouver le sujet un peu cryptique.Il s’agit d’une application que à déclarer, puis à installer au sein de notre repository GitHub. Dans le contexte de cet article, nous allons déclarer notre application GitHub App et l’installer dans notre Repository. La démarche est identique lorsqu’on voudra mettre en place au sein d’une organisation GitHub. Pour commencer, créer cette GitHub App :
- Connectez-vous à GitHub et allez dans votre profil pour sélectionner « Settings »
- Dans « Développer Settings », sélectionnez « GitHub Apps »
- Cliquer sur le bouton « New GitHub Apps »
- Choisissez un nom expressif pour votre GitHub Apps : « NPRD-ACARunner »
- Associez une description qui explique clairement à quoi va service cette GitHub App
- Vous pouvez fournir une URL fictive pour l’URL demandée dans notre contexte
- Assurez-vous que la case d’option « Expire user authorization tokens » est bien cochée

8. Désactivez la prise en charge de la fonctionnalité « Callback URL »
9.Dans la section « Repository Permissions » configurez les options suivantes :
- Action: Read Only
- Administration: Read Only
- MetaData : Read Only
10.Dans la section “Organization permissions, configurez les options suivantes
- Actions – Read-only
- Metadata – Read-only
- Self-hosted Runners – Read & Write
Selon que le déploiement se fasse dans le contexte d’une organisation GitHub ou d’un simple repository GitHub personnel, les permissions ne sont pas les mêmes, tout comme les URL sollicitées pendant l’initialisation de notre GitHub Action Runner éphémère. Une fois l’application créée, assurez-vous de bien conserver l’App ID puis générer une clé privée pouvant être utilisée que nous pourrons utiliser comme méthode d’authentification.



L’application GitHub App est maintenant déclarée, il faut procéder à son installation. Elle peut être rendue disponible à grande échelle au niveau d’une organisation mais j’ai volontairement retenu de ne l’installer que dans mon projet GitHub.

Lors de l’installation de l’application, un « Installation ID » sera généré, celui-ci est visible dans l’URL dans votre navigateur. Nous avons besoin de le conserver.

Etape n°3 : Mise en place des secrets
Lors de la mise en œuvre de GitHub App, nous avons récupéré des identifiants (GitHub AppID & GitHub Installation ID) ainsi qu’une clé privée. Pour les identifiants, j’ai choisi de les consommer directement dans Azure Container Apps comme variables d’environnements de notre futur conteneur.
Cependant, pour la clé privée, une attention particulière est nécessaire. L’infrastructure déployée inclut une instance de Key Vault. Elle permettra d’y stocker notre clé privée. Seul Azure Container Apps y aura accès, il passera cette référence à notre futur conteneur.
Il ne reste plus qu’à uploader le contenu du fichier contenant la clé privée comme secret : az keyvault secret set –vault-name <Key Vault Name> –name GitHubPEM –file <pem file> –output none

Etape n°4 : Construction de notre GitHub Runner
C’est en fait le sujet le plus compliqué. Pour construire cette image, j’avais trois possibilités :
- Utiliser les images mises à disposition par GitHub. Cela fonctionne mais c’est un peu vide
- Utiliser l’excellent travail d’autres : https://github.com/myoung34/docker-github-actions-runner
- Construire son image soi même
C’est cette dernière option que j’ai retenue car je voulais personnaliser les composants mis à disposition dans le GitHub Action Runner, jusqu’à personnaliser la version de chaque composant. En plus, il est essentiel de d’intégrer la prise en charge de l’authentification GitHub App. C’est ce point qui a nécessité le plus de travail.
Etant donné que la construction de l’image est relativement chronophage (presque douze minutes), tout ce qui est nécessaire est présent dans le repository dans le répertoire Docker du Repository Git mis à disposition avec cet article. Les commandes ci-dessous vont nous permettre d’identifier l’instance du service Azure Container Registry (ACR) mis à disposition, s’y connecter et de déclencher la construction de notre conteneur.
- az acr list –query [].name –output tsv
- az acr login –name <Nom de l’instance ACR précédemment identifiée>
- az acr build –registry <Nom de l’instance ACR précédemment identifiée> –image <Nom de l’instance ACR précédemment identifiée>.azurecr.io/runner_base: 2.325.0 . –build-arg ‘RUNNER_VERSION=2.325.0’ –build-arg ‘DOTNET_VERSION=9.0’ –build-arg ‘PS_VERSION=7.4.5’ –build-arg ‘AZACCOUNTS_VERSION=3.0.4’ –build-arg ‘AZKEYVAULT_VERSION=6.2.0’ –build-arg ‘AZSTORAGE_VERSION=7.4.0’ –build-arg ‘AZAPPINSIGHT_VERSION=2.2.5’ –build-arg ‘AZNETWORK_VERSION=7.10.0’ –build-arg ‘AZRESOURCES_VERSION=7.5.0’ –build-arg ‘AZ_TABLE_VERSION=2.1.0’ –build-arg ‘MS_GRAPH_VERSION=2.24.0’ –build-arg ‘MS_ENTRA_VERSION=1.0.1’ –build-arg ‘TERRAFORM_VERSION=1.10.0’ –build-arg ‘TERRAGRUNT_VERSION=0.72.2’ –build-arg ‘AZURECLI_VERSION=2.74.0’ –build-arg ‘UBUNTU_LTS_VERSION=jammy’

Douze minutes plus tard, nous avons une image pour notre futur Runner GitHub Action. Le DockerFile mis à disposition inclus un grand nombre de variables pour personnaliser les versions des composants. Au moment de l’écriture de cet article, c’est la version 2.325.0 de l’image du GitHub Runner qui est disponible. Pensez à aller regarder quelle est la version la plus récente de disponible ici : https://github.com/actions/runner-images/releases.
Une fois l’opération terminée, la présence d’une nouvelle image dans Azure Container Registry peut être vérifié avec la commande suivante : az acr repository show –name <Nom de l’instance ACR précédemment identifiée> –repository runner_base

Maintenant, reste à expliquer comment va fonctionner l’authentification. Le fichier Dockerfile, mentionne la copie et du script entrypoint.sh. Il sera exécuté à chaque instanciation du conteneur. Pour écrire ce script, je suis parti de la documentation GitHub sur le sujet : Generating a JSON Web Token (JWT) for a GitHub App. Le script exploite la clé privée pour négocier un « Access Token » qui sera ensuite soumise à l’API de GitHub Runner registration, obtenant ainsi un « Registration Token » qui sera lui-même utilisé pour enregistrer le GitHub Runner éphémère.
Selon qu’il s’agisse d’une organisation GitHub ou d’un simple GitHub Account, les URL ne sont pas les mêmes. Dans le contexte de cet article, je travaille sur mon repository personnel. Le tableau ci-dessous référence les URL qui seront nécessaires et comment les construire selon les scénarios :

Note : Dans le contexte de cet article (et ne disposant pas d’une organization GitHub personnelle), ce sont donc les URL de mon Repo personnel GitHub qui sont utilisées. Pour un déploiement dans le contexte d’une organisation, il faudra adapter les variables dans le fichier acajob.bicep.
Le conteneur va exploiter les informations suivantes en tant que variables d’environnement :
- APP_ID
- PEM (référencé entant que secret Key Vault)
- ACCESS_TOKEN_URL
- REGISTRATION_TOKEN_API_URL
- RUNNER-REGISTRATION_URL
Etape n°5 : Déploiement de notre GitHub Runner
Le déploiement est assuré par un déploiement Bicep acajob.bicep et son fichier de paramètres associés acajob.json. Pensez bien à actualiser ce dernier. La configuration du Azure Container Apps job comprend :
- L’image à utiliser ainsi que la User-Assigned Identity qu’Azure Container Apps devra utiliser pour « Puller » l’image pour notre conteneur.
- L’identifiant unique de notre GitHub Application ID
- La référence du secret Key Vault contenant la clé privée obtenus lors de la création de notre GitHub Application
- Les trois URL de GitHub (Registration Token API URL, Runner Registration Token API URL, Access Token API URL)
- La configuration de Keda pour le Scaler GitHub
Le déploiement est réalisé avec la commande ci-dessous :
New-AzResourceGroupDeployment -ResourceGroupName RG-nprd-acarunner-1.0 -TemplateFile acajob.bicep -TemplateParameterFile acajob.json

La seule subtilité de configuration concerne le stockage de la clé privée obtenue avec notre GitHub App. Celle-ci a été volontairement stockée dans un Azure Key Vault pour qu’elle ne soit pas directement visible dans la configuration du conteneur comme illustré ci-dessous.

Pour que le conteneur puisse être instancié à la demande, reste encore à configurer Keda et plus particulièrement le scaler GitHub Runner. Le scaler a besoin d’un certain nombre de paramètres. Le point d’attention sera de ne pas être trop agressif au niveau des API de GitHub sous peine de rate limiting. C’est pour cette raison que j’ai retenu de dédier mon GitHub Action Runner à un seul et unique GitHub repo. Ici encore, c’est la référence du secret Key Vaultutilisé pour passer la clé privée de GitHub App.

Tester notre Github Runner éphémère
Pour tester GitHub Action Runner, nous avons besoin d’un Workflow. Pour l’exemple en voilà un : demoRunner.yml. Comme on peut le voir ci-dessous, il est minimaliste, composé d’un seul job contenant quelques commandes pour mettre en évidence quelques composants intégré à notre conteneur. Sa seule particularité, c’est de demander son exécution sur un Runner de type « Self-Hosted ». C’est justement un des tags de notre GitHub Action Runner éphémère.

Une fois ce GitHub Workflow déclenché, au niveau Keda, on peut suivre le bon déroulement des opérations avec une simple requête KQL :
ContainerAppSystemLogs
| where EventSource == ‘KEDA’
| where JobName == « githubactionrunner »

Le Keda initialise correctement le Scaler et est mesure de détecter la présence de GitHub Action workflow en attente d’exécution pour déclencher l’instanciation du GitHub Runner.
Au niveau de notre Container Apps Jobs,l’initialisation s’effectue correctement et les logs restent consultables.

Au niveau GitHub, on peut constater que le GitHub Workflow s’est bien déclenché et a été exécuté depuis notre GitHub Action Runner éphémère.



En agissant rapidement, il est possible deconstater la présence duGitHub Action éphémère.

Pour finir, une fois le GitHub Workflow terminé, il sera posszible de constater le bon fonctionnement de celui-ci avec une simple requête KQL comme illustré ci-dessous :
ContainerAppConsoleLogs_CL
| where Log_s contains « √ »

Conclusion
La mise en œuvre est un peu plus compliquée qu’avec un Personal Access Token (PAT) mais une fois l’intégration de l’authentification avec GitHub Apps, nous disposons d’un GitHub Runner éphémèrefacturé uniquement en fonction de la puissance CPU & consommation mémoire utilisées lors de l’exécution de vos GitHub Action Workflows. Avec quelques modifications au niveau du DockerFile, vous serez en mesure de rapidement développer un GitHub Action Runner répondant à vos besoins.
Quelques lectures additionnelles
Entre ma première lecture de l’article Deploy to Azure Container Apps with GitHub Actions et celui-ci, plusieurs itérations ont été nécessaires pour bien comprendre l’assemblage de tous les composants et les implications de l’utilisation de GitHub Apps comme méthode d’authentification. Je vous recommande les lectures suivantes :
- Les runners GitHub : https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners
- Base image for GitHub self-hosted Runners : https://github.com/actions/runner
- Une excellente base de travail pour des runners GitHub : https://github.com/myoung34/docker-github-actions-runner
- L’implémentation avec Azure Verified Modules : https://github.com/ethorneloe/azure-apps-jobs-github-runners, https://github.com/Azure/terraform-azurerm-avm-ptn-cicd-agents-and-runners
- La documentation officielle de Keda sur la configuration des scalers : https://keda.sh/docs/2.17/scalers/github-runner/
- Comment la version 2.17 de Keda va résoudre certains problèmes rencontrés : https://github.com/microsoft/azure-container-apps/issues/1525