Création d'images Docker optimisées pour l'application Spring Boot

Les conteneurs sont devenus le moyen privilégié d'emballer une application avec toutes les dépendances logicielles et le système d'exploitation, puis de les livrer dans différents environnements.

Cet article aborde les différentes manières de conteneuriser une application Spring Boot :

  • créer une image Docker à l'aide d'un fichier Docker,
  • créer une image OCI à partir du code source à l'aide de Cloud-Native Buildpack,
  • et optimiser l'image à l'exécution en divisant les parties JAR en différents niveaux à l'aide d'outils multi-niveaux.

 Exemple de code

Cet article est accompagné d'un exemple de code fonctionnel sur GitHub .

Terminologie des conteneurs

Nous commencerons par la terminologie des conteneurs utilisée dans l'article :

  • Image de conteneur (Container image): un fichier de format spécifique. Nous convertissons notre application en image de conteneur en exécutant un outil de construction.
  • Conteneur: une instance exécutable de l'image de conteneur.
  • Moteur de conteneur (Container engine): un processus démon responsable de l'exécution du conteneur.
  • Hôte de conteneur (Container host): l'ordinateur hôte sur lequel fonctionne le moteur de conteneur.
  • Registre de conteneurs (Container registry): un emplacement commun utilisé pour publier et distribuer des images de conteneur.
  • Norme OCI: Open Container Initiative (OCI) est une structure de gestion ouverte et légère, formée sous l'égide de la Linux Foundation. La spécification des images OCI définit des normes industry pour les formats d'images de conteneurs et d'exécution, afin de garantir que tous les moteurs de conteneur peuvent exécuter des images de conteneur créées par n'importe quel outil de construction.

Pour mettre une application dans un conteneur, nous encapsulons notre application dans une image de conteneur et publions cette image dans un registre public. L'environnement d'exécution du conteneur extrait cette image du registre, la décompresse et exécute l'application à l'intérieur.

La version 2.3 de Spring Boot fournit des plugins pour créer des images OCI.

Docker c'est l'implémentation de conteneur la plus couramment utilisée, et nous utilisons Docker dans nos exemples, donc toutes les références ultérieures au conteneur dans cet article désigneront Docker.

Construire une image de conteneur de manière traditionnelle

Il est très facile de créer des images Docker pour des applications Spring Boot en ajoutant quelques instructions dans le fichier Docker.

Tout d'abord, nous créons un fichier exécutable JAR et, dans le cadre des instructions du fichier Docker, nous copions le fichier JAR exécutable sur l'image de base JRE après avoir appliqué les paramètres nécessaires.

Créons notre application Spring sur Spring Initializr avec les dépendances web, lomboket actuator. Nous ajoutons également un contrôleur REST pour fournir une API avec GETméthode.

Création du fichier Docker

Ensuite, nous mettons cette application dans un conteneur en ajoutant Dockerfile:

FROM adoptopenjdk:11-jre-hotspot
ARG JAR_FILE=target\/.*.jar
COPY ${JAR_FILE} application.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","\/application.jar"]

Notre fichier Docker contient une image de base de adoptopenjdk, sur laquelle nous copions notre fichier JAR, puis nous ouvrons le port, 8080qui écoutera les requêtes.

Construction de l'application

Nous devons d'abord construire l'application à l'aide de Maven ou Gradle. Ici, nous utilisons Maven :

mvn clean package

Cela crée un fichier JAR exécutable pour l'application. Nous devons convertir ce JAR exécutable en une image Docker pour fonctionner dans le moteur Docker.

Création de l'image du conteneur

Ensuite, nous plaçons ce fichier JAR exécutable dans l'image Docker en exécutant la commande docker buildà partir du répertoire racine du projet, contenant le fichier Docker créé précédemment :

docker build -t usersignup:v1 .

Nous pouvons voir notre image dans la liste en utilisant la commande :

docker images 

Le résultat de l'exécution de la commande ci-dessus inclut notre image usersignupavec l'image de base, adoptopenjdk, spécifiée dans notre fichier Docker.

REPOSITORY          TAG                 SIZE
usersignup          v1                  249MB
adoptopenjdk        11-jre-hotspot      229MB

Examen des couches à l'intérieur de l'image du conteneur

Regardons la pile des couches à l'intérieur de l'image. Nous allons utiliser l'outil  dive, pour explorer ces couches :

dive usersignup:v1

Voici une partie des résultats de l'exécution de la commande Dive : 

Création d'images Docker optimisées pour l'application Spring Boot

Comme nous le voyons, le niveau applicatif représente une part importante de la taille de l'image. Nous souhaitons réduire la taille de cette couche dans les sections suivantes dans le cadre de notre optimisation.

