TP1 – Découverte conteneurisation

Ce TP sert d’introduction à la conteneurisation d’application. Le but est d’apprendre à conteneuriser et exécuter (en répartissant la charge) une application java.

L’application conteneurisée, sera l’application Backend du TP précédent. Les conteneurs seront lancés en plusieurs instances pour être interrogé par le client.

Avant de commencer le TP, il faut bien vérifier d’avoir Docker installé et fonctionnel.

Pour l’évaluation, les corrections devront être poussées dans une branche GIT avec le nom du groupe dans le dépôt du TP.

Conteneurisation de l’application

App container docker

Pour conteneuriser l’application, nous allons utiliser Docker. Sachez que Docker fait partie de l’OCI ce qui fait que les conteneurs générés sont normalisés.

Un autre équivalent à Docker serait Podman. Il fait lui aussi partie de l’OCI, ce qui fait que les conteneurs sont compatibles entre eux.

Pour connaitre la liste des commandes Docker, vous pouvez simplement exécuter la commande docker, ou alors consulter la documentation.

Les programmes JAVA à conteneuriser sont disponibles dans le Dépôt Gitlab M2M.

Ils sont directement fonctionnels et présents dans les dossiers:

Dockerfile

Pour créer une image Docker, il faut créer un fichier Dockerfile. Ce fichier contient une série d’instruction à réaliser pour que l’image soit construite.

La première chose à faire pour un Dockerfile est de se baser sur une image. La plupart du temps, on utilise des images de base tel que Debian, Ubuntu, Alpine

Les images sont téléchargeables à partir de registries. Le plus connu est le DockerHub. Mais d’autres dépôts (public ou privé) existe. Vous pouvez même héberger les vôtres. À la fin de cette partie, il vous sera d’ailleurs demandé de pousser vos images sur un registry externe.

Java container docker

Pour créer une image de notre application, il faut d’abord regarder ce dont l’application a besoin.

Premièrement l’application JAVA a besoin d’une jvm pour fonctionner. Il faudra donc baser l’image sur une image OpenJDK

