docker

Déploiement de projets maven sous docker

La renommée de docker croît tous les jours un peu plus alors que les patterns standard d’utilisation ne sont pas encore bien définis.

Il existe ainsi pour chaque problématique une multitude de réponses possibles. Qu’en est-il de la construction de projets maven?

Voici deux stratégies possibles pour ces projets.

Toutes les sources illustrant l’article sont disponibles sous ce projet github. Il existe une branche pour chacune des statégies.

Pour présenter les différentes stratégies nous utiliserons l’application exemple de fluent-http. Ce projet très simple est représentatif des applications web.

Une image docker, un container docker

Cette version est directement inspirée par l’article de Cédric Exbrayat Développer pour Google Glass.

Elle s’appuie sur un unique container contenant tout ce qui est nécessaire: java, maven et les sources compilées.

Docker maven direct

Voici le Dockerfile

FROM java:8

RUN apt-get update
RUN apt-get install -y maven

ADD . /project
WORKDIR /project

RUN mvn install -DskipTests

EXPOSE 8080

CMD mvn exec:java

Phase de construction de l’image

On part de la version officielle de java. Les 2 lignes suivantes s’occupent de l’installation de maven.

Toutes les sources du projet sont ajoutées dans le répertoire ./project. On se positionne dans ce répertoire avant de lancer la compilation du projet.

Phase de lancement de démarrage de l’image

La dernière ligne, exécuté lors du lancement du container, s’appuie sur le plugin maven exec pour démarrer notre serveur.

On utilise un seul script pour construire et démarrer notre container.

docker build -t helloworld .
docker rm -f helloworld
docker run -d \
   --name helloworld \
   -p 8080:8080 \
   helloworld
  1. La première ligne reconstruit une nouvelle image helloworld.
  2. La deuxième tue et supprime un éventuel container ayant le nom helloworld.
  3. Enfin, la dernière ligne démarre un container utilisant l’image qui vient d’être crée. Le container est également nommé helloworld.

L’avantage de cette stratégie est sa simplicité. Une seule image, un seul script: difficile de faire plus simple.

Son inconvénient majeur: sa lenteur. Lors de la phase de construction, maven télécharge toutes les dépendances nécessaires à la construction du projet (elles sont nombreuses). Lors de la phase de démarrage d’autres dépendances sont téléchargées.

Avec ma connexion internet, la construction dure environ 70 secondes et le démarrage 20 secondes. Ce temps est stable pour chaque redéploiement.

Docker maven direct temps

Pour mon architecture, c’est trop long. Je souhaite mettre à jour un composant le plus rapidement possible.

Voyons une autre stratégie.

Deux images dockers et deux containers

Cette stratégie est un petit peu plus complexe mais nettement plus performante.

docker maven stratégie image mvn

L’image mvn est une image standard, non dépendante du code. Elle n’est quasiment jamais reconstruite.

FROM java:8
VOLUME /project
WORKDIR /project

RUN apt-get update
RUN apt-get install -y maven

ENTRYPOINT ["mvn"]

On précise le point d’entrée comme étant la commande mvn. On déclare aussi /project comme étant un volume. C’est important pour la suite.

La construction de cette image est très simple:

#!/bin/sh
docker build -t mvn . 

On crée ensuite une image spécifique au projet hello-mvn et qui dérive de l’image mvn:

FROM mvn
ADD ./ /project
EXPOSE 8080

Par rapport à l’image mvn de base, on ajoute dans le volume /project toutes nos sources et on expose le port de notre serveur.

Grosse différence par rapport à la première stratégie: les sources ne sont pas compilées durant la construction de l’image.

La construction du l’application est réalisée par le container hello-mvn. Ce container execute la compilation et s’arrête. L’instance arrêtée dispose toujours de l’ensemble des sources et du résultat de sa compilation dans le volume /projet.

docker rm -f hello-mvn
docker run  \
  --name hello-mvn \
  hello-mvn compile -DskipTests

Le container qui porte le serveur web démarre en reférençant le premier container et accède au volume /project :

docker rm -f helloworld
docker run -d \
  --name helloworld \
  --volumes-from hello-mvn \
  -p 8080:8080 \
  hello-mvn exec:java

Les goals maven sont désormais tous executés durant les phases de run des containers et non de build des images. On peut optimiser leur fonctionnement en partageant un volume portant le répertoire maven .m2. Chaque téléchargement sera stocké dans ce répertoire garantissant une execution de maven plus rapide.

docker maven stratégie image mvn avec volume

On modifie les scripts de lancement des containers hello-mvn et helloworld pour bénéficier de ce volume.

Le script final pour redéployer l’application est le suivant:

#!/bin/bash

docker build -t hello-mvn .

docker rm -f hello-mvn
docker run  \
  --name hello-mvn \
  -v ~/.m2/:/root/.m2 \
  hello-mvn compile -DskipTests

docker rm -f helloworld
docker run -d \
  --name helloworld \
  --volumes-from hello-mvn \
  -v ~/.m2/:/root/.m2 \
  -p 8080:8080 \
  hello-mvn exec:java

L’exécution complète du script passe à 9s en cohérence avec une exécution de la compilation et du démarrage dans un environnement de développement.

Docker maven cache temps

Et vous, quelles stratégies de construction et démarrage utilisez-vous pour vos projets maven?

