Halte à l'obsolescence logicielle avec Renovate

Introduction

Arriver sur un projet et découvrir que tous les outils et toutes les dépendances sont obsolètes et avec plusieurs versions majeures de retard vous angoisse ?

Ne vous inquiétez pas, vous n’êtes pas seuls, et c’est d’autant plus dommage qu’il existe des outils relativement simples à mettre en place pour éviter ça et garder vos projets à jour tout au long de leur vie. Aujourd’hui, nous vous proposons de découvrir l’un d’entre eux : Renovate.

À la fin de votre lecture, vous vous surprendrez très probablement à vouloir le mettre en place sur tous vos projets. Cependant, une petite mise en garde s’impose : ce type d’outil nécessite que vos projets possèdent une couverture de tests suffisante et une solution d’automatisation via des pipelines. En effet, mettre en place Renovate sur un projet sans ces pré-requis risquerait de rendre ce dernier inutilisable à cause de mises à jour cassantes non contrôlées.

Mettre à jour ses dépendances

Au-delà de l’expérience développeur qui est bien meilleure quand les technologies sur un projet sont à jour, permettant de bénéficier des dernières fonctionnalités et corrections de bugs, mettre à jour les dépendances est aussi un enjeu de sécurité.

En effet, personne n’est à l’abri d’une nouvelle CVE (Common Vulnerabilities and Exposures) avec un score critique sur l’une des dépendances de son projet. Dans ce cas, 2 solutions s’offrent à vous : mettre à jour votre dépendance pour intégrer un patch qui corrige la faille - et c’est d’autant plus indolore si vous étiez déjà à jour - ou alors faire une étude d’impact de cette faille, afin de savoir si vous êtes vulnérable ou non, et le cas échéant, corriger votre application pour rendre la faille inexploitable. Vous aussi vous trouvez que la première solution est plus simple et moins chronophage ? Alors voyons comment transformer ce rêve en réalité.

Les outils à disposition

Dependabot

Le premier outil, sans doute le plus connu, est Dependabot. Ce dernier est directement intégré à Github et permet de proposer d‘ouvrir des PRs/MRs (Pull Requests / Merge Requests) pour mettre à jour chacune de vos dépendances lorsqu’une nouvelle version est disponible.

Cet outil permet notamment l’affichage des changelogs des dépendances dans les PRs et des mises à jour groupées, mais son principal inconvénient est sa non-compatibilité avec les autres plateformes telles que GitLab ou BitBucket par exemple (il existe des projets open source pour faire fonctionner Dependabot sur d’autres plateformes que Github, mais ces derniers ne sont pas officiels). Par ailleurs, il ne propose pas non plus de tableau de bord de suivi des différentes dépendances d’un projet, contrairement à son principal concurrent : Renovate.

Renovate

Le second outil, le meilleur (en toute objectivité), est Renovate. Outil opensource, il fait globalement la même chose que Dependabot, c'est-à-dire ouvrir des PRs/MRs sur vos projets pour vous proposer des mises à jour, mais il est beaucoup plus flexible et paramétrable. En plus de supporter de nombreuses plateformes (github, gitlab, bitbucket, gitea, local, …), Renovate propose un tableau de bord de suivi des différentes dépendances d’un projet.

Son principal défaut ? Sa documentation qui, à date, est assez rebutante et peu simple d’utilisation pour quelqu’un qui voudrait se jeter à l’eau. Mais qu’importe, nous allons vous expliquer les principaux concepts et vous donner toutes les clés pour vous y retrouver et affronter sereinement votre première implémentation de Renovate.

Renovate - Concepts

Platform

La philosophie de Renovate est d’être agnostique à l’égard de la plateforme utilisée (github, gitlab, bitbucket, etc), tout en tirant parti des fonctionnalités spécifiques à chaque plateforme.

Lors de la configuration, il faut donc spécifier la plateforme sur laquelle le projet est hébergé afin que Renovate puisse adapter son comportement et interagir avec cette dernière : ouverture de PRs/MRs, affectation de reviewers, etc.

Exemples de plateformes : github, gitlab, bitbucket, gitea, local.

Managers

