Plus rapide, plus léger, meilleur avec Quarkus dans k8s

Cet article a été publié initialement sur Medium
Pourquoi un démarrage très rapide, une faible empreinte mémoire et une compilation native avec des frameworks standards s’intègrent parfaitement dans Docker et Kubernetes, en améliorant la scalabilité, la résilience et la sécurité.
En résumé
Dans cet article, je vous montre comment Quarkus permet d’exécuter une application Java nativement (en compilant le bytecode Java en code machine avec GraalVM) dans Docker, en utilisant l’image la plus minimale (et la plus sécurisée) possible avec Docker “FROM SCRATCH”, puis en la déployant sur Kubernetes.
Quarkus ?
Une stack Java native pour Kubernetes, pensée pour OpenJDK HotSpot et GraalVM, construite à partir des meilleures librairies et standards Java du marché.
Quarkus est un framework développé par RedHat, conçu pour le monde des conteneurs, et qui présente les caractéristiques suivantes :
- Démarrage et arrêt très rapides, y compris le temps de première réponse
- Faible empreinte mémoire
- Peut être compilé en code natif avec GraalVM
- Utilise les meilleures librairies et standards Java : Eclipse MicroProfile, CDI, JAX-RS, JPA, Vert.x, Apache Camel, kafka, Vault, etc. (voir https://code.quarkus.io et https://quarkus.io/guides/)
- Supporte le code impératif et réactif, ou un mélange des deux (voir https://quarkus.io/vision/continuum)
- Configuration simple et unifiée (voir https://quarkus.io/guides/all-config)
GraalVM ?
GraalVM est une machine virtuelle universelle permettant d’exécuter des applications écrites en JavaScript, Python, Ruby, R, des langages basés sur la JVM comme Java, Scala, Groovy, Kotlin, Clojure, ainsi que des langages basés sur LLVM comme C et C++. GraalVM supprime l’isolation entre les langages de programmation et permet l’interopérabilité au sein d’un même runtime. Elle peut fonctionner de manière autonome ou dans le contexte d’OpenJDK, Node.js ou Oracle Database.
GraalVM est développée par Oracle et permet d’exécuter nativement des langages basés sur la JVM (en compilant le bytecode Java en code machine), tout en supportant d’autres langages comme JavaScript, Python, Ruby, C, C++, R, etc.
Démo
Pour illustrer cette approche, j’utilise une application de démonstration basée sur https://github.com/quarkusio/quarkus-quickstarts/tree/master/getting-started
Il s’agit d’un service CRUD minimal exposant quelques endpoints REST. En coulisses, cette démo utilise RESTEasy pour exposer les endpoints REST.
Vous trouverez les sources dans le dépôt GitHub suivant : sokube/quarkus-scratch
Build Docker multi-étapes
Afin de générer une image contenant le strict minimum, l’utilisation d’un build Docker multi-étapes dont la dernière étape est basée sur l’image “scratch” est appropriée :
https://github.com/sokube/quarkus-scratch/blob/master/Dockerfile :
|
Ce build Docker multi-étapes est composé de 3 parties :
- Exécuter le plugin de dépendances maven : pour créer une image intermédiaire contenant tous les jars maven, servant de cache pour l’image suivante. Ainsi, la prochaine fois que vous construirez cette image Docker, vous n’aurez pas besoin de recharger toutes les dépendances.
- Créer le binaire exécutable : avec l’image Graalvm et l’objectif native-image du quarkus-maven-plugin, un exécutable Linux 64 bits sera produit. La partie importante se trouve dans le pom.xml : afin de créer une nativeImage capable de s’exécuter nativement sur une distribution Linux ne contenant pas libc (comme Alpine Linux, etc.), la commande “native-image” utilise “additionalBuildArg” pour compiler avec l’argument “–static”.
- Créer l’image finale à partir de l’image scratch : “scratch” est un nom réservé de Docker indiquant au processus de build de sauter cette ligne et de passer à la commande Dockerfile suivante. Ce n’est pas une image que vous pouvez pull ou run (bien qu’elle apparaisse dans le dépôt DockerHub). C’est donc l’ancêtre de base pour toutes les autres images, mais il s’agit d’une image vide, sans dossiers, fichiers, shell, librairies, etc. Pour comprendre comment Docker interprète une image basée sur “scratch”, lisez https://www.mgasch.com/post/scratch/ Ainsi, grâce à l’étape précédente avec l’argument “–static”, l’exécutable peut fonctionner dans une image Docker construite “from scratch”…
Construire et exécuter l’image Docker
Pour exécuter ce build multi-étapes, utilisez la ligne de commande suivante :
|
Compiler un exécutable natif avec GraalVM est assez long et consommateur de CPU. Il est donc préférable de déléguer ce processus à votre pipeline CI/CD. La bonne nouvelle, c’est que, sans compilation native et pendant votre phase de développement, le hot reload de Quarkus est très efficace.
Pour exécuter l’image générée :
|
Cela produira la sortie suivante :
|
Remarquez le temps de démarrage de 0.028s
Vous pouvez tester l’application dans votre navigateur : http://localhost:8888/hello/greeting/SoKube
Ne pensez pas qu’il s’agit d’une simple démo Hello World : sous le capot, le framework Quarkus utilise CDI et Resteasy, comme vous le feriez avec une véritable application Java destinée à traiter des requêtes métier…
Un autre test intéressant consiste à limiter la mémoire et le CPU :
|
Avec 0.05 de CPU et 4m de mémoire, l’application démarre en 2.503s :
|
Toujours impressionnant au regard des ressources allouées !
Déploiement Kubernetes de Quarkus
Pour déployer sur Kubernetes, j’utilise une distribution k3s locale avec k3d, pour plus d’informations voir un article précédent que j’ai écrit “k3d + k3s = k8s perfect match for dev and testing”.
Commencez donc par créer le cluster Kubernetes avec :
|
Déployez l’application en utilisant https://github.com/sokube/quarkus-scratch/blob/master/deploy.yaml
Clonez ce dépôt Git et déplacez-vous dans le répertoire “quarkus-scratch”
|
La première ligne importe l’image précédemment créée, nommée “quarkus-app”, dans le cluster k3s. La seconde ligne déploie l’application.
Vous devriez ensuite pouvoir accéder à l’application via : http://localhost:8085/hello/greeting/SoKube
Requêtes et limites K8s
Dans la spec du Pod, j’ai défini les ressources de requête et de limite pour utiliser au maximum 1 millicore de CPU (1000 millicore = 1 CPU) et 4Mi de mémoire
|
La commande “kubectl logs -l app=quarkus” affiche les logs de démarrage :
|
Toujours très rapide malgré les limitations de ressources ! Essayez un tel scénario avec votre application JEE ou SpringBoot :D
Scaler votre application
Avec un démarrage aussi rapide et une empreinte mémoire aussi faible, vous pouvez facilement scaler votre application avec 50 réplicas sur votre laptop :
|
puis la redescendre tout aussi rapidement :
|
OK sympa, mais pourquoi ?
Déployer une telle application ne vise pas seulement l’effet “Wahoo” (du moins, pas uniquement !).
- Le CPU et la mémoire sont les ressources couramment utilisées pour facturer les environnements cloud ou on premise. Ces ressources ne sont pas illimitées et la facture peut augmenter considérablement. Il existe par ailleurs des solutions à faible coût, comme Google Cloud Platform (GCP), sur lequel vous pouvez commander une VM utilisant des “Shared-core machine types”. Cela peut être une méthode économique pour exécuter de petites applications peu gourmandes en ressources, par exemple une “E2-micro 2vCPU, 1GB memory” ne coûte que 6,5 $ par mois.
- Un démarrage et un arrêt rapides pour le scale up et les rollouts de déploiements sont des aspects clés de Kubernetes. Un temps de démarrage long pour votre application vous obligera à configurer des probes avec des timeouts élevés (sauf depuis k8s 1.16, avec la nouvelle startup probe utilisée pour le premier démarrage, puis les probes liveness et readiness prennent le relais). Mais dans tous les cas, cela simplifiera le rollout des nouvelles versions de l’application, ainsi que les rollbacks.
- Une faible empreinte mémoire sur votre laptop ou votre environnement de développement facilite le développement en local. Combiner une distribution Kubernetes légère comme k3s avec des applications natives permet d’avoir une plateforme k8s complète, plus proche de la situation de production réelle, mais en local !
- La sécurité est une préoccupation majeure en production, et disposer d’un exécutable Linux 64 bits dédié dans une image Docker minimaliste (sans shell, fichiers, dossiers ni librairies) réduit fortement les risques de sécurité. Comme montré dans le fichier de build multi-étapes, n’oubliez pas de conserver les autres bonnes pratiques de sécurité, comme ne pas s’exécuter en root…
- Les architectures et produits serverless peuvent tirer parti de temps de démarrage plus rapides avec de vraies applications. Les fonctions sont exécutées à la demande, avec un “cold start” lorsqu’aucun conteneur “warm” n’est disponible. Dans ces situations, pour éviter une latence élevée, il est donc important d’avoir un temps de démarrage très rapide, temps de réponse à la première requête inclus.
Parfait ?
S’appuyer sur Quarkus avec la compilation native et déployer sur Kubernetes, c’est bien, mais ce n’est pas suffisant !
Votre application doit être conçue comme une Cloud Native Application (CNA), et je ne parle pas ici de microservices. Vous pouvez écrire une CNA comme un service “normal”, mais en respectant certains principes et en évitant, par exemple, une initialisation de démarrage de votre application qui charge en mémoire un cache énorme pendant plusieurs minutes… La conception de votre application est très importante, aucun framework, outil ou produit ne peut compenser une mauvaise conception !
Un inconvénient de la compilation native réside dans les restrictions, notamment autour de l’usage de la réflexion et du chargement dynamique de classes. Cela rend plus difficile (du moins pour l’instant) de migrer toutes les applications vers des binaires natifs, mais à chaque nouvelle version de Graal, la compatibilité s’améliore. C’est pourquoi Quarkus supporte une liste limitée d’extensions, mais cette liste s’agrandit et contient déjà de nombreuses extensions.
Un autre aspect lié à l’image Docker minimaliste concerne le débogage ! Impossible d’exécuter une commande dans le conteneur ! Alors comment déboguer une image qui ne contient ni shell, ni outils comme curl, wget… ni même ls, chmod, chown, mkdir… ? Je n’entrerai pas dans le détail sur la manière d’y parvenir, mais pour résumer : il s’agit d’utiliser une image comme busybox et d’injecter depuis cette image le shell et les outils nécessaires dans l’image minimaliste…
Conclusion
Quarkus s’intègre parfaitement dans l’ère du Cloud, où conteneurs, Kubernetes, microservices ou services, function-as-a-service, et applications conçues nativement pour le Cloud permettent d’atteindre des niveaux élevés de productivité et d’efficacité.
Les services, la scalabilité instantanée, et les plateformes à haute densité comme Kubernetes exigent des applications avec une faible empreinte mémoire et un démarrage rapide. Java n’était pas bien positionné sur ce terrain, car il privilégie les temps de traitement au détriment du CPU et de la RAM. En combinant Quarkus et GraalVM, ce n’est plus le cas !
Kubernetes est là pour durer, préparons donc nos développements, applications et plateformes pour un maximum de flexibilité, d’efficacité et de sécurité.
Pour aller plus loin, découvrez notre formation Cloud Native Applications et notre expertise Cloud Native Applications.