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

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

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

  • construire une image docker à l'aide d'un dockerfile,
  • construire une image OCI à partir de la source à l'aide de Cloud-Native Buildpack,
  • et l'optimisation de l'image au moment de l'exécution en séparant les parties JAR en différents niveaux à l'aide d'outils en couches.

 Exemple de code

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

Terminologie des conteneurs

Nous allons commencer par la terminologie des conteneurs utilisée tout au long de l'article :

  • Image du conteneur: un fichier d'un format spécifique. Nous convertissons notre application en une image de conteneur en exécutant l'outil de génération.
  • récipient: une instance exécutable de l'image du conteneur.
  • Moteur de conteneur: Le processus démon responsable de l'exécution du conteneur.
  • Hôte de conteneur: machine hôte sur laquelle le moteur de conteneur s'exécute.
  • Registre des conteneurs: emplacement général utilisé pour publier et distribuer l'image du conteneur.
  • Norme OCIInitiative pour les conteneurs ouverts (OCI) est un cadre de gestion léger et open source formé par la Fondation Linux. La spécification d'image OCI définit les normes de l'industrie pour les formats d'image de conteneur et le temps 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 génération.

Pour conteneuriser une application, nous encapsulons notre application dans une image de conteneur et publions cette image dans le registre public. L'environnement d'exécution du conteneur récupère cette image à partir du registre, la décompresse et exécute l'application qu'elle contient.

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

Docker est l'implémentation de conteneur la plus couramment utilisée, et nous utilisons Docker dans nos exemples, donc toutes les références de conteneur suivantes dans cet article feront référence à Docker.

Construire une image de conteneur de manière traditionnelle

La création d'images Docker pour les applications Spring Boot est très simple en ajoutant quelques instructions à votre Dockerfile.

Nous créons d'abord un JAR exécutable et, dans le cadre des instructions Dockerfile, copions le JAR exécutable au-dessus de l'image JRE de base après avoir appliqué les personnalisations nécessaires.

Créons notre application Spring sur Initialisation du ressort avec dépendances weblombokи actuator. Nous ajoutons également un contrôleur de repos pour fournir une API avec GETméthode.

Création d'un Dockerfile

Nous plaçons ensuite 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 Dockerfile contient une image de base, de adoptopenjdk, sur lequel nous copions notre fichier JAR puis ouvrons le port, 8080qui écoutera les demandes.

Assemblage d'applications

Vous devez d'abord créer une application en utilisant 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 l'exécuter sur le moteur Docker.

Créer une image de conteneur

Nous mettons ensuite cet exécutable JAR dans l'image Docker en exécutant la commande docker builddepuis le répertoire racine du projet contenant le Dockerfile créé précédemment :

docker build  -t usersignup:v1 .

Nous pouvons voir notre image dans la liste avec la commande :

docker images 

La sortie de la commande ci-dessus inclut notre image usersignupavec l'image de base, adoptopenjdkspécifié dans notre Dockerfile.

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

Afficher les calques à l'intérieur d'une image de conteneur

Regardons la pile de calques à l'intérieur de l'image. Nous utiliserons инструмент  se plonger, pour afficher ces calques :

dive usersignup:v1

Voici une partie de la sortie de la commande Dive : 

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

Comme nous pouvons le voir, la couche d'application représente une partie importante de la taille de l'image. Nous voulons réduire la taille de cette couche dans les sections suivantes dans le cadre de notre optimisation.

Construire une image de conteneur avec Buildpack

Forfaits de montage (packs de construction) est un terme générique utilisé par diverses offres de plate-forme en tant que 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 quelques autres.

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

Avantage des packages Cloud Build

L'un des principaux avantages de l'utilisation de Buildpack pour créer des images est que les changements de configuration d'image peuvent être gérés de manière centralisée (générateur) et propagés à toutes les applications utilisant le générateur.

Les packages de construction étaient étroitement liés à la plate-forme. Les Buildpacks Cloud-Native assurent la standardisation entre les plates-formes 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 plug-in Spring Boot

Le plug-in Spring Boot crée des images OCI à partir de la source à l'aide de Buildpack. Les images sont créées à l'aide bootBuildImagetâches (Gradle) ou spring-boot:build-imagecible (Maven) et installation Docker locale.