Les managers, ou package managers, permettent de gérer différents types de packages ou dépendances, aussi bien traditionnels comme npm ou composer, que des concepts moins traditionnels comme des fichiers de configuration de pipelines / workflows.

Renovate gère donc vos différentes dépendances à l’aide de ces managers.

Exemples de managers : npm, composer, terraform, gitlabci, dockerfile, regex (ce qui permet à Renovate d’être très flexible).

Datasources

Une fois que les managers de Renovate ont analysé les fichiers et extrait les dépendances, ils attribuent une source de données à chaque dépendance extraite.

La source de données indique à Renovate comment rechercher de nouvelles versions.

Eh oui, bien que Renovate soit un outil magique, il ne fait pas de miracle et il a besoin d’un peu d’aide pour vous proposer les mises à jour. Comme nous le verrons par la suite, il est tout à fait possible de configurer une source de données afin d’utiliser des registres privés.

Exemples de datasources : npm, packagist, terraform-provider, terraform-module, gitlab-releases, gitlab-tags, docker, custom (si si, Renovate est vraiment très flexible).

Versioning

Une fois que les managers ont extrait les dépendances et que les sources de données ont permis d’identifier les versions disponibles, Renovate a besoin d’un schéma de gestion de versions, ou versioning, afin d’effectuer le tri des versions identifiées. En effet, chaque manager possède sa propre norme afin de gérer ses versions (parce que c’est plus drôle que d’utiliser tous la même norme voyons !).

Certaines sources de données ont un versioning par défaut, comme par exemple semver pour la datasource go, mais ce n’est pas le cas de toutes les sources de données, et il faut parfois l’indiquer à Renovate.

Exemples de versioning : semver, npm, hashicorp, docker, loose (traduire par “best effort”), regex (nous insistons, mais Renovate est vraiment très flexible).

Renovate - Tableau de bord

Le tableau de bord est une des fonctionnalités majeures de Renovate. Concrètement, il s’agit d’une issue perpétuelle que Renovate va créer et tenir à jour avec un récapitulatif de tout son travail : la liste des dépendances détectées et des MRs/PRs ouvertes, celles que vous avez fermées car vous ne vouliez pas faire ces mises à jour, celles qui ont eu un blocage, les erreurs rencontrées, etc.

C’est notamment au travers de ce tableau de bord que, entre 2 exécutions de Renovate, vous allez pouvoir interagir avec ce dernier pour lui demander de rebase certaines ou l’ensemble des PRs/MRs, rouvrir celles auparavant fermées, ou encore cliquer sur le lien vers ces PRs/MRs pour les étudier de plus près.

Voici à quoi ressemble un tableau de bord Renovate sur GitLab :

Renovate - Pull Requests / Merge Requests

Depuis le début, nous vous parlons de ces fameuses PRs/MRs qui vont révolutionner votre vie de SRE et que Renovate ouvre pour vous, mais concrètement, à quoi ressemblent-elles ?

Chaque PR/MR contient la liste de la / des dépendances que Renovate vous propose de mettre à jour (nous verrons en effet plus tard qu’il est possible de grouper certaines dépendances entre elles), mais également le changelog entre votre version et celle proposée par renovate, ainsi qu’un lien vers la source de la dépendance.

Ainsi, votre seul et unique travail sera de vérifier le changement proposé et le changelog associé, vérifier le cas échéant que la pipeline / le workflow soit valide et sans erreur, et merger. Nous vous avions prévenu que Renovate, ça change la vie !

Renovate - Runner et configuration

La configuration de Renovate se distingue en 2 parties.

Tout d’abord, la partie runner qui concerne l’exécution de Renovate. Cette configuration est commune à tous les projets qui vont être gérés par cette instance de Renovate.

Ensuite, la partie projet, qui va être la configuration propre à un projet pour la mise à jour de ses dépendances (les labels des PR/MR, les managers à ignorer, la politique de séparation/groupement des mise à jour, etc…) et qui va être décrite au sein de chaque projet. Nous reviendrons plus tard sur cette configuration, qui peut être différente d’un projet à l’autre au sein d’une même instanciation de Renovate.

Dans un premier temps, concentrons-nous sur la configuration du runner.

Runner Renovate GitLab

Dans cette section, nous aborderons la mise en place de Renovate sur la plateforme GitLab.

