Github Actions Runner self-hosted et AWS ECR

Github Actions Runner self-hosted et AWS ECR

Utilisation d’un runner self-hosted sur Github Actions avec AWS ECR

Publié le


Do not index
Do not index
Primary Keyword
Lié à Analyse sémantique (Articles liés) 1
Lié à Analyse sémantique (Articles liés)
Statut rédaction
A optimiser SEO
Lié à Analyse sémantique (Articles liés) 2
⚠️
Attention : cet article ne représente pas un guide d’installation, il ne se substitue pas aux instructions officielles. Les commandes données ne suffisent pas à une installation complète, et se contentent juste de donner une idée générale de la procédure à suivre.

Introduction

Il y a peu, nous avons eu un problème intéressant avec l’infrastructure d’un client. Il voulait faire tourner ses jobs Github Actions dans des runners self-hosted sur Kubernetes utilisant une image stockée sur AWS ECR.
Rien d’incroyable me direz-vous ? Eh bien ça n’a finalement pas été si simple, et la solution trouvée n’est toujours pas parfaite.
Mettons des définitions sur les différents mots-clefs que nous venons de citer :
  • Github Actions : service de CI/CD mis à disposition par Github
  • Runner… : machine qui va exécuter les tâches demandées par la CI/CD
  • self-hosted : au lieu d’utiliser ceux fournis par Github, on déploie des nouveaux runners sur notre cluster Kubernetes
  • Kubernetes : un orchestrateur qui simplifie le déploiement de conteneurs Docker
  • AWS ECR : service de registres de conteneurs Docker fourni par AWS
 
Résumé de l’interaction entre les composants
Résumé de l’interaction entre les composants
 
Pour faire fonctionner tout cet ensemble, il faut donc mettre en place un nombre conséquent de composants. Voilà la manière dont nous avons traité ça.

Image Docker à utiliser

Dans un premier temps, nous avons créé le registre ECR, et avons construit une image, puis l’avons téléversée dans ce nouveau registre. L’image part d’un Ubuntu, et ajoute des outils nécessaires à tester l’application du client.
On peut grossièrement résumer le Dockerfile aux commandes suivantes :
$ cat Dockerfile
FROM ubuntu:22.04
# Required packages
RUN apt update && apt install -y curl git unzip make <etc>
# AWS CLI
RUN curl -fsSL -o /tmp/awscliv2.zip "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" && \
    unzip /tmp/awscliv2.zip -d /tmp/awscli && \
    /tmp/awscli/aws/install
# Custom scripts
COPY ./scripts /scripts


$ aws ecr create-repository --repository-name github-runner
$ aws ecr get-login-password | docker login --username AWS --password-stdin <account>.dkr.ecr.eu-west-3.amazonaws.com/github-runner
$ docker build -t <account>.dkr.ecr.eu-west-3.amazonaws.com/github-runner:v1 .
$ docker push <account>.dkr.ecr.eu-west-3.amazonaws.com/github-runner:v1
 
Maintenant que nous avons une image Docker, et qu’elle est mise à disposition sur AWS ECR, on va pouvoir commencer à créer les ressources nécessaires au Runner.

Ressources liées au Runner

Accès à l’image

Pour l’instant, le runner ne peut pas accéder au registre ECR, il faut pour ça créer un rôle IAM, et un rôle EKS, et les lier. On peut donc, dans un premier temps, définir un rôle IAM github-runner de la sorte :
# IAM role `github-runner`: trust relationships
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::<account>:oidc-provider/oidc.eks.eu-west-3.amazonaws.com/id/<cluster>"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringLike": {
                    "oidc.eks.eu-west-3.amazonaws.com/id/<cluster>:sub": "system:serviceaccount:github-runner:github-runner*"
                }
            }
        }
    ]
}

# IAM role `github-runner`: permissions
{
    "Statement": [
        {
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:GetRepositoryPolicy",
                "ecr:DescribeRepositories",
                "ecr:ListImages",
                "ecr:DescribeImages",
                "ecr:BatchGetImage",
                "ecr:GetLifecyclePolicy",
                "ecr:GetLifecyclePolicyPreview",
                "ecr:ListTagsForResource",
                "ecr:DescribeImageScanFindings"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:ecr:eu-west-3:<account>:repository/github-runner"
        }
    ],
    "Version": "2012-10-17"
}
 
