Migration d’une application JavaEE vers une architecture Serverless en minimisant les impacts sur le code

Migration d’une application JavaEE vers une architecture Serverless en minimisant les impacts sur le code

Contexte :

Après avoir opéré la migration vers le cloud AWS il y a 5 ans, en mode Lift and Shift avec l’utilisation d’EC2 pour nos applications JavaEE, nous construisons aujourd’hui nos nouvelles applications directement en serverless en utilisant AWS Lambda. Afin d'homogénéiser le maintien en condition opérationnelle de nos applications nous avons la volonté de migrer également nos applications existantes vers une architecture serverless.

Cependant, nous ne souhaitons pas réécrire l’ensemble de nos applications JavaEE, qui respecte déjà le principe de microservices via des EJB. C’est pour cela que nous avons cherché un moyen simple de faire évoluer le déploiement tout en impactant le moins possible le développement.

Stack Technique avant migration :

Stack Technique avant migration

Nos applications s'exécutent sur un serveur d’application Apache TomEE. Le découpage en services est réalisé avec des EJB. Les appels EJB utilisent JNDI pour la prise de contact et pour la récupération d’un objet Java local qui représente l’instance EJB.

Notre implémentation de JMS est réalisée avec ActiveMQ.

Voici les principaux changements réalisés:

1) EJB vers Lambda :
Afin d’impacter au minimum les développements, nous avons choisi le découpage suivant: 1 EJB = 1 lambda. Pour chaque interface de service, il suffit donc d’écrire un gestionnaire de fonction lambda et de surcharger les appels d’EJB par des appels de lambdas grâce à un proxy qui fait correspondre les JNDI name avec les noms des lambdas. Nos EJB peuvent contenir plusieurs méthodes publiques. Un paramètre est passé en entrée de la lambda permet au gestionnaire de la fonction lambda d'exécuter la bonne méthode publique du service.

2) Expositions des API :
Nous avons remplacé le AWS Application Load Balancer qui, précédemment, exposait les services frontaux (EC2 dans un autoscaling group) par AWS API Gateway.

3) Gestion des queues de messages :
Notre application traite des messages publiés sur une file JMS de manière asynchrone (un exemple de traitement est le transfert de données entre plusieurs systèmes). Nous avons remplacé l’implémentation ActiveMQ de JMS par AWS SQS.

4) Ordonnancement des appels :
Pour le lancement de tâches régulière, nous utilisions Job Quartz en configurant la fréquence (expression cron) et la méthode JAVA à exécuter Nous avons remplacé ce module par des règles configurées dans Cloudwatch Event en spécifiant les expressions Cron et la cible: soit une fonction lambda soit un AWS Batch pour des traitements plus longs.
 

l’architecture d’une application simplifiée de gestion d’équipe

Ce schéma reprend l’architecture d’une application simplifiée de gestion d’équipe, développée avec Java EE. Les services étaient découpés en EJB: History, User, Project, Employee, chacun transformé en lambda.

Nos challenges :

  • Dans notre implémentation, une méthode publique d’EJB pouvait appeler jusqu’à 7 autres EJB afin de réaliser un traitement de bout en bout. En appliquant naïvement la règle de 1 lambda par EJB, une invocation de service pouvait résulter en l’invocation de 7 lambdas séquentiellement.

    Dans le but d’optimiser les performances, nous sommes passés de 7 à 4 appels inter-lambda maximum. Pour cela, certains EJB utilitaires ont été injectés en tant que librairie dans les fonctions Lambda : augmentant ainsi le couplage du code mais optimisant les latences et cold start. Le tout est réalisé sans changer le code des services car les appels sont réalisés dans une classe gestionnaire d’appel qui, en fonction du contexte et du service à appeler, va choisir soit de faire un appel EJB (configuration historique), soit d’invoquer une lambda, soit d’instancier directement ces services.

  • En déployant notre application Java vers Lambda, nous avons rencontré des problèmes de performance à cause du Cold Start de la lambda, principalement pour la lambda chargée de la connexion avec la base de données (utilisant hibernate et jackson). Nous avons priorisé l’optimisation des performances sur cette lambda en utilisant Quarkus et GraalVM afin d’optimiser le runtime.

Next Steps :

  • Déploiement des microservices “à la carte” avec cartographie des dépendances de versions
  • Continuer à améliorer les performances (par exemple en appliquant GraalVM sur l’ensemble du code)
  • Continuer à faire évoluer nos applications existantes vers du serverless
  • Continuer à évangéliser les bonnes pratiques Serverless au sein de nos équipes

Nous proposons un stage pour 2022 avec comme thème la migration d’application vers une architecture serverless, n’hésitez pas à postuler ici !