Simplifiez le lancement de vos applications Spring en local

En tant que développeur, la première épreuve que nous avons à surmonter au début d’une mission est en général de mettre en place notre environnement de développement, et de parvenir à faire tourner les applications sur notre machine. Si cela peut paraître banal, dans un contexte avec une dizaine d'applications inconnues, on peut y perdre beaucoup de temps.

Simplifier au maximum le lancement des applications nous permet d’optimiser cette tâche pour les nouveaux arrivants, et favorise également la répartition des connaissances au sein d’une équipe : les développeurs seront plus aptes à travailler sur n’importe quelle application de leur périmètre.

Cela permet aussi d'accélérer la résolution d’incidents, parce qu’il est alors plus facile de reproduire l’erreur en local pour l’analyser.

   

Simple et efficace : commiter le runner IntelliJ

Tout le monde ne le sait pas, mais IntelliJ propose d’enregistrer la configuration de lancement de votre application sous la forme d’un fichier XML dans votre projet. C’est-à-dire qu’une fois votre application bien paramétrée dans “Run -> Edit configurations…”, les paramètres de VM ajoutés, les profils renseignés, il suffit de cocher la case en haut à droite “Store as project file” pour qu’IntelliJ crée un dossier .run à la racine du projet qu’il suffira de commiter pour que tous les autres développeurs y aient accès.


Un développeur de votre équipe n’aura qu’à cloner le projet, et IntelliJ lui générera automatiquement les runners nécessaires à partir des informations stockées dans le dossier .run.
Chaque fichier XML définit un runner, et le dossier .run peut en contenir autant qu’on veut.


Note : On note l’utilisation d’une variable  CONFIG_REPO dans les spring.config.additional-locations, car dans notre cas la configuration est externalisée dans un projet séparé. Il faudra soit mettre manuellement le vrai chemin, soit définir cette variable d’environnement qui pointe vers la config. Si chaque développeur de l’équipe définit la variable CONFIG_REPO  préalablement sur son poste, ils pourront directement lancer le runner sans avoir à y toucher.
Par exemple :

CONFIG_REPO=$HOME/dev/mon-entreprise/config/

   

Avec Maven

Maven nous offre la possibilité de définir des profils, et de les associer à des profils spring boot :

<profiles>
	<profile>
		<id>api</id>
		<activation>
			<activeByDefault>true</activeByDefault>
		</activation>
		<properties>
			<spring.profiles.active>produit-api</spring.profiles.active>
		</properties>
	</profile>
	<profile>
		<id>kafka</id>
		<properties>
			<spring.profiles.active>produit-kafka</spring.profiles.active>
		</properties>
	</profile>
</profiles>


Ici nous avons défini deux profils :

  • api, qui pointe vers le profil spring produit-api
  • kafka, qui pointe vers le profil spring produit-kafka

J’ai donné des noms différents pour montrer que c’est possible, mais je suggère plutôt de garder le même nom que les profils spring pour ne pas se mélanger, et ne plus savoir quel nom indiquer au lancement de l'application.

Pour simplifier encore, on indique ici le profil à prendre par défaut si aucun n’est renseigné au lancement, avec la balise activeByDefault.

Pour gérer les spring.config.additionnal-location, il est possible d’utiliser le plugin maven spring-boot-maven-plugin.

<plugin>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-maven-plugin</artifactId>
	<version>${spring-boot-maven-plugin.version}</version>
	<configuration>
		<skip>true</skip>
		<profiles>${spring.profiles.active}</profiles>
		<arguments>
			<argument>--spring.config.additional-location=${CONFIG_REPO}/config/produit/local.yml
			</argument>
		</arguments>
	</configuration>
</plugin>

La commande mvn spring-boot:run lancera l’application en chargeant le profil par défaut, et la configuration externe.
Pour préciser un autre profil :

mvn spring-boot:run -P kafka

   

Avec Compose pour une application multi-containers

Nous n’avons pour le moment pas pris en compte les applications complexes qui dépendent d’autres services, comme une base de données. Compose (docker-compose) peut s’en charger, à condition d’être à l’aise avec Docker.

Un premier exemple

Compose nous permet de définir et lancer des applications Docker multi-containers.
Imaginons une application “Produit” qui a la responsabilité des données produit de votre entreprise. On peut supposer qu’elle va avoir besoin :

  • d’une base de données pour les stocker, PostgreSQL par exemple
  • d’un cache Redis pour optimiser les requêtes
  • d’un Redis Commander, une UI pour manager le cache Redis

Pour un développeur qui voudrait cloner et démarrer l’application, il ne sera pas simple de la faire tourner, car elle va exiger que tous ces services soient lancés pour démarrer.

À partir d’un Dockerfile, Docker peut nous containeriser un service. Compose nous permet quant à lui de définir plusieurs containers dans un fichier .yml et de les lancer en une seule commande.

Jetons un œil à ce fichier YAML qui liste tous les services à lancer : db, redis, redis-commander.

version: '3'
services:
	db:
		image: "postgres:11"
		container_name: "postgres-produit"
		environment:
			- POSTGRES_USER=user
			- POSTGRES_PASSWORD=password
			- POSTGRES_DB=produit
		ports:
			- "5432:5432"
	redis:
		image: "redis"
		container_name: "redis-produit"
		restart: always
		ports:
			- "6380:6379"
	redis-commander:
		image: "rediscommander/redis-commander:latest"
		container_name: "redis-commander-produit"
		restart: always
		environment:
			- REDIS_HOSTS=local:redis:6380
		ports:
			- "8009:8081"
	produit:
		build:
		context: .
		dockerfile: Dockerfile
		volumes: *volumes
		ports:
			- "8080:8080"
		environment:
			DB_HOST: postgres-produit