À partir de cette image, il faudra compiler l’application en faisant un mvn clean package à partir du dossier où se trouve le pom.xml. Les jars générés se trouveront dans le dossier sous-jacent target/*.

Ensuite il faut copier la configuration *-app/conf, et le jar de l’application dans l’image: /etc/conf/, /usr/local/bin/app.jar.

L’application java doit être lancée en spécifiant le classpath au lancement: java -cp app.jar:/etc/conf m2m.app.App

m2m.app.App est la classe principale de tout les programmes JAVA.

Enfin, ne pas oublier d’exposer le port de l’application (différent selon le programme).

Les urls/ports pour les différentes applications sont:

Si on résume un Dockerfile contient les instructions suivantes:

FROM <image OpenJDK>

COPY <source> <destination>

RUN <command>

CMD ["java", "-cp", "classpath", "main_class", "args"]

EXPOSE <port>

Une fois que vous avez généré vos images sans erreur, elles seront visibles en faisant un $ docker images

Créer les images correspondantes de l’application.

Build dans Dockerfile

Java build container docker

Une fois que les images sont fonctionnelles, le but est de faire le build de l’application Java pendant la construction du Dockerfile. Pour cela il faut utiliser une image Docker Maven pour effectuer le build.

L’image Maven peut être utilisé directement en ligne de commande (cf doc):

$ docker run -it --rm -v "$(pwd)":/usr/src/mymaven -w /usr/src/mymaven maven:3-jdk-11 mvn clean package

Mais le mieux est de faire un build multi stage. Le build permettra en un seul Dockerfile, de build et de créer une image du programme Java.

Les éléments important du multi stage sont de se baser sur une image Maven:

et de récupérer les fichiers générés avec:

COPY --from=build <src>... <dest>

Ajouter le build intégré aux images.

Docker registry

Push local

Comme évoqué au début du TP, une fois que vos images sont fonctionnelles, elles peuvent être poussées dans un registry privé. Dans le cadre du TP, un registry a été mis en place pour héberger les images des applications.

Pour se connecter au registry, il faut utiliser la commande Docker login. Une fois que Docker est connecté au registry, il ne sera plus nécessaire d’exécuter la commande une seconde fois.

Le mot de passe à renseigner pour le user oauth est le token JWT de la plateforme.

$ docker login -u oauth polytech.overware.fr

Une fois que votre Docker y est connecté, vous allez pouvoir commencer à pousser les images. Pour cela, il faut les tagger pour que Docker sachent où les pousser.

Le registry étant hébergé sur le domaine polytech.overware.fr, il faut envoyer ou récupérer les images comme ceci:

$ docker push polytech.overware.fr/image_name:tag
$ docker pull polytech.overware.fr/image_name:tag

Poussez vos images générées dans le registry.

Vos images devraient apparaitre dans le registry Docker.

Push CI

Pour éviter de pousser les images manuellement à chaque modification, il est possible de mettre en place un CI/CD.

Nous allons le faire sur Gitlab vu que c’est la plateforme sur laquelle les projets sont hébergés. Mais il existe d’autres solutions de CI/CD tel que Jenkins, Woodpecker CI

Sur Gitlab, pour créer un CI pour votre projet, il suffit de créer un fichier .gitlab-ci.yml. Ce fichier est un yaml contenant les jobs à exécuter.

Pour le cas du TP, il faut 3 jobs (1 par application) Par exemple pour la première application, le job doit ressembler à:

app1:
  stage: deploy
  when: manual
  tags:
    - docker
  script:
    - <première commande à executer>
    - <deuxième>
    - <...>

Pour ne pas exécuter trop de CI, les jobs devront être mis avec l’option when: manual qui obligera à les lancer manuellement sur Gitlab. Les tags sont une indication pour utiliser un groupe de runner proposant ce service.

Les runner mis en place accède directement au registry Docker sans authentification. Il n’est donc pas nécessaire d’utiliser des variables pour y stocker les credentials.

Sinon il aurait fallu ajouter des variables (masqué pour le mot de passe) et ajouter par exemple dans le job docker:

before_script:
  - docker login -u $DOCKER_USER -p $DOCKER_PWD

Les runner faisant tourner les commandes dans des conteneurs Docker, il faut indiquer l’image à utiliser pour les builds. Cela peut être spécifié par job ou alors au global avec la configuration en début de fichier:

default:
  image: "docker"

Créez un fichier .gitlab-ci.yml pour pousser vos images directement dans le registry.

Lancement de la stack

Une fois que vos images sont fonctionnelles, la stack va pouvoir être lancée. Pour lancer tous les conteneurs c’est Docker Compose qui sera utilisé.

Dans le dossier, un docker-compose.yml existe déjà pour lancer Traefik, Prometheus, et Grafana. Prometheus et Grafana seront vu ultérieurement dans un autre TP. Dans ce TP, le focus sera fait sur Traefik. Les courbes Grafana serviront uniquement pour Traefik. Aucune courbe ne sera à ajouter.

Ajouter vos images pour qu’elles soient lancées dans le docker-compose.

Puis une fois qu’elles sont ajoutées vous devriez pouvoir démarrer/arrêter votre stack avec les commandes:

# Démarrer votre stack en gardant le focus
docker-compose up

# Démarrer/arrêter votre stack
docker-compose up -d
docker-compose down

Traefik

Une fois que les images sont ajoutées, il va falloir router le flux vers chaque application. Pour cela, il faut utiliser Traefik.

Lorsque vous démarrez la stack, l’interface de Traefik est disponible en localhost. Mais comme vous pourrez le voir, vous ne pourrez pas changer la configuration.

En effet, Traefik est fait pour se configurer par rapport aux conteneurs lancés par Docker. Traefik doit d’ailleurs être relié à l’interface de Docker /var/run/docker.sock

Au niveau du fonctionnement de Traefik, si on prend exemple sur la configuration de Grafana, on a 4 grands principes:

  1. Entrypoints qui correspond aux ports d’entrées du proxy Traefik.
  2. Routers qui est le système de routage de Traefik. Dans notre cas, le routage sera basé sur l’URL HTTP
  3. Middlewares qui est un système de modification de requêtes
  4. Services qui sont les services en sortie

Avec tous ces éléments, faire la configuration de vos services pour que Traefik route les appels du simulateur vers votre application.

Ne pas oublier que les services doivent être appelés en HTTPS. Il faudra donc rajouter aux labels loadbalancer Traefik: traefik.http.services.<service_name>.loadbalancer.server.scheme=https

Répartition de charge

Après avoir configuré vos services, de la charge devrait être observé sur chaque service, en utilisant le script simul, qui utilise la commande: java -cp target/m2m-client-jar-with-dependencies.jar:conf fr.m2m.client.Main simul --client-id AThomas --client-secret theSuperSecret --auth-url https://localhost:8443/auth --metric-url https://localhost:8443/metric --diag-url https://localhost:8443/diag -n 100.

À cette étape, préparer un tableau pour renseigner les TPS et les temps de réponse des différents services. Ce tableau devra regrouper ces informations pour chaque paramètre modifié. Il devra être inclus dans votre dépôt GIT.

Scale up

Un des premiers paramètre sur lequel on peut agir est le nombre d’instance lancé. Pour modifier le nombre d’instance utiliser la commande scale de Docker-compose:

$ docker-compose up --scale app_name=3

Faites varier le nombre d’instance par rapport à vos besoins et observer l’évolution des métriques (n’oubliez pas de tout noter, et de faire des captures d’écrans dans le README de votre projet).

Quand vous augmenterez le nombre d’instance, vous pourrez voir que la charge de vos services sera répartie par Traefik entre vos nouvelles instances.

Trafic entrant

Dans un second temps, on peut faire varier le trafic en entrée pour voir l’impact sur la qualité de service de l’application.

Par Jérémy HERGAULT, le .