Il existe 2 manières de l’exécuter :

  • Soit en configurant un runner Renovate dans chaque projet au sein duquel vous souhaitez voir Renovate vous proposer des mises à jour. Cette approche entraîne une duplication des configurations de la partie Runner,
  • Soit en configurant un Runner commun à plusieurs projets. Cette approche est par ailleurs celle recommandée par Renovate et celle que nous allons vous détailler.

Pour configurer Renovate avec un Runner commun, il est nécessaire de créer un projet GitLab dédié à ce runner. Il faudra alors définir une pipeline Gitlab-CI et créer une scheduled pipeline afin de l’exécuter périodiquement. Cette pipeline est disponible dans ce projet d'exemple.

Vous aurez ensuite besoin de configurer la variable RENOVATE_TOKEN avec un Group Access Token (ou un Personal Access Token) afin de permettre à Renovate de créer des MR/PR sur les différents projets qu’il va scanner.

Ajouter un token github.com

Comme nous l’avons vu précédemment, Renovate est capable de récupérer le changelog des dépendances mises à jour. Pour ce faire, il interroge l’API de github.com, la grande majorité des dépendances étant hébergée sur cette plateforme.

Afin d’augmenter la limite sur le nombre d’appels à l’API de GitHub, il est fortement recommandé de créer un token d’accès personnel de type classic et ayant un scope minimal “public_repo”. Ce token peut ensuite être passé à Renovate via la variable d’environnement GITHUB_COM_TOKEN.

Ce token est également utilisé par la datasource github-tags.

Les options de configuration du runner Renovate

Il existe une multitude d’options de personnalisation de la configuration du runner Renovate que l’on peut retrouver ici. Sans être exhaustifs, nous allons vous en présenter les principales.

Autodiscover

Cette option permet de configurer Renovate pour qu’il aille automatiquement scanner les repositories auxquels il a accès afin de s’exécuter dessus et ouvrir des PR/MR pour mettre à jour les dépendances.

Cette option s’active grâce au paramètre autodiscover. Cependant, nous vous recommandons vivement d’appliquer des filtres afin de définir un périmètre précis sur lequel Renovate va agir. Cela peut être une liste de repositories, une liste de groups Gitlab. Cette configuration se fait grâce aux options autodiscoverFilter, autodiscoverNamespaces ou encore autodiscoverProjects.

Onboarding

L’Onboarding permet à Renovate d’ouvrir une première MR/PR sur un projet s’il ne trouve pas de configuration Renovate. Cette option est particulièrement intéressante quand elle est couplée à celle d’autodiscover : ainsi, chaque fois qu’un nouveau projet est créé dans le scope d’autodiscover de Renovate, ce dernier va automatiquement ouvrir une MR/PR pour ajouter un fichier de configuration Renovate. Cette option s’active grâce au champ onboarding qu’il faut configurer à true.

Le contenu du fichier de configuration Renovate qui va être proposé par la MR/PR d’onboarding est lui aussi configurable via le champ onboardingConfig. Il peut ainsi être intéressant de proposer une configuration standard via cette option que les projets viendront ensuite surcharger en fonction de leurs besoins.

DetectHostRulesFromEnv

Cette option est nécessaire pour permettre aux projets de définir des hostRules via des variables d’environnement. Cela est particulièrement utile pour éviter de mettre des informations de connexion à des registries privés, tels que des tokens, en dur dans la configuration projet. Nous donnerons un exemple plus tard dans cet article sur comment utiliser cette fonctionnalité.

Pour activer l'option, il suffit de mettre le champ detectHostRulesFromEnv à true.

Paralléliser les jobs Renovate par projet

Lorsque le nombre de projets gérés par Renovate augmente, la durée du job augmente. Il devient alors intéressant d’exécuter un job par repository afin de paralléliser le travail sur plusieurs runners GitLab, mais aussi de pouvoir relancer un job précis sans devoir tout relancer, ou encore d’éviter qu’une erreur sur un repository bloque l’exécution des suivants.

La documentation du runner Renovate GitLab redirige vers un exemple de configuration permettant de paralléliser cette exécution, que nous allons détailler.

L’idée est d’utiliser le principe des dynamic child pipelines de GitLab, avec un premier job qui va lister l’ensemble des repositories accessibles grâce au token présent dans la variable d’environnement RENOVATE_TOKEN, puis générer un fichier GitLab CI à partir de cette liste. Ce fichier est ensuite utilisé dans le second job afin de déclencher une dynamic child pipeline.

Pour vous aider à mieux comprendre, voici un exemple de fichier .gitlab-ci.yml utilisé pour le runner Renovate :

include:
 - project: 'renovate-bot/renovate-runner'
   file: '/templates/renovate.gitlab-ci.yml'
   ref: v17.227.1


stages:
 - deploy


renovate:
 variables:
   RENOVATE_AUTODISCOVER: "true"
 script:
   - renovate --write-discovered-repos=renovate-repos.json
   - sed "s~###RENOVATE_REPOS###~$(cat renovate-repos.json)~" templates/.gitlab-ci.yml > .gitlab-renovate-repos.yml
 artifacts:
   expire_in: 1 day
   paths:
     - .gitlab-renovate-repos.yml


renovate:repos:
 stage: deploy
 needs:
   - renovate
 inherit:
   variables: false
 trigger:
   include:
     - job: renovate
       artifact: .gitlab-renovate-repos.yml
 rules:
   - !reference [renovate, rules]

Le premier job renovate utilise le template templates/.gitlab-ci.yml suivant :

include:
 - project: 'renovate-bot/renovate-runner'
   file: '/templates/renovate.gitlab-ci.yml'
   ref: v17.227.1


stages:
 - deploy


variables:
 RENOVATE_ONBOARDING_CONFIG: '{"$$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:recommended", ":disableRateLimiting"], "labels": ["renovate"] }'
 RENOVATE_DETECT_HOST_RULES_FROM_ENV: "true"


renovate:
 parallel:
   matrix:
     - RENOVATE_EXTRA_FLAGS: ###RENOVATE_REPOS###
 resource_group: $RENOVATE_EXTRA_FLAGS
 rules:
   - if: $CI_PIPELINE_SOURCE == "parent_pipeline"

Le second job renovate:repos déclenche donc un nouveau pipeline enfant composé d’un job renovate en mode parallel.matrix, ce qui a pour effet de déclencher un job par repository accessible.

Maintenant que nous avons vu comment exécuter et configurer le runner Renovate, voyons comment configurer le comportement de ce dernier au niveau de chaque repository.

Renovate - Repositories et configuration

Pour activer Renovate sur un repository, il suffit d’avoir un fichier de configuration à la racine de ce dernier puis de lancer un pipeline exécutant Renovate sur ce repository. Ce fichier s’appelle généralement renovate.json, mais Renovate reconnaît plusieurs noms de fichiers par défaut.

La liste des options de configuration étant relativement riche, nous allons détailler celles qui nous paraissent les plus intéressantes et les plus utilisées.

Configuration recommandée par défaut

Renovate propose une configuration recommandée par défaut, qui peut être utilisée ainsi :

{
 "$schema": "https://docs.renovatebot.com/renovate-schema.json",
 "extends": [
   "config:recommended"
 ]
}

Cette configuration recommandée inclut notamment l’activation du tableau de bord de suivi, le fait d’ignorer les mises à jour des dépendances dans les dossiers contenant les dépendances externes et les tests (node_modules, vendor, test(s), …) ou encore le groupement de certaines mises à jours de dépendances.

Les presets

Renovate apporte le concept de presets, qui sont des configurations réutilisables à travers différents repositories, et propose un ensemble de presets par défaut ainsi que d’autres presets en tout genre afin de répondre à des besoins spécifiques et qui reviennent souvent.

Il est par exemple possible d’utiliser le preset disableRateLimiting afin d’éviter d’être limité dans le nombre de PRs/MRs que Renovate peut créer, en ajoutant une ligne à notre configuration :

{
 "$schema": "https://docs.renovatebot.com/renovate-schema.json",
 "extends": [
   "config:recommended",
   ":disableRateLimiting"
 ]
}

Vous remarquerez que la configuration recommandée par défaut est elle-même un preset.

Nous vous recommandons de prendre connaissance des presets proposés par Renovate, ces derniers permettant d’adapter le comportement de Renovate à votre besoin au travers de configurations clé en main et facile à mettre en place (littéralement une ligne !).

