Comment, en refusant de céder aux solutions simplifiées des outils dédiés à l’expérience développeur·euse, on peut finir par investir du temps à améliorer cette même expérience.

Mes collègues et moi discutons parfois de l’expérience développeur·euse (Developer Experience ou DX), et il faut bien admettre que les échanges portent souvent sur ses limites. Selon nous, l’un des problèmes majeurs réside dans le fait que la multitude d’outils disponibles sert avant tout à améliorer la productivité, plutôt qu’à enrichir l’apprentissage et donc l’expérience de la personne qui développe.

Ce constat en tête, nous devions démarrer un nouveau projet développé en Go, reposant sur une base de données MariaDB. Nous avons donc choisi de laisser chaque développeur·euse installer ellui-même les prérequis sur son système, comme Go, plutôt que de fournir un environnement pré-configuré dans un conteneur Docker.

Cependant, si nous sommes d’accord sur de nombreux points, je dois reconnaître que, personnellement, je n’aime pas devoir installer un serveur de base de données spécifique pour travailler sur un projet. Mais je ne veux pas non plus imposer l’utilisation de Docker à tout le monde.

Enfin, je tiens à conserver la simplicité d’une commande unique pour lancer tout mon environnement de développement.

Cet article décrit les solutions que j’ai mises en place pour améliorer mon expérience développeur, tout en restant fidèle à l’idée d’un projet qui encourage une compréhension approfondie de l’environnement d’exécution de l’application.

Juste quelques recettes

Lorsqu’on travaille sur un projet informatique, il y a toujours une liste plus ou moins longue de commandes à exécuter régulièrement : installer ou mettre à jour les dépendances, lancer ou arrêter un ou plusieurs serveurs, effectuer une migration de la base de données, charger des données de développement, exécuter les tests, etc.

Pour qu’une nouvelle personne rejoignant le projet puisse s’y retrouver, il faut qu’elle comprenne ces tâches. Cela nécessite au minimum une documentation. Dans une démarche d’apprentissage, on pourrait même considérer que documenter ces tâches est suffisant. Un peu comme recommander d’installer Arch Linux pour sa documentation exemplaire, et parce qu’elle offre une maîtrise complète d’un système que l’on installe à la main, plutôt que d’opter pour une Debian via son installateur automatisé. L’argument se tient. Pourtant, je reste encore sur Ubuntu.

J’aime donc bien avoir un utilitaire pour automatiser certaines de ces tâches : elles sont décrites dans un fichier de recettes, ce qui constitue en soit une documentation, mais en plus elles peuvent être exécutées d’une simple commande.

En temps normal, j’utilise un Makefile, mais pour ce projet, j’ai préféré Just, découvert en explorant le projet Bonfire. Le pari est qu’il sera plus simple à prendre en main pour quelqu’un sous Windows que make, et comme on souhaite que notre projet puisse intéresser des utilisateur·ices Windows, cela semblait un bon choix.

Voici donc le justfile de base, incluant une commande just install et just start :

// justfile

# Installation des dépendances Go
install:
  cd src && go mod tidy
  
# Lancement de l'application
start:
  cd src && air

Le problème de la base de données

Jusqu’ici, nous avons laissé les développeurs·euses installer elleux-mêmes Go (et air, mais j’y reviendrai) ainsi que Just sur leur machine. Passons maintenant à la gestion de la base de données.

Avant cela, une petite digression. Même si nous ne savons pas encore exactement comment notre projet sera distribué, nous suivons la méthodologie Twelve-Factor, ce qui implique de gérer la configuration via des variables d’environnement. Chacun·e est libre de sa méthode, mais personnellement, j’utilise direnv, malgré quelques déboires occasionnels. Donc un conseil si vous utilisez direnv, n’oubliez pas que le .envrc n’est pas dans le dépôt git lorsque vous déciderez de reformater votre disque pour refaire une installation de votre machine.

Pour en revenir à la base de données, la solution la plus directe est de l’installer sur son système via un gestionnaire de paquets. Mais si une version incompatible avec le projet est déjà installée, les ennuis commencent.

On pourrait s’appuyer sur un outil comme asdf, un gestionnaire de versions universel pour outils de développement. Il permet de gérer différentes versions d’interpréteurs, de compilateurs ou encore de bases de données comme MariaDB. J’ai aussi découvert asdf en explorant Bonfire, où cet outil est souvent recommandé pour installer Elixir (Bonfire étant développé en Elixir). Mais pour être honnête, je n’ai pas du tout aimé l’expérience.

