Installation de Runners avec GitLab CI

Installation de Runners avec GitLab CI

Un peu de contexte

Cet article est la suite et le dernier de la série des articles consacrés à GitLab CI. Dans un premier temps, je vous ai parlé de la création d'une chaîne CI/CD pour Terraform, le deuxième introduisait le concept des pipelines enfants pour les cas avancés et ce dernier article aura pour vocation de présenter les différentes installations des Runners au sein de GitLab.

Oui mais... c'est quoi un Runner ?

Le Runner ou GitLab Runner est l'application qui va exécuter les différents jobs que vous avez définis dans votre pipeline et donc dans votre fichier .gitlab-ci.yml. En d'autres termes, ce sont des machines indépendantes de celle qui exécute l'application web GitLab et les autres composants associés.

Sans le savoir, vous avez déjà utilisé à plusieurs reprises ces Runners dès lors que vous exécutez une CI/CD sur GitLab avec le bouton "Run Pipeline".

Sur GitLab.com, on retrouve notamment les Runners par défaut qui sont aussi couramment appelés Runners partagés (Shared runners). Ils ne nécessitent aucune installation, ni configuration et peuvent être amenés à exécuter vos tâches.

Voici un exemple de Runner, vous pouvez retrouver ces informations dans Settings > CI/CD > Runners :

gitlab-ci-runners_shared-runners

Chaque Runner dispose d'un ensemble de tags permettant d'exécuter des tâches ou jobs spécifiques en fonction de ceux mentionnés dans le fichier gitlab-ci.yml. L'objectif est d'assigner à un Runner des tâches particulières en fonction de plusieurs paramètres : système d'exploitation, environnement d'exécution, capacités techniques (CPU et RAM) de la machine.

C'était une présentation succincte des Runners partagés. Maintenant, on continue avec les Runners à installer et configurer soi-même ou self-managed.

Pourquoi installer ses propres Runners ?

Bonne question ! Pourquoi s'embêter à installer ses propres Runners alors qu'il y en a disposition sans rien faire sur GitLab.com ?

Plusieurs éléments permettent de répondre à cette question, cette liste n'est bien sûr pas exhaustive, mais permet d'avoir un aperçu des cas que j'ai le plus souvent rencontrés :

  • Minimiser le temps d'exécution de la pipeline : Les Runners appelés "partagés" sont donc partagés avec l'ensemble des utilisateurs de la plateforme. Dans le cas de GitLab.com, il peut arriver qu'à certaines heures, les temps d'attente et d'exécution soient plus longs ;

  • Avoir la main ou contrôler la configuration du Runner, mais aussi les couches plus basses comme le système d'exploitation et pourquoi pas, la politique de sécurité associée à la machine virtuelle. C'est souvent le cas dans des contextes où il est nécessaire de se conformer à des exigences en matière de sécurité ;

  • Déployer au plus proche de l'environnement cible. Ce cas de figure peut se poser pour des binaires que l'on souhaiterait déployer vers un environnement donné notamment pour ne pas traverser l'internet public ;

  • Utiliser les identités ou comptes de service du Cloud pour éviter de stocker des informations sensibles, par exemple, lors d'un déploiement d'infrastructure avec Terraform sur Google Cloud où on utiliserait le compte de service de la machine virtuelle pour déployer l'infrastructure sans générer de clé JSON ;

  • Héberger sa propre installation de GitLab. Dans cette situation, une installation d'au moins un Runner est requise car il n'y en a pas par défaut.

Ces Runners sont à enregistrer sur votre instance GitLab ou sur GitLab.com directement en fonction de là où vous hébergez votre code.

Ils peuvent être enregistrés à différents endroits :

  • À la racine de votre hébergement, ce sont donc des Runners partagés (Shared Runners) et sont disponibles pour l'ensemble des groupes et projets de votre instance GitLab ;
  • Au niveau d'un groupe, on les appelle Group Runners et sont disponibles pour tous les projets et sous-groupes du groupe dans lequel ils sont enregistrés ;
  • Au niveau d'un projet, ils sont donc dédiés à un projet particulier et se nomme Project Runners.

Il est temps de parler de la méthode d'installation...

Installation et configuration

Il y a peu, la méthode d'installation des Runners GitLab a changé. En effet, précédemment la méthode était d'utiliser un registration-token en exécutant la commande gitlab-runner register.

