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 OCI: Initiative 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 web, lombokи 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:
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 :
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.
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:
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.
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 :
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 :
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 :
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 :
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'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 :
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 :
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 list, extractи 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
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.
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:
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 :