Note : pour le service Produit, le fichier indique à Compose le Dockerfile à aller chercher. On ne va pas aborder Docker et les Dockerfiles dans cet article.

Pour lancer tous les services, il suffit de saisir :

docker-compose -f monfichier.yml up


Le -f sert à préciser le nom du fichier. En nommant le fichier docker-compose.yml, on n’a plus besoin de préciser le nom, ce qui donne alors :

docker-compose up

On peut ajouter -d à la commande pour libérer le terminal.

docker-compose nous répond :

Starting postgres-produit     ... done
Starting redis-produit       ... done
Starting redis-commander-produit ... done
Starting produit ... done


Le compte est bon, tous les services ont démarré et l’application tourne. Il suffit de commit ce fichier dans le projet, et il suffira à un nouvel arrivant de cloner le projet et de lancer cette commande.

Note : Par défaut, Compose crée tous les conteneurs définis dans un même fichier sur le même network. Ils peuvent donc communiquer entre eux. Si on a plusieurs fichiers .yml, il faudra définir dans chaque fichier le network utilisé pour permettre à vos services d'interagir.

   

Application multi-profils avec configuration externe

Complexifions le problème, et supposons que notre application Produit contient deux profils Spring qui sont indépendants.

  • produit-kafka reçoit le flux produit par events dans un topic kafka.
  • produit-api expose une API REST qui permet aux autres applications du système de récupérer les informations produit.

La configuration est aussi externalisée dans un autre projet, disons config, cloné également en local. Pour démarrer l’application produit, il faut lui indiquer en paramètre l’endroit où la trouver. Comme noté au début de l’article, on va s’appuyer sur la variable d’environnement CONFIG_REPO.

Concentrons-nous uniquement sur la définition du service produit dans notre fichier docker-compose.yml. Il y en a deux maintenant, un par profil :

produit-api:
	build:
	context: .
	dockerfile: Dockerfile
	volumes:
		- ${CONFIG_REPO}/produit/local.yml:/deployments/config/produit-config.yml
	ports:
		- "8080:8080"
	environment:
		DB_HOST: postgres-produit
		spring_profiles_active: produit-api
produit-kafka:
	build:
	context: .
	dockerfile: Dockerfile
	volumes:
		- ${CONFIG_REPO}/produit/local.yml:/deployments/config/produit-config.yml
	ports:
		- "8888:8080"
	environment:
		DB_HOST: postgres-produit
		spring_profiles_active: produit-kafka

Ça fonctionne, mais c’est trop redondant. Presque tout est en doublon : build, volumes, environment… Si on avait davantage de profils et une modification à faire dans le fichier, on devrait la faire autant de fois qu’il y a de profils !

Le format YAML nous propose des fonctionnalités qui se rapprochent de l’héritage et de l’extension pour éviter les répétitions. On va définir un service abstrait qui va regrouper la configuration commune, dont nos deux profils hériteront.

& sert à définir une variable réutilisable

* permet d’appeler la variable

<<: permet de l’étendre

application-abstraite:
	profiles: [ "donotstart" ]
	build: &build
	context: .
	dockerfile: Dockerfile
	environment: &environment
		DB_HOST: postgres-produit
	volumes: &volumes
		- ${CONFIG_REPO}/produit/local.yml:/deployments/config/produit-config.yml
produit-api:
	build: *build
	volumes: *volumes
	ports:
		- "8080:8080"
	environment:
		<<: *environment
		spring_profiles_active: produit-api
produit-kafka:
	build: *build
	volumes: *volumes
	ports:
		- "8888:8080"
	environment:
		<<: *environment
		spring_profiles_active: produit-kafka

Ainsi, pour produit-api et produit-kafka, il suffit de rappeler les valeurs prédéfinies &build, &volumes et &environnement.

Le problème c’est que par défaut, docker-compose up lance tous les services qui sont définis. Il essayera donc de lancer notre application-abstraite, alors qu’elle ne représente pas de service réel, mais n’est là que pour régler notre souci de redondance.

On peut bricoler un peu, et définir un profil Compose.  Les profils Compose servent de discriminant pour ne lancer que certains services du fichier.

docker compose --profile monprofil up

Ici on va s’en servir dans l’autre sens : donner un profil à notre application abstraite pour qu’elle ne soit pas lancée avec le reste des services. Tant que le profil qu’on a défini n’est pas précisé lors du lancement des services, celui-ci ne sera jamais lancé. Ici, j’ai donc attribué le profil donotstart au service abstrait, mais n’importe quel nom fonctionnerait.

docker-compose up

Et notre BDD, notre cache, notre cache manager, et les deux profils de notre application Spring Boot sont lancés dans Docker, sur un même network.

Conclusion

Je vous ai présenté 3 solutions pour faciliter l’onboarding d’un développeur sur vos applications, qu’il s’agisse d’un ancien développeur qui n’y a jamais touché ou d’un nouvel arrivant.

Ces solutions peuvent bien entendu s’étendre et s’adapter pour prendre en compte d’autres éléments nécessaires aux applications : gestions de secrets par exemple, avec une variable SECRET_REPO à définir, ou encore gestions des environnements avec un fichier .env à la racine du projet…

Il est aussi possible de combiner ces méthodes. Personnellement, j’ai préféré utiliser Compose pour lancer les bases de données et les caches, et plutôt un Runner IntelliJ pour lancer l’application elle-même pour pouvoir se servir directement des outils de débug proposés par IntelliJ.

A vous de choisir ce qui s'adapte le mieux à votre façon de travailler.