Par exemple, Renovate propose le preset customManagers:gitlabPipelineVersions qui permet de détecter des versions de dépendances présentes dans des variables GitLab CI à l’aide d’un simple commentaire précisant la datasource et le nom de la dépendance :

my-job:
 variables:
   # renovate: datasource=npm depName=semantic-release
   SEMANTIC_RELEASE_VERSION: "23.1.1"

De même, il existe le preset customManagers:dockerfileVersions qui permet de faire la même chose mais dans des arguments de fichier Dockerfile :

FROM node:20.13.1-slim

# renovate: datasource=npm depName=@semantic-release/gitlab
ARG SEMANTIC_RELEASE_GITLAB_VERSION=13.1.0

Maintenant que vous connaissez le concept des presets et que vous savez les activer, voyons comment configurer le comportement de Renovate à l’aide des options de configuration à disposition.

Les options de configuration les plus courantes

Parmi les options de configuration les plus courantes, on retrouve l’ajout automatique de labels sur les PRs/MRs, l’ajout d’assignees, de reviewers, ou encore la personnalisation des noms des commits et PRs/MRs.

À ce niveau, la documentation est plutôt bien faite et donne des exemples d’utilisation de ces différentes options de configuration.

Par exemple, pour ajouter un label “renovate” aux PRs/MRs créées par Renovate, il suffit d’ajouter la ligne suivante à notre fichier de configuration renovate.json :

{
 "labels": ["renovate"]
}

Il est également possible de modifier ces options en fonction de critères comme par exemple le manager ou la datasource utilisée. Ceci est rendu possible grâce aux package rules. Par exemple, si l’on veut ajouter un label “docker” aux PRs/MRs liées au manager dockerfile, il faut utiliser la configuration suivante :

{
 "labels": ["renovate"],
 "packageRules": [
   {
     "matchManagers": ["dockerfile"],
     "labels": ["renovate", "docker"]
   }
 ]
}

Un autre exemple de configuration concerne la personnalisation des noms des commits. Par défaut, Renovate utilise “chore(deps):”, de la forme “semanticCommitType(semanticCommitScope):”, mais il est possible de le modifier, ce qui peut être pratique lorsque l’on couple Renovate avec Semantic Release. Par exemple, si l’on souhaite utiliser le type sémantique “build” pour les managers gomod et dockerfile, on peut utiliser la configuration suivante :

{
 "packageRules": [
   {
     "matchManagers": ["gomod", "dockerfile"],
     "semanticCommitType": "build"
   }
 ]
}

Utiliser des registres privés

Dans certains contextes, les dépendances se trouvent dans des registres privés. Dans ce cas, Renovate propose l’option registryUrls permettant d’ajouter des sources de données :

{
 "packageRules": [
   {
     "matchDatasources": ["docker"],
     "registryUrls": ["https://docker.mycompany.domain"]
   }
 ]
}

Cependant, il est très fréquent que ces registres privés nécessitent une authentification afin de lister les versions disponibles. Il est alors possible d’utiliser l’option detectHostRulesFromEnv. Attention cependant, cette option sert à configurer le runner Renovate, et non le repository, étant donné que c’est bien le runner qui va interroger le registre privé.

Une fois ce mode activé, il est possible d’utiliser des variables d’environnement afin de passer au runner Renovate les identifiants permettant de s’authentifier auprès des registres privés.

Ainsi, pour se connecter au registre privé de notre exemple ci-dessus, il suffit d’utiliser les variables DOCKER_DOCKER_MYCOMPANY_DOMAIN_USERNAME et DOCKER_DOCKER_MYCOMPANY_DOMAIN_PASSWORD.

D’autres exemples et explications sont disponibles dans la documentation.

Grouper des dépendances dans une même MR/PR

Dans certains cas, il peut être intéressant de grouper la mise à jour de dépendances dans une même MR/PR. Cela peut se faire avec l’option groupName.

Par exemple, il est possible de grouper les dépendances nommées golang et go dans une même MR/PR ainsi :

{
 "packageRules": [
   {
     "matchDepNames": ["golang", "go"],
     "groupName": "go"
   }
 ]
}

Les managers custom