Sauf que depuis la version 15.6, cette façon est devenue dépréciée et il est recommandé d'utiliser des authentication tokens. Néanmoins, cette première méthode restera toujours valide jusque la version 17.0.

La méthode d'installation diffère peu, mais nécessite de configurer sur votre instance GitLab ou GitLab.com, quelques éléments ci-dessous.

Avant d'installer quoi ce soit, il y a plusieurs actions à faire au préalable.

Pour un Runner associé à un ou plusieurs projets spécifiques, il est nécessaire d'aller sur votre projet GitLab puis d'accéder à Settings > CI/CD > Runners > Project Runners > New Project Runner :

gitlab-ci-runners_new-project-runner

Plusieurs informations sont à compléter, notamment le système d'exploitation, les tags associés, une description, etc.

Une fois ces informations complétées, validez le tout avec le bouton Create Runner.

Stockez précieusement le token du Runner, cette information ne sera plus accessible dès lors que vous quitterez la page.

Plusieurs types d'installation

Des Runners, il y en a pour tous les goûts !

Que ce soit sur des machines virtuelles qui vont se mettre à l'échelle automatiquement ou non, sur Kubernetes, sur AWS Fargate, etc.

Un des avantages de déployer ses propres Runners sur le Cloud est aussi de bénéficier des types de machine peu coûteuses comme les instances éphémères (Preemptible ou Spot). Les réductions peuvent aller jusqu'à 91 % du prix de l'instance. Attention toutefois à exécuter avec ces Runners, des tâches qui ne sont pas critiques et qui peuvent être rejoués en cas de récupération de l'instance par le Cloud Provider.

Je vous propose de nous attarder sur le déploiement d'un Runner dit "standard" exécutant directement des conteneurs Docker et d'un autre appelé autoscaler qui se mettra à l'échelle en fonction des besoins en créant des machines virtuelles "agents" pour exécuter les tâches demandées.

Les mains dans le code

C'est parti pour déployer et installer notre Runner, pour cela j'ai choisi Terraform comme outil et Google Cloud comme environnement.

Vous pouvez retrouver le code avec le lien ci-dessous :

https://gitlab.com/filador-public/gitlab-ci-runners

Généralités

Le code est décomposé en deux modules dans le dossier modules :

  • gitlab_ci_autoscaler_runner
  • gitlab_ci_standard_runner

Le code à la racine représente la création d'un VPC et d'un sous-réseau pour héberger le déploiement de nos deux Runners ainsi qu'une Cloud NAT pour télécharger du contenu (paquets, images de conteneur, etc.) depuis Internet.

D'autre part, je vous conseille de stocker dans Secret Manager le(s) token(s) pour enregistrer vos Runners. N'oubliez pas de donner le rôle Secret Manager Secret Accessor (roles/secretmanager.secretAccessor) au compte de service associé au Runner pour qu'il puisse récupérer le token lors du startup script.

C'est parti avec le Runner standard.

Runner standard

Le Runner standard, comme dit plus haut, se charge d'exécuter des conteneurs Docker directement sur la machine virtuelle du Runner. C'est le cas le plus classique.

Le module se compose d'une ressource principale qui est cette machine virtuelle :

resource "google_compute_instance" "gitlab_runner" {
  project = var.project_id

  name         = local.gitlab_runner_name
  machine_type = var.virtual_machine_type
  zone         = var.zone
  [...]
}

On associe à cette dernière deux scripts :

  • metadata_startup_script : Initialisation jusqu'à l'enregistrement du Runner ;
  • shutdown-script : Désenregistrement du Runner lors de l'arrêt ou de la destruction de la machine.
  metadata = {
    app  = "gitlab-runner",
    type = "standard",
    shutdown-script = templatefile("${path.module}/files/gitlab-runner-unregister.sh", {
      gitlab_runner_name = local.gitlab_runner_name
    })
  }

  metadata_startup_script = templatefile(
    "${path.module}/files/gitlab-runner-register.sh", {
      gitlab_server_url         = var.gitlab_server_url
      gitlab_runner_name        = local.gitlab_runner_name
      secret_manager_token_name = var.secret_manager_token_name
  })