Création de l'image du conteneur avec Buildpack

Buildpacks (Buildpacks) est un terme générique utilisé par diverses offres « Platform as a Service » (PaaS) pour créer une image de conteneur à partir du code source. Il a été lancé par Heroku en 2011 et a depuis été adopté par Cloud Foundry, Google App Engine, Gitlab, Knative et d'autres.

Création d'images Docker optimisées pour l'application Spring Boot

L'avantage des buildpacks dans le cloud

Un des principaux avantages de l'utilisation de Buildpack pour créer des images est que Les modifications de configuration des images peuvent être gérées de manière centralisée (builder) et diffusées sur toutes les applications utilisant le builder.

Les paquets de construction étaient étroitement liés à la plateforme. Les Cloud-Native Buildpacks assurent une standardisation entre les plateformes, en prenant en charge le format d'image OCI, qui garantit que l'image peut être exécutée par le moteur Docker.

Utilisation du plugin Spring Boot

Le plugin Spring Boot crée des images OCI à partir du code source à l'aide de Buildpack. Les images sont créées en utilisant bootBuildImageles tâches (Gradle) ou spring-boot:build-imageles cibles (Maven) et l'installation locale de Docker.

Nous pouvons configurer le nom de l'image requise pour être envoyée au registre Docker en spécifiant le nom dans image tag:

<plugin>
  <groupId>org.springframework.boot<\/groupId>
  <artifactId>spring-boot-maven-plugin<\/artifactId>
  <configuration>
    <image>
      <name>docker.io\/pratikdas\/${project.artifactId}:v1<\/name>
    <\/image>
  <\/configuration>
<\/plugin>

Utilisons Maven pour exécuter build-imagecible pour créer l'application et créer l'image du conteneur. Nous n'utilisons actuellement aucun fichier Docker.

mvn spring-boot:build-image

Le résultat sera environ comme suit :

[INFO] --- spring-boot-maven-plugin:2.3.3.RELEASE:build-image (default-cli) @ usersignup ---
[INFO] Construction de l'image 'docker.io\/pratikdas\/usersignup:v1'
[INFO] 
[INFO]  > Récupération de l'image builder 'gcr.io\/paketo-buildpacks\/builder:base-platform-api-0.3' 0%
.
.
.. [créateur]     Ajout de l'étiquette 'org.springframework.boot.version'
.. [créateur]     *** Images (c311fe74ec73):
.. [créateur]           docker.io\/pratikdas\/usersignup:v1
[INFO] 
[INFO] Image 'docker.io\/pratikdas\/usersignup:v1' construite avec succès

D'après la sortie, nous voyons que le paketo Cloud-Native buildpackest utilisé pour créer une image OCI fonctionnelle. Comme auparavant, nous pouvons voir l'image indiquée comme une image Docker en exécutant la commande :

docker images 

Conclusion :

REPOSITORY                             SIZE
paketobuildpacks\/run                  84.3MB
gcr.io\/paketo-buildpacks\/builder      652MB
pratikdas\/usersignup                  257MB

Création d'une image de conteneur avec Jib

Jib est un plugin pour créer des images proposé par Google, qui fournit une méthode alternative pour créer une image de conteneur à partir du code source.

Configurer jib-maven-plugindans le pom.xml :

      <plugin>
        <groupId>com.google.cloud.tools<\/groupId>
        <artifactId>jib-maven-plugin<\/artifactId>
        <version>2.5.2<\/version>
      <\/plugin>

Ensuite, nous exécutons le plugin Jib avec la commande Maven pour construire l'application et créer l'image du conteneur. Comme auparavant, ici nous n'utilisons aucun fichier Docker :

mvn compile jib:build -Dimage=<docker registry name>\/usersignup:v1

Après avoir exécuté la commande Maven ci-dessus, nous obtenons la sortie suivante :

[INFO] Conteneurisation de l'application en pratikdas/usersignup:v1...
.
.
[INFO] Point d'entrée du conteneur défini sur [java, -cp, /app/resources:/app/classes:/app/libs/*, io.pratik.users.UsersignupApplication]
[INFO] 
[INFO] Image construite et poussée en tant que pratikdas/usersignup:v1
[INFO] Exécution des tâches :
[INFO] [==============================] 100.0% complet

Les résultats montrent que l'image du conteneur a été créée et placée dans le registre.

Motivations et méthodes d'optimisation des images

Nous avons deux principales raisons pour l'optimisation :

  • Performance: dans un système d'orchestration de conteneurs, l'image du conteneur est extraite du registre sur l'hôte où le moteur de conteneur est en cours d'exécution. Ce processus s'appelle le lancement. L'extraction d'images de grande taille à partir du registre entraîne des temps de lancement prolongés dans les systèmes d'orchestration de conteneurs et des délais de construction longs dans les pipelines CI.
  • Sécurité: les images de grande taille présentent également une large surface d'attaques potentielles.