Nous pouvons personnaliser le nom de l'image que nous devons envoyer 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-imageobjectifs pour la création d'une application et la création d'une image de conteneur. Nous n'utilisons actuellement aucun Dockerfiles.

mvn spring-boot:build-image

Le résultat sera quelque chose comme ça :

[INFO] --- spring-boot-maven-plugin:2.3.3.RELEASE:build-image (default-cli) @ usersignup ---
[INFO] Building image 'docker.io/pratikdas/usersignup:v1'
[INFO] 
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 0%
.
.
.. [creator]     Adding label 'org.springframework.boot.version'
.. [creator]     *** Images (c311fe74ec73):
.. [creator]           docker.io/pratikdas/usersignup:v1
[INFO] 
[INFO] Successfully built image 'docker.io/pratikdas/usersignup:v1'

De la sortie, nous voyons que paketo Cloud-Native buildpackutilisé pour créer une image OCI fonctionnelle. Comme précédemment, nous pouvons voir l'image répertoriée en tant qu'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éer une image de conteneur avec Jib

Jib est un plugin de création d'images de Google qui fournit une méthode alternative pour créer une image de conteneur à partir de la source.

Configurer jib-maven-plugindans 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 à l'aide de la commande Maven pour construire l'application et créer l'image du conteneur. Comme auparavant, nous n'utilisons aucun Dockerfiles ici :

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] Containerizing application to pratikdas/usersignup:v1...
.
.
[INFO] Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*, io.pratik.users.UsersignupApplication]
[INFO] 
[INFO] Built and pushed image as pratikdas/usersignup:v1
[INFO] Executing tasks:
[INFO] [==============================] 100.0% complete

La sortie indique que l'image du conteneur a été créée et placée dans le registre.

Motivations et méthodes pour créer des images optimisées

Nous avons deux raisons principales pour optimiser :

  • Performance: Dans un système d'orchestration de conteneurs, une image de conteneur est extraite du registre d'images vers l'hôte exécutant le moteur de conteneur. Ce processus s'appelle la planification. L'extraction d'images volumineuses du registre entraîne de longs temps de planification dans les systèmes d'orchestration de conteneurs et de longs temps de construction dans les pipelines CI.
  • sécurité: les grandes images ont également une grande zone de vulnérabilités.

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

Spring Boot utilise "gros pot" dans comme format d'emballage par défaut. Lorsque nous examinons un gros JAR, 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. Le reste est constitué de dépendances Spring Framework.

La formule d'optimisation est centrée sur l'isolation de l'application à un niveau distinct des dépendances de Spring Framework.

La couche de dépendance qui forme la majeure partie du fichier JAR épais n'est téléchargée qu'une seule fois et mise en cache sur le système hôte.

Seule une fine couche de l'application est extraite lors des mises à jour de l'application et de la planification des conteneurs, comme indiqué sur ce schéma :

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

Dans les sections suivantes, nous verrons comment créer ces images optimisées pour une application Spring Boot.

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

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

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <layers>
      <enabled>true</enabled>
    </layers>
  </configuration> 
</plugin>

Nous utiliserons cette configuration pour construire notre image de conteneur d'abord avec Buildpack puis avec Docker dans les sections suivantes.

Courons build-imageCible Maven pour créer une image de conteneur :

mvn spring-boot:build-image

Si nous exécutons Dive pour voir les couches dans l'image résultante, nous pouvons voir que la couche d'application (entourée en rouge) est beaucoup plus petite dans la plage de kilo-octets par rapport à ce que nous avons obtenu en utilisant le format JAR épais :

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

Création d'une image de conteneur optimisée pour une application Spring Boot avec Docker

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

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

Le contenu du JAR résultant après la construction avec Maven avec la superposition 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 fichier JAR supplémentaire nommé spring-boot-jarmode-layertoolsи layersfle.idxdéposer. Ce fichier JAR supplémentaire fournit des fonctionnalités de superposition, comme décrit dans la section suivante.

Extraire les dépendances sur des couches distinctes

Pour afficher et extraire les couches de notre JAR en couches, nous utilisons la propriété système -Djarmode=layertoolscourir spring-boot-jarmode-layertoolsJAR au lieu d'application :

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

L'exécution de cette commande génère une sortie contenant les options de commande disponibles :

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

Available commands:
  list     List layers from the jar that can be extracted
  extract  Extracts layers from the jar for image creation
  help     Help about any command