Grâce au rôle créé, on autorise désormais le compte de service Kubernetes github-runer:github-runner à endosser ce rôle, qui lui, permet de lister et récupérer des images sur ECR.

Actions Runner Controller

ARC est un logiciel open-source développé en collaboration avec Github qui permet de contrôler et gérer des Runners pour Actions. Il permet de gérer l’authentification auprès de Github (pour recevoir les nouveaux jobs), et de scale les Runners en cas de sur-utilisation.
Il faut dans un premier temps créer un secret qui va contenir l’authentification auprès de Github, les instructions sont disponibles sur la documentation officielle.
# Secret creation
$ kubectl create namespace github-runner
$ kubectl create secret generic controller-manager \
    -n github-runner \
		--from-literal=github_token=${GITHUB_TOKEN}

# ARC deployment
$ kubectl create -n github-runner -f \
		https://github.com/actions/actions-runner-controller/releases/download/v0.27.4/actions-runner-controller.yaml
 
Nous avons donc à notre disposition le contrôleur, il faut ensuite créer ce qui nous intéresse le plus : le Runner.

Le Runner en lui-même

C’est ici que se passe la partie qui nous a causée beaucoup de problèmes.
Commençons par lancer un Runner, et le compte de service associé :
# runner.yaml
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
  name: github-runner
	namespace: github-runner
spec:
  template:
    spec:
      organization: my-org
			serviceAccountName: github-runner

---
# service-account.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: github-runner
	namespace: github-runner
	annotations:
		eks.amazonaws.com/role-arn: arn:aws:iam::<account>:role/github-runner
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: github-runner
	namespace: github-runner
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: github-runner
	namespace: github-runner
subjects:
- kind: ServiceAccount
  name: github-runner
	namespace: github-runner
  apiGroup: ""
roleRef:
  kind: Role
  name: github-runner
  apiGroup: rbac.authorization.k8s.io
 
Le Runner est désormais disponible à l’utilisation par Github Actions. Le compte de service existe, et notre Runner a les permissions nécessaires pour accéder à ECR sauf que… son démon Docker n’est pas authentifié.
Dans le cadre de la CI/CD de notre client, les jobs sont à lancer dans une image particulière, et donc la première étape du Runner est de récupérer cette image puis d’y lancer les différents jobs.
Pour récupérer cette image, le démon doit s’authentifier auprès d’ECR. Il existe pour cela plusieurs moyens :
  • amazon-credentials-ecr-helper : ce programme est censé remplacer et abstraire l’authentification auprès d’ECR. Nous l’avons déjà utilisé dans d’autres projets, mais il nous a été impossible de l’intégrer au Runner, sans trop comprendre pourquoi.
  • aws ecr get-login-password : la méthode “classique” utilisant le CLI d’AWS pour générer un token temporaire
 
C’est donc cette seconde option que nous avons choisie, mais elle cause encore des problèmes : comment effectuer la connexion à ECR, sans surcharger l’entrypoint de l’image Docker du runner ?
Nous avons choisi de contourner le problème, et d’ajouter un initContainer au Runner, et d’y effectuer la connexion. On peut donc modifier le déploiement du Runner avec nos nouvelles ressources :
# runner.yaml
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
  name: github-runner
	namespace: github-runner
spec:
  template:
    spec:
      organization: my-org
			serviceAccountName: github-runner

			volumes:
        - name: dockerconfig
          emptyDir: {}
			volumeMounts:
        - mountPath: /home/runner/.docker/config.json
          name: dockerconfig
          subPath: config.json

			securityContext:
        fsGroup: 1000
			
			initContainers:
        - name: login-ecr
          image: ubuntu:latest
          command:
            - sh
            - -c
            - apt update && apt install -y --no-install-recommends curl unzip ca-certificates gnupg
              && curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
              && unzip awscliv2.zip > /dev/null
              && ./aws/install
              && install -m 0755 -d /etc/apt/keyrings
              && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
              && chmod a+r /etc/apt/keyrings/docker.gpg
              && echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
              && apt update && apt install -y --no-install-recommends docker-ce-cli
              && aws ecr get-login-password | docker login --username AWS --password-stdin <account>.dkr.ecr.eu-west-3.amazonaws.com/github-runner
              && cp ~/.docker/config.json /dockerconfig/config.json
              && chown 1000:1000 /dockerconfig/config.json
              && chmod 644 /dockerconfig/config.json
          volumeMounts:
            - mountPath: /dockerconfig
              name: dockerconfig
 