Si l'on regarde le script d'initialisation du Runner de plus près, modules/gitlab_ci_standard_runner/files/gitlab-runner-register.sh, plusieurs actions sont réalisées :

#!/bin/bash

# Configure GitLab Repository
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash

#  Install Docker and GitLab Runner packages
apt-get update
apt-get install docker gitlab-runner -y

# Retrieve GitLab token from Secret Manager
gitlab_runner_token=$(gcloud secrets versions access latest \
  --secret="${secret_manager_token_name}" \
  --format='get(payload.data)' | tr '_-' '/+' | base64 -d)

# Configure GCR
yes | gcloud auth configure-docker eu.gcr.io

# Remove unhealthy GitLab Runner
gitlab-runner verify --delete

# Register GitLab Runner
gitlab-runner register \
  --non-interactive \
  --name="${gitlab_runner_name}" \
  --url="https://${gitlab_server_url}/" \
  --token="$${gitlab_runner_token}" \
  --executor="docker" \
  --limit=3 \
  --docker-image="scratch:latest" \
  --docker-privileged=false \
  --docker-disable-cache=true \

Dans l'ordre, on va tout d'abord :

  • Installer les paquets Docker et GitLab Runner ;
  • Récupérer le token permettant d'enregistrer le Runner auprès de l'instance GitLab définie (attention à ne pas oublier de donner les permissions au compte de service de la machine virtuelle !) ;
  • Configurer GCR dans le cas où l'on stocke des images de conteneurs qui devront être exéucutés au sein de notre pipeline ;
  • On nettoie la configuration en supprimant les Runners qui ne sont plus enregistrés ;
  • Et finalement, on enregistre le Runner en spécifiant que c'est un Runner qui utilise Docker.

Voilà pour le premier type, on poursuit avec l'autoscaler.

Runner autoscaler

Le Runner autoscaler est différent du premier concept car ce n'est pas l'instance même qui exécutera les conteneurs et donc vos jobs GitLab CI, mais elle pilotera comme une tour de contrôle la création d'autres machines virtuelles, qui elles dérouleront vos tâches définies. Une fois les jobs terminés ou si le Runner n'est plus sollicité, les machines "agents" seront détruites.

C'est le composant Docker Machine (forké et maintenu par GitLab) qui est en charge de créer les agents. Il est d'ailleurs ajouté au niveau du startup script :

# Configure Docker Machine
curl -O "https://gitlab-docker-machine-downloads.s3.amazonaws.com/v0.16.2-gitlab.21/docker-machine-Linux-x86_64"
cp docker-machine-Linux-x86_64 /usr/local/bin/docker-machine
chmod +x /usr/local/bin/docker-machine

La commande d'enregistrement du Runner est différente :

# Register GitLab Runner
gitlab-runner register \
  --non-interactive \
  --name="${gitlab_runner_name}" \
  --url="https://${gitlab_server_url}/" \
  --token="$${gitlab_runner_token}" \
  --executor="docker+machine" \
  --limit=20 \
  --docker-image="scratch:latest" \
  --docker-privileged=false \
  --docker-disable-cache=true \
  --machine-machine-driver=google \
  --machine-machine-name="${gitlab_runner_name_suffix}-%s" \
  --machine-idle-count-min=0 \
  --machine-idle-time=400 \
  --machine-max-builds=100 \
  --machine-machine-options=google-project=${gitlab_runner_google_project} \
  --machine-machine-options=google-network=${gitlab_runner_google_network} \
  --machine-machine-options=google-subnetwork=${gitlab_runner_google_subnetwork} \
  --machine-machine-options=google-machine-type=n1-standard-1 \
  --machine-machine-options=google-service-account=${gitlab_runner_google_service_account} \
  --machine-machine-options=google-scopes=https://www.googleapis.com/auth/cloud-platform \
  --machine-machine-options=google-zone=${gitlab_runner_google_zone} \
  --machine-machine-options=google-use-internal-ip=true \
  --machine-machine-options=google-use-internal-ip-only=true \
  --machine-machine-options=google-tags=gitlab-runner \
  --machine-machine-options=google-username=cosadmin \
  --machine-machine-options=google-metadata=enable-oslogin=FALSE \
  --machine-machine-options=google-machine-image=cos-cloud/global/images/cos-stable-105-17412-156-23 \
  --machine-machine-options=google-preemptible=true \

En effet, le paramètre executor n'est plus docker mais docker+machine. Ce qui implique de configurer une multitude de paramètres supplémentaires commençant par --machine-machine.

Dans les grandes lignes, on doit spécifier le contexte dans lequel les agents vont être créés. Docker Machine pilote les agents Docker qui sont sur de nouvelles machines virtuelles créées pour l'occasion. De plus, on utilise des instances éphémères avec google-preemptible=true. Ce qui permet d'avoir un coût de facturation réduit comme dit plus haut, mais les jobs de CI/CD peuvent être interrompus à tout moment !

Ces agents seront accessibles dans le réseau où le Runner "tour de contrôle" sera créé, avec uniquement un accès via le réseau privé avec les paramètres google-use-internal-ip=true et use-internal-ip-only=true. Enfin, le système d'exploitation de ces agents sera Container-Optimized OS représenté par le paramètre google-machine-image=cos-cloud/global/images/cos-stable-105-17412-156-23.

Pourquoi ce choix ? Plusieurs avantages :

  • Docker et containerd sont déjà préinstallés, ce qui évite de perdre du temps à installer un moteur de conteneurisation ;
  • Ces images ont des paramètres de sécurité avancés ce qui réduit la surface d'attaque : une partie du système est en lecture seule ;
  • Mises à jour de sécurité automatiques. Peu intéressant dans notre cas où les agents seront créés et détruits sans arrêt, mais c'est à signaler.

Avec ces différents changements, il y a quelques ressources additionnelles côté Terraform. Notamment dans le fichier modules/gitlab_ci_autoscaler_runner/iam.tf où l'on crée un compte de service associé à notre Runner.

Le rôle personnalisé dockerMachine.admin permettra de mettre à jour les règles de pare-feu et gérer la communication entre la tour de contrôle (notre Runner) et les agents. D'autres rôles sont ajoutés via ce bloc de code :

resource "google_project_iam_member" "gitlab_runner_autoscaler" {
  for_each = var.gitlab_runner_autoscaler_roles

  project = var.project_id

  role   = each.value
  member = "serviceAccount:${google_service_account.gitlab_runner_autoscaler.email}"
}

Tels que roles/compute.instanceAdmin.v1 et roles/iap.tunnelResourceAccessor pour créer les machines virtuelles "agents" et faire en sorte que le Runner soit en mesure d'atteindre ces machines en SSH pour les configurer.

Une fois le Runner déployé et enregistré au sein de GitLab. Voici ce qui se passe côté Google Cloud lors de l'exécution d'un job de CI/CD :

gitlab-ci-runners_autoscaler-google-cloud

Un agent est automatiquement déployé par notre tour de contrôle et exécute l'ensemble des tâches demandées !

Axes d'amélioration

Pour aller plus loin et dans le but de rendre les choses plus fluides pour chacun des types de Runner, il est préférable de créer une image prête à l'emploi avec l'ensemble des binaires disponibles (Docker, Docker Machine, GitLab Runner) avec comme objectif de gagner du temps lors du déploiement.

Pour cela, je vous recommande d'utiliser des outils comme Packer et Ansible pour créer une image de base et d'utiliser le startup script uniquement pour enregistrer le Runner vers l'instance GitLab.

Deuxième point à ne pas négliger est la mise en place d'un cache partagé pour votre flotte de Runners dans le but d'éviter de retélécharger des contenus, dépendances, etc. à chaque exécution de vos jobs GitLab CI.

Pour Google Cloud, la documentation évoque les différents paramètres à configurer. À noter que vous pouvez utiliser le compte de service de la machine virtuelle pour accéder au cache Google Cloud Storage, ce qui évite de stocker une clé JSON au sein de la machine.

C'est déjà la fin...

L'univers GitLab est très riche comme vous pouvez le constater avec énormément de façon de configurer sa pipeline au sein de GitLab CI, mais aussi avec plusieurs types d'installation de Runner. À vous de sélectionner celui qui convient le mieux à votre cas d'utilisation.

N'oubliez pas non plus de vous intéresser dès le départ à l'aspect financier de vos Runners en utilisant des types de machine appropriés ou en utilisant les notions d'instances éphémères.

Voilà qui clôture cette série d'articles sur GitLab CI, j'espère que vous avez appris ou redécouvert certains concepts.