La sortie affiche les commandes listextractи helpс helpêtre la valeur par défaut. Exécutons la commande avec listoption:

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

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

Calques par défaut :

Nom du calque

Teneur

dependencies

toute dépendance dont la version ne contient pas SNAPSHOT

spring-boot-loader

Classes de chargeur JAR

snapshot-dependencies

toute dépendance dont la version contient SNAPSHOT

application

classes d'application et ressources

Les calques sont définis dans layers.idxfichier dans l'ordre dans lequel ils doivent être ajoutés à l'image Docker. Ces couches sont mises en cache sur l'hôte après la première récupération car elles ne changent pas. Seule la couche d'application mise à jour est téléchargée sur l'hôte, ce qui est plus rapide en raison de la taille réduite .

Construire une image avec des dépendances extraites dans des calques séparés

Nous allons construire l'image finale en deux étapes en utilisant une méthode appelée assemblage en plusieurs étapes . Dans la première étape, nous allons extraire les dépendances et dans la deuxième étape, nous copierons les dépendances extraites dans le fichier final .

Modifions notre Dockerfile pour une construction en plusieurs é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"]

Nous enregistrons cette configuration dans un fichier séparé - Dockerfile2.

Nous construisons l'image Docker à l'aide de la commande :

docker build -f Dockerfile2 -t usersignup:v1 .

Après avoir exécuté cette commande, nous obtenons le résultat suivant :

Sending build context to Docker daemon  20.41MB
Step 1/12 : FROM adoptopenjdk:14-jre-hotspot as builder
14-jre-hotspot: Pulling from library/adoptopenjdk
.
.
Successfully built a9ebf6970841
Successfully tagged userssignup:v1

Nous pouvons voir que l'image Docker est créée avec un ID d'image, puis étiquetée.

Enfin, nous exécutons la commande Dive comme précédemment pour vérifier les calques à l'intérieur de l'image Docker générée. Nous pouvons fournir un ID d'image ou une balise en entrée de la commande Dive :

dive userssignup:v1

Comme vous pouvez le voir sur la sortie, la couche contenant l'application ne fait plus que 11 Ko et les dépendances sont mises en cache dans des couches distinctes. 

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

Extraire les dépendances internes sur des couches distinctes

Nous pouvons réduire davantage la taille de la couche d'application en extrayant l'une de nos dépendances personnalisées dans une couche distincte au lieu de les emballer avec l'application en les déclarant dans ymlfichier similaire nommé 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 dossier layers.idxnous avons ajouté une dépendance personnalisée nommée, io.myorgcontenant les dépendances d'organisation extraites du référentiel partagé.

conclusion

Dans cet article, nous avons examiné l'utilisation de Buildpacks Cloud-Native pour créer une image de conteneur directement à partir de la source. Il s'agit d'une alternative à l'utilisation de Docker pour créer une image de conteneur de la manière habituelle : tout d'abord, un fichier JAR exécutable épais est créé, puis emballé dans une image de conteneur en spécifiant les instructions dans le Dockerfile.

Nous avons également cherché à optimiser notre conteneur en incluant une fonctionnalité de superposition qui extrait les dépendances dans des couches distinctes qui sont mises en cache sur l'hôte et une fine couche d'application est chargée au moment de la planification dans les moteurs d'exécution du conteneur.

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

Référence des commandes

Voici un résumé des commandes que nous avons utilisées dans cet article pour une référence rapide.

Effacement du contexte :

docker system prune -a

Construire une image de conteneur avec un Dockerfile :

docker build -f <Docker file name> -t <tag> .

Créer une image de conteneur à partir de la source (sans Dockerfile) :

mvn spring-boot:build-image

Afficher les couches de dépendance. Avant de créer le fichier jar de l'application, assurez-vous que la fonctionnalité de superposition est activée dans le plugin spring-boot-maven-plugin :

java -Djarmode=layertools -jar application.jar list

Extraire les couches de dépendance. Avant de créer le fichier jar de l'application, assurez-vous que la fonctionnalité de superposition est activée dans le plugin spring-boot-maven-plugin :

 java -Djarmode=layertools -jar application.jar extract

Affichage d'une liste d'images de conteneur

docker images

Vue sur la gauche à l'intérieur de l'image du conteneur (assurez-vous que l'outil de plongée est installé) :

dive <image ID or image tag>

Source: habr.com