Le initContainer que nous avons ajouté se charge des tâches suivantes :
  • Installer le CLI de AWS
  • Installer le client Docker
  • Se connecter à ECR
  • Déplacer la configuration créée dans le nouveau volume partagé entre les conteneurs du Runner
Il faut également changer le contexte de sécurité, afin que l’utilisateur dans le conteneur principal du runner puisse accéder aux fichiers montés.

Utilisation dans la pipeline

Maintenant que nous avons un Runner self-hosted, et une image à utiliser pour lancer les jobs la nécessitant, on peut mettre à jour les pipelines de CI/CD pour les utiliser, ce qui donne ce genre de jobs :
name: My Actions workflow

on: push

jobs:
	my-job:
		name: My first job
		runs-on: [self-hosted]
		container: <account>.dkr.ecr.eu-west-3.amazonaws.com/github-runner:v1
		steps:
			- run: /scripts/say 'Hey from the container :)'

Améliorations

Durée de validité des jetons d’authentification

Cette manière de faire n’est pas optimisée, en effet, les jetons générés par aws ecr get-login-password ne durent que 12h. Si aucun job n’est lancé pendant 12h, alors le jeton est expiré et le prochain job, s’il nécessite l’utilisation de l’image dans le ECR privé, ne fonctionnera pas et il faudra le relancer.

Temps à initialiser un nouveau conteneur

Le initContainer met quelques dizaines de secondes à terminer, plusieurs paquets sont téléchargés, et ce à chaque fois qu’un runner s’initialise.
On pourrait imaginer utiliser une image de base plus fournie (pour l’instant c’est ubuntu:latest), mais ça revient au même si cette image est stockée sur un registre privé. Le problème du serpent qui se mord la queue. On pourrait laisser cette image publique puisqu’elle ne contient que le CLI Docker et le CLI AWS.

Complexité d’utilisation

Il ne faut pas non plus perdre de vue que l’infrastructure déployée reste assez complexe, on utilise : une image Docker, un registre ECR, un rôle IAM, un déploiement de ARC, un déploiement du Runner, un secret Kubernetes et enfin un compte de service. Au dessus de ça, il faut encore ajouter la gestion du volume dans le Runner, ainsi que le initContainer.
C’est beaucoup. D’autant que la partie authentification auprès d’ECR aurait pu être beaucoup simplifiée si ARC intégrait des méthodes d’authentification plus simples auprès des cloud providers principaux (voir
Using private ECR repos for actions
Github
Using private ECR repos for actions
Updated
Oct 2, 2022
pour en apprendre davantage).

Conclusion

Durant cet article, nous avons vu comment déployer un Runner sur Kubernetes afin de ne plus utiliser les Runners publics de Github, nous avons également mis en place un rôle IAM destiné à ce runner pour qu’il puisse utiliser une image stockée sur ECR, construite par la même occasion.
Cette solution est en place depuis plusieurs semaines chez notre client, et pour l’instant, nous n’avons eu aucun problème à déployer. Mis à part les défauts que nous avons listés au dessus, le client et nous-mêmes sommes satisfaits de la solution.
Si vous avez déjà rencontré ce problème, contactez-nous avec grand plaisir pour que nous puissions comparer les solutions que nous avons trouvées !
 

S'inscrire à la newsletter DevSecOps Keltio

Pour recevoir tous les mois des articles d'expertise du domaine

S'inscrire

Écrit par

Victor Franzi
Victor Franzi

Victor est notre première recrue, il est passionné depuis son plus jeune âge et ne veut jamais s’arrêter d’apprendre 🧠