Nettoyer le registre de conteneur GitLab après la fusion

Utiliser le registre d'image Docker de Gitlab est une bonne chose. Mais il arrive régulièrement que l'espace disque soit vite saturé. Voyons comme il est possible d'éviter cela.

Pour un projet d'un client, nous avons mis en place un processus d'intégration continue (en anglais CI pour Continuous Intégration) et de déploiement continue (en anglais CD pour Continuous Delivery).

Dans le cadre du déploiement continue, nous utilisons Docker pour nous permettre le déploiement d'un environnement de test par fonctionnalité à tester. Pour chacune de ces branches, il est nécessaire de construire une image Docker contenant tous les outils nécessaires au bon fonctionnement de l'application PHP avec le code de l'application.

Chaque image est générée par le processus d'intégration continue qui enregistre en suite l'image dans un registre. Dans notre cas, c'est GitLab qui gère le service de registre pour les images Docker.

Bien évidement, une fois l'image Docker construite, elle est déployée sur les serveurs de tests mais ce n'est pas le sujet de cette article.

Rapidement, nous avons été confronté a un problème d'espace disque manquant sur le registre géré par GitLab. Il faut penser à supprimer les images Docker dans le registre à la fin du développement de la fonctionnalité. C'est manuel, c'est pénible et surtout il faut y penser. C'est encore plus contraignant quand il faut rechercher les images Docker inutiles.

C'est pour cela que j'ai décide d'automatiser la suppression des images et le nettoyage complet des environnements lors de l’arrêt d'un environnement de test.

Etape de nettoyage dans la CI GitLab

Dans la CI GitLab, il est possible d'exécuter des commandes à différents moments selon ce qu'il se passe sur le projet. Lors de l'ouverture d'une demande de fusion (pull request ou merge request selon les outils) il est possible de déployer automatiquement l'application sur les serveurs de test et de déclarer un environnement.

Il est également possible d'exécuter des actions après la fusion de code dans la branche principale. C'est à ce moment que l'on détruit les environnements de test déployés plus tôt et qu'il est opportun de nettoyer le registre GitLab des images inutiles.

Cela ce traduit par cette configuration dans le fichier .gitlab-ci.yml situé à la racine du projet :

 

   

stop_staging:
    environment:
        name: ${CI_BUILD_REF_SLUG}
        action: stop
    script:
      - # Le script de nettoyage ici 
   

Ce qu'il nous faut...

Pour réaliser cette action, nous avons besoin d'un TOKEN d'identification d'un utilisateur (ayant les droits nécessaires sur le registre du projet) de GitLab pour appeler les API de GitLab. Nous avons également besoin de l'outil en ligne de commande jq  pour extraire des données dans du JSON et de curl pour réaliser les appels HTTP.

Pour obtenir le token, aller sur votre page de profil GitLab (cliquer sur votre avatar, puis "Edit Profile") puis cliquer sur le menu "Access Token".

Maintenant cliquer sur le bouton "New Token". Un formulaire s'affiche pour configurer le token. Nommer le "CI GitLab Registry Clean", puis cocher "API" dans la liste des "Scopes"

 

Formulaire d'ajout d'un token d'authentification pour l'utilisation des API de GitLab.

 

Quelles API GitLab appeler ?

 

L'URL qui nous intéressent est celle nous permettant de supprimer un tag dans le registre des images de GitLab. Il s'agit de l'URL 

DELETE /projects/:id/registry/repositories/:repository_id/tags/:tag_name
 

Quels sont les paramètres nécessaires pour l'appel de cette URL:

  • :id correspond à l'ID GitLab du projet. Cette information est disponible dans la variable d'environnement prédéfinie CI_PROJECT_ID lors de l'exécution du job de la CI.
  • :repository_id correspond à l'ID du repository, pour le récupérer il est nécessaire de réaliser une autre requête à l'API GitLab pour lister les repositories du projet
  • :tag_name correspond au tag à supprimer. Par commodité, le tag utilisé lors de la construction de l'image est la valeur de la variable d’environnement CI_COMMIT_REF_SLUG .

Voyons maintenant comment récupérer l'identifiant du registre pour le projet via cette URL:

GET /projects/:id/registry/repositories

Le seul paramètre nécessaire est l'ID GitLab du projet que nous avons dans la variable d'environnement CI_PROJECT_ID .

