Apprendre à développer une application basée sur une architecture microservices avec Docker et le langage Java

Mickael Baron

L'objectif de cette quatrième série d'exercices est d'apprendre à construire une application en respectant une architecture à base de microservices en se focalisant sur le langage Java. Nous utiliserons pour cela plusieurs technologies : la bibliothèque KumuluzEE pour packager et exécuter une application microservice respectant la spécification MicroProfile, l'outil Docker pour l'isolation des microservices, la bibliothèque et le serveur RabbitMQ pour la gestion d'un bus d'événements afin que les microservices communiquent de manière asyncrhone et finalement l'outil Docker Compose pour la composition des microservices.

La grande majorité du code contenu dans les microservices vous sera donnée comme support. En effet, ces exercices se focaliseront principalement sur les problématiques de déploiement.

Dans la suite, on appelle « microservice », un programme qui implémente une fonctionnalité dans un langage donné (par exemple Java) et qui est isolé dans un conteneur Docker.

Nous invitons les lecteurs à consulter les deux supports de cours qui présentent les concepts mis en avant dans cette série d'exercices :

- Introduction aux architectures microservices ;

- Mise en œuvre des architectures microservices.

Les codes pour les exercices sont disponibles sur le dépôt Git suivant : https://github.com/mickaelbaron/javamicroservices-tutorial (pour récupérer le code faire : $ git clone https://github.com/mickaelbaron/javamicroservices-tutorial).

La solution de tous les exercices est disponible sur la branche solution : $ git checkout solution

Pour réagir au contenu de ce tutoriel, un espace de dialogue vous est proposé sur le forum Commentez .

- Titre : Apprendre à développer une application basée sur une architecture microservices avec Docker et le langage Java

- Auteur : Mickael Baron

- Parution : 4 avril 2016

- Mise à jour : 18 mars 2019

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2019 Mickael Baron. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Prérequis logiciels

- II. Présentation de l'étude de cas

- III. Exercice 1 : préparer le programme Java du service web HelloWorld (Rest)

- III-A. But

- III-B. Description

- III-C. Étapes à suivre