3 Comments

  1. Pour mes images docker, j’essaie de respecter quelques principes qui facilitent la vie pour le passage en prod :

    – images les plus petites possibles (incorporer le moins de choses qui ne sert pas au runtime). Je ne vais pas jusquà essayer de tout reconstruire from scratch, j’utilise le coeur ubuntu14.04 (j’essaierai l’image officielle java, pas vue sortir celle-là).
    – images les plus indépendantes possibles.

    Ces 2 aspects me semblent violés par la proposition n°2 :
    – l’image de runtime est dépendante de beaucoup de choses : l’image de build, le container de build, son volume.
    – je n’ai pas compris pourquoi le volume .m2/ ne peut pas être directement récupéré de l’image de build, tant qu’à faire. Pourquoi passer par un volume sur l’hôte ? Peut-être pour pouvoir récupérer le .m2/ entre des suppressions de containeurs ? Je trouve qu’il est utile, lorsqu’on décide de supprimer un container, qu’on supprime également, tant qu’à faire, toute donnée qui peut être récupérée. Ca fait le ménage. Ca ralentira le premier build, mais basta. Un container peut très bien être démarré et arrêté sans avoir à être supprimé à chaque fois, surtout un container de build.
    – le répertoire .m2/ contient trop de choses. En plus de contenir les dépendances nécessaires au runtime, il va également contenir toutes les dépendances nécessaires à la compilation (tous les plugins maven, par ex., junit éventuellement et d’autres choses).

    Au final, on est dans un mode où la construction doit nécessairement se faire sur le serveur hôte docker. Il est impossible de déployer simplement l’application produite sur un autre hôte. Sur l’hôte il faut :
    – déployer 1 image générique (normal)
    – démarrer 2 containers dont 1 de build
    – installer un script non standard pour enchaîner les démarrages / arrêts, et partager des choses entre l’environnement de build et l’environnement de runtime.

    Je pense que le tout peut être simplifié, selon 2 axes opposés, mais qui chacun peuvent faire sens en fonction de la philosophie qu’on adopte :

    a) tant qu’à mélanger environnement de compilation et de runtime, autant aller jusqu’au bout et n’avoir qu’un seul container.
    ou
    b) mieux séparer la phase de compilation / packaging, dont l’output sera la fabrication d’une image ; et la phase d’exécution, qui lance l’image auto-suffisante créée par la phase de compilation.

    La phase b) nécessite d’ajouter un volume sur la socket docker pour pouvoir lancer docker sur l’hôte à partir du docker de build. Elle ne peut se faire qu’en environnement maîtrisé.

    La phase a) assume bien le mélange compilation/run, et en cela est plus fidèle aux exemple que l’on voit dans le monde ruby/python.

    Actuellement, au travail, nous faisons la philosophie de la phase b), mais l’étape de compilation n’est pas encore faite sous docker (c’est aussi l’avantage de séparer mieux les choses, il y a plus d’indépendance, et donc plus de choix).

  2. En fait, la première version, en prenant juste une petite idée de la deuxième version, pourrait tourner en réglant le problème de la lenteur :

    – en déclarant ~/.m2/ comme un volume

    selon que tu veux que ~/.m2 soit perdu ou non si tu supprimes le container, alors soit tu le laisses en volume anonyme, soit tu lui fixes un emplacement sur ton docker hôte.

    Idée encore meilleure (?) : ne pas déclarer de volume pour ~/.m2 dans le dockerfile, mais quand meme monter un volume sur ~/.m2 sur le poste de développement => c’est rapide en développement, mais lorsque tu veux créer l’image finale, tu ne mets pas le ~/.m2 en volume et zou, le .m2 se retrouvera dans l’image qui partira en prod.

  3. Je suis absolument désolé de violer tes principes. Ce n’est pas dans mes habitudes ;-).

    L’utilisation du volume ~/.m2 n’est pas obligatoire.

    Elle est là pour profiter du cache offert par maven. Mon objectif est de déployer le plus rapidement possible.

    Si je n’utilise pas le cache maven, je me retrouve à télécharger des dizaines de dépendances que j’avais déjà téléchargées la fois précédente : bof, bof,… Le repository maven local est justement là pour ça. Dans mon cas, j’ai partagé le répertoire maven de l’utilisateur ce qui permet aux différents container de bénéficier des téléchargements réalisé par les autres containers. Si tu veux restreindre le périmètre de partage, il est possible d’utiliser un répertoire maven pour le projet :

    docker run -v $(pwd)/.m2/:/root/.m2 …

    Dans les faits je n’arrête (presque) jamais un container pour le redémarrer ensuite. Si je touche un container c’est pour une nouvelle livraison qui implique donc une reconstruction de l’image. Pour le moment, je n’ai pas de registry docker interne. Mes scripts de déploiement utilisent git pour obtenir la dernière version du projet, créent l’image et démarrent le container.

    C’est l’objectif de vitesse qui m’a poussé à utiliser exactement le même environnement de démarrage pour le dev et la prod. Je ne package pas l’application fluent-http sous la forme de jar. C’est pourquoi j’ai besoin de cette dépendance entre les deux containers. Supprimer cette dépendance impliquerait de passer l’artefact issu de la phase de construction au container en charge de l’exécution. C’est une autre forme de dépendance.

    Pour terminer, l’image «maven» est pratique dans beaucoup de scénarios. Je la mets en oeuvre par exemple pour gérer le versioning de mes bases de données. Un mini dockerfile :

    FROM mvn
    ADD ./pom.xml /project/pom.xml
    ADD ./src /project/src

    avec un script pas beaucoup plus grand : 
    #!/bin/sh
    docker build -t db-migration-mvn .
    docker run \
    –rm \
    -v ~/.m2:/root/.m2 \
    –link dbcrm:dbcrm \
    db-migration-mvn \
    compile flyway:migrate

    et on a un processus propre et indépendant. Le projet principal n’a pas à importer les dépendances de flyway.

    Ce qui ressort de tout ça est une grande variété dans les possibilités de mise en oeuvre de docker. Chacun peut définir les mécanismes les plus adaptés à son projet.

Répondre à gaetan Annuler la réponse.

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Vous pouvez utiliser ces balises et attributs HTML : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>