Pour tous les appels à l'API, nous avons besoin de deux autres éléments:

  • Le nom de domaine de GitLab, qui est automatiquement disponible dans la variable d’environnement CI_SERVER_URL
  • Le token d'authentification généré plus haut et qui a été ajouté dans les variables d'environnement de la CI sous le nom REGISTRY_GARBAGE_COLLECTOR_TOKEN

 

Comment réaliser les appels à l'API ?

 

Réalisons maintenant la première requête avec cURL :

curl --silent --header "PRIVATE-TOKEN: ${REGISTRY_GARBAGE_COLLECTOR_TOKEN}" ${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/registry/repositories

La réponse est un texte JSON dans lequel il faut chercher. C'est là que l'utilitaire jq est utile. Il permet de rechercher des données dans un JSON selon des critères.

   

[
  {
    "id": 1,
    "name": "",
    "path": "group/project",
    "project_id": 9,
    "location": "gitlab.example.com:5000/group/project",
    "created_at": "2019-01-10T13:38:57.391Z",
    "cleanup_policy_started_at": "2020-01-10T15:40:57.391Z"
  },
  {
    "id": 2,
    "name": "releases",
    "path": "group/project/releases",
    "project_id": 9,
    "location": "gitlab.example.com:5000/group/project/releases",
    "created_at": "2019-01-10T13:39:08.229Z",
    "cleanup_policy_started_at": "2020-08-17T03:12:35.489Z"
  }
] 
   

Le registre qui nous intéresse est celui qui correspond au projet "group/project". Donc nous devons récupérer l'ID 1.

La commande jq suivante permet d'extraire l'ID :

jq ".[] | select(.path==\"group/project\") | .id"

Chaque opération est séparée par un pipe "|", voici la lecture de la commande :

  • .[] permet de sélectionner le contenu du tableau de premier niveau.
  • select(.path==\"group/project\") permet de sélectionner l'objet dont la propriété "path " qui est égale à une valeur fixe.
  • .id permet de retourne la valeur de la propriété "id " de l'objet sélectionné à l'étape précédente.

Maintenant enchainons ces étapes et enregistrons la valeur de retour dans une variable d'environnement : 

export REGISTRY_ID=`curl --silent --header "PRIVATE-TOKEN: ${REGISTRY_GARBAGE_COLLECTOR_TOKEN}" ${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/registry/repositories | jq ".[] | select(.path==\"$CI_PROJECT_PATH\") | .id"`

Nous disposons de toutes les informations pour supprimer le tag dans le registre exécutons la requête avec cURL :

curl --silent --request DELETE --header "PRIVATE-TOKEN: $REGISTRY_GARBAGE_COLLECTOR_TOKEN" ${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/registry/repositories/${REGISTRY_ID}/tags/${CI_COMMIT_REF_SLUG}

Que ce passe t'il en cas d'erreur ?

Actuellement, l’enchaînement des deux requêtes fonctionne de manière très optimiste. Ajoutons une gestion d'erreur.

Le premier cas est l'absence de la variable d’environnement REGISTRY_GARBAGE_COLLECTOR_TOKEN .

Si le le token est bien présent, il est possible que la récupération de l'ID du registre ne fonctionne pas. Pour cela nous allons tester la variable REGISTRY_ID .

Voici le code complet :

   

stop_env_test:
    environment:
        name: ${CI_BUILD_REF_SLUG}
        action: stop
    script:
        - |
            if ! [[ -z $REGISTRY_GARBAGE_COLLECTOR_TOKEN ]]; then
              # Installer ici jq et curl
              export REGISTRY_ID=`curl --silent --header "PRIVATE-TOKEN: ${REGISTRY_GARBAGE_COLLECTOR_TOKEN}" ${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/registry/repositories | jq ".[] | select(.path==\"$CI_PROJECT_PATH\") | .id"`
              if ! [[ -z $REGISTRY_ID ]]; then
                curl --silent --request DELETE --header "PRIVATE-TOKEN: $REGISTRY_GARBAGE_COLLECTOR_TOKEN" ${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/registry/repositories/${REGISTRY_ID}/tags/${CI_COMMIT_REF_SLUG}
              fi
            fi 
   

Conclusion

Même si GitLab ne prévoit pas nativement le nettoyage du registre, il est possible de réaliser cette action automatiquement dans la CI.

Il reste un dernier point, la suppression du tag dans  le registre des images docker de GitLab ne permet pas la récupération réelle de l'espace disque. Vous devez exécuter périodiquement la commande gitlab-ctl registry-garbage-collect pour récupérer l'espace disque.

Besoin d'aide ? Une question ? Les commentaires sont les bienvenus !