- IV. Exercice 2 : préparer le microservice Redis (créer un conteneur à partir d'une image existante)

- IV-A. But

- IV-B. Description

- IV-C. Étapes à suivre

- V. Exercice 3 : tester le service web HelloWorld (Rest) avec le microservice Redis

- V-A. But

- V-B. Description

- V-C. Étapes à suivre

- VI. Exercice 4 : préparer le microservice Rest (écrire un Dockerfile et créer sa propre image Docker)

- VI-A. But

- VI-B. Description

- VI-C. Étapes à suivre

- VII. Exercice 5 : Lier les microservices Redis et Rest

- VII-A. But

- VII-B. Description

- VII-C. Étapes à suivre

- VIII. Exercice 6 : émettre et recevoir des événements

- VIII-A. But

- VIII-B. Description

- VIII-C. Étapes à suivre

- IX. Exercice 7 : composer tous les microservices avec DockerCompose

- IX-A. But

- IX-B. Description

- IX-C. Étapes à suivre

- X. Conclusion, perspectives et remerciements

I - Prérequis logiciels

Avant de démarrer cette série d'exercices, veuillez préparer votre environnement de développement en installant les outils suivants :

- Docker ;

- Eclipse ;

- cURL ;

- Java via OpenJDK ;

- Maven.

Eclipse, Java et Maven sont nécessaires uniquement pour la partie liée à KumuluzEE. Même si ces outils ne sont pas installés, vous pourrez sans problème réaliser les exercices puisque nous ferons la compilation Java lors de la création d'une image Docker.

Toutes les expérimentations se feront sur un poste en local à partir d'un système d'exploitation Linux. Vous pouvez sans problème réaliser ces expérimentations depuis les systèmes OS X ou Windows en utilisant Docker Toolbox (voir l'installation de Docker pour ces systèmes) ou via Vagrant par exemple. Dans tous les cas où vous utiliserez une virtualisation, vous aurez à rediriger les ports réseaux suivants :

- 6379 : port utilisé par le serveur Redis ;

- 15672 et 5672 : ports utilisés par RabbitMQ (interface de gestion de RabbitMQ et serveur RabbitMQ) ;

- 80: pour la redirection du port http.

II - Présentation de l'étude de cas

L'étude de cas utilisée est une application permettant de diffuser des messages « HelloWorld ». Elle fournit une interface web pour la saisie et pour la consultation des messages (voir figure ci-dessous).

Sur le schéma proposé ci-dessous, nous détaillons la décomposition en microservices de cette application.

L'architecture dispose de sept microservices.

- Le microservice Web (contenu dans le projet helloworldwebmicroservice) fournit à l'utilisateur une interface web. La technologie utilisée sera du HTML/JavaScript pour le client et NodeJS pour créer un petit serveur web.

- Le microservice Rest (contenu dans le projet helloworldrestmicroservice) a pour rôle de fournir une API de type service web pour le microservice Web. Lors de la réception d'un message « HelloWorld » celui-ci est envoyé au microservice Redis à des fins de stockage. Il publie également un événement vers le microservice RabbitMQ.

- Le microservice Redis fournit un serveur Redis pour le stockage des messages « HelloWorld ».

- Le microservice RabbitMQ fournit un bus d'événements basé sur RabbitMQ.

- Le microservice Log (contenu dans le projet helloworldlogmicroservice) s'abonne au bus d'événements et affiche sur la sortie console les événements envoyés.

- Les microservices Email et Twitter s'abonnent au bus d'événement et envoient respectivement un email ou un tweet. Ces deux microservices ne seront pas traités.

III - Exercice 1 : préparer le programme Java du service web HelloWorld (Rest)

III-A - But

- Configurer un microservice respectant MicroProfile avec KumuluzEE.

- Configurer un fichier pom.xml pour gérer les dépendances Maven.

- Exécuter un microservice Java en ligne de commande.

III-B - Description

Le projet Java (celui qui servira pour le microservice Rest) du service web HelloWorld a été développé en utilisant les spécifications JAX-RS et CDI. Ces deux spécifications font parties du modèle de programmation MicroProfile permettant le développement de microservice avec le langage Java. Nous utiliserons KumuluzEE comme solution d'implémentation à MicroProfile. À noter que KumuluzEE utilise l'implémentation JERSEY pour JAX-RS.

Le code du projet Java est assez commun. Un package service pour la gestion des services web REST et un package dao pour la gestion des données avec la base de données Redis. Dans cet exercice on va partir d'un code déjà tout prêt et nous allons nous attacher à le configurer pour l'utiliser avec KumuluzEE.

III-C - Étapes à suivre

- Démarrer l'environnement de développement Eclipse.

- Importer le projet Maven helloworldrestmicroservice (File -> Import -> General -> Existing Projects into Workspace, choisir le répertoire du projet puis faire Finish).

- Examiner les différents packages et classes. Vous remarquerez que le projet contient des erreurs de compilation dues à l'absence des dépendances vers KumuluzEE et indirectement vers JERSEY.

- Ouvrir le fichier pom.xml et compléter le contenu de la balise <dependencies> par les dépendances suivantes :

1. ...

2. <dependencies>

3. <dependency>

4. <groupId>com.kumuluz.ee</groupId>

5. <artifactId>kumuluzee-core</artifactId>

6. <version>${kumuluzee.version}</version>

7. </dependency>

8. <dependency>

9. <groupId>com.kumuluz.ee</groupId>

10. <artifactId>kumuluzee-servlet-jetty</artifactId>

11. <version>${kumuluzee.version}</version>

12. </dependency>

13. <dependency>

14. <groupId>com.kumuluz.ee</groupId>

15. <artifactId>kumuluzee-jax-rs-jersey</artifactId>

16. <version>${kumuluzee.version}</version>

17. </dependency>

18. <dependency>

19. <groupId>com.kumuluz.ee</groupId>

20. <artifactId>kumuluzee-cdi-weld</artifactId>

21. <version>${kumuluzee.version}</version>

22. </dependency>

23. ...

24. </dependencies>

25. ...

- Compléter dans la balise <properties> le numéro de version de KumuluzEE par la valeur 3.0.0 (version octobre 2018)

1. ...

2. <properties>

3. <kumuluzee.version>3.0.0</kumuluzee.version>

4. ...

5. </properties>

6. ...

Désormais le projet ne contient plus d'erreur et peut être compilé en totalité (Eclipse s'en charge via la compilation incrémentale).

- Pour exécuter le projet depuis Eclipse, créer une configuration d'exécution que vous appellerez HelloworldRESTMicroservice et dont la classe principale (Main class) sera com.kumuluz.ee.EeApplication puis faire Run.

Votre programme s'exécute par l'intermédiaire de KumuluzEE. Pour tester, nous pourrions utiliser l'adresse http://localhost:8080/helloworld, mais comme le serveur Redis n'est pas encore opérationnel nous ne pourrons pas à cet instant aller plus loin dans les tests.

- Avant de continuer, arrêter l'exécution du programme depuis le bouton Terminate (bouton rouge) de la console Eclipse.

Notre programme Java doit s'exécuter en ligne de commande et non pas depuis Eclipse quand il sera exécuté depuis un conteneur Docker. Nous allons donc réaliser une dernière modification sur le fichier pom.xml afin de préparer le terrain. Nous allons préciser à Maven que l'on souhaite que toutes les dépendances soient présentes dans le répertoire target du projet.

- Ouvrir le fichier pom.xml.

- Ajouter dans la balise <plugins> le plugin maven-dependency-plugin comme montré sur le code suivant.

1. ...

2. <plugin>

3. <groupId>org.apache.maven.plugins</groupId>

4. <artifactId>maven-dependency-plugin</artifactId>

5. <version>2.10</version>

6. <executions>

7. <execution>

8. <id>copy-dependencies</id>

9. <phase>package</phase>

10. <goals>

11. <goal>copy-dependencies</goal>

12. </goals>

13. </execution>

14. </executions>

15. </plugin>

16. </plugins>

17. </build>

18. </project>

- Depuis Eclipse, exécuter la configuration d'exécution appelée alldependencies (package). Si vous rencontrez des soucis avec l'intégration Maven sous Eclipse, exécuter la ligne de commande suivante à la racine de votre projet $ mvn package. Toutes les dépendances seront copiées par le plugin maven-dependency-plugin et localisées dans le répertoire helloworldrestmicroservice/target/dependency.

- Ouvrir une invite de commande shell à la racine du projet helloworldrestmicroservice, puis exécuter la ligne de commande suivante.

1. $ java -cp 'target/classes:target/dependency/*' com.kumuluz.ee.EeApplication

2. 2018-12-28 18:56:55.562 INFO -- com.kumuluz.ee.configuration.sources.FileConfigurationSource -- Unable to load configuration from file. No configuration files were found.

3. 2018-12-28 18:56:55.580 INFO -- com.kumuluz.ee.EeApplication -- Initialized configuration source: EnvironmentConfigurationSource

4. 2018-12-28 18:56:55.580 INFO -- com.kumuluz.ee.EeApplication -- Initialized configuration source: SystemPropertyConfigurationSource

5. 2018-12-28 18:56:55.581 INFO -- com.kumuluz.ee.EeApplication -- Initialized configuration source: FileConfigurationSource

6. 2018-12-28 18:56:55.581 INFO -- com.kumuluz.ee.EeApplication -- Initializing KumuluzEE

7. 2018-12-28 18:56:55.581 INFO -- com.kumuluz.ee.EeApplication -- Checking for requirements

8. 2018-12-28 18:56:55.583 INFO -- com.kumuluz.ee.EeApplication -- KumuluzEE running in an exploded class and dependency runtime.

9. ...

Votre microservice Rest est désormais opérationnel et l'instruction pour la démarrer fonctionne.

- Arrêter l'exécution du programme en faisant simplement un CTRL-C.

- Avant de passer à l'exercice suivant qui nous permettra de disposer d'un serveur Redis, essayons de comprendre comment la communication est réalisée entre l'application Java EE et le serveur Redis. Ouvrir la classe fr.mickaelbaron.helloworldrestmicroservice.dao.redis.JedisFactory et examiner la méthode URI getRedisURI().

1. private static final String REDIS_HOST_ENV = "REDIS_HOST";

2. private URI getRedisURI() {

3. String redisPort = System.getenv(REDIS_HOST_ENV);

4. return URI.create(redisPort != null && !redisPort.isEmpty() ? redisPort : "tcp://localhost:6379");

5. }

Vous remarquerez que l'accès à l'hôte de Redis se fait par une variable d'environnement REDIS_HOST (qui sera utilisée plus tard quand le projet sera un microservice) ou se fait via l'adresse localhost (pratique pour les tests).

IV - Exercice 2 : préparer le microservice Redis (créer un conteneur à partir d'une image existante)

IV-A - But

- Récupérer depuis Docker Hub une image prête à l'emploi.

- Créer un conteneur depuis une image avec un volume.

- Lister des images Docker.

- Lister les conteneurs disponibles et inspecter un conteneur.

IV-B - Description

Le microservice Redis a pour objectif de conserver l'état des différents messages « HelloWorld » traités par le microservice Rest. Cette conservation de données se fera par l'intermédiaire d'un serveur de données NoSQL Redis. Ce serveur permettra au microservice Rest de s'y connecter afin de conserver la création des messages « HelloWorld ».

IV-C - Étapes à suivre

- Allez sur le site Docker Hub et faire une recherche avec Redis. Vous remarquerez un nombre important de dépôts (Repositories) destiné à Redis. De manière générale, veuillez privilégier les dépôts officiels.

- Ouvrir une invite de commande et se placer à la racine du dossier helloworldmicroservices.

- Saisir la ligne de commande suivante pour télécharger la dernière version de l'image Docker Redis. Cela prendra un petit peu de temps, car toutes les couches (layers) de l'image doivent être téléchargées. À noter que chaque couche est une image qui fait référence à une image parente.

1. $ docker pull redis

2. Using default tag: latest

3. latest: Pulling from library/redis

4. a5a6f2f73cd8: Pull complete

5. a6d0f7688756: Pull complete

6. 53e16f6135a5: Pull complete

7. f52b0cc4e76a: Pull complete

8. e841feee049e: Pull complete

9. ccf45e5191d0: Pull complete

10. Digest: sha256:bf65ecee69c43e52d0e065d094fbdfe4df6e408d47a96e56c7a29caaf31d3c35

11. Status: Downloaded newer image for redis:latest

- S'assurer que l'image a été correctement téléchargée en utilisant la commande images de l'outil docker.

1. $ docker images

2. REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE

3. redis               latest              ce25c7293564        2 weeks ago         95MB

- Les différentes couches de l'image peuvent être consultées en utilisant la commande history de l'outil Docker.

1. $ docker history redis

2. ce25c7293564        2 weeks ago         /bin/sh -c #(nop)  CMD ["redis-server"]         0B

3. <missing>           2 weeks ago         /bin/sh -c #(nop)  EXPOSE 6379/tcp              0B

4. <missing>           2 weeks ago         /bin/sh -c #(nop)  ENTRYPOINT ["docker-entry…   0B

5. <missing>           2 weeks ago         /bin/sh -c #(nop) COPY file:b63bb2d2b8d09598…   374B

6. ...

7. <missing>           6 weeks ago         /bin/sh -c groupadd -r redis && useradd -r -…   329kB

8. <missing>           6 weeks ago         /bin/sh -c #(nop)  CMD ["bash"]                 0B

9. <missing>           6 weeks ago         /bin/sh -c #(nop) ADD file:dab9baf938799c515…   55.3MB

- Créer un conteneur à partir de l'image Docker de Redis en saisissant la ligne de commande suivante.

1. $ docker run --name redis -v $(pwd)/data:/data -d redis redis-server --appendonly yes

2. c95b96730f1e3ff7e99d5d380b7d871fcd1a7dab2883be41b50e237f8012763c

Un conteneur appelé redis sera créé. L'option -v $(pwd)/data:/data permet de partager le répertoire $(pwd)/data de l'hôte avec le répertoire /data du conteneur. L'option -d permet de créer un conteneur en mode détaché (similaire au classique &). redis permet de désigner l'image et redis-server --apprendonly yes sont des options pour démarrer le serveur Redis.

- Exécuter la ligne de commande suivante pour vérifier que le conteneur a été créé.

1. $ docker ps

2. CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES

3. c95b96730f1e        redis               "docker-entrypoint.s…"   2 minutes ago       Up 2 minutes        6379/tcp            redis

Nous venons de créer dans un conteneur une instance d'un serveur Redis. Nous allons pouvoir l'utiliser pour tester notre programme Java.

V - Exercice 3 : tester le service web HelloWorld (Rest) avec le microservice Redis

V-A - But

- Rediriger des ports avec Docker.

- Inspecter des métadonnées d'un conteneur Docker.

V-B - Description

Revenons un instant sur le test du projet helloworldrestmicroservice (voir premier exercice). Pour assurer la communication entre ce projet (pas encore isolé dans un conteneur Docker) et le microservice Redis la solution est de rediriger le port 6379 de l'hôte vers le port 6379 du conteneur (le port par défaut de Redis).

V-C - Étapes à suivre

Pour la redirection du port 6379 de l'hôte vers le port 6379 du conteneur vous devez ajouter un paramètre lors de la construction du conteneur Redis : -p 6379:6379, mais il faut avant tout supprimer le conteneur existant créé depuis l'exercice 2.

- Supprimer le conteneur nommé redis via la ligne de commande suivante.

1. docker rm -f redis

2. redis

- Créer un nouvelle fois le conteneur redis en ajoutant le paramètre -p 6379:6379 permettant de rediriger le port 6379 de l'hôte vers le port 6379 du conteneur.

1. docker run --name redis -v $(pwd)/data:/data -p 6379:6379 -d redis redis-server --appendonly yes

2. a1f4e49c2ea5af796012ca5665bb57daba47158bb855cf6dcd4bca79382d025f

- Exécuter la ligne de commande suivante pour vérifier que le conteneur a été créé et que le port 6379 a été redirigé. Une nouvelle colonne PORTS fait son apparition et précise que le port de l'hôte est redirigé vers le port 6379 du conteneur.

1. $ docker ps

2. CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES

3. a1f4e49c2ea5        redis               "docker-entrypoint.s…"   8 hours ago         Up 8 hours          0.0.0.0:6379->6379/tcp   redis

- Exécuter la ligne de commande suivante pour obtenir plus d'information sur le conteneur nommé redis.

1. $ docker inspect redis

2. [

3. {

4. "Id": "a1f4e49c2ea5af796012ca5665bb57daba47158bb855cf6dcd4bca79382d025f",

5. "Created": "2018-12-28T21:49:21.1928527Z",

6. "Path": "docker-entrypoint.sh",

7. "Args": [

8. "redis-server",

9. "--appendonly",

10. "yes"

11. ],

12. "State": {

13. "Status": "running",

14. "Running": true,

15. "Paused": false,

16. "Restarting": false,

17. "OOMKilled": false,

18. "Dead": false,

19. "Pid": 14589,

20. "ExitCode": 0,

21. "Error": "",

22. "StartedAt": "2018-12-28T21:49:21.83339Z",

23. "FinishedAt": "0001-01-01T00:00:00Z"

24. },

25. ...

26. "NetworkSettings": {

27. "Bridge": "",

28. "SandboxID": "aa5a1e2b2b1937acf1cf9f43ea1851a350481197620a628a15cd89fd32f299bb",

29. "HairpinMode": false,

30. "LinkLocalIPv6Address": "",

31. "LinkLocalIPv6PrefixLen": 0,

32. "Ports": {

33. "6379/tcp": [

34. {

35. "HostIp": "0.0.0.0",

36. "HostPort": "6379"

37. }

38. ]

39. },

40. ...

41. }

42. ]

Désormais nous pouvons démarrer le projet Java helloworldrestmicroservice afin qu'il se connecte au serveur Redis.

- Depuis la configuration d'exécution, ajouter une variable d'environnement appelée (onglet Environment) REDIS_HOST avec la valeur tcp://0.0.0.0:6379, puis faire Run.

- Pour tester le service web HelloWorld, nous utiliserons l'outil cURL. Exécuter les deux lignes de commandes suivantes afin de poster un message « HelloWorld » et de récupérer les messages « HelloWorld » envoyés.

1. # Création d'un message HelloWorld à partir d'un contenu JSON

2. $ curl -H "Content-Type: application/json" -X POST -d '{"message":"Mon HelloWorld"}' http://localhost:8080/helloworld

3. # Lister les messages HelloWorld

4. $ curl http://localhost:8080/helloworld

5. [{"rid":1,"message":"Mon HelloWorld","startDate":"Sat Dec 29 07:38:01 CET 2018"}]

Tout fonctionne parfaitement. Notre programme Java du service web HelloWorld est prêt à être isolé dans un conteneur Docker afin de devenir le microservice Rest.

VI - Exercice 4 : préparer le microservice Rest (écrire un Dockerfile et créer sa propre image Docker)

VI-A - But

- Écrire un fichier Dockerfile.

- Construire une image Docker.

- Isoler un programme Java.

- Réduire la taille d'une image Docker avec le Multi-Stage Build.

VI-B - Description

Le microservice Rest a pour objectif d'isoler dans un conteneur Docker le projet Java du service web HelloWorld. Une image Docker à base de Java et de Maven sera utilisée. Elle sera construite en s'assurant que le projet Maven compile correctement.

VI-C - Étapes à suivre

- Créer un fichier Dockerfile à la racine du projet helloworldrestmicroservice.

- Ouvrir un éditeur de texte et saisir le contenu présenté ci-dessous :

1. FROM java:openjdk-8-jdk

2. MAINTAINER Mickael BARON

4. ENV MAVEN_VERSION 3.3.9

5. RUN curl -fsSLk https://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz | tar xzf - -C /usr/share \

6. && mv /usr/share/apache-maven-$MAVEN_VERSION /usr/share/maven \

7. && ln -s /usr/share/maven/bin/mvn /usr/bin/mvn

8. ENV MAVEN_HOME /usr/share/maven

10. ADD pom.xml /work/pom.xml

11. WORKDIR /work

12. RUN ["mvn", "dependency:go-offline"]

14. ADD ["src", "/work/src"]

15. RUN ["mvn", "package"]

16. EXPOSE 8080

17. ENTRYPOINT ["java", "-cp", "target/classes:target/dependency/*", "com.kumuluz.ee.EeApplication"]

Donnons quelques détails sur le contenu de ce fichier. À la ligne 1, il est précisé que l'image que nous souhaitons construire se basera sur une image fournissant Java 8. De la ligne 4 à la ligne 8, nous précisons comment doit être installé Maven. La ligne 10 précise que le fichier pom.xml sera copié dans le répertoire /work/pom.xml de l'image. La ligne 11 indique que le répertoire courant sera /work. La ligne 12 demande à Maven de télécharger tout ce dont il a besoin. L'avantage de procéder de cette manière c'est que si vous modifiez le code source de votre projet, les dépendances n'auront pas à être de nouveau téléchargées puisqu'elles auront été faites avant. Toutefois, si vous modifiez le fichier pom.xml, la reconstruction de l'image se fera à partir de la ligne 10. La ligne 14 précise que le répertoire src sera copié dans le répertoire /work/src de l'image. À la ligne 15, il est demandé de faire une construction de package via Maven. À la ligne 17, il est indiqué que le port 8080 sera exposé. Enfin à la ligne 18, il est indiqué comment démarrer le programme Java. La ligne de commande est identique à celle que nous avions vue dans l'exercice 1.

- Nous allons construire l'image à partir du fichier Dockerfile, exécuter la ligne de commande suivante.

1. $ docker build -t mickaelbaron/helloworldrestmicroservice .

2. ...

3. Step 8/12 : RUN ["mvn", "dependency:go-offline"]

4. ---> Running in 32928666e2c3

5. [INFO] Scanning for projects...

6. Downloading: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-dependency-plugin/2.10/maven-dependency-plugin-2.10.pom

7. Downloaded: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-dependency-plugin/2.10/maven-dependency-plugin-2.10.pom (12 KB at 15.6 KB/sec)

8. Downloading: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-plugins/27/maven-plugins-27.pom

9. Downloaded: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-plugins/27/maven-plugins-27.pom (12 KB at 217.6 KB/sec)

10. Downloading: https://repo.maven.apache.org/maven2/org/apache/maven/maven-parent/26/maven-parent-26.pom

11. Downloaded: https://repo.maven.apache.org/maven2/org/apache/maven/maven-parent/26/maven-parent-26.pom (39 KB at 607.1 KB/sec)

12. Downloading: https://repo.maven.apache.org/maven2/org/apache/apache/16/apache-16.pom

13. Downloaded: https://repo.maven.apache.org/maven2/org/apache/apache/16/apache-16.pom (16 KB at 300.7 KB/sec)

14. Downloading: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-dependency-plugin/2.10/maven-dependency-plugin-2.10.jar

15. Downloaded: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-dependency-plugin/2.10/maven-dependency-plugin-2.10.jar (157 KB at 1174.3 KB/sec)

16. ...

17. Step 11/12 : EXPOSE 8080

18. ---> Running in 6f6725702850

19. Removing intermediate container 6f6725702850

20. ---> 2dc65427ed4f

21. Step 12/12 : ENTRYPOINT ["java", "-cp", "target/classes:target/dependency/*", "com.kumuluz.ee.EeApplication"]

22. ---> Running in a4290ac7fa93

23. Removing intermediate container a4290ac7fa93

24. ---> e04b1b2fc0aa

25. Successfully built e04b1b2fc0aa

26. Successfully tagged mickaelbaron/helloworldrestmicroservice:latest

Remarquer le téléchargement des dépendances Java (les fichiers Jar) réalisées lors de la commande $mvn dependendy:go-offline.

- S'assurer que l'image a été correctement construite en exécutant la ligne de commande suivante.

1. $ docker images

2. REPOSITORY                                       TAG                 IMAGE ID            CREATED             SIZE

3. mickaelbaron/helloworldrestmicroservice          latest              e04b1b2fc0aa        9 minutes ago       706MB

4. redis                                            latest              ce25c7293564        2 weeks ago         95MB

5. java                                             openjdk-8-jdk       d23bdf5b1b1b        23 months ago       643MB

Vous remarquerez que l'image java est disponible puisque l'image que nous venons de construire est basée sur celle-ci. Noter également la taille de notre image 706 Mo. En fait l'image mickaelbaron/helloworldrestmicroservice ne pèse que 60 Mo, car toute la partie Java 8 est déjà présente dans l'image de base java.

- Vérifions que les dépendances Java (les fichiers Jar) ont correctement été stockées dans les couches de notre image. Exécuter la ligne de commande shell suivante :

1. $ docker history mickaelbaron/helloworldrestmicroservice

2. IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT

3. e04b1b2fc0aa        13 minutes ago      /bin/sh -c #(nop)  ENTRYPOINT ["java" "-cp" …   0B

4. 2dc65427ed4f        13 minutes ago      /bin/sh -c #(nop)  EXPOSE 8080                  0B

5. f2be75fce3d1        13 minutes ago      mvn package                                     18.4MB

6. 7ecda19aca78        14 minutes ago      /bin/sh -c #(nop) ADD dir:6c964c0442e33433ff…   11.1kB

7. 0f237d437944        14 minutes ago      mvn dependency:go-offline                       34.1MB

8. 55ee0829d3eb        15 minutes ago      /bin/sh -c #(nop) WORKDIR /work                 0B

9. 194da6ab61a8        15 minutes ago      /bin/sh -c #(nop) ADD file:3ac8f7547512fa916…   2.44kB

10. 0fcad87e84f6        15 minutes ago      /bin/sh -c #(nop)  ENV MAVEN_HOME=/usr/share…   0B

11. 0da24f3c9e32        15 minutes ago      /bin/sh -c curl -fsSLk https://archive.apach…   10MB

12. 6ba73ad7b2d7        15 minutes ago      /bin/sh -c #(nop)  ENV MAVEN_VERSION=3.3.9      0B

13. e7bd770a86bd        15 minutes ago      /bin/sh -c #(nop)  LABEL maintener=Mickael B…   0B

14. d23bdf5b1b1b        23 months ago       /bin/sh -c /var/lib/dpkg/info/ca-certificate…   419kB

15. <missing>           23 months ago       /bin/sh -c set -x  && apt-get update  && apt…   352MB

Nous remarquons à ligne 7, l'exécution de notre commande $mvn dependency:go-offline dont le résultat a fait augmenter la taille de l'image de 34 Mo. Ceci est dû aux dépendances Java qui ont été téléchargées. Par conséquent, si nous modifions uniquement le code source de notre projet, seule la compilation des sources sera réalisée.

- Éditer la classe fr.mickaelbaron.helloworldrestmicroservice.service.HelloWorldResource et faire une modification (ajouter par exemple un espace), puis relancer la construction de l'image.

1. $ docker build -t mickaelbaron/helloworldrestmicroservice .

2. ...

3. Step 9/12 : ADD ["src", "/work/src"]

4. ---> 0b5eb5a40241

5. Step 10/12 : RUN ["mvn", "package"]

6. ---> Running in 39c1681dcef3

7. [INFO] Scanning for projects...

8. [INFO]

9. [INFO] ------------------------------------------------------------------------

10. [INFO] Building helloworldrestmicroservice 1.0-SNAPSHOT

11. [INFO] ------------------------------------------------------------------------

12. [INFO]

13. [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ helloworldrestmicroservice ---

14. ...

Vous noterez que la construction de l'image se fait plus rapidement, car les étapes précédant l'étape 9 sont en cache.

À cet étape, nous constatons que nous venons de construire une image Docker qui fournit Java dans sa version JDK, l'outil Maven, les dépendances Maven nécessaires à son fonctionnement, les classes compilées et les dépendances Maven pour l'exécution du projet *helloworldrestmicroservice*. Or, seuls ces deux deniers sont nécessaires pour la suite. Par ailleurs, la taille de l'image est quand même assez imposante : 706 Mo.

Pour résoudre ces problèmes, nous allons utiliser le Multi-Stage Build de Docker qui permet de découper un fichier Dockerfile en plusieurs phases. Dans notre cas, nous aurons ainsi une phase de compilation et une phase d'exécution. La phase exécution qui sera finale fournira une image Docker plus réduite puisqu'elle sera basée sur une image JRE et non JDK.

- Créer un fichier *DockerfileMSB* à la racine du projet *helloworldrestmicroservice*, éditer le fichier et saisir le contenu présenté ci-dessous.

1. # Build env

2. FROM maven:3-jdk-8 AS build-java-stage

3. LABEL maintainer="Mickael BARON"

5. ADD pom.xml /work/pom.xml

6. WORKDIR /work

7. RUN ["mvn", "dependency:go-offline"]

9. ADD ["src", "/work/src"]

10. RUN ["mvn", "package"]

12. # Run env

13. FROM openjdk:8-jre-slim

14. COPY --from=build-java-stage /work/target/classes /classes/

15. COPY --from=build-java-stage /work/target/dependency/*.jar /dependency/

16. EXPOSE 8080

17. ENTRYPOINT ["java", "-cp", "classes:dependency/*", "com.kumuluz.ee.EeApplication"]

Donnons quelques détails sur le contenu de ce fichier. De la ligne 1 à la ligne 10, les instructions décrivent la phase de compilation qui porte le nom de build-java-stage. De la ligne 12 à la ligne 18, les instructions décrivent la phase d'exécution. Pour la phase de compilation, contrairement à notre premier fichier Dockerfile, nous sommes partis d'une image de base Maven. Il n'est donc pas nécessaire d'installer Maven. À la fin de cette phase de compilation, les classes compilées et les dépendances seront disponibles. Dans la phase d'exécution, ces fichiers et dépendances seront copiés depuis la première phase vers les répertoires classes/ et dependency/ (lignes 14 et 15).

- Nous allons construire l'image à partir du fichier DockerfileMSB, exécuter la ligne de commande suivante.

1. $ docker build -t mickaelbaron/helloworldrestmicroservice -f DockerfileMSB .

2. Sending build context to Docker daemon  110.6kB

3. Step 1/12 : FROM maven:3-jdk-8 AS build-java-stage

4. ---> c77985f27eaf

5. Step 2/12 : LABEL maintainer="Mickael BARON"

6. ---> Using cache

7. ---> 18f92fa6c3db

8. Step 3/12 : ADD pom.xml /work/pom.xml

9. ---> Using cache

10. ---> 3fe1ae36df19

11. Step 4/12 : WORKDIR /work

12. ---> Using cache

13. ---> 5e8d3303b024

14. Step 5/12 : RUN ["mvn", "dependency:go-offline"]

15. ---> Using cache

16. ---> cf79c4db5ad8

17. Step 6/12 : ADD ["src", "/work/src"]

18. ---> Using cache

19. ---> f414fc12fca2

20. Step 7/12 : RUN ["mvn", "package"]

21. ---> Using cache

22. ---> 77d18dc14f6b

23. Step 8/12 : FROM openjdk:8-jre-slim

24. ---> 7c6b62cf60ee

25. Step 9/12 : COPY --from=build-java-stage /work/target/classes /classes/

26. ---> Using cache

27. ---> e5c2fcda00f0

28. Step 10/12 : COPY --from=build-java-stage /work/target/dependency/*.jar /dependency/

29. ---> Using cache

30. ---> 973604af82e1

31. Step 11/12 : EXPOSE 8080

32. ---> Using cache

33. ---> 78950f5169e1

34. Step 12/12 : ENTRYPOINT ["java", "-cp", "classes:dependency/*", "com.kumuluz.ee.EeApplication"]

35. ---> Using cache

36. ---> 4f6445d0688f

37. Successfully built 4f6445d0688f

38. Successfully tagged mickaelbaron/helloworldrestmicroservice:latest

- S'assurer que l'image a été correctement construite en exécutant la ligne de commande suivante.

1. $ docker images

2. REPOSITORY                                       TAG                 IMAGE ID            CREATED             SIZE

3. mickaelbaron/helloworldrestmicroservice          latest              e04b1b2fc0aa        1 minutes ago       184MB

4. redis                                            latest              ce25c7293564        2 weeks ago         95MB

5. java                                             openjdk-8-jdk       d23bdf5b1b1b        23 months ago       643MB

Nous constatons que l'image construite a diminué en taille. Elle est passée de 706 Mo à 184 Mo.

VII - Exercice 5 : Lier les microservices Redis et Rest

VII-A - But

- Créer un réseau Docker.

- Associer un conteneur existant à un réseau Docker.

- Créer un conteneur en spécifiant un réseau Docker.

- Définir des variables d'environnement à la création d'un conteneur Docker.

- Exécuter une commande Linux à partir d'un conteneur Docker en cours.

VII-B - Description

À cette étape nous disposons d'un conteneur correspondant au microservice Redis et d'une image Docker pour le futur microservice Rest dont le code se trouve dans le projet helloworldrestmicroservice. Nous allons nous intéresser dans cet exercice à créer le conteneur du microservice Rest et lui associer le conteneur du microservice Redis. Pour réaliser cette association, nous utilisons des réseaux Docker.

VII-C - Étapes à suivre

- Créer un réseau Docker appelé helloworldnetwork en exécutant la ligne de commande suivante.

1. $ docker network create helloworldnetwork

2. 14109f963b014b322fbbd3094af191517cd95d42244115d491e9abf8063d1d16

- Afficher la liste des réseaux Docker afin d'assurer que votre réseau précédent créé à bien été créé.

1. $ docker network ls

2. NETWORK ID          NAME                DRIVER              SCOPE

3. 61f6b732863b        bridge              bridge              local

4. 3017d43b85a0        helloworldnetwork   bridge              local

5. 4523411e4251        host                host                local

6. 50afa90b1502        none                null                local

Vous remarquerez que des réseaux Docker sont déjà existants (bridge, host et none). Ils sont créés par défaut lors de l'installation de Docker. Par exemple le réseau bridge est le réseau par défaut. Si aucun réseau n’est spécifié à la construction d’un conteneur, le conteneur se connectera sur le réseau par défaut bridge. Le conteneur nommé redis créé dans les précédents exercices utilise ce réseau. Les conteneurs connectés sur le réseau par défaut bridge se voient tous mais sont UNIQUEMENT accessibles en utilisant les IPs (pas les noms). Le réseau par défaut bridge n’est pas RECOMMANDÉ pour la mise en production.

Chaque conteneur connecté à un réseau Docker se verra identifié dans ce réseau par son nom. Dans le réseau Docker helloworldnetwork le conteneur redis sera identifié par redis.

- Connecter le conteneur Redis précédemment créé au réseau Docker helloworldnetwork.

1. docker network connect helloworldnetwork redis

- Pour vérifier que le conteneur Redis est bien connecté au réseau Docker helloworldnetwork deux solutions sont possibles : en interrogeant soit le conteneur soit le réseau Docker.

1. Afficher les informations du conteneur redis via l'option inspect.

1. $ docker inspect redis

2. ...

3. "Networks": {

4. "bridge": {

5. "IPAMConfig": null,

6. "Links": null,

7. "Aliases": null,

8. "NetworkID": "61f6b732863bb4652e386b60eea6ae1b1c06a76ced9f6d0b2e81779c275ef5f7",

9. "EndpointID": "302fb6762520789065c3b7430fe3ec63fac1db90dae1487bd13829c6c46e3ce4",

10. "Gateway": "172.17.0.1",

11. "IPAddress": "172.17.0.2",

12. "IPPrefixLen": 16,

13. "IPv6Gateway": "",

14. "GlobalIPv6Address": "",

15. "GlobalIPv6PrefixLen": 0,

16. "MacAddress": "02:42:ac:11:00:02",

17. "DriverOpts": null

18. },

19. "helloworldnetwork": {

20. "IPAMConfig": {},

21. "Links": null,

22. "Aliases": [

23. "f9a0f6d81967"

24. ],

25. "NetworkID": "3017d43b85a088fa605e74dc9122b69a02ea4bd2f4fb0cd3286f60a11ddee7df",

26. "EndpointID": "feb4cd85c146b58d728b47848de6f000d7c23e74a5a550fdc06cff8d5220b5c9",

27. "Gateway": "172.19.0.1",

28. "IPAddress": "172.19.0.2",

29. "IPPrefixLen": 16,

30. "IPv6Gateway": "",

31. "GlobalIPv6Address": "",

32. "GlobalIPv6PrefixLen": 0,

33. "MacAddress": "02:42:ac:13:00:02",

34. "DriverOpts": null

35. }

36. }

37. ...

1. Afficher les informations du réseau Docker helloworldnetwork.

1. $ docker network inspect helloworldnetwork

2. [

3. {

4. "Name": "helloworldnetwork",

5. "Id": "3017d43b85a088fa605e74dc9122b69a02ea4bd2f4fb0cd3286f60a11ddee7df",

6. "Created": "2018-12-30T15:49:18.1104193Z",

7. "Scope": "local",

8. "Driver": "bridge",

9. "EnableIPv6": false,

10. "IPAM": {

11. "Driver": "default",

12. "Options": {},

13. "Config": [

14. {

15. "Subnet": "172.19.0.0/16",

16. "Gateway": "172.19.0.1"

17. }

18. ]

19. },

20. "Internal": false,

21. "Attachable": false,

22. "Ingress": false,

23. "ConfigFrom": {

24. "Network": ""

25. },

26. "ConfigOnly": false,

27. "Containers": {

28. "f9a0f6d81967414672aba2edecf7e570010f1de7a61f17424287430753212d8a": {

29. "Name": "redis",

30. "EndpointID": "feb4cd85c146b58d728b47848de6f000d7c23e74a5a550fdc06cff8d5220b5c9",

31. "MacAddress": "02:42:ac:13:00:02",

32. "IPv4Address": "172.19.0.2/16",

33. "IPv6Address": ""

34. }

35. },

36. "Options": {},

37. "Labels": {}

38. }

39. ]

- Créer un conteneur pour le microservice Rest en exécutant la ligne de commande suivante.

1. $ docker run --name rest -p 8080:8080 -d --network helloworldnetwork --env REDIS_HOST=tcp://redis:6379 mickaelbaron/helloworldrestmicroservice

2. 28eba01ed267a1618019df724cbc7686b574c2f73b0575ea1352f58cdaa4afc5

Cette instruction crée un conteneur Docker appelé Rest (--name rest), dont le port 8080 est redirigé sur le port 8080 du hôte (-p 8080:8080), lancé en tâche de fond (-d), connecté au réseau Docker helloworldnetwork à partir de l'image appelée mickaelbaron/helloworldrestmicroservice. Enfin, l'option --env REDIS_HOST=tcp://redis:6379 permet de créer une variable d'environnement lors de la création du conteneur. Cette variable utilisée dans notre projet helloworldrestmicroservice sert à identifer l'accès au serveur Redis. Ici la valeur donnée est redis puisque les conteneurs dans un réseau Docker sont identifiés par leur nom de création.

- Assurons-nous que les deux conteneurs redis et rest sont connectés dans le réseau Docker helloworldnetwork.

1. $ docker network inspect helloworldnetwork

2. ...

3. "Containers": {

4. "28eba01ed267a1618019df724cbc7686b574c2f73b0575ea1352f58cdaa4afc5": {

5. "Name": "rest",

6. "EndpointID": "79c6e4ce51b6d57caddd25c9bf9f2e5bcb83d298b42477840cfeb479af1bf249",

7. "MacAddress": "02:42:ac:13:00:03",

8. "IPv4Address": "172.19.0.3/16",

9. "IPv6Address": ""

10. },

11. "f9a0f6d81967414672aba2edecf7e570010f1de7a61f17424287430753212d8a": {

12. "Name": "redis",

13. "EndpointID": "feb4cd85c146b58d728b47848de6f000d7c23e74a5a550fdc06cff8d5220b5c9",

14. "MacAddress": "02:42:ac:13:00:02",

15. "IPv4Address": "172.19.0.2/16",

16. "IPv6Address": ""

17. }

18. },

19. ...

- Vérifions également que depuis le conteneur rest le dialogue peut s'opérer via le conteneur redis, exécuter la ligne de commande suivante.

1. $ docker exec -it rest /bin/sh -c 'ping redis'

2. PING redis (172.19.0.2): 56 data bytes

3. 64 bytes from 172.19.0.2: icmp_seq=0 ttl=64 time=0.115 ms

4. 64 bytes from 172.19.0.2: icmp_seq=1 ttl=64 time=0.213 ms

5. 64 bytes from 172.19.0.2: icmp_seq=2 ttl=64 time=0.212 ms

6. ...

- Il nous reste plus qu'à tester le service web contenu dans le conteneur rest. Exécuter les deux lignes de commandes suivantes afin de poster un message « HelloWorld » et de récupérer les messages « HelloWorld » envoyés.

1. # Création d'un message « HelloWorld » à partir d'un contenu JSON

2. $ curl -H "Content-Type: application/json" -X POST -d '{"message":"Mon HelloWorld"}' http://localhost:8080/helloworld

3. # Lister les messages « HelloWorld »

4. $ curl http://localhost:8080/helloworld

5. [{"rid":1,"message":"Mon HelloWorld","startDate":"Sat Dec 29 07:38:01 CET 2018"},{"rid":1,"message":"Mon HelloWorld","startDate":"Sat Dec 29 07:38:01 CET 2018"}]

VIII - Exercice 6 : émettre et recevoir des événements

VIII-A - But

- Publier un message « HelloWorld » vers RabbitMQ.

- Afficher le contenu d'un conteneur Docker.

VIII-B - Description

Nous allons enrichir le projet Java du service web HelloWorld de façon à ce qu'un message « HelloWorld » puisse être publié sur le bus d'événements de RabbitMQ à chaque fois que le service web de création est appelé. Nous allons également créer un nouveau microservice appelé Log (contenu dans le projet helloworldlogmicroservice) qui se chargera de réceptionner les événements envoyés au bus d'événements de RabbitMQ. Pour cela, nous allons utiliser un nouveau projet Java pour l'affichage des logs sur la console.

VIII-C - Étapes à suivre

- Avant de continuer, nous allons arrêter et supprimer les conteneurs rest et redis. Exécuter les lignes de commande suivantes.

1. $ docker rm -f rest

2. $ docker rm -f redis

3. redis

- Depuis l'environnement de développement Eclipse, modifier la classe HelloWorldResource pour ajouter l'attribut currentProduceer et le contenu dans la méthode addHelloWorld.

1. @Path("/helloworld")

2. @Produces(MediaType.APPLICATION_JSON)

3. @Consumes(MediaType.APPLICATION_JSON)

4. public class HelloWorldResource {

6. @Inject

7. @Named("redis")

8. private IHelloWorldDAO currentDAO;

10. @Inject

11. private IHelloWorldEventProducer currentProducer;

13. @GET

14. public Response getHelloWorlds() {

15. return Response.ok(currentDAO.getHelloWorlds()).build();

16. }

18. @POST

19. public Response addHelloWorld(HelloWorld newHelloWorld) {

20. if (newHelloWorld != null) {

21. newHelloWorld.setStartDate(new Date().toString());

22. }

24. currentDAO.addHelloWorld(newHelloWorld);

25. currentProducer.sendMessage(newHelloWorld);

26. return Response.status(Status.CREATED).build();

27. }

28. }

- Mettre à jour l'image du microservice Rest défini dans le projet helloworldrestmicroservice, se placer à la racine du projet helloworldrestmicroservice et exécuter la ligne de commande suivante.

1. $ docker build -t mickaelbaron/helloworldrestmicroservice .

2. ...

- Importer le projet Maven helloworldlogmicroservice (File -> Import -> General -> Existing Maven Projects, choisir le répertoire du projet puis faire Finish).

- Examiner la classe HelloWorldLogMicroservice. Les événements reçus sont récupérés et affichés sur la sortie console.

- Créer un fichier Dockerfile à la racine du projet helloworldlogmicroservice.

- Ouvrir un éditeur de texte et saisir le contenu présenté ci-dessous.

1. FROM java:openjdk-8-jdk

2. LABEL MAINTAINER="Mickael BARON"

4. ENV MAVEN_VERSION 3.3.9

5. RUN curl -fsSLk https://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz | tar xzf - -C /usr/share \

6. && mv /usr/share/apache-maven-$MAVEN_VERSION /usr/share/maven \

7. && ln -s /usr/share/maven/bin/mvn /usr/bin/mvn

8. ENV MAVEN_HOME /usr/share/maven

10. ADD pom.xml /work/pom.xml

11. WORKDIR /work

12. RUN ["mvn", "dependency:go-offline"]

14. ADD ["src", "/work/src"]

15. RUN ["mvn", "package"]

16. ENTRYPOINT ["java", "-cp", "target/classes:target/dependency/*", "fr.mickaelbaron.helloworldlogmicroservice.HelloWorldLogMicroservice"]

17. CMD [localhost]

- Nous allons construire l'image à partir de fichier Dockerfile, exécuter la ligne de commande suivante depuis la racine du projet helloworldlogmicroservice :

1. $ docker build -t mickaelbaron/helloworldlogmicroservice .

2. ... // Des tonnes de lignes

3. Step 6/12 : ADD pom.xml /work/pom.xml

4. ---> bc656dde42fc

5. Step 7/12 : WORKDIR /work

6. ---> Running in 33d8f9fb4cf8

7. Removing intermediate container 33d8f9fb4cf8

8. ...

Vous remarquerez que la construction de l'image commence réellement à partir de l'étape 6. Tout ce qui est avant cette étape est déjà présent dans le cache de Docker.

- Nous ne pouvons pas continuer tant que le serveur RabbitMQ n'est pas mis en place. Nous allons donc l'installer en l'isolant dans un conteneur. Le microservice résultat s'appellera Rabbitmq. Nous utiliserons une image de RabbitMQ contenant une interface web pour la gestion des événements reçus et envoyés (rabbitmq:management). Exécuter la ligne de commande suivante.

1. docker pull rabbitmq:management

2. management: Pulling from library/rabbitmq

3. f17d81b4b692: Pull complete

4. ...

5. Digest: sha256:3eb2fa0f83914999846f831f14b900c0c85cea8e5d2db48ff73cf7defa12fe96

6. Status: Downloaded newer image for rabbitmq:management

- Il ne nous reste plus qu'à créer tous les conteneurs rest, redis et rabbitmq et de les connecter au réseau Docker helloworldnetwork. Exécuter les lignes de commande suivantes en faisant attention d'être à la racine du répertoire workspace.

1. $ docker run --name redis -d --network helloworldnetwork -v $(pwd)/data:/data redis redis-server --appendonly yes

2. $ docker run --name rabbitmq -d --network helloworldnetwork -p 5672:5672 -p 15672:15672 --hostname my-rabbit rabbitmq:management

3. $ docker run --name log -d --network helloworldnetwork mickaelbaron/helloworldlogmicroservice rabbitmq

4. $ docker run --name rest -d --network helloworldnetwork -p 8080:8080 --env REDIS_HOST=tcp://redis:6379 --env RABBITMQ_HOST=rabbitmq  mickaelbaron/helloworldrestmicroservicee

- Assurons-nous que tous les conteneurs soient opérationnels en affichant le statut des conteneurs. Exécuter la ligne de commande shell suivante :

1. $ docker ps

2. CONTAINER ID        IMAGE                                     COMMAND                  CREATED             STATUS              PORTS                                     NAMES

3. d3a1e7f0594b        mickaelbaron/helloworldrestmicroservice   "java -cp target/cla…"   4 minutes ago       Up 4 minutes        0.0.0.0:8080->8080/tcp                    rest

4. 3b6430dd7c62        mickaelbaron/helloworldlogmicroservice    "java -cp target/cla…"   13 minutes ago      Up 13 minutes                                                 log

5. ac2314ccc4bf        rabbitmq:management                       "docker-entrypoint.s…"   16 minutes ago      Up 16 minutes       4369/tcp, ..., 0.0.0.0:15672->15672/tcp   rabbitmq

6. 57f69c0deabe        redis                                     "docker-entrypoint.s…"   16 minutes ago      Up 16 minutes       6379/tcp                                  redis

- Assurons-nous que l'interface d'administration de RabbitMQ fonctionne. Ouvrir un navigateur et saisissir l'adresse http://localhost:15672 (utilisateur : guest, mot de passe : guest)

- Appeler le service web HelloWorld pour tester la chaîne complète des microservices en exécutant les commandes suivantes.

1. # Création d'un message « HelloWorld » à partir d'un contenu JSON

2. $ curl -H "Content-Type: application/json" -X POST -d '{"message":"Mon HelloWorld"}' http://localhost:8080/helloworld

3. # Lister les messages « HelloWorld »

4. $ curl http://localhost:8080/helloworld

5. [{"rid":3,"message":"Mon HelloWorld avec presque tous les microservices","startDate":"Sun Dec 30 21:10:09 UTC 2018"},{"rid":2,"message":"Mon HelloWorld","startDate":"Sat Dec 29 07:38:01 CET 2018"},{"rid":1,"message":"Mon HelloWorld","startDate":"Sat Dec 29 07:30:01 CET 2018"}]

- Afficher le contenu des logs du conteneur log en exécutant la commande suivante.

1. $ docker logs log

2. [x] Received '{"rid":2,"message":"Mon HelloWorld avec presque tous les microservices","startDate":"Mon Feb 01 16:30:08 UTC 2016"}'

IX - Exercice 7 : composer tous les microservices avec DockerCompose

IX-A - But

- Ecrire un fichier docker-compose.yml.

- Utiliser l'outil Docker Compose pour composer des conteneurs Docker.

IX-B - Description

Dans les précédents exercices, nous avons procédé à la création de chaque image et nous avons ensuite créé les conteneurs associés. Toutes ces tâches ont été réalisées via les commandes docker pull, docker build et docker run. Comme nous avons pu le constater, cela reste utilisable quand il y a peu de conteneurs, mais lorsqu'il y a plus de deux conteneurs cela devient difficile de tout gérer. C'est pour cette raison que nous allons employer l'outil Docker Compose.

IX-C - Étapes à suivre

- Avant de commencer, faire « table rase » en supprimant tous les conteneurs précédemment créés. Exécuter la ligne commande suivante.

1. $ docker rm -f $(docker ps -q)

2. d3a1e7f0594b

3. 3b6430dd7c62

4. ac2314ccc4bf

5. 57f69c0deabe

- S'assurer que tous les conteneurs ont été supprimés en exécutant la commande shell suivante.

1. $ docker ps

2. CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

Nous allons également introduire un nouveau microservice qui se chargera de fournir une interface web à notre application : le microservice Web. Le code de cette application est dans le projet helloworldwebmicroservice. Le contenu du code est réalisé en HTML, JavaScript, Bootstrap et JQuery. Les dépendances des bibliothèques JavaScript sont gérées par l'outil Bower et la version distribuable du projet est obtenue par des tâches Grunt.

- Parcourir les fichiers contenus dans le projet helloworldwebmicroservice. Vous remarquerez dans le fichier resthosts.js une variable restHostUrl qui prend la valeur de l'URL du microservice Rest. Dans notre cas, il s'agit de l'URL http://localhost:8080/helloworld. Modifier la valeur de cette variable.

- Créer un fichier docker-compose.yml à la racine du workspace.

- Éditer le fichier et compléter comme ci-dessous.

1. version: '3'

3. services:

4. redis:

5. image: redis

6. command: redis-server --appendonly yes

7. volumes:

8. - ./data:/data

9. networks:

10. - helloworldnet

12. rabbitmq:

13. image: rabbitmq:management

14. hostname: my-rabbit

15. ports:

16. - 5672:5672

17. - 15672:15672

18. networks:

19. - helloworldnet

21. rest:

22. build: helloworldrestmicroservice/

23. image: mickaelbaron/helloworldrestmicroservice:latest

24. depends_on:

25. - rabbitmq

26. - redis

27. ports:

28. - 8080:8080

29. environment:

30. REDIS_HOST: tcp://redis:6379

31. RABBITMQ_HOST: rabbitmq

32. networks:

33. - helloworldnet

35. web:

36. build: helloworldwebmicroservice/

37. image: mickaelbaron/helloworldwebmicroservice:latest

38. ports:

39. - 80:8080

40. networks:

41. - helloworldnet

43. log:

44. build: helloworldlogmicroservice/

45. image: mickaelbaron/helloworldlogmicroservice:latest

46. depends_on:

47. - rabbitmq

48. command: rabbitmq

49. networks:

50. - helloworldnet

51. networks:

52. helloworldnet:

53. external:

54. name: helloworldnetwork

Veuillez noter deux choses.

1. toutes les options passées en paramètres de la commande docker run des exercices précédents sont réutilisées ;

2. le paramètre build permet de construire l'image en se basant sur les fichiers Dockerfile présents dans les répertoires des projets.

- Nous pouvons donc exécuter ce fichier en utilisant la commande docker-compose up comme précisé ci-dessous (s'assurer d'être à la racine du répertoire workspace).

1. $ docker-compose up -d

2. Building web

3. Step 1/10 : FROM node:latest

4. latest: Pulling from library/node

5. cd8eada9c7bb: Pull complete

6. c2677faec825: Pull complete

7. fcce419a96b1: Pull complete

8. 045b51e26e75: Pull complete

9. 83aa5374cd04: Pull complete

10. bb9752e24b3a: Pull complete

11. 04b2f7baa231: Pull complete

12. 4dad5ba692b4: Pull complete

13. Digest: sha256:fa9ac2ee6b4bab070e669f5a88da97cad75e9ed6fa785a5b3dc22b42d6c41149

14. Status: Downloaded newer image for node:latest

15. ---> 2718f90558b7

16. Step 2/10 : LABEL MAINTAINER="Mickael BARON"

17. ---> Running in 8875e6cecd05

18. Removing intermediate container 8875e6cecd05

19. ---> e8634d57c589

20. Step 3/10 : RUN npm install -g grunt-cli && npm install -g http-server

21. ---> Running in 95993e32cc3f

22. /usr/local/bin/grunt -> /usr/local/lib/node_modules/grunt-cli/bin/grunt

23. ...

24. Creating workspace_web_1      ... done

25. Creating workspace_redis_1    ... done

26. Creating workspace_rabbitmq_1 ... done

27. Creating workspace_log_1      ... done

28. Creating workspace_rest_1     ... done

- Affichez les logs des conteneurs en exécutant la commande suivante.

1. $ docker-compose logs

2. redis_1     | 1:M 31 Dec 2018 14:34:55.652 * Ready to accept connections

3. web_1       | Starting up http-server, serving /workdir/site

4. web_1       | Available on:

5. web_1       |   http://127.0.0.1:8080

6. web_1       |   http://172.19.0.4:8080

7. web_1       | Hit CTRL-C to stop the server

8. ...

Pour différencier de quelle sortie les logs sont issus, il est précisé sur la zone de gauche le nom du conteneur en suivant la convention <nom>_<indice>. <nom> précise le nom du conteneur et <nom> précise de quelle instance du conteneur il s'agit. Cette information est importante quand vous faites usage de docker-compose scale pour augmenter le nombre d'instances d'un conteneur. Nous étudierons ce point dans un prochain tutoriel.

- Vérifier que tous les conteneurs ont été correctement créés en exécutant la ligne commande suivante.

1. $ docker-compose ps

2. Name                      Command               State                                 Ports

3. ---------------------------------------------------------------------------------------------------------------------------------

4. workspace_log_1        java -cp target/classes:ta ...   Exit 1

5. workspace_rabbitmq_1   docker-entrypoint.sh rabbi ...   Up       15671/tcp, 0.0.0.0:15672->15672/tcp, ..., 0.0.0.0:5672->5672/tcp

6. workspace_redis_1      docker-entrypoint.sh redis ...   Up       6379/tcp

7. workspace_rest_1       java -cp target/classes:ta ...   Up       0.0.0.0:8080->8080/tcp

8. workspace_web_1        http-server /workdir/site  ...   Up       0.0.0.0:80->8080/tcp

Nous remarquons que le conteneur microservices_log_1 est arrêté (State = Exit 1).

- Examinons les logs du conteneur du microservice Log pour comprendre la raison de son arrêt. Exécuter la ligne de commande suivante.

1. $ docker-compose logs log

2. Attaching to workspace_log_1

3. log_1       | Exception in thread "main" java.net.ConnectException: Connection refused (Connection refused)

4. log_1       |     at java.net.PlainSocketImpl.socketConnect(Native Method)

5. log_1       |     at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)

6. log_1       |     at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)

7. log_1       |     at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)

8. log_1       |     at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)

9. log_1       |     at java.net.Socket.connect(Socket.java:589)

10. log_1       |     at com.rabbitmq.client.impl.FrameHandlerFactory.create(FrameHandlerFactory.java:32)

11. log_1       |     at com.rabbitmq.client.ConnectionFactory.newConnection(ConnectionFactory.java:676)

12. log_1       |     at com.rabbitmq.client.ConnectionFactory.newConnection(ConnectionFactory.java:722)

13. log_1       |     at fr.mickaelbaron.helloworldlogmicroservice.HelloWorldLogMicroservice.<init>(HelloWorldLogMicroservice.java:26)

14. log_1       |     at fr.mickaelbaron.helloworldlogmicroservice.HelloWorldLogMicroservice.main(HelloWorldLogMicroservice.java:72)

Le microservice Log n'arrive pas à se connecter au serveur RabbitMQ puisque ce dernier a un temps de démarrage relativement long. Par conséquent la connexion au serveur RabbitMQ est faite trop tôt. Cette problématique est connue chez les utilisateurs de l'outil Docker Compose https://docs.docker.com/compose/faq/#how-do-i-get-compose-to-wait-for-my-database-to-be-ready-before-starting-my-application. Pour pallier ce problème, nous allons modifier le programme Java afin de pouvoir tenter une nouvelle connexion en cas d'échec. Cette solution est connue sous le nom de healthcheck.

- Ouvrir et éditer la classe HelloWorldLogMicroservice et compléter/ajouter le code suivant.

1. public HelloWorldLogMicroservice(String rabbitMQHosts) ... {

2. ConnectionFactory factory = new ConnectionFactory();

3. factory.setHost(rabbitMQHosts);

5. final Connection connection = createConnection(factory);

6. final Channel channel = connection.createChannel();

7. ...

8. }

10. private Connection createConnection(ConnectionFactory factory) ... {

11. // We implement an healthcheck.

13. boolean connectionIsReady = false;

14. Connection connection = null;

15. while(!connectionIsReady) {

16. try {

17. connection = factory.newConnection();

18. connectionIsReady = true;

19. } catch (Exception e) {

20. System.out.println("Problem:" + e.getMessage());

21. System.out.println("We will try to connect to RabbitMQ in 5s.");

22. Thread.sleep(5000);

23. }

24. }

26. System.out.println("Great !! Connected to RabbitMQ.");

27. return connection;

28. }

- Pour recompiler uniquement l'image du conteneur *log*, exécuter la ligne de commande suivante.

1. $ docker-compose build log

2. Building log

3. Step 1/12 : FROM java:openjdk-8-jdk

4. ---> d23bdf5b1b1b

5. Step 2/12 : LABEL MAINTAINER="Mickael BARON"

6. ---> Using cache

7. ---> 0daf28a8fb12

8. Step 3/12 : ENV MAVEN_VERSION 3.3.9

9. ---> Using cache

10. ---> e9a99812c33e

11. Step 4/12 : RUN curl -fsSLk https://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz | tar xzf - -C /usr/share     && mv /usr/share/apache-maven-$MAVEN_VERSION /usr/share/maven     && ln -s /usr/share/maven/bin/mvn /usr/bin/mvn

12. ---> Using cache

13. ---> 72d4cf747bb2

14. ...

15. Step 11/12 : ENTRYPOINT ["java", "-cp", "target/classes:target/dependency/*", "fr.mickaelbaron.helloworldlogmicroservice.HelloWorldLogMicroservice"]

16. ---> Running in 0f31033988a7

17. Removing intermediate container 0f31033988a7

18. ---> 048016e32f90

19. Step 12/12 : CMD [localhost]

20. ---> Running in 2cab3b4ecd76

21. Removing intermediate container 2cab3b4ecd76

22. ---> 95abc3ec1549

23. Successfully built 95abc3ec1549

24. Successfully tagged mickaelbaron/helloworldlogmicroservice:latest

- Pour recréer les conteneurs, exécuter la ligne de commande suivante.

1. $ docker-compose up -d

2. workspace_web_1 is up-to-date

3. workspace_redis_1 is up-to-date

4. workspace_rabbitmq_1 is up-to-date

5. workspace_rest_1 is up-to-date

6. Recreating workspace_log_1 ... done

- Vérifier que les conteneurs ont correctement été créés en exécutant la ligne de commande suivante.

1. $ docker-compose ps

2. Name                      Command               State                                Ports

3. --------------------------------------------------------------------------------------------------------------------------------

4. workspace_log_1        java -cp target/classes:ta ...   Up

5. workspace_rabbitmq_1   docker-entrypoint.sh rabbi ...   Up      15671/tcp, 0.0.0.0:15672->15672/tcp, ..., 0.0.0.0:5672->5672/tcp

6. workspace_redis_1      docker-entrypoint.sh redis ...   Up      6379/tcp

7. workspace_rest_1       java -cp target/classes:ta ...   Up      0.0.0.0:8080->8080/tcp

8. workspace_web_1        http-server /workdir/site  ...   Up      0.0.0.0:80->8080/tcp

- Il ne nous reste plus qu'à tester. Ouvrir un navigateur et se rendre à cette adresse : http://localhost.

X - Conclusion, perspectives et remerciements

Nous avons vu au travers de huit exercices comment construire une application en respectant une architecture à base de microservices. Nous avons utilisé un ensemble de technologies dont notamment Docker pour l'isolation, RabbitMQ pour la communication asynchrone via un bus d'événements, Docker Compose pour la composition et des services web REST pour la communication synchrone.

Dans un prochain atelier, nous montrerons les aspects de montée en charge et notamment l'avantage d'utiliser Docker Compose pour le pilotage du nombre de conteneurs d'une même image. Nous insisterons aussi sur l'intérêt d'utiliser un reverse proxy et un load balancer dans ce cas d'usage.

Pour aller plus loin, vous pouvez consulter les ressources suivantes :

- Support de cours SOA

- Support de cours WSDL

- Support de cours SOAP

- Support de cours JAX-WS

- Support de cours REST

- Support de cours JAX-RS

- Exercices sur le test fonctionnel de services web avec SoapUI

- Exercices sur le développement de services web étendus avec JAX-WS, Maven et Eclipse

Vous pouvez retrouver cette série d'exercices sur mon compte Github : https://github.com/mickaelbaron/javamicroservices-tutorial.

Je tiens à remercier Claude Leloup pour la relecture orthographique de cet article et Logan Mauzaize pour sa relecture technique attentive et ses nombreuses bonnes pratiques.