Une image Docker se compose d'une pile de couches, chacune représentant une instruction dans notre Dockerfile. Chaque couche représente une delta des modifications de la couche sous-jacente. Lorsque nous extrayons une image Docker du registre, elle est extraite par couches et mise en cache sur l'hôte.

Spring Boot utilise le « JAR épais » comme format d'emballage par défaut. Lorsque nous examinons le JAR épais, nous voyons que l'application ne représente qu'une très petite partie de l'ensemble du JAR. C'est la partie qui change le plus souvent. Le reste est constitué des dépendances du Spring Framework. La formule d'optimisation est centrée sur l'isolation de l'application à un niveau séparé des dépendances du Spring Framework.

La couche des dépendances, qui constitue la majeure partie du fichier JAR épais, est chargée une seule fois et mise en cache dans le système hôte.

Seule la couche fine de l'application est extraite lors des mises à jour de l'application et du lancement des conteneurs,

comme illustré dans ce diagramme : Dans les sections suivantes, nous verrons comment créer ces images optimisées pour l'application Spring Boot.

Création d'images Docker optimisées pour l'application Spring Boot

Création d'une image de conteneur optimisée pour l'application Spring Boot à l'aide de Buildpack

Spring Boot 2.3 prend en charge la superposition en extrayant des parties du fichier JAR épais en couches séparées. La fonction de superposition est désactivée par défaut et doit être explicitement activée à l'aide du plugin Spring Boot Maven :

Spring Boot 2.3 prend en charge la stratification en extrayant des parties d'un gros fichier JAR dans des couches distinctes. La fonctionnalité de stratification est désactivée par défaut et doit être activée explicitement à l'aide du plugin Spring Boot Maven :

org.springframework.boot
  spring-boot-maven-plugin
  
    
      true

Nous allons utiliser cette configuration pour créer notre image de conteneur d'abord avec Buildpack, puis avec Docker dans les sections suivantes.

Lançons build-imagela cible Maven pour créer l'image de conteneur :

mvn spring-boot:build-image

Si nous lançons Dive pour voir les couches dans l'image résultante, nous verrons que la couche de l'application (entourée en rouge) est beaucoup plus petite, dans la plage des kilooctets, par rapport à ce que nous avons obtenu avec le format JAR épais :

Création d'images Docker optimisées pour l'application Spring Boot

Créer une image de conteneur optimisée pour une application Spring Boot avec Docker

Au lieu d'utiliser le plugin Maven ou Gradle, nous pouvons également créer une image JAR multi-couches Docker avec un fichier Docker.

Lorsque nous utilisons Docker, nous devons effectuer deux étapes supplémentaires pour extraire les couches et les copier dans l'image finale.

Le contenu du JAR obtenu après la construction avec Maven avec la fonction de couches activée ressemblera à ceci :

META-INF/
.
BOOT-INF/lib/
.
BOOT-INF/lib/spring-boot-jarmode-layertools-2.3.3.RELEASE.jar
BOOT-INF/classpath.idx
BOOT-INF/layers.idx

La sortie affiche un JAR supplémentaire nommé spring-boot-jarmode-layertoolset layersfle.idxfichier. Ce fichier JAR supplémentaire permet le traitement multi-couches, comme décrit dans la section suivante.

Extraction des dépendances sur des couches séparées

Pour visualiser et extraire les couches de notre JAR multi-couches, nous utilisons la propriété système -Djarmode=layertoolspour exécuter spring-boot-jarmode-layertoolsle JAR au lieu de l'application :

java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar

L'exécution de cette commande donne une sortie contenant les options disponibles de la commande :

Utilisation :
  java -Djarmode=layertools -jar usersignup-0.0.1-SNAPSHOT.jar

Commandes disponibles :
  list     Lister les couches du jar qui peuvent être extraites
  extract  Extrait les couches du jar pour la création d'image
  help     Aide pour toute commande

La sortie montre les commandes list, extractet helpavec helppar défaut. Lançons la commande avec listl'option :

java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar list
dependencies
spring-boot-loader
snapshot-dependencies
application

Nous voyons la liste des dépendances qui peuvent être ajoutées en tant que couches.

Couches par défaut :

Nom de la couche

Contenu

dépendances

toute dépendance dont la version ne contient pas SNAPSHOT

spring-boot-loader

Classes du chargeur JAR

snapshot-dependencies

toute dépendance dont la version contient SNAPSHOT

application

classes d'applications et ressources