Pour installer et gérer des serveurs, je trouve Docker particulièrement pertinent. Cela dit, je comprends que tout le monde ne l’apprécie pas, et c’est important que chaque développeur·euse puisse choisir sa propre méthode. Mais ce qui m’embête, c’est qu’avec Docker, il va falloir se rappeler de lancer la base de données avant de lancer le just start. Et j’aime qu’une commande comme just start s’occupe de tout.

J’ai donc mis en place une méthode s’appuyant sur une variable d’environnement (MARIADB_WITH_DOCKER), permettant de lancer automatiquement une base de données MariaDB dans un conteneur Docker, uniquement si on le souhaite.

// justfile

DcDev := "docker compose -p my-project -f docker-compose.db.yml"

# Lancement de l'application
start:
  if [ "$MARIADB_WITH_DOCKER" = "true" ]; then just start-db; fi
  cd src && air

# Arret de l'environnement de dev local
stop:
  if [ "$MARIADB_WITH_DOCKER" = "true" ]; then just stop-db; fi
  echo "Environnement de dev arrêté."

# Démarrage de la db dans un conteneur Docker
start-db:
  {{DcDev}} up -d

# Arret de Docker de db
stop-db:
  {{DcDev}} down

Tout démarrer et tout éteindre en une seule commande

On s’approche du but, mais une dernière chose me chiffonnait : la base de données démarrait seule, mais ne s’éteignait pas en même temps que le serveur Go.

En effet, nous utilisons air pour relancer la compilation du serveur Go lors des modifications de code en développement. Oui, nous aurions pu nous en passer, mais c’est … juste un paradoxe. Toujours est-il que l’on stoppe le serveur Go contrôlé par air avec un ctrl +c . Mais cela n’arrête donc pas la base, et il faut penser à lancer la commande just stop-db. Autant dire que le risque est grand de l’oublier.

J’espérais pouvoir régler ce problème directement dans le justfile, en m’appuyant sur la commande trap afin de capter le signal de fin du processus de air.

// justfile

start:
  trap 'just stop; exit' INT
  if [ "$MARIADB_WITH_DOCKER" = "true" ]; then just start-db; fi
  cd src && air

Mais cela ne fonctionnait pas, sans que je sache vraiment pourquoi. Ma recette just ne capturait jamais le signal d’interruption…

C’est donc avec la configuration de air que j’ai trouvé une solution de repli. En effet, on peut lui indiquer une commande à lancer lors de l’arrêt :

// in .air.toml
  post_cmd = ["just stop"]

Enfin, la magie du partage, lors de la relecture de ce post de blog, Thomas B. m’a donné la vraie jolie solution, permettant d’émuler n’importe quel script shell dans just en utilisant #!/usr/bin/env sh en début de recette, et donc de faire fonctionner correctement trap. Voilà ce que cela donne au final :

//in justfile

DcDev := "docker compose -p my-project -f docker-compose.db.yml"

# Lancement de l'application
start:
  #!/usr/bin/env sh
  if [ "${MARIADB_WITH_DOCKER}" = true ]; then
      trap 'just stop; exit' INT
      just start-db
      cd src && air
  else
      cd src && air
  fi

# Arret de l'environnement de dev local
stop:
  if [ "$MARIADB_WITH_DOCKER" = "true" ]; then just stop-db; fi
  echo "Environnement de dev arrêté."

# Démarrage de la db dans un conteneur Docker
start-db:
  {{DcDev}} up -d

# Arret de Docker de db
stop-db:
  {{DcDev}} down

Sans oublier d’indiquer à air de bien envoyer le signal d’interruption de script :

// in .air.toml
  post_cmd = []
  send_interrupt = true

Conclusion

Chez INCAYA, s’intéresser à l’expérience développeur·euse, c’est naviguer entre la simplicité d’utilisation et la compréhension des conditions d’exécution de l’applicatif à développer. Dans notre projet en Go, l’intégration de recettes exécutables et d’un choix flexible pour la gestion de la base de données est l’une des solutions qui nous a permis de trouver un équilibre entre automatisation, documentation implicite et maîtrise de l’environnement.

Et quoi de mieux qu’un post de blog pour partager en plus le cheminement qui nous a menés à ce “livre de recettes” ?