Il arrive parfois que l’on ait besoin de créer nos propres managers personnalisés, les managers existants ne couvrant pas tous les besoins.

Un exemple d’utilisation serait de vouloir mettre à jour la documentation, sous forme de fichier README.md, en même temps que la dépendance. En s’inspirant du preset customManagers:gitlabPipelineVersions, qui est en fait un customManager de type regex, on peut plus ou moins facilement (ah… les regex…) l’adapter à notre besoin :

{
 "customManagers": [
   {
     "customType": "regex",
     "description": "Update versions in README.md",
     "fileMatch": [
       "README\\.md$"
     ],
     "matchStrings": [
       "<!-- renovate: datasource=(?<datasource>[a-z-.]+?) depName=(?<depName>[^\\s]+?)(?: (?:packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))? -->\\s+Version actuelle\\s?:\\s*[\"']?(?<currentValue>.+?)[\"']?\\s"
     ]
   }
 ]
}

Ainsi, il nous suffit d'ajouter un commentaire markdown dans notre fichier README.md pour que Renovate mette à jour la version en même temps (dans la même PR/MR) que la dépendance associée :

## Image docker golang
<!-- renovate: datasource=docker depName=golang -->
Version actuelle : 1.22.3

Ici, l’exemple est assez simpliste, mais cela vous donne un aperçu de ce qu’il est possible de faire en termes de personnalisation.

Le versioning regex

Une autre fonctionnalité qui fait de Renovate un outil très flexible et personnalisable est le versioning de type regex.

En effet, certains contextes nécessitent l’utilisation de versions non conventionnelles. Dans ce cas, il est possible d’indiquer à Renovate le format de versioning utilisé à l’aide d’une regex :

my-job:
 variables:
   # renovate: datasource=docker depName=my-company.com/docker versioning=regex:^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)-my-company-(?<build>\d+)$
   DOCKER_VERSION: 26.1.2-my-company-1

Dans cet exemple, Renovate va être en mesure d’augmenter les numéros de versions majeure, mineure, patch et build, malgré le format non conventionnel du tag de l’image Docker.

Les post upgrade tasks

Renovate propose également une fonctionnalité puissante permettant de lui faire exécuter du code après la mise à jour des dépendances et avant de commit et push sur la MR/PR associée. Un cas d’usage peut par exemple être la mise à jour de documentation générée via un outil tiers, tels que terraform-docs ou helm-docs.

Dans ce cas, il faudra effectuer de la configuration à 2 endroits :

  • Côté runner : il faut spécifier au runner quelles sont les commandes autorisées en spécifiant le champ allowedPostUpgradeCommands
  • Côté projet : il faut indiquer quelle commande ou script exécuter après la mise à jour des dépendances avec le champ postUpgradeTasks

Par exemple, voici la configuration nécessaire pour exécuter un script nommé renovate-docs.sh.

Côté runner :

"allowedPostUpgradeCommands": ["./renovate-docs.sh"]

Côté projet :

"postUpgradeTasks": {
    "commands": [
      "./renovate-docs.sh"
    ],
    "fileFilters": [
      "**/README.md"
    ],
    "executionMode": "branch"
  }

Dans cet exemple, seules les modifications du fichier README.md seront prises en compte.

Activer le merge automatique

Maintenant que vous avez correctement configuré Renovate et que vous avez suffisamment de tests sur votre projet pour être serein, pourquoi ne pas activer le merge automatique ?

Peut-être pas dans tous les cas, mais peut-être pour certains managers, pour les versions patch ou mineures, avec peu de risques ? Comme ça, votre travail sera encore plus simple : vous n’aurez plus rien à faire !

Ça vous tente ? Rien de plus simple, il suffit de rajouter l’option automerge dans votre configuration de repository :

"packageRules": [
   {
     "matchUpdateTypes": ["minor", "patch", "pin", "digest"],
     "automerge": true
   }
 ]

Conclusion

Vous avez désormais toutes les clés en main pour maintenir vos projets à jour. Si jamais vous êtes encore un peu frileux à l’idée et qu’une démonstration en live vous aiderait à vous décider, vous pouvez aller visionner le replay de notre live twitch où nous partons de zéro et où nous configurons renovate sur différents repos.

Sources