Les couches sont définies dans layers.idxle fichier dans l'ordre dans lequel elles doivent être ajoutées à l'image Docker. Ces couches sont mises en cache sur l'hôte après la première extraction, car elles ne changent pas. Seule la couche mise à jour de l'application est chargée sur l'hôte, ce qui se fait plus rapidement grâce à la taille réduite. .

Création de l'image avec les dépendances extraites dans des couches séparées.

Nous allons construire l'image finale en deux étapes, en utilisant une méthode appelée construction multi-étapes . Au cours de la première étape, nous allons extraire les dépendances, et à la deuxième étape, nous allons copier les dépendances extraites dans l'image finale.

Modifions notre fichier Docker pour la construction multi-étapes :

# the first stage of our build will extract the layers
FROM adoptopenjdk:14-jre-hotspot as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

# the second stage of our build will copy the extracted layers
FROM adoptopenjdk:14-jre-hotspot
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

Sauvegardons cette configuration dans un fichier séparé — Dockerfile2.

Construisons l'image Docker avec la commande :

docker build -f Dockerfile2 -t usersignup:v1 .

Après avoir exécuté cette commande, nous obtenons la sortie suivante :

Envoi du contexte de construction au démon Docker  20.41MB
Étape 1/12 : FROM adoptopenjdk:14-jre-hotspot en tant que builder
14-jre-hotspot : Tirage depuis library/adoptopenjdk
.
.
Construit avec succès a9ebf6970841
Étiqueté avec succès userssignup:v1

Nous voyons que l'image Docker est créée avec un identifiant d'image, puis étiquetée.

Enfin, nous exécutons la commande Dive, comme auparavant, pour vérifier les couches à l'intérieur de l'image Docker générée. Nous pouvons fournir l'identifiant d'image ou l'étiquette comme entrée pour la commande Dive :

dive userssignup:v1

Comme le montre la sortie, la couche contenant l'application ne prend maintenant que 11 Ko, tandis que les dépendances sont mises en cache dans des couches séparées. 

Création d'images Docker optimisées pour l'application Spring Boot

Extraction des dépendances internes dans des couches séparées.

Nous pouvons réduire davantage la taille de la couche de l'application en extrayant l'une de nos dépendances personnalisées dans une couche séparée au lieu de les emballer avec l'application, en les déclarant dans un fichier ymlnommé layers.idx:

- "dependencies":
  - "BOOT-INF/lib/"
- "spring-boot-loader":
  - "org/"
- "snapshot-dependencies":
- "custom-dependencies":
  - "io/myorg/"
- "application":
  - "BOOT-INF/classes/"
  - "BOOT-INF/classpath.idx"
  - "BOOT-INF/layers.idx"
  - "META-INF/"

Dans ce fichier, layers.idxnous avons ajouté une dépendance personnalisée nommée, io.myorgcontenant des dépendances de l'organisation obtenues à partir d'un dépôt partagé.

Sortie

Dans cet article, nous avons examiné l'utilisation des Cloud-Native Buildpacks pour créer une image de conteneur directement à partir du code source. C'est une alternative à l'utilisation de Docker pour créer une image de conteneur de manière traditionnelle : d'abord, un exécutable JAR volumineux est créé, puis il est emballé dans une image de conteneur en spécifiant des instructions dans le fichier Docker.

Nous avons également abordé l'optimisation de notre conteneur en incluant la fonction de superposition qui extrait les dépendances dans des couches distinctes, qui sont mises en cache sur l'hôte, tandis que la couche fine de l'application est chargée lors de la planification dans les mécanismes d'exécution des conteneurs.

Vous pouvez trouver tout le code source utilisé dans cet article sur Github .

Référentiel de commandes

Voici un résumé des commandes que nous avons utilisées dans cet article pour vous y familiariser rapidement.

Nettoyage du contexte :

docker system prune -a

Création d'une image de conteneur à l'aide d'un fichier Docker :

docker build -f  -t  .

Nous construisons une image de conteneur à partir du code source (sans Dockerfile) :

mvn spring-boot:build-image

Vérifiez les couches de dépendances. Avant de construire le fichier JAR de l'application, assurez-vous que la fonction de superposition est activée dans le spring-boot-maven-plugin :

java -Djarmode=layertools -jar application.jar list

Extraction des couches de dépendances. Avant de construire le fichier JAR de l'application, assurez-vous que la fonction de superposition est activée dans le spring-boot-maven-plugin :

 java -Djarmode=layertools -jar application.jar extract

Affichage de la liste des images de conteneurs

docker images

Affichage des couches à l'intérieur de l'image du conteneur (assurez-vous d'avoir l'outil pour plonger) :

dive

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster