Quantcast
Channel: Publicis Sapient Engineering – Engineering Done Right
Viewing all 1865 articles
Browse latest View live

Les 17, 18 et 19 avril, nous serons à Devoxx avec Publicis Sapient

$
0
0

Devoxx France, vous connaissez forcément (pour celles et ceux qui auraient vécu loin de tout cette dernière décennie, il s’agit de l’une des conférences tech les plus en vue).

Trois jours intenses de conférences, de hands-on, de découvertes et de rencontres.

C’est la conférence que nous avons choisie pour vous parler de notre avenir commun avec Publicis Sapient. Les 17, 18 et 19 avril, on vous donne rendez-vous stand P01 et sur les réseaux sociaux pour vous présenter les terrains jeux que nous offrent cette nouvelle collaboration entre les métiers du conseil, de la création et du développement agile “done right”.

 

Notre stand : lab & shoebox

Sur notre stand nous offrirons un voyage dans nos univers, une navigation entre les projets R&D développés par le Labs de Publicis Sapient et nos projets “homemade” réalisés à grand renfort de kit raspberry, d’imprimante 3D et de boîte à chaussures Vans (pointure 38 ;)).

Les 18 et 19 avril, vous serez à Devoxx ? Venez tester nos créations au stand P01. Le plus chanceux(se) d’entre vous repartira une surprise en poche (et on espère qu’elles sont capables de contenir un objet de dimension 148 x 99 x 99 mm).

 

Nos Talks : Deep Learning, AWS Cloud Formation, Kafka-Streams, Go…

“Sharing Knowledge”, “Learning Mindset” deux valeurs qui nous réunissent et qui nous permettent de vous proposer cette année 7 talks :

 

Mercredi

13h30 – Université : “Dynamique de groupes et structuration du temps : la théorie organisationnelle de Berne, une autre approche agile”

De l’individu seul aux interactions de groupes, tout est échange, tout est transaction. C’est alimentée par ce constat et par sa riche expérience qu’Anne-Sophie G., Coach Agile, accompagnée de Laurène T., Agile Delivery Manager et de Yoann B., Dev Data, fera voyager son public à travers la théorie organisationnelle de Berne avec son talk : “Dynamique de groupes et structuration du temps : la théorie organisationnelle de Berne, une autre approche agile.”

17h45 – Tools-in-action : “Deep Learning sur la JVM : DL4J”

Du Deep Learning en Java ? Aujourd’hui quand on parle de Deep Learning on pense aux Tensorflow, Keras ou Pytorch… Sandra P., Data Scientist, et son acolyte prouveront qu’il existe un autre framework possible avec leur talk : “Deep Learning sur la JVM : DL4J”

 

Jeudi

10h45 – Conférence : “Streaming Apps Poison Pill: Comment Kafka-Streams compte faire passer la pilule”

Des techniques comme les sentinel values ou encore les dead letter queues rendent possible la gestion des messages erronés au sein des applications temps réel. Avec son talk : Streaming Apps Poison Pill: Comment Kafka-Streams compte faire passer la pilule” Loïc D., Data Engineer, embarquera l’assistance dans exploration de l’API StreamDSL de Kafka-Streams.

20h00 – BOF (Bird of a Feather) : “Go pour DevOps et le futur de Go”

Vous faîtes partie du gang des Go lovers ? Diana O., Dev Back et Data Lover, vous invite à la rejoindre pour une session spéciale “Go pour DevOps et le futur de Go”, avec les meetups Golang Paris et Women Who Go.

 

Vendredi

10h45 – Hands-On : “Créer votre application Serverless sur AWS en utilisant SAM et CloudFormation”

Faire des applications Serverless avec Java ou Go dans AWS, c’est ce que proposent Diana O., Dev Back et Data Lover et son partenaire avec leur hands-on : “Créer votre application Serverless sur AWS en utilisant SAM et CloudFormation”.

10h45 – Hands-On :“Google Cloud Function”

Un jeu de rôle, un drone virtuel, un scénario digne des plus grandes productions hollywoodiennes, du JavaScript, du Python, un lot à gagner…  En somme un hands-on où l’expression “apprendre en s’amusant” prend tout son sens. Rendez-vous avec Jean-Baptiste C., Dev Back/Cloud, Nicolas D., Dev Back et Antoine LT., Dev Fullstack et leur hands-on qui livre les secrets de “Google Cloud Function”.

10h45 – Hands-On : “Manipulation des APIs haut niveau de TensorFlow”

Redécouvrir TensorFlow grâce à ses APIs haut niveau (notamment tf.data, tf.keras, tf.estimator, ainsi que le mode d’exécution impératif -eager mode – et TensorFlow Hub) à partir d’une base de code TensorFlow modifiable de manière incrémentale : Giulia B. et Yoann B., tous deux Data Scientists, démontrent avec leur hands-on  “Manipulation des APIs haut niveau de TensorFlow” que l’outil a bel et bien évolué.

 

Xebia x Publicis Sapient : rendez-vous sur nos réseaux sociaux pour suivre nos aventures

Xebia France Publicis Sapient France
   @xebiafr @pubsapientFR
Xebia France Publicis Sapient
XebiaFr publicissapientfrance
Youtube | XebiaTV Vimeo | Publicis Sapient France

 

 


Pépite #12 – Comment déboguer une application Android en WiFi ?

$
0
0

Et voilà la pépite de la semaine ! Aujourd’hui, nous allons nous concentrer sur Android en abordant le sujet du débogage en Wi-Fi, une astuce très utile lorsque vous développez une application pour objet connecté.

Dans cet article, nous allons parler d’ADB, logiciel déjà bien connu des développeurs Android et utilisé pour installer nos applications sur un appareil par exemple. Nous devrons entrer des commandes dans le terminal, vérifiez donc que votre PATH contient bien le chemin vers votre SDK Android et plus précisément vers platform-tools/.

Afin de déboguer votre application en Wi-Fi, vous devrez effectuer 6 étapes distinctes :

1) Connecter l’appareil à l’ordinateur en USB

2) Vérifier que l’appareil est connecté au même réseau que l’ordinateur

3) Vérifier que l’appareil est bien connecté sur ADB

Entrez la commande :

adb devices

Cette commande vous donne la liste des appareils connectés à ADB (appareils réels et emulateurs) :

List of devices attached
M2HDU18207001720	device

4) Ouvrir un port de l’appareil

Vous pouvez choisir le port de votre choix (le port par défaut étant le 5555).

adb tcpip <optionnel: nom du device vu au dessus> <numéro du port>

5) Connecter l’appareil en Wi-Fi

adb connect <adresse ip de l'appareil duquel on vient d'ouvrir le port>

6) Vous pouvez maintenant débrancher l’appareil

Maintenant vous pouvez lancer votre application en mode debug directement sur Android Studio et voilà : l’application se lance.

 

 

Certaines surcouches d’Android fonctionnent assez mal avec ce type de connexion et pourraient déconnecter votre device d’Android Studio assez fréquemment.

 

Une dernière chose (et pas des moindres !) il existe un plugin sur Android Studio qui permet d’effectuer ces commandes en un clic, il s’agit du plugin Android WiFi ADB dont l’article suivant parle très bien Connect Android Device with Wifi within Android Studio.

Des applications communicantes et ouvertes

$
0
0

Configurer, fiabiliser, sécuriser, mesurer et garantir la performance de la communication d’applications qu’elles soient conteneurisées, orchestrées ou pensées sous forme de micro-services, avec des cycles de déploiement qui s’accélèrent, peut vite devenir un cauchemar.

Pour aider à l’orchestration de la communication entre services, le Service Mesh se profile comme étant la solution qui risque bien de cannibaliser toute la galaxie d’outils ad hoc existants. GraphQL devient un standard pour consommer des APIs depuis une interface utilisateur moderne (cf. L’interface utilisateur ce n’est pas que des pixels). Sous l’impulsion de Google gRPC gagne du terrain et pourrait également bien devenir un standard pour la communication entre applications backend.

Qui dit applications ouvertes dit également documentation, accessibilité, on se doit donc de parler d’API blueprints, d’OpenAPI, d’OAuth, d’API Gateway telles que Tyk et Kong, ou encore des services managés des cloud providers, mais également de gestion saine des utilisateurs (rate limiting, délégation, …).

Keywords

  • API Gateway
  • gRPC
  • GraphQL
  • Messaging
  • Service Mesh
  • Istio
  • Linkerd
  • Envoy
  • Proxy
  • OAuth
  • OpenAPI
  • API Blueprint
  • API Management
  • Tyk
  • Kong

Notre partage sur le sujet

Vidéos

La Data Science, de l’idée à la production

$
0
0

Pour qu’un projet de Data Science puisse aller au-delà de la simple étape du PoC, il est nécessaire de penser au plus tôt à sa mise en production, c’est à dire penser au cycle de vie complet du développement : Exploration ↔ Industrialisation ↔ Mise en Production. Nous parlons ici bien de cycle, pas uniquement d’un workflow unique.

Il ne faut notamment pas tomber dans le piège d’une phase exploratoire interminable, sans aucun objectif préalable de mise en production. De trop nombreux projets s’enferment dans un tunnel de plusieurs mois d’exploration pour trouver le modèle le plus performant qui soit, et, lorsque (ou plutôt si) l’équipe est satisfaite, décident de le mettre en production, pour finalement se rendre compte que cette étape est impossible ou trop coûteuse en termes de bibliothèques à disposition ou de temps de calcul.

Penser à une mise en production au plus tôt, c’est s’assurer qu’une chaîne de traitement complète est mise en place dès les premières étapes d’un projet, afin de valider sa faisabilité et l’automatisation de nombreuses parties. C’est aussi s’assurer que tout ce qui est fait en phase exploratoire se fasse avec des contraintes qui permettent une industrialisation à coûts maîtrisés. C’est enfin mener des analyses d’erreurs et des boucles de feedback efficaces afin de repartir sur une phase exploratoire contrôlée qui se focalise sur ce qui permettra d’améliorer au mieux les performances. L’industrialisation est souvent vécue comme un frein à l’innovation en Data Science, alors qu’elle devrait en être un accélérateur.

La Data Science en production, ce sont des bonnes pratiques issues du Software Craftsmanship et du développement agile, adaptées aux problématiques spécifiques à ce type de projet. C’est aussi une organisation d’équipe mêlant plusieurs profils différents, ainsi qu’une vision produit claire dès les premières étapes d’un projet pour s’assurer de sa bonne utilisation par la suite.

Keywords

  • Packaging
  • Model Repository
  • Knowledge Repository
  • Data Science Design Sprint
  • Model Serving
  • Monitoring
  • Du notebook au code propre
  • MLFlow
  • KubeFlow
  • Machine Learning Platforms
  • Scheduling / Gestion de workflow
  • Design Thinking
  • Vision Produit
  • Data Science Agile
  • Ré-entraînement automatique de modèles

Notre partage sur le sujet

Articles de blog

Vidéos

Roadmap 2019 – Les 12 travaux des TOs

$
0
0

Chez Xebia, aucune réalisation ne ressemble à la précédente : chaque année, les évolutions technologiques, de services et de paradigmes d’architecture changent, parfois radicalement, la stack technique de notre prochain projet.

Il y a quelque mois, en parlant de nos rêves et ambitions pour 2019, nous nous sommes demandé : “À quoi ressemblerait notre prochain projet ?”. En cherchant à répondre à cette question, nous avons commencé à dessiner, de façon très approximative et ensuite de plus en plus précise, un schéma regroupant technologies, architectures et solutions que nous aimerions proposer à notre prochain client.

Le résultat, c’est 12 axes de travail regroupés dans un document appelé (faute d’imagination) le “12 Travaux des TOs”. Il s’agit, pour nous, d’une véritable Roadmap pour l’année 2019, présentant nos convictions sur les choix clés en termes de pratiques et d’approches techniques, que nous appliquons pour répondre aux problématiques logicielles qui se posent aux différents niveaux du SI.

 

Dans cette roadmap, on peut y retrouver des notions de Machine Learning, dont la maîtrise des modèles mathématiques sous-jacents et l’industrialisation de leur déploiement en production restent des défis passionnants.

Les architectures nécessitent également de nouveaux paradigmes : l’infrastructure moderne se gère désormais via API et tire parti des environnements Cloud Native. Ce modèle couplé à une abstraction des ressources amène les architectures Serverless, qui se doivent aujourd’hui de faire partie intégrante de son parc applicatif.

Et si certains traitements restent tout même à leur avantage dans une infrastructure On-Premise, une interopérabilité entre cette dernière et le Cloud reste nécessaire, avec des outils permettant de faire la passerelle entre les deux mondes. Au même titre que l’hybridation, la décentralisation est également une option sérieuse, voire obligatoire, dans certains cas d’usage.

Les modèles de traitement de la donnée voient leur codes bousculés par l’adoption massive de Kubernetes et les intégrations “Data” dont ils se voient pourvus. Ces outils amènent également les notions de temps réel et considèrent toute activité au sein de son système comme une source d’événements sur laquelle on peut s’interfacer.

Quant à la présentation de l’information aux utilisateurs, cette dernière requiert des architectures ainsi que de technologies modulaires et performantes pour être en accord avec les standards actuels. Il en va de même pour la communication entre applications qui a été mise sous pression suite à l’arrivée des orchestrateurs et des architectures microservices, ce qui a amené de nouvelles approches et de nouveaux outils.

Enfin, pour mettre en place tous ces changements, il est impératif d’être en mesure de sécuriser et contrôler l’activité et les changements apportés à son système et de s’appuyer sur une organisation humaine structurée et efficace, afin de tirer parti des compétences de chacun.

Pour chacun de ces axes, nous vous proposons de découvrir les articles, vidéos et événements que nous avons réalisés au cours de ces derniers mois et que nous tâchons de mettre à jour régulièrement afin de fournir une image représentative de nos avancements en la matière.

Les 12 thématiques

La Data Science, de l’idée à la production

Les projets Data Science restent trop souvent à l’état de PoC ou ont de grandes difficultés à passer en production, souvent pour de mauvaises raisons. Les enjeux principaux sont l’organisation et l’approche : définir une vision produit claire, éviter le piège d’une phase exploratoire interminable, penser industrialisation dès le début, monter une usine logicielle adaptée et avoir des boucles de feedback courtes.

En savoir plus sur La Data Science, de l’idée à la production

 

Embrasser les architectures Serverless

Serverless est sans conteste l’un des buzzwords du moment. Il est temps pour tous d’embrasser pleinement le Cloud, les services managés, le FaaS et au passage de répondre aux questions inévitables de déploiement, de lock-in, de test, de monitoring

En savoir plus sur Embrasser les architectures Serverless

 

Résilience, fiabilité et vélocité : le SI Cloud Native

L’infrastructure est maintenant un enabler et une force. L’approche de cette infrastructure sous forme de services, d’APIs et d’outils est au service de la vélocité des équipes, du confort des développeurs et de la fiabilité des applications résultantes. Orchestration, automatisation, etc. nous voilà !

En savoir plus sur Résilience, fiabilité et vélocité : le SI Cloud Native

 

Les mathématiques au cœur de l’IA

Derrière la hype du Machine Learning et du Deep Learning se cachent bien souvent des modèles mathématiques. C’est encore plus vrai dès que l’on parle d’IA au sens large : statistiques et sciences cognitives rentrent rapidement dans la danse. Il est dangereux de penser et d’utiliser l’IA comme une black box impénétrable et c’est avec une compréhension des mécanismes sous-jacents que l’on peut construire des produits robustes répondant aux besoins.

En savoir plus sur Les Mathématiques au cœur de I’IA

 

La Data dans l’ère post-Hadoop

Hadoop n’est plus la solution à tout, vive la Data sur Kubernetes et dans le Cloud ! Des projets tels que Kubeflow, Spark on K8s et Kafka Operator permettent d’envisager une infra Data full Kubernetes, de même qu’AWS & GCP sont aujourd’hui capables de proposer des alternatives riches et matures.

En savoir plus sur La Data dans l’ère post-Hadoop

 

Décentraliser vos traitements : On-Device Computing

L’intelligence se dirige vers les devices périphériques. Les traitements et les prises de décisions se font de plus en plus souvent au sein des terminaux grâce à la convergence des évolutions technologiques. Le futur est ubiquitaire !

En savoir plus sur Décentraliser vos traitements : On-Device Computing

 

L’interface utilisateur, ce n’est pas que des pixels

La création d’interfaces utilisateurs nécessite bien plus de talent que de la simple mise en page. Une UX réussie, passe par la mise en place d’architectures front et mobile permettant de répondre aux besoins en performance, qualité, modularité, sécurité et métrologie imposés par des usages toujours plus exigeants.

En savoir plus sur L’interface utilisateur, ce n’est pas que des pixels

 

Contrôler et Fiabiliser

La sécurité, c’est indispensable et pas forcément pénible ! La gestion des secrets s’aborde désormais via des services et des APIs. Le chiffrement des données stockées et de leur transport n’est plus optionnel. Enfin, le scan de vulnérabilité est une nécessité lorsque l’on utilise des bibliothèques tierces.

En savoir plus sur Contrôler et Fiabiliser

 

Unifier le SI avec l’hybridation

Le Système d’Information de nos clients est multiple et ne se cantonne pas seulement à du tout cloud ou du tout on-premise. Une migration vers le Cloud, ce n’est pas nécessairement du All In et Kubernetes se présente au premier plan. Le multi-cloud reste également un sujet qui semble de plus en plus réalisable.

En savoir plus sur Unifier le SI avec l’hybridation

 

Un SI temps réel, centré sur les événements

“Tout business peut être représenté sous forme d’événements”. Ce n’est pas nous qui le disons, c’est Neha Narkhede, co-créatrice de Kafka ! Le temps réel pour tout est désormais une possibilité sérieusement envisageable et peut s’appliquer non seulement au business mais aussi aux outils. Events everywhere!

En savoir plus sur Un SI temps réel, centré sur les événements

 

Des applications communicantes et ouvertes

Des applications c’est bien beau, des applications communiquant efficacement entre elles et ouvertes c’est mieux ! Disons donc bonjour au messaging, aux formats: protocoles de communications tels que GraphQL, gRPC, et consor, ainsi qu’au Service Mesh, à OpenAPI, aux API Gateway et tous leurs amis.

En savoir plus sur Des applications communicantes et ouvertes

 

Mon organisation évolue au rythme de mon architecture

Les architectures, technologies et approches évoluant, il est naturel et souhaité que l’organisation s’adapte et évolue également. L’agilité commence à être un acquis, le rôle de SRE est encore flou, on commence seulement à parler de Platform Teams, sans parler des Data {Scientists, Ops, Engineer, Architect} !

En savoir plus sur Mon organisation évolue au rythme de mon architecture

Écho des TOs n°5 : Service Mesh, comment en est-on arrivé là ?

$
0
0
photo
L’Echo des TOs #5

Service Mesh, comment en est-on arrivé là ?

 

Régulièrement, nous vous proposons l’écho des TO. Les Technical Officers de Xebia vous proposent un focus sur les sujets phares de 2019, retours d’experience, présentation de projets internes, etc. (Re) découvrez les précédents articles :

Bienvenue dans ce nouvel écho des TOs

La communication entre applications a été mise sous pression par l’émergence des architectures microservices, la conteneurisation et l’orchestration. Dans un tel contexte comment configurer, mesurer, fiabiliser, sécuriser et rendre les échanges entre applications performants ? Résumons ce qu’il s’est passé ces cinq dernières années pour mieux comprendre ce qui a fait émerger les outils de Service Mesh et détaillons ce en quoi ils consistent.


Microservices ?

La mouvance microservices vise à sortir des applications monolithiques considérées trop lourdes, trop dures et trop risquées à faire évoluer. Le but ? Livrer des correctifs de bugs et des évolutions plus rapidement. En somme, les monolithes ne tenaient pas le rythme de l’agilité. On a donc commencé à les découper : plus petit, c’est mieux. Conséquence de ce découpage les appels de fonctions internes ont été remplacés par des appels réseaux.

L’ère des conteneurs

A peu près en même temps a été démocratisée la conteneurisation avec une solution à très fort succès que nous connaissons tous : Docker. Fini les procédures d’installations avec description des pré-requis en termes de bibliothèques systèmes, versions de paquets ou de machines virtuelles. Il est désormais possible de livrer tout le contenu nécessaire à l’exécution d’un programme dans une image que l’on peut déployer à l’infini, de manière reproductible et répétable, sous la forme de conteneurs.

Livrer, déployer ne consiste donc plus à mettre à jour, patcher ou écraser tel binaire ou fichier de configuration, mais plutôt à supprimer un conteneur et à en créer un nouveau à partir de la dernière image disponible (cf. infrastructure immutable).

Bienvenue aux orchestrateurs

La guerre du déploiement vaincue pour le bonheur des devs et des ops, il restait toutefois un manque qui n’a pas mis très longtemps à être comblé. Nos micro applications conteneurisées étant nombreuses, il fallait un outil pour les déployer toutes et sur un parc de machines indifférenciées. Une application pouvant dans l’idéal être déployée sur n’importe quelle machine. Pour gérer le failover des conteneurs, les mises à jour ou l’adaptation aux besoins en capacité de calcul ou de mémoire, il était nécessaire de disposer d’un outil permettant de les créer simplement et surtout rapidement. Et l’orchestrateur était né.

Avec un orchestrateur, on exprime un état cible de déploiement, un niveau de service et c’est l’orchestrateur qui s’occupe de le réaliser pour nous, tout en optimisant l’usage des ressources (algorithmes de binpacking). Besoin de passer de 1 à 10 instances du même conteneur ? Besoin de maintenir ces 10 instances en vie quoi qu’il en coûte ? Pas de problème, l’orchestrateur fera au mieux avec les ressources disponibles.

 

Dans un tel contexte les applications n’arrêtent pas d’être déplacées en fonction des évènements de failover, de mises à jour, etc. Elles doivent toutefois toujours réussir à communiquer entre elles. La pression sur la bonne configuration de la communication entre applications est énorme et de nouveaux challenges ont émergés.

Communication inter-applications

Il faut, en effet, être capable de déterminer dynamiquement les adresses des applications en état de fonctionnement, parmi celles-ci, trouver lesquelles présentent la latence la plus faible, lesquelles sont dans la version adéquate et ensuite distribuer de manière équitable (round-robin ou autre) les requêtes. Une fois la requête émise, il faut gérer les erreurs, gérer le rejeu, protéger les applications vis à vis d’un client (rate limiting), empêcher la saturation d’un service distant (circuit breaker). Il faut également mesurer les échanges effectués sur la plateforme et assurer le chiffrement des données transportées. En résumé, comment gérer de manière fiable, sécurisée et performante les nombreux échanges et les mesurer, dans un contexte aussi dynamique ?

La nature ayant horreur du vide, ces nouveaux challenges ont été adressés et solutionnés par de nombreux outils. Ces outils nous les connaissons tous : annuaires de découverte de services (Consul, Etcd, Zookeeper, Eureka), load balancers (HAProxy, Nginx, Traefik), bibliothèques et outils de métrologie (metrics, Prometheus), de tracing (OpenTracing, Jaeger) et de résilience (Hystrix, Resilience4j, rate limiting, exponential backoff, back pressure, circuit breaker, timeouts, pools de connexions).

L’approche qui repose sur des éléments d’infrastructure (Load Balancer) a l’inconvénient de déporter une partie de la configuration de la communication des applications dans un élément externe. Cet élément externe est bien souvent mutualisé et les équipes applicatives n’ont pas forcément la main dessus.

L’approche qui repose sur des bibliothèques intégrées aux applications a, quant-à elle, l’inconvénient d’être spécifique au langage, au framework ou à la stack technique mise en place. Il est par ailleurs bien souvent complexe d’intégrer tous ces outils entre eux ou de modifier leur configuration à chaud. Pouvoir les configurer de manière homogène au sein de toute sa plateforme relève de l’utopie et s’assurer qu’ils sont correctement configurés ou mis en place, relève de la gageure.

L’ère du service Mesh ?

Les micro services, la conteneurisation et l’orchestration ont amené un outillage conséquent pour faciliter la communication entre services. Ce patchwork d’outils hétérogènes est complexe à intégrer et le manque d’homogénéité dans la configuration a donc favorisé l’émergence d’une solution ad-hoc.

Le service mesh est une solution dédiée à la gestion des problématiques de communication entre services. Ce “nouveau” concept adresse la problématique de la communication entre applications de manière centralisée et propose de remplacer la galaxie d’outils existants par un outillage unique.

Service Mesh : Concept

L’idée du Service Mesh consiste à placer un proxy devant chaque instance applicative.

Note : D’autres solutions mettent en avant la possibilité d’utiliser un proxy centralisé – un par machine par exemple – mais ces possibilités sont cependant minoritaires et ne sont pas en phase avec la tendance générale qui émerge du concept.

Ce proxy est le même pour toutes les applications et n’est donc pas dépendant de la stack applicative. Tout le trafic réseau de l’instance applicative est redirigé via le proxy de manière totalement transparente pour l’application. Les proxys de chaque application communiquent ensuite entre eux, aucune communication n’est effectuée en direct entre les applications !

Note: Dans un contexte Kubernetes, cette caractéristique est offerte par les PODs. Un POD est un ensemble de conteneurs : (dans notre cas : proxy + application) qui sont nécessairement déployés sur une même machine et qui bénéficient d’une isolation réseau. Au sein de ce réseau, le trafic est configuré par l’outil de service mesh qui force la redirection du trafic via le proxy.

Le proxy acquiert ainsi la vision et le contrôle de tous les échanges réseaux qui transitent entre chaque application et au sein de toute la plateforme.

Observabilité

Ce principe permet aux proxys de collecter les métriques concernant les échanges : temps de requêtes, volume de données échangé, statut de réponse et de les remonter dans une base de données appropriée (Prometheus, time series database, …). Une application, dès lors qu’elle est déployée dans le Mesh, bénéficie automatiquement de ces métriques techniques. Il n’est plus nécessaire de contrôler que chaque application a correctement intégré une bibliothèque permettant de le faire.

Il en va de même pour les logs des appels qui peuvent facilement être déversés dans un système de centralisation des logs et qui sont pertinemment étiquetés afin de bénéficier aux analyses des problèmes de production.

La mise en place de tracing est également facilitée. L’appel d’un service à un autre passe obligatoirement par le proxy qui remplit parfaitement son rôle de contrôleur et génère les traces (spans) automatiquement ou à moindre coût.

Contrôle intelligent du trafic

En plus des aspects observabilité, le service mesh permet de connecter les services entre eux. Pour y parvenir l’outil de service mesh (Istio, Linkerd) écoute les événements qui ont lieu sur l’orchestrateur : ajout, suppression, panne, disponibilité d’instances et propage en conséquence la configuration adéquate aux différents proxys qui peuvent décider, de manière intelligente, sur quelle instance diriger le trafic.

Bien sûr, ce trafic peut également faire l’objet de contrôles. Un opérateur de la plateforme ou un développeur d’une application peut configurer des règles (écrites sous forme de code – On peut parler de “communication as code”) qui permettent de définir des redirections, du partage de trafic entre versions, de la bascule de trafic pour des montées de versions, des règles permettant d’injecter des erreurs, autoriser ou interdire des requêtes, d’introduire de la latence, de faire du mirroring, du rejeu, de configurer du circuit breaking, des timeouts, … Ces règles sont bien sûr déployées sur les proxys qui se chargent ensuite de les appliquer.

Du chiffrement pour tous

Un aspect non négligeable consiste aussi à offrir le chiffrement des données transportées. Comme tous les échanges sont contrôlés par les proxys, tous les échanges entre proxys peuvent être chiffrés aisément. L’outil de service mesh s’occupe de déployer et renouveler les certificats au niveau de chaque proxy. Il n’est donc plus nécessaire de le faire au niveau de chaque application. Le mTLS devient la norme et se configure en quelques lignes de YAML.

Résumé : le Service Mesh en 2 paragraphes

Le Service Mesh se traduit finalement par un réseau de proxy applicatifs, qui est configuré et piloté de manière centralisée. Il s’agit donc avant tout d’un changement de localisation de fonctionnalités. Les fonctionnalités qui permettent de mesurer, fiabiliser, sécuriser et rendre les échanges performants sont déjà connues. Dans le cadre du service mesh elles sont simplement facilitées, découplées d’un élément d’infrastructure mutualisé (de type Load Balancer) et découplées de bibliothèques applicatives.

Ce changement de localisation impacte les développeurs, car il décharge l’application d’un outillage lourd, et “impose” une nouvelle responsabilité : garder un oeil attentif sur les propositions de modification de la configuration de la communication au sein de toute la plateforme.

Android Makers 2019, rendez-vous les 23 & 24 avril

$
0
0

Pour la troisième année consécutive, Xebia sponsorise Android Makers, la conférence des Android Lovers ! Vous y participez ? Venez nous rencontrer sur notre stand et échanger lors des conférences avec Michaël, Jordan, Julien, Arnaud et Simone nos speakers 2019.

En attendant les 23 et 24 avril prochains, voici un flashback des éditions précédentes… et on vous dévoile ce que nous vous proposons cette année.

Android Makers 2017

Pour l’année 2017, nous avions créé un Photobooth avec Android Things et le SDK Mobile Vision (succédé par ML Kit) pour animer notre stand. Les participants pouvaient se prendre en photo et rajouter des accessoires (lunettes, moustache…). Les photos étaient ensuite tweetées via l’API de Twitter et imprimées pour que les participants repartent avec.

Pour cette première édition, Qian Jin, développeuse Android et Yoann Benoit, Data Scientist, présentaient la conférence Faites chauffer les neurones de votre Smartphone Android avec du Deep Learning pour montrer la puissance du Machine Learning sur les appareils mobiles.

Android Makers 2018

En 2018, nous vous avions proposé un jeu de mémoire réalisé grâce à Android Things. Ce jeu est composé de boutons lumineux créés à l’aide d’une imprimante 3D, d’un retour sonore ainsi que d’un tableau des scores en temps réel.

Le fonctionnement est simple : la machine génère une séquence aléatoire de boutons à presser que le joueur doit répéter sans se tromper. A la fin de la séquence une nouvelle instruction est ajoutée. La difficulté du jeu se situe donc au niveau de la mémoire, la liste s’allongeant au fur et à mesure de la partie. La plateforme Android Things propose, en plus du framework Android, la possibilité de piloter des composants électroniques.

Pour en savoir plus voici l’article sur les coulisses du jeu Simon.

Durant cette deuxième édition, Qian présentait le sujet From ProGuard to R8: Take care of your bytecode, tandis que notre développeur iOS Simone Civetta se lançait sur du Kotlin/Native avec son talk Reusable Cross-Platform Frameworks with Kotlin/Native.

Et pour cette année ?

Cette année les Xebians vous préparent 3 talks divers et variés :

Sur notre stand, vous trouverez un nouveau jeu pour challenger vos connaissances des speakers d’Android Markers ! Ce « Qui est-ce ? » édition spéciale Android Makers est la preuve de notre passion pour Kotlin. Le backend du jeu est développé en Ktor. Les applications mobiles Android et iOS partagent le code de la logique du jeu grâce à Kotlin/Native. Jordan et Julien vous feront découvrir les coulisses de ce projet pendant leur talk Kotlin end-to-end: du client au serveur à ne surtout pas rater :)

Xebia en quelques mots

Depuis trois ans, Xebia est partenaire d’Android Makers. Cette conférence est pour nous l’occasion de partager nos compétences, notre savoir-faire et notre passion pour le développement mobile ainsi que jouer avec l’univers Android !

N’hésitez pas à passer nous voir sur notre stand et à échanger avec nous lors de nos talks. Partager notre passion, c’est ce que nous préférons :)

Chez Xebia, nous ne faisons pas que du dev mobile. Découvrez notre univers via :

  • Notre blog technique (blog.xebia.fr),
  • Le Twitter (@xebiafr),
  • Nos conférences tech, comme la FrenchKit (La conférence iOS à Paris que nous co-organisons avec Cocoaheads),
  • Notre Meetup : Mobile Things
  • Nos livres blancs, dont le mobile disponible en téléchargement pdf et epub sur le site xebia.fr.

Vous souhaitez en savoir plus sur Xebia et les Xebians ? Découvrez le site vismavie.xebia.fr ou contactez-nous à recrutement@xebia.fr.

Le programme du Paris Container Day est disponible !

$
0
0

Après un cru 2018 mémorable, Xebia et WeScale organisent le 4 juin prochain la nouvelle édition du Paris Container Day au New Cap Event Center.

 

Le Paris Container Day, c’est quoi ?

Le Paris Container Day est la conférence pionnière en France dédiée à l’écosystème des conteneurs et de ses bonnes pratiques. 2019 marquera la 4e édition de l’événement. Vous pouvez réserver votre place ici.

 

Un thème dédié pour la journée

L’utilisation des conteneurs dans l’industrie informatique est maintenant naturelle. L’orchestration était une étape nécessaire, à présent acquise. Qu’en est-il à présent des bonnes pratiques et des standards technologiques, pour fiabiliser nos usages courants ?
Pérenniser, stabiliser le marché et l’outillage, c’est le thème de cette quatrième édition : Standards & Craftsmanship.

 

Le programme est en ligne

Le programme de l’événement est maintenant disponible sur notre site. Suivez notre twitter @ContainerDayFR pour ne manquer aucune annonce !

Retrouvez ci-dessous un avant-goût de l’agenda avec les 3 keynotes  :

Dan Lorenc – Technical Lead Manager – Google

A Case for Standardization in Container CI/CD

The rise of containers and the adoption of DevOps practices have led to a shift in the role and methods of continuous integration, deployment and delivery across the industry. This talk will cover how and why these practices changing and where we should expect them to go in the future. This talk will explain why now is the right time to start converging on shared standards and best practices, and how these will enable us all to deliver higher quality software to our users rapidly and securely.


Liz Rice – Technology Evangelist – Aqua Security

The State of Kubernetes Security

Kubernetes is fundamentally a complex system with lots of different potential attack vectors aimed at data theft, currency mining and other threats. This talk provides an overview of the current state of security-related features in Kubernetes, and gives directional starting points on how to secure Kubernetes components and the applications that run on top of these Kubernetes components. For the topics explored, pointers on where to further investigate will be offered.


Scott McCarty – Principal Product Manager Containers – Red Hat

The Standards and technologies behind a container engine

So, you know that containers are fancy processes, and you know that the kubelet, docker engine, runc and the kernel work together to somehow create containers, but you have gaps in knowledge on exactly what happens in between kubectl run (or docker run, or podman run) and ps -ef on a node.

If you can’t explain it on a napkin and that drives you nuts, this talk is for you.

There are a lot of technologies working together to make a simple command so simple. It’s like an iceberg of technology below the water, and we are going to scuba dive below the surface and explore what’s going on. Also, we are going to give you a pewter challenge coin to prove you were there and did it.

After attending this talk, you should be able to impress your friends, influence people and become rich using your new, deeper understanding of how the orchestration node (kubelet), container engine (CRI-O, dockerd, containerd), container runtime (runc, kata, gvisor), and Linux kernel work together to create and manage containers.

 

Une journée repartie en 3 tracks et organisée par niveau de difficultés

Comme durant l’édition 2018, les participants auront le choix entre 3 types d’intervention : Conférences de 45 min, des Retours d’expériences de 45 mins et des Fast tracks de 20 mins. Chacune de ces interventions sera repartie selon 2 niveaux de difficultés (Apprenti et Maître) pour que chaque participant puisse profiter pleinement de sa journée. Un track sera exclusivement consacré aux Retours d’Expérience durant toute la durée de la conférence.

 

Profitez de cette journée pour rencontrer des acteurs de l’environnement des conteneurs

Initié lors l’édition 2017, un espace dédié aux sponsors de l’événement sera installé au rez-de-chaussée. Vous pourrez venir les rencontrer (Aqua, F5 Networks, Microsoft, Rancher, AVI Networks, Docker, HAProxy, Instana et Scaleway) et discuter de leurs dernières actualités. Les pauses et buffets prévus tout au long de la journée seront aussi l’occasion pour les participants d’échanger entre eux et avec les speakers.


Appliquez vos décisions d’architecture avec ArchUnit (1/2)

$
0
0

ArchUnit est une bibliothèque qui propose une fluent API pour tester l’architecture d’applications Java.

L’objectif de cet article est de vous donner un aperçu des possibilités techniques d’ArchUnit. Il sera suivi par un second article qui apportera une vision plus théorique sur l’intégration d’ArchUnit par rapport aux problématiques de gestion et de documentation de l’architecture.

Introduction

Périmètre

ArchUnit est une bibliothèque qui est utilisée pour mettre en place des tests automatisés dans le but de vérifier l’application de décisions d’architecture.

Martin Fowler définit l’architecture logicielle comme l’ensemble des décisions qui sont à la fois importantes et sur lesquelles il est difficile de revenir après coup. ArchUnit ne cible pas toutes les décisions d’architecture : il n’est bien évidemment pas question de vérifier l’application de décisions au niveau d’une entreprise, mais uniquement celles qui concernent la conception d’une application.

Intégration dans votre environnement

ArchUnit est conçu pour Java et Kotlin. Il est possible de l’utiliser avec n’importe quel autre langage de la JVM, comme Scala, mais un certain nombre de contournements sont alors nécessaires. La fluent API, dont le plus gros atout est l’expressivité, devient alors au contraire contre-intuitive.

ArchUnit supporte tous les principaux frameworks de test et s’intègre donc vraisemblablement sans problème dans votre environnement de développement. Au-delà de la bibliothèque de base, des surcouches sont disponibles pour JUnit 4 et JUnit 5. Celles-ci fournissent des annotations pour une meilleure lisibilité et plus de concision.

Au moment de la rédaction de cet article, la version 0.10.2 est la dernière en date. Le code source est disponible sur le dépôt GitHub officiel d’ArchUnit, avec notamment un jeu d’exemples très clairs.

Glossaire

Afin d’éviter les ambiguïtés dans la suite de l’article, il est primordial de bien comprendre la distinction entre ce que nous appellerons une « décision » et une « règle ».

Une décision désigne quelque chose qui affecte le code source et que l’équipe décide d’appliquer. Une décision peut tout à fait avoir une origine extérieure à l’équipe, à condition que celle-ci ait une autorité reconnue par l’équipe.

Une règle désigne le test correspondant à une décision, tel qu’implémenté avec ArchUnit. On peut considérer qu’il s’agit de la formalisation de la décision en tant que test.

Avantages

L’intérêt principal d’ArchUnit est le même que celui de l’automatisation des tests de manière plus générale : vous savez si vous êtes conforme à l’attendu ou non, au moment de l’implémentation du test tout d’abord, puis à chaque fois que votre application est modifiée. Assurer ainsi la non-régression est particulièrement pertinent dans la mesure où cela garantit que votre architecture ne partira pas dans des directions incohérentes.

Par ailleurs, il arrive fréquemment que des décisions ne soient pas connues de tous les membres d’une équipe. Et quand bien même elles le seraient, une erreur est vite arrivée. On remédie généralement à ce problème par des revues de code. La définition de règles avec ArchUnit y apporte une réponse qui a l’avantage d’être automatique et déterministe, et par conséquent plus fiable. De plus, le temps ainsi économisé en revue peut être accordé à des problèmes plus importants, comme par exemple la discussion des décisions d’architecture.

API et cas d’usage

Bien qu’ArchUnit ne prétende pas gérer tous les cas possibles et imaginables, les 3 couches d’API proposées permettent de s’adapter à de nombreux contextes :

  • La couche Core met à disposition une API extrêmement flexible, qui peut être utilisée pour implémenter à peu près n’importe quelle règle.
  • La couche Lang simplifie l’API de la couche Core pour fournir une fluent API qui peut être utilisée pour implémenter la plupart des règles avec une syntaxe très expressive.
  • La couche Library propose des syntaxes très spécifiques à un certain nombre de règles prédéfinies. Celles-ci sont ainsi encore plus simples qu’avec la couche Lang, mais ceci n’est disponible que pour un nombre de cas d’usage très limité.

La couche Library

Le cas d’usage principal d’ArchUnit est la vérification de la structure globale d’une application : comment l’application est-elle découpée, et comment les sous-parties résultantes interagissent-elles ?

Étant donné que ce type de règles est quasiment toujours pertinent, la couche Library propose quelques syntaxes très spécifiques dans l’objectif de les rendre aussi expressives et concises que possible.

Architecture en couches

La couche Library peut typiquement être utilisée pour définir des règles sur les architectures en couches. Dans l’exemple ci-dessous, l’application est découpée en 3 couches, avec une couche Service, qui contient toute la logique métier, définie pour ne pas dépendre des autres couches dans une logique qui a trait aux architectures hexagonales.

 

@ArchTest
public static final ArchRule layer_dependencies_are_respected = layeredArchitecture()
       .layer("Controller").definedBy("fr.xebia.archunit.controller..")
       .layer("Service").definedBy("fr.xebia.archunit.service..")
       .layer("Adapter").definedBy("fr.xebia.archunit.adapter..")
       .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
       .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller", "Adapter")
       .whereLayer("Adapter").mayNotBeAccessedByAnyLayer();

La définition d’une couche se base sur une syntaxe dans laquelle fr.xebia.archunit.controller.. prend en compte tout le contenu du package controller et de ses sous-packages.

Structure interne des couches

Il est également possible de définir des règles qui portent sur la structure interne des couches.

Par exemple, en considérant que la couche Adapter se compose de plusieurs packages, dont chacun correspond à un adaptateur utilisé pour échanger avec un système externe différent, on peut définir une règle selon laquelle les adaptateurs ne dépendent jamais les uns des autres.

@ArchTest
public static final ArchRule adapters_do_not_depend_on_one_another = slices()
       .matching("fr.xebia.archunit.(adapter).(*)..").namingSlices("$1 '$2'")
       .ignoreDependency("fr.xebia.archunit.adapter.*..", "fr.xebia.archunit.adapter.common..")
       .should().notDependOnEachOther();

On pourra pour cela définir des slices (littéralement, des tranches) de l’application par le biais d’une syntaxe de pattern matching. Par exemple, fr.xebia.archunit.adapter.*.. prend en compte toutes les classes des packages qui correspondent à ce pattern, c’est-à-dire tous les sous-packages directs du package adapter.

Des règles peuvent être définies pour assurer que les slices soient complètement indépendants les uns des autres, ou que leurs dépendances ne soient jamais cycliques.

On notera également la possibilité de définir des exceptions à la règle, ici dans le cas d’un package common qui contient des ressources partagées par les différents adaptateurs.

L’astérisque peut être utilisé pour mettre en place des structures cohérentes dans des couches similaires.

La couche Lang

Bien que la couche Library nous propose une syntaxe sur-mesure, optimale aussi bien en terme d’expressivité que de concision, il s’agit de quelque chose qui n’est disponible que pour un nombre de règles très limité.

La couche Lang propose donc une fluent API qui peut être utilisée pour implémenter la majorité des règles avec une syntaxe qui reste très expressive.

Garder la main sur les dépendances externes

Par exemple, il est possible de vérifier les dépendances externes qui sont utilisées dans les différentes parties de votre code. Cette problématique, à laquelle on répond traditionnellement en créant des modules Maven/Gradle distincts, peut être résolue avec ArchUnit en créant des contraintes au niveau d’un package ou d’une classe.

@ArchTest
public static final ArchRule only_adapters_pull_model_dependencies = noClasses()
       .that().resideOutsideOfPackage("fr.xebia.archunit.adapter..")
       .should().accessClassesThat().resideInAnyPackage("com.fasterxml..");

 Cette vérification ne s’applique pas aux annotations. Ainsi, dans l’exemple ci-dessus, même si des annotations Jackson sont utilisées dans le code source, la règle sera considérée comme satisfaite. Il s’agit d’un problème connu et identifié comme bloquant pour la sortie de la version 1.0.0.

Il existe un problème similaire au niveau de la prise en compte des paramètres de types. Pour reprendre l’exemple précédent, une classe qui manipule des listes de parsers JSON ne sera pas considérée comme dépendante de la bibliothèque Jackson.

Conventions de nommage

ArchUnit permet de définir des règles par rapport au nommage des classes, des méthodes, etc.

@ArchTest
public static final ArchRule controllers_should_be_suffixed_as_such = classes()
       .that().areAnnotatedWith(Controller.class)
       .should().haveSimpleNameEndingWith("Controller");

Cette fonctionnalité n’est intéressante que pour des règles complexes, ou dont l’application est très localisée, sans quoi d’autres outils plus adaptés permettront de mettre en place ces règles de manière moins coûteuse.

La couche Core

Dans la majorité des cas, les couches Library et Lang devraient couvrir tous vos besoins. Toutefois, pour implémenter des règles très spécifiques, ArchUnit expose la couche qui leur sert de base : la couche Core.

La couche Core propose une API extrêmement flexible qui peut être utilisée pour implémenter à peu près n’importe quelle règle.

À titre d’exemple, jusqu’à la version 0.10.0 d’ArchUnit, la couche Lang ne permettait pas de définir de règles au niveau des méthodes. Afin de contourner cette limitation, il était toutefois possible d’utiliser la couche Core directement.

@ArchTest
public static final ArchRule all_entry_points_shoud_be_annotated_with_log_description = all(new AbstractClassesTransformer<JavaMethod>("methods") {
    @Override
    public Iterable<JavaMethod> doTransform(final JavaClasses javaClasses) {
        return StreamSupport.stream(javaClasses.spliterator(), false)
                .flatMap(javaClass -> javaClass.getMethods().stream())
                .collect(toList());
    }
})
        .that(HasOwner.Functions.Get.<JavaClass>owner().is(resideInAPackage("fr.xebia.archunit.controller..")))
        .and(modifier(PUBLIC).as("are public"))
        .should(new ArchCondition<JavaMethod>("annotated with " + LogDescription.class) {
            @Override
            public void check(final JavaMethod method, final ConditionEvents events) {
                boolean typeMatches = method.isAnnotatedWith(LogDescription.class);
                final String message = format("%s annotated with %s",
                        method.getFullName(), method.getAnnotations().stream()
                                .map(annotation -> annotation.getType().getSimpleName())
                                .collect(toList()));
                events.add(new SimpleConditionEvent(method, typeMatches, message));
            }
        });

Comme on peut l’observer ci-dessus, la couche Core est verbeuse et manipule des concepts très génériques, ce qui aboutit à des règles difficiles à comprendre et à maintenir. C’est un problème qui peut être facilement résolu par l’implémentation d’une couche intermédiaire, dans une logique similaire à celle de la couche Lang d’ArchUnit. Cette couche additionnelle pourra ensuite être partagée entre les modules sous la forme d’une bibliothèque spécifique. Pour la même règle, on aboutit ainsi à une syntaxe beaucoup plus simple :

@ArchTest
public static final ArchRule all_entry_points_shoud_be_annotated_with_log_description = all(methods())
       .that(areDefinedInAPackage("fr.xebia.archunit.controller.."))
       .and(arePublic())
       .should(beAnnotatedWith(LogDescription.class));

Regrouper les règles

Étant donné que les règles sont définies sous la forme de constantes publiques, il est possible de les définir dans une classe et de les utiliser dans une autre, ce qui est particulièrement utile quand on veut partager des règles entre différents modules.

public class HexagonalArchitectures {
    @ArchTest
    public static final ArchRule adapters_should_not_depend_on_one_another = ...
}

@RunWith(ArchUnitRunner.class)
@AnalyzeClasses(packages = "fr.xebia.archunit")
public class ArchitectureTest {
    @ArchTest
    public static final ArchRule adapters_should_not_depend_on_one_another = HexagonalArchitectures.adapters_should_not_depend_on_one_another;
}

Il est également possible d’importer l’ensemble des règles définies dans une classe. Cela rend la définition des règles considérablement moins verbeuse ; il est donc intéressant de considérer la constitution d’ensembles cohérents au moment de la définition des règles.

@RunWith(ArchUnitRunner.class)
@AnalyzeClasses(packages = "fr.xebia.archunit")
public class ArchitectureTest {
    @ArchTest
    public static final ArchRules hexagonal_architecture = ArchRules.in(HexagonalArchitectures.class);
}

 

Nous avons désormais un bon aperçu des possibilités techniques offertes par ArchUnit. Mais alors comment l’intégrer dans votre manière de gérer l’architecture de manière plus globale ? Que peut-il vous apporter du point de vue de la documentation ? Et quels sont les pièges à éviter lors de sa mise en œuvre ? Rendez-vous dans un deuxième article pour en savoir plus !

Pépite #13 – JSON To Kotlin Class

$
0
0

Dans une application cliente, nous avons très souvent besoin de parser des services JSON en plusieurs classes.

Le plugin Json to Kotlin vous permet de créer des data class plus vite que votre ombre à partir d’un JSON.

Pour l’installer : allez dans les « Paramètres » puis « Plugins » et « Browse Repositories« .

Pour l’utiliser : cliquez sur « Code » puis « Générer » et enfin « Convert Json to Kotlin Class ». Une fenêtre vous permet de spécifier votre Json et votre nom de classe.

Prenons le JSON suivant pour exemple :

{
  "search_key":"guinness",
  "title":"Guinness",
  "synonyms": "Guinness Draught",
  "price": 4.70,
  "quantity": 1,
  "srm":null,
  "tags":["irish_dry_stout","dry_stout","stout"],
  "brewery":
  {
    "key": "guinness",
    "title": "St. James's Gate Brewery / Guinness Brewery"
  },
  "country_description":
  {
    "key":"ie",
    "title":"Irland"
  }
}

Voici le résultat des classes générées :

data class Beer(
    val brewery: Brewery,
    val country_description: CountryDescription,
    val price: Double,
    val quantity: Int,
    val search_key: String,
    val srm: Any,
    val synonyms: String,
    val tags: List<String>,
    val title: String
)

data class Brewery(
    val key: String,
    val title: String
)

data class CountryDescription(
    val key: String,
    val title: String
)

Si un champ contient un espace { "search key": "my beer" } la data class générée contiendra une erreur car une variable ne peut pas avoir d’espace dans le nom.

Bien que cet outil puisse améliorer votre productivité, je regarde les classes générées pour faire des ajustements : supprimer les champs inutiles et respecter la conformité avec le coding style guide de l’équipe.

TADA it’s magic !

Google Cloud : Démarrer une VM depuis une Cloud Function pour décompresser des fichiers

$
0
0

Introduction

Dans cet article nous allons développer pas à pas une Cloud Function pour décompresser des archives. Elle sera déclenchée lors d’un événement de dépôt de fichier dans GCS et démarrera une VM pour réaliser l’extraction des fichiers.

Pour donner un peu de contexte, dans notre projet nous recevons des fichiers compressés que nous devons extraire. Nous avons immédiatement pensé à utiliser les Cloud Functions pour répondre à ce besoin: lorsqu’un fichier tar est déposé, Cloud Storage déclenche une Cloud Function qui extrait les fichiers et les dépose dans le bucket.

Malheureusement ce fonctionnement atteint ses limites lorsque l’on reçoit des fichiers supérieurs à environs 50 Mo. La Cloud Function atteint alors son délai maximum d’exécution (9 minutes au moment de l’écriture de cet article) sans avoir pu finir l’extraction des fichiers !

Nous avons alors décidé de creuser une solution hybride pour résoudre ce problème. La Cloud Function démarre une VM et délègue à celle-ci l’extraction des fichiers. Puis, une fois la tâche terminée, la VM s’éteint automatiquement.

Le schéma ci-dessous illustre le fonctionnement global de la solution:

Démarrer une VM avec l’API Node.js

Nous allons utiliser l’environnement de développement accessible depuis Cloud Shell pour développer notre code. Cet environnement est implicitement authentifié pour utiliser les services GCP, nous pourrons coder facilement dans de nombreux langages : idéal donc pour rapidement “POCer” notre idée.

Dans la console Google Cloud nous allons sur le projet GCP qui va héberger notre VM et Cloud Function, nous démarrons Cloud Shell puis nous ouvrons l’éditeur de code intégré:

Voilà !

Depuis la console de Cloud Shell nous nous créons un projet Node.js

mkdir poc_vm
cd poc_vm
npm init

Nous désirons utiliser l’API Compute Engine nous installons donc les packages nécessaires:

npm install --.save @google-cloud/compute

Puis dans un fichier createSimpleVM.js nous copions le code suivant (voir la documentation de l’API Compute):

const Compute = require('@google-cloud/compute');
const compute = new Compute();
(async () => {
   try {
       const zone = compute.zone('europe-west1-b');
       const name = 'my-ubuntu-vm';
       const data = await zone.createVM(name, {os: 'ubuntu'});

       const vm = data[0];
       const operation = data[1]; 
      
       console.log('\nLa demande de création de la VM est faite');
       console.log('\nLes metadatas de la VM en cours de création:');
       console.log(JSON.stringify(await vm.getMetadata()));

       await operation.promise();
      
       console.log('\nLa VM est créée');

       console.log('\nLes metadatas de la VM créée:');
       console.log(JSON.stringify(await vm.getMetadata()));
       console.log(JSON.stringify(await operation.getMetadata()));
   } catch(err) {
       console.error(err);
   }
})();

Les deux grandes étapes de ce code consistent à :

  • demander la création d’une VM :
await zone.createVM(name, {os: 'ubuntu'});
  • attendre que la création de la VM soit terminée :
await operation.promise();

Pour exécuter ce script Node.js nous exécutons la commande suivante:

node createSimpleVM.js

Un petit tour dans la console sur la partie Compute VM nous permet de vérifier que la VM est bien présente:

La capture d’écran suivante montre les différences dans les données « metadatas » loggées avant et après la création de la VM. Certains attributs sont en plus tel que l’adresse IP.

Cela clôt cette première étape, nous savons comment créer une VM avec l’API Node.js.

Etre notifié du dépôt d’un fichier dans Cloud Storage

D’abord nous créons notre bucket qui recevra nos fichiers:

gsutil mb gs://decompress-from-bucket-2019

Puis nous créons un nouveau fichier index.js qui contiendra le code de la Cloud Function.

C’est cette Cloud Function qui sera appelée par les notifications provenant de Cloud Storage.

/**
* Generic background Cloud Function to be triggered by Cloud Storage.
*
* @param {object} data The event payload.
* @param {object} context The event metadata.
*/
exports.helloGCSGeneric = (data, context) => {
 const file = data;
 console.log(`  Event ${context.eventId}`);
 console.log(`  Event Type: ${context.eventType}`);
 console.log(`  Bucket: ${file.bucket}`);
 console.log(`  File: ${file.name}`);
 console.log(`  Metageneration: ${file.metageneration}`);
 console.log(`  Created: ${file.timeCreated}`);
 console.log(`  Updated: ${file.updated}`);
};

(Voir la documentation GCP https://cloud.google.com/functions/docs/tutorials/storage#functions-update-install-gcloud-node8)

Déployons notre Cloud Function en la déclarant comme étant déclenchée par notre bucket précédemment créé:

gcloud functions deploy helloGCSGeneric --runtime nodejs8 --trigger-bucket decompress-from-bucket-2019

Dans la console GCP nous pouvons constater qu’elle est bien déployée et déclenchée par les notifications du bucket

Finalement pour tester il suffit de déposer un fichier dans notre bucket et vérifier les logs:

Nous avons donc le code pour créer une VM et une Cloud Function qui se déclenche sur les événements d’un bucket.

La prochaine étape est donc de créer la VM depuis notre Cloud Function sur un événement déclenché par Cloud Storage.

Créer une VM depuis un Cloud Function

Le code suivant fusionne les deux étapes précédentes:

const Compute = require('@google-cloud/compute');

const compute = new Compute();

/**
* Generic background Cloud Function to be triggered by Cloud Storage.
*
* @param {object} data The event payload.
* @param {object} context The event metadata.
*/
exports.spawnVM = async (file, context) => {   
   console.log(`  Event ${context.eventId}`);
   console.log(`  Event Type: ${context.eventType}`);
   console.log(`  Bucket: ${file.bucket}`);
   console.log(`  File: ${file.name}`);
   console.log(`  Metageneration: ${file.metageneration}`);
   console.log(`  Created: ${file.timeCreated}`);
   console.log(`  Updated: ${file.updated}`);

   const zone = compute.zone('europe-west1-b');
   const name = 'my-ubuntu-vm';
   const data = await zone.createVM(name, {os: 'ubuntu'});

   const vm = data[0];
   const operation = data[1]; 

   console.log('\nLa demande de création de la VM est faite');

   console.log(JSON.stringify(await vm.getMetadata()));

   await operation.promise();

   console.log('\nLa VM est créée');
};

Nous déployons:

gcloud functions deploy spawnVM --runtime nodejs8 --trigger-resource decompress-from-bucket-2019 --trigger-event google.storage.object.finalize

A noter que la commande de déploiement est légèrement modifiée par rapport à la fois précédente : nous ne déclenchons la Cloud Function que sur la création d’un objet dans le bucket car nous avons ajouté l’option –trigger-event google.storage.object.finalize

Nous déposons un fichier dans le bucket et nous pouvons constater qu’une VM a bien été démarrée. Nous la supprimons via la console afin d’éviter de payer inutilement pour celle-ci.

Décompresser le fichier reçu

Maintenant que nous avons notre VM démarrée, il nous faut:

  • télécharger sur la VM le fichier compressé,
  • le décompresser
  • transférer les fichiers décompressés dans notre bucket Cloud Storage

Nous allons fournir un script shell à notre VM, celui-ci sera automatiquement exécuté lorsque celle-ci est prête (https://cloud.google.com/compute/docs/startupscript).

Nous modifions le code précédent avec les éléments suivants:

exports.spawnVM = async (file, context) => {   
   const zone = compute.zone('europe-west1-b');

   if ((!file.name.toLowerCase().endsWith('.tar.gz'))) {
       console.log('This is not a tar.gz file');
       return;
   }

   const bucketFile = `gs://${file.bucket}/${file.name}`

   const bashScript = `#! /bin/bash
                       function log_debug() {
                           gcloud logging write uncompress_tar_gz_logs "$1" --severity=DEBUG
                       }
                      
                       function log_error() {
                           gcloud logging write uncompress_tar_gz_logs "$1" --severity=ERROR
                       }

                       log_debug "starting startup script"

                       if ! gsutil cp gs://${file.bucket}/${file.name} . &> /dev/null; then
                           log_error "cannot download from gs://${file.bucket}/${file.name}"                           
                       fi
                    
                       if ! tar -zxvf ${file.name} &> /dev/null; then
                           log_error "cannot decompress $filename"
                       fi

                       if ! gsutil cp *.csv gs://${file.bucket} &> /dev/null; then
                           log_error "cannot upload to gs://${file.bucket}"                           
                       fi

                       log_debug "ending startup script"
                       `;

   const config = {
       os: 'ubuntu',   
       http: true,
       metadata: {
           items: [
               {
                   value: bashScript,
                   key: 'startup-script'
               },
           ],
       },
       serviceAccounts: [
           {
               email: '360728966861-compute@developer.gserviceaccount.com',
               scopes: [
               'https://www.googleapis.com/auth/cloud-platform'
               ]
           }
       ],         
   };

   const name = 'my-ubuntu-vm';
   const data = await zone.createVM(name, config);

   const vm = data[0];
   const operation = data[1]; 

   console.log('\nLa demande de création de la VM est faite');

   console.log(JSON.stringify(await vm.getMetadata()));

   await operation.promise();

   console.log('\nLa VM est créée');
};

Dans le script bash nous appelons l’API de logging de gcloud (gcloud logging write …) ce qui nous permet d’avoir des infos sur ce qui se passe dans notre VM lors de la décompression.

Quelques petites explications sont nécessaires pour l’attribut “serviceAccounts” que nous avons ajouté dans la configuration passée à la création de notre VM:

Nous avons spécifié que la VM serait identifiée comme utilisant le “service account” par défaut pour une instance compute engine dans notre projet. En l’occurrence pour notre projet le service account en question porte le nom 360728966861-compute@developer.gserviceaccount.com

La gestion des authorizations avec l’utilisation des service account mériterait un article à elle seule sur le sujet. Pour ce cas concret nous spécifions à la VM que les interactions qu’elle aura avec les autres ressources GCP se fera en tant qu’utilisateur technique ayant pour identifiant 360728966861-compute@developer.gserviceaccount.com

En l’occurrence nous souhaitons avoir des interactions en lecture et en écriture sur notre bucket “decompress-from-bucket-2019”.

Nous devons donc ajouter au service account le rôle “roles/storage.objectAdmin” (https://cloud.google.com/storage/docs/access-control/iam-roles).

Pour cela nous exécutons la commande gsutil suivante :

gsutil iam ch serviceAccount:360728966861-compute@developer.gserviceaccount.com:objectAdmin gs://decompress-from-bucket-2019

Finalement nous déployons ensuite cette nouvelle version de la Cloud Function:

gcloud functions deploy spawnVM --runtime nodejs8 --trigger-resource decompress-from-bucket-2019 --trigger-event google.storage.object.finalize

Nous déposons un fichier tar.gz dans le bucket, la VM démarre et quelques secondes plus tard les fichiers csv apparaissent dans le bucket !

Quelques améliorations nécessaires

Il nous reste un petit souci, notre VM reste allumée après avoir décompressé le fichier, ça va vite coûter cher cette histoire !

Pour limiter au maximum les coûts il serait peut-être possible d’utiliser des instances préemptives (https://cloud.google.com/compute/docs/instances/preemptible) (en quelques mots, une instance préemptive est moins chère mais on ne peut la conserver plus de 24h).

Dans la configuration transmise à l’API pour créer la VM, nous ajoutons une option pour utiliser une instance préemptive :

   const config = {
       os: 'ubuntu',   
       http: true,
       scheduling: {
           preemptible: true
       },
       metadata: {
           items: [
               {
                   value: bashScript,
                   key: 'startup-script'
               },
           ],
       },
       serviceAccounts: [
           {
               email: '360728966861-compute@developer.gserviceaccount.com',
               scopes: [
               'https://www.googleapis.com/auth/cloud-platform'
               ]
           }
       ],         
   };

Nous ajoutons le code suivant dans le script bash afin que notre VM se “suicide” à la fin de la décompression:

const bashScript = `#! /bin/bash
                   function log_debug() {
                       gcloud logging write uncompress_tar_gz_logs "$1" --severity=DEBUG
                   }
                  
                   function log_error() {
                       gcloud logging write uncompress_tar_gz_logs "$1" --severity=ERROR
                   }

                   function shutdownVM {                     
                       VMNAME=$(curl -H Metadata-Flavor:Google http://metadata/computeMetadata/v1/instance/hostname | cut -d. -f1) &&
                       ZONE=$(curl -H Metadata-Flavor:Google http://metadata/computeMetadata/v1/instance/zone | cut -d/ -f4) &&
                       log_debug "shutting down VM $VMNAME"
                       gcloud compute instances delete $VMNAME --zone $ZONE --quiet                           
                       exit
                   }

                   log_debug "starting startup script"

                   if ! gsutil cp gs://${file.bucket}/${file.name} . &> /dev/null; then
                       log_error "cannot download from gs://${file.bucket}/${file.name}"                           
                   fi
                  
                   if ! tar -zxvf ${file.name} &> /dev/null; then
                       log_error "cannot decompress $filename"
                   fi

                   if ! gsutil cp *.csv gs://${file.bucket} &> /dev/null; then
                       log_error "cannot upload to gs://${file.bucket}"                           
                   fi

                   log_debug "ending startup script"

                   shutdownVM
                   `;

Nous déployons la Cloud Function puis déposons un fichier de test dans le bucket.

Les fichiers sont bien décompressés et la VM disparaît une fois la tâche terminée !

Conclusion

La mise en oeuvre est plutôt simple et permet de contourner les quotas (temps, mémoire, puissance CPU, …) imposés par Cloud Functions afin de décompresser des fichiers de taille conséquentes.

Si vous vous inspirez de ce qui est fait ici il conviendra d’y ajouter du monitoring afin de déclencher des alertes sur des problèmes qui pourraient (inévitablement) se produire (fichier corrompu, …) en utilisant par exemple les logs, produits par le script bash, avec Stackdriver.

 

 

Améliorez vos tests d’intégration grâce à Testcontainers

$
0
0

Introduction

On ne doute plus de l’importance des tests dans une application robuste. Citons tout d’abord les tests unitaires qui, comme leur nom l’indique, permettent de tester unitairement chaque brique de l’application (au niveau d’un service, d’une classe, d’une méthode…). Viennent ensuite les tests d’intégration permettant quant à eux de tester les liens entre les différentes briques de l’application, que ce soit des applications différentes, des services, des bases de données, des files de message… Ils sont indispensables pour vérifier le bon fonctionnement de bout en bout de l’application.

Nous pouvons prendre pour exemple une simple application Web exposant des APIs permettant la recherche de données dans une base SQL. Pour nos tests d’intégration, nous devons requêter cette base contenant des données viables. Il peut cependant être compliqué de tester le bon interfaçage avec des gros systèmes extérieurs tels que les systèmes d’échange de messages, AWS, etc. Il est rapidement coûteux de créer un environnement uniquement pour des tests automatiques. D’autre part, avoir un système centralisé sur lequel les tests se connectent empêche chaque test de facilement gérer ses propres données et de les rendre indépendants.

Testcontainers simplifie cela en nous offrant une bibliothèque Java permettant d’instancier dans des containers Docker de nombreux systèmes comme des bases de données, les services AWS, Redis, Elasticsearch…
Chaque test peut ainsi lancer son propre container Docker avec les systèmes dont il dépend et y insérer son propre jeu de données. Il devient alors très simple de tester les interactions entre notre système et celui lancé via Testcontainers.

Testcontainers est nativement compatible avec JUnit 4 et 5 ainsi que Spock.

Fonctionnalités

Dépendance

La librairie est séparée en plusieurs JARs :

  • un JAR « cœur » pour les fonctionnalités génériques et le support de docker-compose
  • plusieurs JARs pour les modules proposés par Testcontainers (nous détaillerons ces modules dans la suite de l’article)

Au moment de la rédaction, Testcontainers est disponible en version 1.11.1.

Pour ajouter la dépendance à votre projet :

Via Maven :

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.11.1</version>
    <scope>test</scope>
</dependency>

Via Gradle :

testCompile "org.testcontainers:testcontainers:1.11.1"

Cycle de vie

L’instanciation d’un container Docker avec Testcontainers se fait extrêmement simplement :

new GenericContainer("couchbase:6.0.1")

Il suffit ensuite d’appeler les méthodes start() ou stop() de ce GenericContainer pour démarrer ou arrêter un container de Couchbase en version 6.0.1. Testcontainers s’occupera bien évidemment de télécharger l’image si nécessaire et effectuera aussi le nettoyage des containers grâce à Ryuk à l’arrêt de la JVM.

Il est aussi possible de déclarer un container comme @Rule pour JUnit 4. Ainsi, le container va automatiquement démarrer au début du test, puis s’arrêter à la fin :

@Rule
public GenericContainer redis = new GenericContainer("redis:5.0.3").withExposedPorts(6379);

L’intégration dans JUnit 5 / Jupiter est fournie avec l’annotation @Testcontainers. Cette extension va ensuite chercher les champs annotés @Container pour savoir quel container démarrer :

@Testcontainers
class TestcontainersJUnit5 {

    @Container
    private static final GenericContainer REDIS = new GenericContainer("redis:5.0.3").withExposedPorts(6379);

	...
}

On remarque dans l’exemple précédent que Testcontainers permet d’exposer des ports sur le container.
D’autres actions sont également possibles :

  • exécuter des commandes via withCommand
  • faire le lien entre un fichier ou un dossier du Classpath et le container en utilisant withClasspathResourceMapping
  • définir des variables d’environnement avec withEnv

Une fois le container démarré, on peut récupérer l’adresse IP et les ports de celui-ci (pratique pour instancier un client tiers par exemple) :

String ipAddress = redis.getContainerIpAddress();
Integer port = redis.getMappedPort(6379);

L’un des gros avantages de Testcontainers est de pouvoir définir une stratégie d’attente afin de savoir si notre container est prêt.

Soit via HTTP :

try (var nginx = new GenericContainer("nginx:1.9.4")
        .withExposedPorts(80)
        .waitingFor(Wait.forHttp("/"))) {

    nginx.start();

    // Requête HTTP vers le container
}

Ou encore par log :

GenericContainer containerWithLogWait = new GenericContainer("redis:5.0.3")
    .withExposedPorts(6379)
    .waitingFor(
        Wait.forLogMessage(".*Ready to accept connections.*\\n", 1)
    );

Utilisation d’image à la volée

L’utilisation de Dockerfile, le fichier texte contenant les commandes pour assembler une image Docker, est également possible grâce à Testcontainers.

En utilisant un ImageFromDockerfile lors de la création de notre GenericContainer :

GenericContainer dslContainer = new GenericContainer(
    new ImageFromDockerfile()
            .withFileFromString("folder/someFile.txt", "hello")
            .withFileFromClasspath("test.txt", "mappable-resource/test-resource.txt")
            .withFileFromClasspath("Dockerfile", "mappable-dockerfile/Dockerfile"))

Ou alors avec le Dockerfile DSL proposé par Testcontainers :

new GenericContainer(
        new ImageFromDockerfile()
                .withDockerfileFromBuilder(builder ->
                        builder
                                .from("alpine:3.2")
                                .run("apk add --update nginx")
                                .cmd("nginx", "-g", "daemon off;")
                                .build()))
                .withExposedPorts(80);

Modules

En plus d’un GenericContainer, Testcontainers propose différents modules dans des dépendances séparées apportant des fonctionnalités supplémentaires.

Il existe des modules pour la plupart des systèmes de gestion de bases de données, Elasticsearch, Kafka, LocalStack (pour les services d’AWS), etc. Nous vous invitons à vous rendre sur la documentation de Testcontainers pour y découvrir la liste des modules qu’offre la librairie.

Nous allons vous présenter quelques-uns de ces modules.

Docker Compose

Compose est l’outil de Docker permettant de démarrer plusieurs containers liés les uns aux autres.

Pour utiliser un fichier docker-compose.yml avec Testcontainers, il faut instancier un DockerComposeContainer de cette manière :

DockerComposeContainer environment = new DockerComposeContainer(new File("src/test/resources/compose-test.yml"))

DockerComposeContainer expose les méthodes getServiceHost et getServicePort afin de récupérer l’adresse IP et le port d’un service défini dans le docker-compose :

String redisUrl = environment.getServiceHost("redis_1", REDIS_PORT) + ":" + environment.getServicePort("redis_1", REDIS_PORT);

Il est aussi possible de définir des stratégies d’attente différentes pour les différents services du docker-compose :

public static DockerComposeContainer environment =
    new DockerComposeContainer(new File("src/test/resources/compose-test.yml"))
            .withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort())
            .withExposedService("elasticsearch_1", ELASTICSEARCH_PORT, 
                Wait.forHttp("/all")
                    .forStatusCode(200)
                    .forStatusCode(401)
                    .usingTls());

Bases de données

C’est probablement l’un des besoins les plus courants lorsqu’on fait des tests d’intégration : interagir avec une base de données. On a souvent recours à plusieurs solutions pour avoir une base de données utilisable pour nos tests :

  • déployée sur un serveur : cela peut rapidement coûter cher et rendre les tests fragiles car dépendants d’un autre serveur et du réseau
  • déployée en local ou dans une VM : il faut garantir la réinitialisation de la base entre chaque test
  • en mémoire comme H2 : ce type de base est rapide et efficace mais empêche l’utilisation de fonctionnalités spécifiques du SGBD de l’application

Testcontainers offre la possibilité d’instancier des containers pour différents systèmes de gestion de base de données, qu’ils soient relationnels (MySQL, Postgres, Oracle…) ou NoSQL (Cassandra ou Couchbase).

Ces containers spécifiques offrent de nouvelles méthodes liées au SGBD utilisé et est ainsi plus avantageux que l’utilisation d’un simple GenericContainer.

Par exemple, pour une base relationnelle comme Microsoft SQL Server :

    @Rule
    public MSSQLServerContainer mssqlserver = new MSSQLServerContainer();

    @Test
    public void someTestMethod() {
        String url = mssqlserver.getJdbcUrl();
	    ...
	}

LocalStack

Si vous utilisez AWS régulièrement, vous avez potentiellement rencontré des difficultés à écrire des tests d’intégration avec les différents services offerts par Amazon.

Une des solutions possibles est l’utilisation de LocalStack, bibliothèque permettant de simuler la plupart des services AWS et ainsi effectuer des tests d’intégration. Son installation n’est cependant pas forcément aisée et peut obliger chaque développeur, pour pouvoir l’utiliser, à configurer soit LocalStack directement, soit un container Docker.

Testcontainers offre directement un container Docker intégrant LocalStack, rendant très simple et rapide la rédaction des tests en lien avec AWS.

Prenons par exemple un service basique qui envoie des messages dans une file SQS :

public class SqsService {

    public static final String QUEUE_NAME = "myQueueUrl";

    private final AmazonSQS sqs;

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    public SqsService(AmazonSQS sqs) {
        this.sqs = sqs;
    }

    public void sendMessage(CustomMessage customMessage) throws JsonProcessingException {
        sqs.sendMessage(getQueueUrl(), OBJECT_MAPPER.writeValueAsString(customMessage));
    }

    private String getQueueUrl(){
        return sqs.getQueueUrl(QUEUE_NAME).getQueueUrl();
    }
}

Après avoir importé la bonne dépendance, nous devons déclarer un container Docker dans notre test, initialiser un client amazonSQS et enfin créer notre file SQS :

public class SqsServiceTest {

    @Rule
    public LocalStackContainer localstack = new LocalStackContainer().withServices(SQS);
    
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    @Test
    public void should_send_message_in_sqs_queue() throws JsonProcessingException, org.testcontainers.shaded.com.fasterxml.jackson.core.JsonProcessingException {
        // Given
        AmazonSQS amazonSQS = AmazonSQSClient
                .builder()
                .withEndpointConfiguration(localstack.getEndpointConfiguration(SQS))
                .withCredentials(localstack.getDefaultCredentialsProvider()).build();
        String queueUrl = amazonSQS.createQueue(SqsService.QUEUE_NAME).getQueueUrl();
        SqsService sqsService = new SqsService(amazonSQS);
        CustomMessage customMessage = new CustomMessage(1, "firstContent", "secondContent");

        // When
        sqsService.sendMessage(customMessage);

        // Then
        List<Message> messagesInSqsQueue =  amazonSQS.receiveMessage(queueUrl).getMessages();
        assertThat(messagesInSqsQueue).hasSize(1);
        assertThat(messagesInSqsQueue.get(0).getBody())
                .isEqualTo(OBJECT_MAPPER.writeValueAsString(customMessage));
    }
}

Notre client amazonSQS, paramétré grâce à LocalStack, se comporte exactement de la même manière que le client classique. Nous devons donc utiliser les méthodes du SDK Amazon dans notre test afin de vérifier que nos services fonctionnent. Dans notre cas, nous avons lu les messages dans la queue pour vérifier que sqsService y avait bien envoyé un message.

Il faut donc être prudent quant à notre utilisation du client afin de ne pas se retrouver avec des tests en échec, non pas à cause d’une erreur dans le service testé, mais à cause d’une mauvaise utilisation d’AWS directement dans nos tests.

Conclusion

Testcontainers nous offre une bibliothèque à la fois efficace et très simple d’utilisation. Les nombreux modules proposés nous permettent de l’utiliser avec de nombreuses technologies, dont plusieurs habituellement complexes à intégrer dans des tests.

Il faut toutefois noter que Testcontainers n’offre qu’un moyen d’interagir avec des systèmes complexes, mais laisse au développeur la gestion des jeux de données, leur automatisation et leur centralisation. Vous devrez vous-même écrire le code permettant de remplir un index Elasticsearch ou d’envoyer des messages dans une file SQS directement dans les tests.

L’utilisation de Testcontainers pourrait s’avérer moins efficace que d’autres solutions, par exemple des bases de données embarquées. Cependant, la solution proposée ici correspond en tout point à la réalité et augmente ainsi l’utilité et l’efficacité des tests, offrant même la possibilité d’effectuer des tests de résilience.

Quant à l’impact sur la durée d’exécution de vos tests, celui-ci sera légèrement rallongé, d’autant plus à la première exécution (le temps de télécharger les images Docker). Tout dépendra bien évidemment de votre utilisation de Testcontainers. Préférez par exemple un container singleton que vous démarrerez au début de l’exécution de vos tests, mais veillez cependant à bien garantir l’idempotence de vos tests.

Ainsi, Testcontainers est un outil très pratique que nous vous conseillons pour vos tests d’intégration avec des systèmes complexes.

Kiss séries –épisode 5 – Ce que mon cheval m’a appris au sujet de la motivation au travail

$
0
0

Keep It Simple and Stupid. Chacun connaît l’acronyme du baiser. Et pourtant … Simplicité ne saurait s’apparenter à bêtise. C’est même tout l’inverse. “La simplicité est la sophistication suprême.” Léonard de Vinci.

Dans cette série d’articles, je vous propose de revenir sur des bases, des ateliers et outils agiles connus de tous, mais sous un format accessible, empreint de quotidien et de bon sens. Pour expliquer simplement les choses et donner à chacun le loisir de comprendre. L’intelligence, “inter” et “ligare” pour la racine latine, faire du lien entre. Car c’est bien de cela dont il s’agit : faire du lien entre ce que chacun sait, connaît, entrevoit, déjà dans sa vie personnelle et ce qu’il ou elle vit ou expérimente dans le monde professionnel. Un fossé en apparence : si peu en réalité.

Bon voyage !

AU MENU ?

Je suis coach agile. Mon travail consiste à accompagner des individus embarqués dans une transformation au sein de leur entreprise, la plupart du temps sans avoir été questionnés au préalable quant à cette démarche. Comme la plupart de mes collègues, je me suis rapidement intéressée au principe de motivation. Qu’est-ce que la motivation ? D’où vient-elle ? Peut-on la créer, la trouver, la faire évoluer, la réduire, l’abîmer, la détruire ? On trouve aujourd’hui sur internet toutes les explications sur toutes les théories qui adressent ce sujet.

De la pyramide des besoins de Maslow, reprise par Clayton Alderfer et ses trois facteurs d’existence, sociabilité et croissance, en passant par la théorie des besoins de réalisation, pouvoir et affiliation de McClelland, de Douglas McGregor avec ses théories X et Y divisant les individus aimant travailler et les autres, de Herzberg et ses facteurs internes et externes de motivation, à Hackman qui vise plus spécifiquement les caractéristiques des tâches, à Edwin A. Locke qui s’intéresse aux buts d’accomplissement via un objectif précis, difficile à atteindre et suivi d’un feed-back, ou encore via la théorie cognitiviste du résultat escompté reprise par C. Levy-Leboyer, sans oublier la théorie de l’équité d’Adams selon lequel tout individu calcule un score pour lui même et autrui, ou bien encore Michael Apter et sa théorie du renversement avec ses variations inter et intra individuelles, Skinner et son renforcement positif ou négatif par le prisme du béhaviorismLe lecteur courageux trouvera aujourd’hui autant de possibilités de se plonger dans un puits immense de connaissances mais non sans prendre le risque d’une profonde confusion voire d’une violente migraine associée.

Pour soulager mon cerveau, depuis plus de 30 ans, quant à moi, je monte à cheval. J’en possède deux. Une jument dont j’ai fait récemment l’acquisition, et un cheval que je détiens avec mon conjoint. Avant d’acheter ma jument, j’ai travaillé pendant deux ans ce cheval. Ce cheval s’appelle Dandy et c’est un pur-sang, réformé de courses, ancien galopeur.

La motivation humaine se veut extrêmement différente de la motivation animale si l’on s’en tient à la littérature actuelle. Les études scientifiques dont nous disposons aujourd’hui démontrent des différences importantes qui semblent interdire le parallèle entre les deux espèces. Pourtant, j’ai lu récemment qu’une étude avait fait grand bruit. Le cheval serait en capacité de reconnaître sur photographies des êtres humains, alors même qu’on ne lui aurait présenté ces clichés qu’une minute et ce, plusieurs heures auparavant. Et fait plus étonnant encore, le cheval présenterait des réactions diamétralement opposées face aux individus, si les clichés vus auparavant les montraient souriants, joviaux, ou à l’inverse en colère et agressifs. La science n’a assurément pas encore tout découvert. Pour autant, ce présent article, à l’image de tous les autres de cette série, n’a aucune prétention scientifique. Il s’agit simplement de donner à réfléchir et proposer, simplement, des parallèles porteurs de sens métaphoriquement.

Des limites de la récompense et de la rétribution

Le cas Dandy

Dandy, comme tous les pur-sangs, est un cheval hyper sensible. Une ombre dans la nuit suffit à provoquer une attaque de panique, un petit vent frais hivernal une explosion de cabrioles, mais plus subtil, il perçoit parfaitement les émotions qui sont les miennes. Si je suis angoissée, il piaffe sur place. Si je suis sereine, il se montre attentif. « C’est de ta peur dont j’ai peur », prend ici tout son sens.

Lorsque j’ai commencé à travailler Dandy, j’ai fait ce que tout le monde fait en pareille situation, précisément avec un animal. Autrement dit : en partie une erreur. Pour l’inciter à travailler, j’ai misé l’apprentissage sur un système de récompenses. Puisqu’il disposait d’une somme d’énergie, comme tout être vivant, j’ai pensé qu’il me suffisait de jouer sur ce système mécanique pour le pousser à travailler comme je le souhaitais. J’ai pensé ainsi trouver pour lui, une motivation. Renforcement positif – un bonbon contre une bonne action– et renforcement négatif – pas de bonbon et des actions de jambes et mains plus dures en cas de mauvaise action– autrement dit Skinner et le béhaviorisme ou encore Bentham et son arithmétique des plaisirs, pour faire simple.

Les premières semaines de l’expérimentation furent relativement concluantes mais surtout dans la mesure où nous ne partions de rien. Pour le préparer au box, lui demander ses pieds, ou même en selle féliciter des actions simples, parcellaires et séquencées, le système de renforcement positif ou négatif fonctionnait décemment. Mais le système trouvait une limite : l’ennui prit rapidement le pas malgré les conséquences agréables représentées par les friandises. Pire : lorsque mon conjoint ou une autre personne venait à s’occuper de Dandy ou le monter, celui-ci se montrait rétif si la récompense ne venait pas immédiatement. Après tout, ne dit-on pas que toute peine mérite salaire ? L’unique source de motivation venait de la récompense pour Dandy, et les semaines passant, le cheval commençait à stagner dans sa progression. Il lui fallait sans cesse plus de bonbons et de carottes pour réaliser ses actions. En outre, il n’avait aucune conscience de liens logiques entre les actions que je lui demandais. Passer une barre au sol n’était rien d’autre qu’une barre. Il ne comprenait pas que celle-ci s’inscrivait dans le cadre d’un parcours de plusieurs barres. On pourrait dire qu’il n’avait pas acquis de vision du tout…

Le cas des Hommes

Pendant quelques années, je distribuais des chocolats à mes équipes au travail pour chaque user story testée par les développeurs. Lorsque j’étais product owner, je le confesse, je donnais des bonbons Haribo quand mes équipes acceptaient de s’engager davantage sur un sprint que ce qu’il aurait été raisonnable de faire au regard de la vélocité. Scrum master, je demandais à celui ou celle qui arrivait en retard au daily d’apporter des croissants le lendemain. Autant d’exemples qui ne sont pas si éloignés de ce que j’ai intuitivement fait avec mon cheval. J’ai moi-même longtemps considéré le salaire, les primes sur contribution, comme autant de sources de motivation. Je ne dirai jamais que ces rétributions en carottes ou en k€ sont vaines, elles offrent assurément un levier puissant de motivation. Je crois aujourd’hui simplement qu’elles doivent être maniées avec discernement et jamais exclusivement. Le taylorisme illustre d’ailleurs assez bien les limites du modèle qui n’entrevoit que le salaire comme source de motivation au travail.

Hackman à la rescousse

Le cas Dandy

Pour Hackman, la motivation se base sur 5 facteurs. Varier les tâches, permettre qu’elles puissent être réalisées entièrement, leur donner une signification ainsi que l’autonomie pour les accomplir et donner un feedback.

Avec Dandy, j’ai commencé à jouer sur le premier facteur: la variété des tâches.

Au lieu de lui demander de passer une barre au sol pendant 20 minutes, ou de rester sur une ligne droite, j’ai commencé à alterner les exercices. Tantôt un cercle à droite, tantôt une serpentine à gauche, j’ai varié les allures entre le pas, le trot et le galop, et nous sommes sortis du lieu d’entraînement habituel pour essayer un environnement extérieur. Il s’est montré immédiatement plus réceptif. Les bénéfices de la stabilité et de la prévisibilité, qui sont pourtant sources de motivation, toujours selon certaines théories scientifiques sérieuses, trouvent encore et toujours des limites et imposent des nuances.

Pour sortir du mode silos dans lequel j’avais mis Dandy, j’ai entrepris de lui faire faire un parcours entier. Au lieu de le faire travailler sur une barre isolée, j’ai retiré les barres pour ne laisser que les chandeliers créant ainsi des obstacles virtuels (je compte déposer un brevet sur le Air Obstacle !) et ainsi travailler les directions. Dandy a ainsi rapidement compris qu’après le passage des deux premiers chandeliers illustrant le premier obstacle, venait le second, puis le troisième et ainsi de suite. Comme les obstacles invisibles n’avaient pas tous le même placement dans l’espace, qu’ils nécessitaient des tracés et trajectoires différents, et donc d’adapter les allures, le cheval a vite compris pourquoi je lui demandais des accélérations et des ralentissements. L’exercice prenait une signification et il pouvait être réalisé entièrement.

Quant à l’autonomie, avec du temps et de la patience, il a été possible de laisser Dandy réguler lui-même son allure. Cela n’a pas été toujours couronné de vif succès, mais au moins, je n’étais pas toujours en contrôle pour le faire à sa place.

Enfin, bien évidemment, le feedback était encore assorti de friandises, mais j’y ai ajouté les caresses et les félicitations à la voix. Dandy aime qu’on le câline et se montre sensible aux émotions humaines, il perçoit très clairement la joie ou la colère. Il est depuis aussi réceptif à un bonbon, qu’à une caresse ou encore une félicitation à la voix si celle-ci est assortie de joie chez l’humain. L’absence de tout ceci est également très bien assimilé comme étant une absence de feedback voire même comme un feedback négatif. Il couche les oreilles et tape du pied pour indiquer qu’il a tout de même essayé et aimerait s’en voir félicité d’une manière ou d’une autre.

Mon travail avec lui allait dans le bon sens, cependant, il manquait encore quelque chose. Toutes ces expérimentations étaient intimement liées à mon intervention. Autrement dit, pour Dandy, les motivations étaient toujours extrinsèques. Sans ma présence, rien ne pourrait se produire. Donner un sens aux choses, permettre leur accomplissement total, varier les tâches, rétribuer, donner de l’autonomie, donner du feedback : tout partait et revenait à moi. Que pouvais-je trouver qui le ramène à lui ? Comment générer une motivation intrinsèque chez un cheval ?

Le cas des Hommes

Demander à un développeur aussi passionné et brillant soit-il de s’en tenir à coder s’avère un calcul perdant. Dans ma pratique professionnelle, j’aime convier les développeurs à toutes sortes d’ateliers, y compris des ateliers en théorie réservés aux gens du métier. Par delà l’aspect collaboratif qui permet aux individus de se connaître, et par delà l’aspect stratégique qui consiste à mettre immédiatement en présence les gens qui veulent quelque chose avec ceux qui vont devoir le réaliser, je constate aussi que varier les activités des individus s’avère toujours bénéfique quant à leur moral, et de facto ce qui s’approcherait sans doute le plus de la motivation. Je donne l’exemple des développeurs, mais je pourrais tout aussi bien parler des product owners, testeurs, business analysts : peu importe, le sujet consiste à varier les tâches des individus. Le coach ne fera d’ailleurs pas exception.

Quant à la possibilité de gérer une tâche de bout en bout, l’exemple de la user story me semble assez parlant à lui seul. Un développeur verra davantage de motivation à tester une user story s’il a pu s’y intéresser en amont, y compris sur l’analyse : cela permettra en outre de lui donner une vision plus large de ce qu’il accomplit. Et là encore, l’exemple vaut aussi évidemment pour les fonctionnels que j’aime convier à des ateliers de TDD (Test Driven Development) pour ne citer que cela.

Quant à l’autonomie, là encore, en tant que coach, je laisse les individus se réguler comme mon cheval sur le parcours d’air obstacles. J’aide lorsque cela est nécessaire, mais toujours le moins possible. C’est un des fondements de l’agilité que de laisser de l’autonomie aux personnes, j’enfonce ici des portes ouvertes.

Enfin quant au feedback, la retrospective de scrum qu’on préconise de varier sur sa forme mais jamais tant sur le fond (pour donner relief  et réflexion à la nécessité de stabilité et de prévisibilité) s’avère assez en ligne avec les travaux de Hackman. Et à l’image de mon cheval, les individus ont souvent beaucoup à redire au moment du feedback !

Et à l’image de mon cheval, tout ceci est fort bien, mais pas suffisant pour comprendre la motivation.

Cause commune

Les travaux sur la motivation sont assez clairs et alignés pour constater qu’on peut grossièrement la résumer ainsi : la motivation est affaire de choix, de hiérarchie entre ces choix et souvent de concurrence. A l’échelle individuelle, la concurrence s’illustre lorsqu’il s’agit de savoir si le choix se porte davantage entre l’action ou le repos par exemple (qui n’a pas vécu ce dilemme, un dimanche hivernal dans son canapé ?).

Mais il existe une autre dimension à cette concurrence et celle-ci émerge face à des situations collectives. Face aux mêmes exigences, les facteurs individuels vont créer une différence entre les personnes et leurs conduites : la concurrence de l’individu face aux autres va donc occasionner selon les cas, apprentissage, compétition (positive ou négative) ou encore activité collective.

Le cas Dandy

Ma petite jument n’a que 3 ans, autrement dit, un enfant à l’échelle d’une vie de cheval. Elle s’appelle Holly. Elle sait à ce stade peu, voire rien. Nous l’avons installée dans le box d’à côté de Dandy et à présent, je monte Holly tandis que mon conjoint monte Dandy. Parce que le cheval est un être intelligent au sens sémantique du terme, c’est à dire apte à faire des liens dans sa compréhension, il n’a pas fallu longtemps à Dandy pour comprendre que Holly n’était pas considérée par mon conjoint et moi comme un cheval lambda. À l’identique, Holly a aussi compris que l’humain qui venait régulièrement la voir avait un relationnel spécial avec cet autre animal.

Récemment, Holly a été montée par un cavalier n’ayant pas le niveau nécessaire. Elle a pris peur et est partie dans un galop furieux, les yeux injectés de sang et la queue sur le dos. Elle a trouvé refuge pour se sécuriser non pas auprès des autres chevaux de l’écurie, mais auprès de Dandy, qui s’est montré, contrairement à son habitude, extrêmement calme voyant cette petite chose lui foncer dessus et se blottir violemment contre lui. Ensemble, ils ont formé ce qui ressemble fortement à un duo, autrement dit, déjà un groupeDandy se montre extrêmement agacé pour tout et pour rien, je crois l’avoir assez fait transparaître dans ces lignes.

J’apprends les bases à Holly, comme par exemple à marcher droit et calmement. Dandy, du haut de ses 12 ans, se montre toujours très explosif sur cet exercice de base, même en compagnie d’un autre cheval de l’écurie. Mais depuis quelques semaines, mon conjoint et moi lui faisons pratiquer cet exercice à côté de Holly. Et étonnement, Dandy semble se prêter au jeu sans que mon conjoint n’ait absolument rien à lui demander. Il marche calmement à côté de ce tout jeune cheval. Apprentissage ? Emulation ? Activité collective ? Instinct grégaire ? Hasard ? La science a sans doute beaucoup à redire sur le sujet, je n’ai pour ma part que mon expérience individuelle et parcellaire à partager. Et une conviction profonde : Dandy sait beaucoup de choses, et lorsqu’il s’agit de les exécuter il trouve une motivation inexplicable à les réaliser, sans que mon conjoint ait autant à batailler et sans que ma jument ne fasse quoi que ce soit. Sa simple présence génère chez Dandy une motivation à faire. Qui ne s’exprime pas avec un autre cheval de l’écurie. Et cette motivation apparaît comme venant de lui.

Je laisse à chacun le soin de se faire une conviction car la métaphore trouve ses limites ici.

Le cas de Hommes

Ce qui ne peut se prouver pour mon cheval, le peut davantage pour les individus. La motivation est aussi un rapport à l’autre. Qui n’a jamais vu un senior trouver la ressource pour faire une tâche jugée rébarbative simplement pour apprendre à un junior ? Qui n’a jamais trouvé le courage de terminer son travail fut-il ennuyeux sur le moment dans l’émulation de groupe qui vise un objectif commun ? Qui n’a jamais repoussé de limites professionnelles selon l’adage «  si lui le peut, pourquoi pas moi ? » Qui n’a jamais trouvé dans l’intelligence collective la motivation pour résoudre un problème ? La motivation est aussi un concept social. L’agilité pousse à la collaboration et elle est avisée de le faire car la motivation vise en réalité aussi bien le contenu que le processus : le contenu s’intéresse aux forces qui nous poussent intérieurement à agir tandis que le processus se focalise davantage sur l’interaction entre ces forces et l’environnement extérieur provoquant un comportement – autrement dit, grandement les autres.

En conclusion

Pour résumer tout ceci ? Rien n’est bien ou mal, blanc ou noir, théorie ou pratique : faisons confiance à notre bon sens, à notre écoute, notre observation et notre expérience, y compris hors cadre professionnel.

Récompenser n’est pas une erreur, cela peut et possède une utilité surtout en tant que leader. (Et avec Dandy, il a vite fallu lui faire comprendre qui de nous deux l’était. Malheureusement pour moi, c’est lui qui a pris le leadership ! Je ferai mieux avec Holly.)

Varier les activités, donner du sens, véhiculer une vision globale, permettre des buts atteignables, pousser à l’autonomie, donner de la reconnaissance sont de bonnes pratiques : elles ne sont simplement pas le gage d’une réussite absolue en matière de motivation. L’animal est complexe, en apparence souvent incohérent. Et cela vaut pour le cheval comme pour l’Homme. Est simplement incohérent ce que nous ne comprenons pas-encore.

Reste alors à garder en tête le pouvoir du groupe, du rapport à l’autre. Le collectif est une ressource bien plus précieuse qu’il n’y paraît car paradoxalement, c’est souvent au contact de l’autre qu’on découvre ce qui nous anime individuellement, même si nous n’en dirons rien. C’est  toute la difficulté et la richesse pour le coach qui accompagne.

Les livres que j’ai lus ne m’ont rien appris de plus que l’observation de mes chevaux depuis trente ans. Comme souvent, les réponses sont sous nos yeux et les clefs à portée de main : il faut parfois prendre simplement le temps de regarder.

Appliquez vos décisions d’architecture avec ArchUnit (2/2)

$
0
0

 

ArchUnit est une bibliothèque qui propose une fluent API pour tester l’architecture d’applications Java.

Nous avons vu dans un premier article les possibilités techniques offertes par ArchUnit. Ce second article a pour objectif d’aller plus loin en voyant en quoi ArchUnit peut vous aider dans la gestion de votre architecture, dans sa documentation, et en présentant les pièges à éviter lors de sa mise en oeuvre.

Glossaire (rappel)

Afin d’éviter les ambiguïtés dans la suite de l’article, il est primordial de bien comprendre la distinction entre ce que nous appellerons une « décision » et une « règle ».

Une décision désigne quelque chose qui affecte le code source et que l’équipe décide d’appliquer. Une décision peut tout à fait avoir une origine extérieure à l’équipe, à condition que celle-ci ait une autorité reconnue par l’équipe.

Une règle désigne le test correspondant à une décision, tel qu’implémenté avec ArchUnit. On peut considérer qu’il s’agit de la formalisation de la décision en tant que test.

La documentation

Pourquoi vous en avez besoin

La documentation des décisions d’architecture est un enjeu majeur : en son absence, des informations critiques sont perdues, ce qui conduit à de mauvais choix et à une perte de temps considérable.

Le problème est que la documentation est généralement coûteuse et difficile à maintenir. Le maintien à jour de la documentation s’avère particulièrement critique : si les lecteurs doivent systématiquement vérifier la véracité de l’information au niveau de l’implémentation parce qu’ils ne font pas confiance à la documentation, cela fait de cette dernière une source de coût totalement inutile.

Ce que nous apporte ArchUnit

Comme tout test automatisé, les règles définies dans ArchUnit peuvent être considérées comme de la documentation en tant que telles.

« Good code is its own best documentation »

— Steve McConnell

Afin d’améliorer la pertinence de cette source de documentation, ArchUnit propose quelques méthodes dans un but documentaire : la méthode as est utilisée pour définir un alias, c’est-à-dire un énoncé de la décision, tandis que la méthode because est prévue pour spécifier une justification à la règle.

@ArchTest
public static final ArchRule adapters_do_not_depend_on_one_another = slices()
       .matching("fr.xebia.archunit.(adapter).(*)..").namingSlices("$1 '$2'")
       .should().notDependOnEachOther()
       .as("Adapters do not depend on one another")
       .because("Adapters should only depend on one external system; depending on other adapters is likely to imply pulling dependencies towards other external systems");

En clarifiant l’intention derrière la règle, ces méthodes rendent la lecture du code source plus aisée. De plus, leur utilisation s’avère particulièrement utile quand une règle est enfreinte car les valeurs ainsi définies sont utilisées dans la composition du message d’erreur.

com.tngtech.archunit.lang.ArchRule$AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'Adapters do not depend on one another, because Adapters should only depend on one external system; depending on other adapters is likely to imply pulling dependencies towards other external systems' was violated (1 times):
adapter 'source2' calls adapter 'source1':
Method <fr.xebia.archunit.adapter.source2.InternalAccountAdapter.getAccounts()> calls method <fr.xebia.archunit.adapter.source1.ExternalAccountUtils.accountInEuros(java.lang.String, long)> in (InternalAccountAdapter.java:17)

Comment ArchUnit s’intègre dans tout cela

Il existe de nombreuses manières de documenter les décisions d’architecture. Plusieurs critères sont importants dans leur choix :

  • Les décisions doivent être facilement accessibles aux personnes concernées. C’est-à-dire avant tout l’équipe en charge de leur implémentation.
  • Comme déjà évoqué plus haut, le maintien à jour de la documentation est une problématique critique. C’est pourquoi il doit être possible de mettre à jour les informations simplement.
  • Un historique des décisions doit être accessible. Le moment auquel une décision a été prise, les différents changements qu’elle a pu subir, etc. sont autant d’éléments essentiels à une bonne compréhension de la décision.

Par exemple, les Architecture Decision Records (ADR) proposent de traiter ces points en se concentrant sur quelques éléments essentiels de la décision qui seront stockés dans un système de versioning tel que Git, c’est-à-dire aussi proche que possible de la mise en œuvre de la décision. Pour plus d’informations au sujet des ADRs, vous pouvez vous référer à l’article Architecture et documentation : les ADRs.

À l’aune de ces contraintes, la matérialisation des décisions sous la forme de tests automatisés grâce à ArchUnit peut sembler constituer une alternative supérieure à toutes les autres solutions disponibles. Et cette impression ne peut être que renforcée par le fait que ces tests fournissent une boucle de feedback quant au statut d’application de chacune des règles, ce qui en fait une source d’information fiable ! Alors pourquoi ne pas tout jeter pour ne plus se reposer que sur ArchUnit ?

Tout d’abord, ce ne serait pas possible, car toutes les décisions d’architecture ne sont pas matérialisables en tant que règles dans ArchUnit ; comme on l’a déjà dit, ArchUnit ne cible que les décisions qui ont trait à la conception d’une application.

Par ailleurs, les ADRs ont pour caractéristique de contenir les éléments essentiels à une bonne compréhension de la décision : la décision elle-même, le contexte dans lequel elle a été prise, les alternatives qui ont éventuellement été considérées, les conséquences qui ont été anticipées… Certes, ArchUnit fournit la méthode because comme un champ libre, mais il est difficile de l’envisager comme une solution parfaitement adaptée pour la documentation systématique de chacun des éléments essentiels cités plus tôt.

C’est pourquoi l’utilisation d’ArchUnit doit être réalisée en complément d’un mode de documentation plus exhaustif, tel que les ADRs :

  • Pour les décisions qui ne peuvent pas être matérialisées en tant que règles ArchUnit, un ADR est disponible.
  • Pour les décisions de conception trop évidentes pour nécessiter une explication (ce qui ne devrait quasiment jamais arriver si vous parvenez à vous concentrer sur les décisions significatives uniquement), une règle ArchUnit suffit.
  • Pour les décisions de conception qui nécessitent une vraie documentation, un ADR et une règle ArchUnit sont tous deux pertinents. Il est alors nécessaire de définir une manière de gérer les liens de l’un à l’autre afin de faciliter l’accès à la documentation quand celui-ci s’avère nécessaire. On pourra par exemple noter la référence (immuable) de l’ADR dans la méthode because de la règle ArchUnit.

Les pièges à éviter

Il existe un certain nombre de pièges à éviter lors de la mise en place d’ArchUnit.

Pour commencer, ArchUnit n’est pas une solution à vos problèmes d’architecture, et ne doit surtout pas être considéré comme tel. En vérifiant l’application des décisions, ArchUnit peut aider à assainir l’architecture de votre application, mais il n’a qu’un rôle subordonné à la prise de ces décisions. En effet, pas de miracle : de mauvaises décisions, même bien appliquées, n’amèneront pas à une bonne architecture.

D’autre part, quand on commence à mettre en place un outil, la tentation de s’en servir à la moindre opportunité est grande. C’est particulièrement le cas avec ArchUnit en raison de sa grande versatilité. Malgré tout, il est important de ne pas s’éparpiller dans des règles très locales ou anecdotiques, mais de bien se concentrer sur les décisions importantes. Au-delà du fait que ces décisions sont généralement celles pour lesquelles l’implémentation de règles est la plus simple (ce qui nous apporte le meilleur retour sur investissement), le fait de restreindre le nombre de règles permet de s’y retrouver facilement et de limiter la maintenance nécessaire. Il est préférable d’attendre que l’équipe acquière un peu de recul sur les possibilités de l’outil avant de décider si on souhaite multiplier les cas d’usage ou introduire des cas de plus en plus spécifiques.

Enfin, il est préférable de se concentrer sur ce qu’ArchUnit sait bien faire. La frontière entre ArchUnit et les autres outils doit être définie au plus tôt (quitte à être remise en cause par la suite), afin d’éviter d’avoir à maintenir un grand nombre de règles redondantes, ce qui crée de la confusion et fait perdre beaucoup de temps. Par exemple, on préférera des outils dédiés, tels que Checkstyle ou Findbugs, pour gérer les conventions de nommage ou les règles de code. En effet, ceux-ci s’intègrent parfaitement dans votre IDE ou dans un SonarQube, et ne nécessitent qu’une configuration triviale, tandis qu’ArchUnit nécessite l’implémentation et la maintenance de tests qui, aussi simples soient-ils, reste plus complexe et répétitif.

La propriété des décisions

Dans les structures les plus traditionnelles, dans lesquelles le cycle en V est roi, ce sont bien souvent des comités d’architectes qui sont à la source des décisions d’architecture. Dans un tel paradigme, un outil qui permet de vérifier que les décisions sont bien appliquées peut facilement être perçu comme un moyen de contraindre les équipes de développement.

Cependant, ceci ne devrait pas être le cas pour ArchUnit. En effet, il s’agit d’un outil qui se base sur l’implémentation de tests par les équipes de développement. Pour créer ou modifier une règle, comme pour tout autre test, il est nécessaire de faire partie de l’équipe de développement. Certes, les personnes extérieures à l’équipe peuvent avoir accès aux rapports de test, mais les tests ArchUnit y apparaissent comme tous les autres tests. Ceci implique que les décisions qui proviennent de l’extérieur doivent être acceptées par l’équipe pour être implémentées en tant que règles. À l’inverse, il est tout à fait possible que des décisions soient prises au sein de l’équipe indépendamment des personnes qui en sont théoriquement en charge.

D’un point de vue plus général, considérer que la responsabilité des décisions à ce niveau d’architecture (une nouvelle fois, on parle ici de conception d’applications, non de décisions au niveau d’une entreprise) incombe à l’équipe de développement elle-même, et non à des personnes extérieures, est tout à fait raisonnable. En effet, ces personnes extérieures auront tendance à imposer des décisions dans un objectif de cohérence globale qui n’est pas forcément nécessaire, au lieu de prendre en compte les spécificités du contexte qui auraient permis de prendre la décision la plus appropriée.

Dans la mesure où ce niveau de décision est directement lié au code, il est possible de considérer ceci tout simplement comme une extension au principe de propriété collective du code.

Les architectures évolutives

Dans leur livre de 2017, Building Evolutionary Architectures, Neal Ford, Rebecca Parsons et Patrick Kua présentent la notion d’architecture évolutive, qui vise à gérer le besoin de s’adapter aux changements continuels de votre système en faisant de l’évolutibilité un citoyen de premier ordre des projets logiciels. Les architectures évolutives se basent sur 3 concepts :

  • C’est devenu quelque chose d’acquis pour beaucoup : les changements dans votre application doivent être introduits de manière incrémentale. Les changements liés à l’architecture ne doivent pas constituer une exception à cette règle.
  • Vous devez être en mesure d’évaluer de manière objective la proximité d’une solution par rapport aux objectifs définis. Ainsi, vous pouvez confirmer que les évolutions aboutissent bien à l’amélioration du système, et qu’ils n’introduisent pas de problèmes inattendus. Les fonctions de fitness (qu’on pourrait également appeler les fonctions de qualité de l’architecture) se définissent comme tout ce qui rend cette évaluation possible. On pensera par exemple aux métriques ou aux tests.
  • Les architectures ont des dimensions multiples : les exigences fonctionnelles, mais également les questions techniques, la sécurité, l’extensibilité, etc. Chaque architecte logiciel a naturellement tendance à se concentrer sur un certain nombre de ces dimensions et à délaisser les autres. C’est pourquoi il est crucial de leur permettre de vérifier l’impact de leurs modifications sur l’ensemble des dimensions de l’architecture.

On peut considérer que le rôle des fonctions de fitness est de protéger les caractéristiques de votre architecture au fur et à mesure de son évolution. Les règles ArchUnit, à condition d’être vérifiées en continu dans des pipelines de CI, peuvent être considérées comme des fonctions de fitness.

Si vous voulez que votre application puisse évoluer facilement, il est primordial de comprendre les règles qu’elle est censée suivre afin d’être en mesure de vérifier qu’elle les suit. La mise à jour incrémentale des règles permet d’avancer pas à pas dans des évolutions de l’architecture. ArchUnit facilite cette approche en donnant la possibilité de créer de nouvelles règles et de supprimer les anciennes de manière extrêmement simple, en cohérence avec l’idée que l’architecture doit évoluer continuellement.

Conclusion

Comme on l’a vu dans l‘article précédent, ArchUnit n’est pas encore complètement mature. Malgré tout, la version 1.0.0 qui apportera les fonctionnalités primordiales qui sont encore manquantes n’est plus très loin, et à partir du moment où vous définissez une structure à votre code, il s’agit d’un outil qui peut vous apporter une grande valeur ajoutée pour un effort relativement faible.

Alors, le mieux, c’est encore de l’essayer ! Pour cela, n’oubliez pas :

  • Concentrez-vous sur quelques décisions importantes, au moins jusqu’à ce que vous ayez suffisamment de recul pour aller plus loin.
  • Pour que ça fonctionne, il faut que l’équipe soit en accord avec les décisions qui sont à vérifier.
  • Utilisez ArchUnit en complément, et non en remplacement, d’un autre mode de documentation.

Les frameworks et librairies Java under the hood

$
0
0

En tant que développeurs Java, nous utilisons de nombreux frameworks et librairies. Parmi les plus populaires, nous retrouvons Spring, Lombok, ainsi que beaucoup d’outils de test tels que JUnit ou Mockito.

Leur utilisation est simplifiée par le biais d’annotations et de fluent API. Cela les rend moins intrusifs dans le code et surtout nous fait économiser quelques lignes de développement. Mais ce qui est le plus marquant, ce sont les comportements qu’ils ajoutent à nos objets.

Afin de mieux comprendre leur fonctionnement, je vous propose d’en étudier les mécanismes sous-jacents en répondant aux questions suivantes :

  • Quels sont les mécanismes proposés par Java ?
  • Comment générer du code au runtime ?
  • Comment générer du code en précompilation ?

L’introspection

Un des principes de la programmation orientée objet est l’encapsulation. Cela signifie que les attributs d’un objet font partie de sa représentation interne et sont donc cachés. Les frameworks ont parfois besoin d’explorer la structure interne de nos classes et donc de casser ce principe d’encapsulation. Java permet de faire de l’introspection, c’est-à-dire de s’introduire à l’intérieur de nos classes pour y consulter, appeler ou initialiser les méthodes et les attributs.

Parmi les évolutions majeures de la version 1.5 de Java, on retrouve les annotations. Elles permettent d’ajouter des méta-données aux classes, méthodes, attributs, paramètres et variables locales. La consultation des annotations se fait aussi par introspection.

Prenons l’exemple d’une classe ordinaire, avec un attribut annoté et non initialisé, et d’une méthode affichant cet attribut sur la console lorsqu’elle est appelée :

public class MyClass { 
    @SetValue("myValue")
    private String myField; 

    public void displayMyField() {
        System.out.printf(myField);
    }
} 

Dans le code suivant, l’instance de la classe est créée par introspection. Il en est de même pour initialiser l’attribut myField avec son annotation et pour l’exécution de la méthode displayMyField.

import java.lang.reflect.Field;
import java.lang.reflect.Method;

// Créer une instance de classe
Class<MyClass> clazz = MyClass.class;
MyClass myClass = clazz.newInstance();

// Récupère l'attribut myField et l'instancie avec la valeur fournie dans l'annotation @SetValue
Field myField = clazz.getDeclaredField("myField");
SetValue setValue = myField.getDeclaredAnnotation(SetValue.class);
myField.setAccessible(true);
myField.set(myClass, setValue.value());
myField.setAccessible(false);

// Récupère la méthode displayMyField et l'exécute
Method myMethod = clazz.getDeclaredMethod("displayMyField");
myMethod.invoke(myClass);

Le résultat obtenu suite à l’exécution de ce code est :

myValue

C’est la classe java.lang.Class de Java qui permet de faire de l’introspection. C’est une classe générique ayant pour paramètre de type la classe qui est introspectée. Pour la récupérer il existe 3 méthodes :

  • Via une instance de la classe : Tous les objets Java héritent de la classe Object et donc de ses méthodes. La méthode getClass() permet de récupérer une instance de Class sous la forme Class<? extends T>.
  • Via le nom de la classe : Class.forName() permet de récupérer une classe à partir de son nom. En revanche, cette méthode retournera une instance de Class avec un type anonyme : Class<?>. Si la classe demandée n’existe pas, une ClassNotFoundException est lancée.
  • Via la classe elle même : Il est possible de prendre n’importe quelle classe et de lui ajouter l’extension .class. Ceci aura pour but de créer une instance de Class, ayant pour type la classe à introspecter.

Une fois l’instance de Class récupérée, il est possible d’accéder par introspection à toutes les données de la classe, comme les méthodes, les attributs, les constructeurs et les annotations.

L’introspection est très utilisée dans les frameworks et les librairies. Lorsque nous utilisons un framework tel que Spring pour faire de l’IoC (Inversion of Control), l’injection de dépendances se fait via cette méthode. La lecture des annotations conservées au runtime l’utilise également. Autre exemple, JUnit utilise également l’introspection pour lancer les méthodes de test.

L’introspection permet de contourner la notion d’encapsulation des objets. En revanche, cette souplesse à un coût. En effet, accéder aux éléments d’un objet ralentit fortement l’exécution du code.

Le Proxy dynamique

Pour un framework ou une librairie, changer les valeurs des attributs, collecter les annotations, invoquer des méthodes ou instancier des classes par introspection est une première étape, mais ce n’est pas suffisant. Ils ont également besoin d’ajouter du comportement de manière dynamique, c’est-à-dire pendant l’exécution du code.

Le pattern proxy est une manière de répondre à cette problématique. Son but est de représenter les fonctionnalités d’une autre classe, en se positionnant entre la classe appelante et la classe appelée. Pour mettre en œuvre ce mécanisme, la classe proxy doit implémenter la même interface que la classe appelée, et garder une dépendance vers celle-ci. Ainsi, lorsque la classe appelante exécute une méthode de la classe appelée, c’est la classe proxy qui a le contrôle et choisit ou non d’acheminer l’appel vers la classe appelée.

Java possède une classe nommée Proxy, qui permet de créer des proxys dynamiques. Pour en créer une instance, il suffit de fournir un ClassLoader qui chargera le bytecode, une ou plusieurs interfaces qui définiront sa signature, et une classe de type InvocationHandler qui interceptera tous les appels de méthodes.

Pour commencer, prenons une interface simple avec une méthode :

public interface MyInterface {
  String doSomething(int i, String s);
}

Le code suivant permet de créer une instance de l’interface ci-dessus, sans pour autant avoir codé une classe qui l’implémente. Toutes les interactions avec cette instance sont capturées par un InvocationHandler, créé sous forme de lambda.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.function.Function;

import static java.util.Arrays.stream;
import static java.util.stream.Collectors.joining;

//Récupère le class loader (les interfaces et la classe proxy doivent
//appartenir au même class loader)
ClassLoader classLoader = MyClass.class.getClassLoader();

//La liste des interfaces qui seront implémentées
//Ici seule l'interface MyInterface sera utilisée
Class[] interfaces = {MyInterface.class};

//Variable Fonction pour le parsing des paramètres
//Voir handler ci-dessous
Function<Object[], String> format_parameters = (parameters)
      -> stream(parameters)
          .map(o -> "(" + o.getClass().getName() + " = " + o + ")" )
          .collect(joining(", "));

//Invoke Handler : Intercepte tous les appels de méthodes
//Retournera une chaine de caractères avec le nom de la méthode
//et la liste des paramètres avec leur type et leur valeur
InvocationHandler handler = (proxyObject, method, parameters)
      -> "{ method name : " + method.getName()
          + ", parameter [" + parameters.length + "] : "
          +  " { " + format_parameters.apply(parameters) + "}";

//Création de l'instance qui implémente MyInterface
MyInterface myInterfaceProxyfied = (MyInterface) Proxy.newProxyInstance(classLoader, interfaces, handler);

//Appel de la méthode doSomething
System.out.println("Result : " + myInterfaceProxyfied.doSomething(20, "hello"));

Le résultat obtenu suite à l’exécution de ce code est :

Result : { method name : doSomething, parameter [2] :  { (java.lang.Integer = 20), (java.lang.String = hello)}

Penchons-nous un peu plus sur l’implémentation du InvocationHandler :

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MyInterfaceHandler implements InvocationHandler {
  private MyInterface myInterface;

  public MyInterfaceHandler(MyInterface myInterface) {
      this.myInterface = myInterface;
  }

  @Override
  public Object invoke(Object o, Method method, Object[] objects) throws Exception {
      System.out.println("Proxy class Before");
      Object toReturn = method.invoke(myInterface, objects);
      System.out.println("Proxy class after");
      return toReturn;
  }
}

InvocationHandler possède juste une méthode : invoke. Cette dernière possède 3 arguments :

  • Object : qui fournit l’instance de l’objet proxy.
  • Method : La méthode qui a été invoquée.
  • Object[] : La liste des arguments qui a été passée à la méthode appelée.

Toutes les méthodes appelées sur une instance proxy passeront par cette méthode. Dans cet exemple, nous avons proxyfié une autre instance de MyInterface. Nous pouvons voir qu’il est assez facile de rajouter du comportement autour de cette classe, appeler la méthode d’origine de celle-ci ou non. Le proxy dynamique a notamment été utilisé pour mettre au point les EJB. Mais cette méthode a depuis été remplacée par des techniques plus modernes, car l’obligation d’utiliser une interface reste très contraignante.

CGLIB

Le problème du Proxy dynamique présenté ci-dessus est que nous avons besoin de définir une interface. Les frameworks ont donc besoin d’une autre méthode pour pouvoir ajouter du comportement aux objets : l’héritage dynamique.

Le principe de substitution de Liskov nous met en garde sur l’héritage, en définissant la propriété suivante : si B est un sous-type de A, alors tout objet de A peut être remplacé par un objet de type B, sans altérer les propriétés du système. Pour plus d’informations, voir l’article sur les principes SOLID.

L’héritage dynamique consiste en la création d’une sous-classe au runtime. Il a pour but de surcharger une ou plusieurs méthodes, afin d’en modifier le comportement, tout en respectant le principe de Liskov décrit ci-dessus.

CGLIB est une librairie qui permet de créer de manière dynamique du bytecode en faisant de l’héritage dynamique. Ainsi, il substitue les classes par des sous-classes, ce qui donne l’impression que le comportement a changé.

Prenons par exemple une classe avec deux méthodes qui contiennent chacune un paramètre :

public class MyClass {
  public String sayHello(String name) {
      return "Hello" + name;
  }

  public int incrementNumber(int i) {
      return i++;
  }
}

Le code suivant va permettre de créer une classe dynamiquement et d’en changer le comportement.

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;

//Classe qui permet de créer des sous-classes d'une autre classe
Enhancer enhancer = new Enhancer();

//Classe à étendre
enhancer.setSuperclass(MyClass.class);

//L'objet MethodInterceptor permet d'intercepter tous les appels de méthodes
//de l'objet créé.
//Attention même les méthodes de la classe object seront interceptées
MethodInterceptor methodInterceptor = (obj, method, args, proxy) -> {
  if ("sayHello".equals(method.getName())) {
      return "Good bye " + args[0];
  }

  if ("incrementNumber".equals(method.getName())) {
      return ((Integer) args[0]) + 2;
  }

  return proxy.invokeSuper(obj, args);
};

//Création de l'instance, avec le comportement voulu
enhancer.setCallback(methodInterceptor);
MyClass myClass = (MyClass) enhancer.create();

System.out.println(myClass.sayHello("John"));
System.out.println(myClass.incrementNumber(5));

Le résultat obtenu suite à l’exécution de ce code est :

Good byeJohn
7

La classe principale de CGLIB est la classe Enhancer. Nous lui fournissons, via la méthode setSuperClass(), la classe dont nous souhaitons hériter. Ensuite, via la méthode setCallback(), nous lui fournissons une instance de la classe MethodInterceptor. Comme son nom l’indique, elle interceptera tous les appels de méthodes, même les méthodes étant héritées de la classe Object. Enfin, la méthode create() permet de créer la classe avec les nouveaux comportements.

Spring utilise CGLIB pour instancier ses beans. Cela permet de faire par exemple de l’AOP (Aspect-Oriented Programming). Mais il existe également d’autres alternatives à CGLIB.

Bytebuddy

Bytebuddy est une alternative à CGLIB. Il se présente sous la forme d’une fluent API. Mockito est basé dessus depuis la version 2.1. L’un des gros avantages de cette librairie est qu’il n’est plus nécessaire de passer par une classe qui implémente InvokeHandler.

Réécrivons l’exemple de CGLIB, cette fois-ci avec Bytebuddy :

import net.bytebuddy.ByteBuddy;

import static net.bytebuddy.implementation.FixedValue.value;
import static net.bytebuddy.implementation.InvocationHandlerAdapter.of;
import static net.bytebuddy.matcher.ElementMatchers.named;

MyClass myClass = new ByteBuddy()
      .subclass(MyClass.class)
      .method(named("sayHello"))
          .intercept(value("Good bye John"))
      .method(named("incrementNumber"))
          .intercept(of((o, m, p) -> (int)p[0] + 2))
      .make()
      .load(getClass()
      .getClassLoader()).getLoaded()
      .newInstance(); 

System.out.println(myClass.sayHello("John"));
System.out.println(myClass.incrementNumber(2));

Il existe encore d’autres alternatives qui ne seront pas présentées dans cet article. Nous pouvons par exemple nommer javassist, BCEL ou encore ASM. Ces implémentations sont plus bas-niveau et permettent de manipuler directement le bytecode.

APT

Les Annotation Processing Tools ont été apportés avec Java 6. Cette fonctionnalité permet, via des annotations, d’exécuter du code à la compilation. Le but est de générer de nouveaux fichiers, qu’il s’agisse de code, de documentation, de configuration ou de tout autre type de fichier.

Cette méthode a un avantage par rapport aux méthodes présentées précédemment : rien n’est exécuté au runtime. En revanche, elle est plus compliquée à implémenter et ne permet pas de modifier des fichiers existants.

Prenons une classe simple, avec un attribut et une méthode. Cette classe contient une annotation que nous avons créée :

@MyAptAnnotation
public class AptClassTest {
  @MyAptAnnotation
  private String var;

  @MyAptAnnotation
  public void printVar() {}
}

La classe suivante sera exécutée lors de la compilation de la classe AptClassTest et va l’introspecter. Il aurait été tout à fait possible de créer d’autres classes à partir de cette introspection, qui ensuite auraient été compilées par Javac.

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;

import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;

import java.util.Set;
import java.util.stream.Collectors;

@SupportedAnnotationTypes({"org.myapt.MyAptAnnotation"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyAptImpl extends AbstractProcessor {
  @Override
  public boolean process(Set<? extends TypeElement> typeElements,
  RoundEnvironment roundEnvironment) {
  typeElements.stream()
     .flatMap(e -> roundEnvironment.getElementsAnnotatedWith(e).stream())
     .forEach(
        e -> {
           processingEnv.getMessager().printMessage(NOTE, "Element found :");
           processingEnv.getMessager().printMessage(
              NOTE, " - name : " + e.getSimpleName()
           );
           processingEnv.getMessager().printMessage(
              NOTE, " - type : " + e.getKind()
           );
           processingEnv.getMessager().printMessage(
              NOTE, " - enclosed elements : " +
                 e.getEnclosedElements().stream()
                    .map(e2 -> e2.getSimpleName())
                    .collect(Collectors.joining(", "))
           );
        }
     );
     return true;
  }
}

Nous les compilons manuellement de la manière suivante :

$ javac org/myapt/MyAptAnnotation.java
$ javac org/myapt/MyAptImpl.java
$ javac -processor org/myapt/MyAptImpl org/myapt/AptClassTest.java

Nous obtenons la sortie suivante :

Note: Element found :
Note:  - name : AptClassTest
Note:  - type : CLASS
Note:  - enclosed elements : <init>, var, printVar
Note: Element found :
Note:  - name : var
Note:  - type : FIELD
Note:  - enclosed elements :
Note: Element found :
Note:  - name : printVar
Note:  - type : METHOD
Note:  - enclosed elements :

Pour que les APT soient pris en compte dans un projet, il faut une classe qui implémente l’interface Processor (ou étend la classe AbstractProcessor) et qu’elle soit déjà compilée. C’est la raison pour laquelle, la plupart du temps, il y a 2 projets : le premier qui contient les classes implémentant l’interface Processor et le second sur lequel les APT sont appliqués.

Ensuite, il faut indiquer au compilateur que l’on souhaite utiliser les APT. Il y a 2 manières de le faire :

  • Avec le compilateur Javac, via le paramètre -processor suivi de la liste des classes implémentant les APT, séparés par des virgules.
  • En créant un fichier META-INF/services/javax.annotation.processing dans le JAR de la librairie, en indiquant sur chaque ligne les classes implémentant les APT.

Exemple de fichier META-INF/services/javax.annotation.processing, déclarant deux classes :

org.xebia.maclasse1
org.xebia.maclasse2

L’avantage de la seconde solution est que la librairie contient les informations. Il n’y a donc plus besoin d’appliquer une configuration spécifique lors de la compilation.

L’une des librairies qui utilise les APT est Lombok. Mais contrairement à ce qui est dit plus haut, Lombok modifie le code. En réalité, il y arrive en modifiant les AST (Abstract Syntax Tree) générés par les compilateurs.

En effet, lorsque des sources sont compilées avec les APT, il y a 3 phases de compilation :

  1. Parsing : Cette étape permet de transformer le code Java des fichiers en AST.
  2. Annotation Processing : Permet de récupérer toutes les annotations qui viennent d’être « parsées » dans l’étape 1, puis d’exécuter les APT correspondants. Si celles-ci ont créé de nouveaux fichiers, l’étape 1 est exécutée à nouveau.
  3. Bytecode generation : C’est la phase de compilation finale qui va générer le code binaire pour la JVM.

En théorie, les APT ne sont pas censés pouvoir modifier l’arbre syntaxique. Mais pour ajouter sa fonctionnalité, Lombok le fait en utilisant des API cachées de Javac. Le revers de la médaille est que ce hack rend Lombok très sensible aux évolutions des JDK.

Conclusion

Le but de cet article a été de démystifier ce que font des frameworks et librairies que nous utilisons bien souvent comme des boîtes noires. Nous avons étudié les différentes méthodes qu’ils utilisent pour simplifier nos développements logiciels. Ce sont bien ces méthodes qui les rendent flexibles et moins intrusives, ce qui nous évitent de forcer nos classes à faire de l’héritage par exemple.

Néanmoins, il est important de noter que lorsque l’on rentre dans ces configurations là, nous sortons du paradigme Objet de Java. Et mal utilisé, vous pourriez tomber dans des anti-patterns et dégrader les performances de votre application.

 


Flogger : le logger fluent de Google pour Java

$
0
0

Qui pensait que le logging en Java pouvait encore évoluer ? C’est pourtant ce qu’a réussi à faire Google avec Flogger : leur framework de logging pour Java.

Après les classiques SLF4J ou Apache Log4j, découvrons ce qu’apporte cette API de logging.

Présentation

Flogger est une API de logging fluent pour Java qui se veut auto-documentée, performante et extensible.

Le logging avec Flogger ressemble à ça :

private static final FluentLogger LOGGER = FluentLogger.forEnclosingClass();

public void updateUser(User user) {
    try {
        ...
    } catch (Exception e) {
        LOGGER.atSevere().withCause(e).log("Error while updating user %s", user.getEmail());
    }
}

Pour ajouter la bibliothèque à vos dépendances, vous aurez besoin de com.google.flogger:flogger: et com.google.flogger:flogger-system-backend:.

Avantages

Lisibilité

Si l’on considère le logger fourni avec le JDK, JUL (pour java.util.logging, et non pas l’artiste Julien Mari), celui-ci n’expose que quelques méthodes, dont log, qui est surchargée plusieurs fois avec des paramètres différents. Cela rajoute une confusion sur ce qui doit être passé en paramètre :

LOGGER.log(Level.INFO, "Hello world {0}", name);

SLF4J quant à lui propose différentes méthodes pour les différents niveaux de log : debug, info, error, … Mais l’ambiguïté reste tout de même présente :

try {
    ...
} catch (Exception e) {
    LOGGER.error("Error", e); // Pas besoin du placeholder {}, la signature de la méthode est : error(String msg, Throwable t)
    LOGGER.error("Error - code={} - {}", param, e); // Le placeholder {} est nécessaire pour logger la stack trace : error(String format, Object arg1, Object arg2)
}

Et voici enfin la syntaxe proposée par Flogger :

LOGGER.atInfo().withCause(exception).log("Log message with: %s", argument);

Le formatage des messages se fait grâce aux spécificateurs de Java : %s , %d , %t , etc.

Flogger gère aussi le logging lorsqu’un événement arrive toutes les n fois, évitant ainsi d’entourer son log dans un if :

// Sans Flogger :
private static final AtomicInteger LOG_COUNTER = new AtomicInteger();
...
if ((LOG_COUNTER.incrementAndGet() % 100) == 0) {
  LOGGER.info("My log message {}", arg);
}

// Avec Flogger :
FLOGGER.atInfo().every(100).log("My log message %s", arg);
FLOGGER.atSevere().atMostEvery(30, SECONDS).log("My log message %s", arg);

Rapidité

Logger peut-être coûteux en performance, c’est pour cela qu’il faut faire attention aux paramètres passés aux méthodes de logging :

LOGGER.debug("Doing someting with {}", myObject); // Recommandé : toString() sera appelée si les logs en debug doivent être affichés
LOGGER.debug("Doing someting with {}", myObject.toString()); // A éviter
LOGGER.debug("Doing someting with " + myObject.toString()); // A proscrire !

Prenons ensuite la méthode Logger#info(String, Object...) de SLF4J. Elle semble être efficace, mais lorsqu’elle est appelée, Java va automatiquement instancier un nouvel Object[], et les types primitifs seront auto-boxés, et cela coûte cher en bytecode généré à la compilation. C’est pourquoi les mainteneurs de Guice et Guava ont fait le choix d’implémenter plus d’une cinquantaine de méthodes log différentes, comme par exemple :

void log(String msg, @Nullable Object p1);
void log(String msg, long p1);
void log(String msg, int p1, boolean p2);
void log(String msg, boolean p1, byte p2);
void log(
      String msg,
      @Nullable Object p1,
      @Nullable Object p2,
      @Nullable Object p3,
      @Nullable Object p4);
void log(
      String msg,
      @Nullable Object p1,
      @Nullable Object p2,
      @Nullable Object p3,
      @Nullable Object p4,
      @Nullable Object p5,
      @Nullable Object p6,
      @Nullable Object p7,
      @Nullable Object p8,
      @Nullable Object p9,
      @Nullable Object p10,
      Object... rest);
...

Le second avantage de Flogger est de proposer la prise en compte des lambdas en paramètres des logs, qui ne seront évalués que si le log doit être affiché :

LOGGER.atInfo().log("stats=%s", LazyArgs.lazy(() -> createSummaryOf(stats)));

Extensibilité

Le dernier avantage que Flogger se vante d’offrir est sa flexibilité.

Si l’on souhaite rajouter des fonctionnalités au logger pour un besoin métier particulier, comme par exemple rajouter des informations pour un utilisateur, c’est possible en implémentant une classe UserLogger héritant de l’API Flogger et en l’utilisant ainsi :

LOGGER.at(INFO).forUserId(id).log("Message: %s", param);

Malheureusement, la documentation de Flogger étant inexistante sur ce sujet, je n’ai pas pu expérimenter cette solution.

Conclusion

Flogger existe depuis un peu plus d’un an aujourd’hui et n’est qu’en version 0.4 (depuis mars 2019). Le site de l’API n’est pas encore complet, il y manque notamment de la documentation sur son utilisation, sur les différents systèmes de backend compatibles avec Flogger, ainsi que sur la customisation du logger.

D’après Google, la bibliothèque est déjà utilisée par la majorité de leurs projets en Java et leur a permis de corriger des milliers de bugs.

D’après moi, Flogger est une API à surveiller du coin de l’œil, de par son originalité.

Kafka-Streams : une voie vers l’autoscaling avec Kubernetes

$
0
0

Kafka-Streams est la bibliothèque de stream processing associée à la streaming platform Apache Kafka. Kafka vient donc en plusieurs parties avec notamment : une partie persistance (core) qui donne la faculté de publier et consommer des messages, puis une partie traitement (streams) qui rend possible le traitement à la volée de ces messages. Kafka-Streams a pour point fort d’être une simple bibliothèque, une dépendance Java / Scala qui peut être incluse dans des projets sans perturber les processus d’ingénierie logicielle en place. Il est donc possible de livrer ces projets sans changer nos habitudes et profiter du pouvoir du traitement temps réel.

Cet article, lui, prend un autre point de vue et montre comment quelques pratiques simples (conteneurisation, orchestration) nous permettent d’exploiter un peu plus les capacités de nos applications Kafka-Streams. Parmi les promesses du framework, il y a celle d’être hautement scalable. Avec les outils qui se dégagent de la tendance cloud native nous allons même essayer d’aller plus loin, d’être auto-scalables. À la fin de cet article vous aurez une idée de comment assurer le passage à l’échelle de vos applications Kafka-Streams uniquement à partir d’outils existants.

tl;dr

Cet article a été écrit à la suite de la première représentation du talk « Scale in / Scale out with Kafka-Streams and Kubernetes«  donné en novembre 2018 à la XebiCon. Il traite d’une expérience dont le code se trouve dans le dépôt GitHub xke-kingof-scaling. Ce repo contient les manifestes YAML utilisés en exemple dans cet article.

Record-lag, le coeur du problème

Pourquoi, et dans quels cas, voudrions-nous avoir un comportement élastique des applications Kafka-Streams en particulier ? Dans cette première partie, on s’intéresse à ces applications de stream processing et à leur lien avec Apache Kafka pour répondre à cette question.

Consommation des messages

Les clients Kafka, producteurs et consommateurs, sont définis au sein même du projet Apache Kafka et établissent certaines règles utilisées pour traiter les évènements du bus de messages. Parmi ces idées il y a le polling des consommateurs. Une fois qu’un consommateur a souscrit à un topic, il peut alors tirer un certain nombre de messages (limité par les configurations fetch.max.bytes et max.poll.records), les traiter et committer leurs offsets pour les indiquer comme traités. Voilà pour les clients bas niveau. Pour Kafka-Streams, les débits de Kafka sont si importants et les polls sont réalisés à des intervalles si rapprochés que l’on atteint cette notion de traitement en temps réel.

Alors quel va être le problème ici ?

Pour plusieurs raisons, une application peut prendre du retard. L’écart entre l’offset du message en cours de traitement et le dernier posé dans la file s’agrandit. On dit alors que le groupe de consommation présente un lag important. Cette notion est capitale pour une application temps réel et a des conséquences importantes :

  • Elle ne valide plus le besoin, puisqu’elle réagit trop tard aux événements ;
  • Sous cette pression des messages non traités, cette application peut être sujette à des crashes

Quelles peuvent être les raisons de ces retards ?

Ce problème a quelques causes bien connues:

  • Une reprise sur erreur. Les différents producteurs de données ont alors le temps de construire ce lag.
  • Une transformation qui implique la construction d’un état interne toujours plus grand (corrélation ou enrichissement sur de longues périodes).
  • Où tout simplement à cause un pic de fréquentation d’un service.
  • etc …

La maxime « diviser pour mieux régner » est largement utilisée en Big/Fast Data et, là encore, c’est en jouant sur le parallélisme que Kafka-Streams va nous permettre de résoudre ce problème.

Le consumer protocol

Lorsqu’une seule instance de notre application de streaming tourne, elle se voit attribuer l’ensemble des partitions qui composent les input topics.

Rappel : les messages sont attribués aux partitions en fonction de leur clé. Un hash est déterminé en fonction de la clé et du nombre de partitions. Les messages de même hash sont alors attribués à la même partition.

Si une deuxième instance est lancée avec le même identifiant d’application, elle déclenche alors un partition rebalance qui redistribue les partitions aux deux instances. Cela allège déjà notre première instance et accélère le rattrapage de son retard. Enfin, le parallélisme est limité par le nombre de partitions. Soit N le nombre de partitions d’un topic d’entrée. Lorsque N instances de notre application sont lancées, elles se chargent chacune d’une partition (dans le cas idéal). Une application supplémentaire (n° N+1) serait donc au repos. L’ensemble de nos instances forment un groupe de consommation et le retard de ce groupe sur un topic est la somme du retard de chaque instance.

Pour réaliser une expérience capable de mettre en avant l’accumulation du retard sur nos applications, on proposera un cas d’usage fictif.

Figure 1 : groupe de consommation Kafka-Streams

Kubernetes et le support des custom metrics

Kubernetes est un orchestrateur de conteneurs. Le sujet est vaste, mais dans le cadre de notre problématique, nous pouvons nous servir de la description suivante : il s’agit d’un cluster manager composé de nodes et d’un master. Le master a la connaissance des ressources sur le cluster, tels que les CPU, la mémoire, les disques des machines et le réseau qui les relie. Son seul objectif est de maintenir le nombre de déploiements demandés pour vos applications conteneurisées.

Avec cette description, Kubernetes semble tout indiqué pour résoudre notre problème. L’idée d’auto-scaling y est présente depuis longtemps et se base sur l’usage CPU ou la consommation mémoire. Mais nous avons affaire à une application spéciale, puisqu’il s’agit d’une application de streaming. C’est donc sur le retard décrit dans le chapitre précédent que nous aimerions baser le scaling de notre application.

Et justement, Kubernetes a peut-être quelque chose sous le coude pour nous…

Depuis sa version 1.6, Kubernetes permet de récolter et d’utiliser des custom metrics. Des métriques personnalisées, potentiellement porteuses d’informations métier, que vous voudriez exposer vous-même dans un objectif de monitoring. Mais plutôt que d’inventer des métriques, nous allons ici faire le lien avec les informations exposées par les clients Kafka via JMX.

Une proposition d’implémentation peut être la suivante :

Figure 2 : Repport des metrics JMX à Kubernetes via Stackdriver

Les métriques de notre application sont exposées au format Prometheus. Ces informations sont alors récupérées par le scraper prometheus-to-sd qui va émettre les informations techniques de lag dans Stackdriver. Le retard est alors disponible pour son affichage, le monitoring / l’alerting et, surtout, il est sauvegardé pour alimenter le Metric Server custom-metrics-stackdriver-adapter qui mettra à disposition ces métriques au master de notre cluster.

Exposition des métriques JMX au format Prometheus

Prometheus est un logiciel de monitoring / alerting open source et un des premiers projets membres de la CNCF. Bien connu dans le monde DevOps, il définit un format d’exposition de métriques qui est de plus en plus utilisé. Ce format sera incontournable par la suite et nous utiliserons donc le projet jmx-exporter pour afficher les Mbeans de cette manière.

Les paramètres suivants sont ajoutés à la JVM de notre application de streaming :

-Djava.rmi.server.hostname=127.0.0.1 
-Djava.rmi.server.port=7071
-javaagent:/&amp;amp;lt;&amp;amp;gt;/jmx_prometheus_&amp;amp;lt;version&amp;amp;gt;.jar=9001:/&amp;amp;lt;&amp;amp;gt;/config.yaml

Cela nous permet d’exposer les métriques JMX sur le port 7071 et leur conversion au format Prometheus sur le port 9001. Le fichier config.yaml décrit les métriques à exporter.

config.yaml
global:
rules:
pattern:"kafka.consumer&amp;amp;lt;type=consumer-fetch-manager-metrics,client-id=(.*),topic=GAME-FRAME-RS, partition=(.*)&amp;amp;gt;&amp;amp;lt;&amp;amp;gt;records-lag:(.*)"
   labels: { client: $1, partition: $2, topic: GAME-FRAME-RS, metric: records-lag }
   name: "consumer_lag_game_frame_rs"
   type: GAUGE

En d’autres termes, nous exportons ici parmi les métriques kafka.consumer de types consumer-fetch-manager-metrics l’information records-lag qui concerne le topic GAME-FRAME-RS, pour toutes les partitions et pour tous les client-id. On donne un nom à cette métrique et indique qu’il s’agit d’une GAUGE (obligatoire pour stackdriver). Nous répéterons l’opération pour tous les input topics. (Le fichier complet)

Construction d’une image Docker de notre streaming app

En développement, les métriques peuvent maintenant être exposées en HTTP.

Figure 3 : présentation des métriques JMX au format Prometheus

Mais le tout devra être packagé pour être également applicable dans une image docker, prête à être orchestrée par Kubernetes. L’outil de build choisi ici est Gradle, auquel on ajoute un plugin docker :

build.gradle
plugins { id 'com.palantir.docker' version '0.20.1' }
apply plugin: 'com.palantir.docker'
mainClassName = 'fr.xebia.ldi.stream.Main'
docker {
   tags version
   dockerfile file('docker/Dockerfile')
   name 'gcr.io/cloud-fighter-101/kos-streaming-app'
}

En très peu de configuration, nous sommes capables de déclarer les éléments suivants :

  • Le DockerFile à builder ;
  • Le point d’entrée de notre streaming app (class Main) ;
  • Le nom, la version et le repository sur laquelle uploader l’image.

Au sein du conteneur, nous ajouterons les fichiers suivants :

$ tree -l 3
#.
#└── opt
#    └── kos-stream
#        ├── config.yaml
#        └── jmx_prometheus_javaagent-0.3.1.jar

 

Remontée du lag des consommateurs à Stackdriver

C’est maintenant notre conteneur qui expose les métriques sur un port HTTP (accessible dans un même namespace Kubernetes). On cherche maintenant un outil de monitoring pour les persister et les visualiser. Notre choix va se porter sur Stackdriver. Ce choix est favorisé par les outils développés dans le projet : k8s-stackdriver. En effet on va y trouver des applications toutes faites pour faire le pont entre notre streaming-app et Stackdriver. Dans un même déploiement, notre conteneur sera accompagné d’une instance de l’application prometheus-to-sd dont le but est d’accéder régulièrement au port qui expose les métriques JMX pour les pousser à Stackdriver. Ce scraper est positionné à côté de notre application comme un side-car.

deployment.yaml
- name: prometheus-to-sd
 image: gcr.io/google-containers/prometheus-to-sd:v0.2.6
 command:
 - /monitor
 - --source=:http://localhost:9001
 - --stackdriver-prefix=custom.googleapis.com
 - --pod-id=$(POD_ID)
 - --namespace-id=$(POD_NAMESPACE)

On retrouve le port 9001 dans la source (désigné dans les paramètres JVM). Le simple ajout de ce side-car va nous permettre de créer les dashboards suivants.

Figure 4 : Affichage des métrics de lag, cpu et mémoire de l’application Kafka-Streams

Mise en place du MetricServer

Ces métriques sont persistées et alimentent de jolis dashboards. Mais le master Kubernetes doit maintenant en être informé. En effet lorsque l’on sonde les l’API on obtient :

$ kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq
# Error from server (NotFound): the server could not find the requested resource

Le résultat ne contient pas encore les custom metrics qui nous intéressent.

L’application custom-metrics-stackdriver-adapter est précisément là pour enrichir l’API Kubernetes avec des informations personnalisées. Cette application implémente les specs de la custom metrics API et l’external metric API et se sert de Stackdriver comme backend. Elle est pensée pour l’élasticité des applications monitorées par Stackdriver. Nous aurions pu choisir une autre implémentation, voire la refaire soi-même si Stackdriver n’était pas dans la boucle.

Pour la déployer, le simple usage de la commande suivante est nécessaire :

kubectl create -f \
https://raw.githubusercontent.com/GoogleCloudPlatform/k8s-stackdriver/master/custom-metrics-stackdriver-adapter/deploy/production/adapter.yaml

Figure 5 : Affichage du pod contenant le metric server

$ kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq
#{
#  "name": "*/custom.googleapis.com|consumer_lag_game_frame_rs",
#  ...
#  "name": "*/custom.googleapis.com|consumer_lead_game_frame_rq",
#  ...
#}

Configuration de Horizontal Pod Autoscaler

Pour reprendre la cinématique complète au sein de notre cluster Kubernetes, nous avons le schéma suivant :

Figure 6 : Repport des metrics JMX à Kubernetes via Stackdriver

Notre application de stream processing expose sur un endpoint accessible depuis son pod depuis ses métriques JMX au format prometheus. Elle est accompagnée d’un side-car, qui scrape et exporte régulièrement ces informations à Stackdriver. Ce dernier sert de backend à notre metric server. Le Master est donc en possession de toutes les informations nécessaires pour décider d’ajouter (ou pas) un pod à notre déploiement. C’est cette idée de prise de décision qui va nous intéresser pour finir. Comme pour un autoscaling classique, c’est un HPA (Horizontal Pod Autoscaler) qui portera cette responsabilité.

Sa configuration est la suivante :

hpa.yaml
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
spec:
 scaleTargetRef:
   apiVersion: apps/v1
   kind: Deployment
   name: kstreams
 minReplicas: 1
 maxReplicas: 4
 metrics:
  - type: Pods
   pods:
     metricName: custom.googleapis.com|consumer_lag_game_frame_rq
     targetAverageValue: 100000
  // - other metrics

On notera l’usage de la version v2beta1 de l’api autoscaling. Le nombre maximal de replicas est à 4 (le nombre de partitions des inputs topics de l’expérience). Le nom de la métrique ciblée est custom.googleapis.com suivi du nom attribué dans le fichier de configuration de jmx-exporter.

Conclusion

Après la génération d’une quantité importante de données, le nombre de messages en retard finit par dépasser les seuils spécifiés dans le HPA. Des pods supplémentaires sont donc ajoutés. Ils contiennent chacun une instance de notre application de streaming et un side-car prometheus-to-sd. On peut constater la chute du lag liée aux input topics puisqu’il est réparti sur différentes instances. Ces instances, puisqu’elles supportent une charge réduite, sont plus susceptibles de rattraper le retard accumulé. Les courbes qui apparaissent en milieu de graphe correspondent au métriques émises par les nouvelles applications.

Figure 7 : Repport des metrics JMX à Kubernetes via Stackdriver

Nous avons vu comment, via des outils existants comme Stackdriver et Kubernetes, il est simple d’augmenter le pouvoir de nos applications kafka-streams. Nous avons donc abordé la question « could we? ». La partie qui n’a pas été traitée ici c’est « should we? ». Le scaling d’application a des avantages évidents qui ont été listés en début d’article. Mais il n’est pas aussi efficace pour toutes les applications de streaming. Le passage à l’échelle a un coût, et pour certains cas d’usage ou traitements, ce coût peut être non négligeable. La migration des états internes est une des premières problématiques. Il faut donc s’intéresser aux StateFullSets, une alternative aux déploiements Kubernetes qui permettent un stockage persistant, et quantifier ce coût pour un cas d’usage précis.

Where to GO Next

Pour terminer, voici une liste des liens qui m’ont aidé à réaliser cette expérience :

 

 

Meetup Paris Container Day, venez rencontrer Rancher et HAProxy !

$
0
0

Le 4 juin prochain aura lieu la conférence Paris Container Day portée sur l’écosystème des conteneurs, organisée par WeScale et Xebia.

À cette occasion, nous avons décidé de vous donner un avant goût de cette conférence en organisant un Meetup « Spécial Conteneurs » qui vous sera présenté par Rancher et Haproxy le 15 mai prochain à partir de 19h00 dans les locaux de WeScale (23 Rue Taitbout, 75009 Paris).

L’agenda de la soirée

7pm: « Building a DevOps pipeline with Kubernetes and Rancher 2.2 » by Cyrille Riviere, Field Engineer at Rancher Labs

One of the most common uses for Kubernetes is to improve development operations, and as part of that, teams need to determine the best way to integrate their CI/CD workflows with Kubernetes.

From Developers to Operations teams, each pipeline contributor participates to accelerate
the application delivery while responding to What, Where and How to deploy?
We dive into how to build a DevOps workflow with Rancher 2.2 and Kubernetes. We look at best practices for building pipelines with containers, and some of the tools that make it easier.

8pm: « Re-architecting the Microservice Chain » by Baptiste Assmann, Principal Solutions Architect at HAProxy

When you think « microservices architecture », maybe you visualize a chain of services calling one another. A calls B, B calls C, and so on. Yet, in order to properly manage queuing, even distribution of load, and health monitoring, a reverse proxy often sits in front of each group of services or nearby each service endpoint, as a sidecar. The main role of this sidecar is to cover « network functions » that are complicated to develop and maintain at the service level.

Sometimes, you realize that it makes more sense to move certain logic into the reverse proxy itself, instead of either creating another service or adding more functions into an existing one. This tendency to add more and more features into the proxy can make it the new monolith: WAF, authentication, authorization, etc…After all, the proxy is on the path between the two services. In this talk, I’ll demonstrate how you can simplify your design by using HAProxy, the SPOE suite, and your favorite programming language!

8:45pm: Meet and greet

Chaque participant au Meetup recevra un code de réduction de 40€ à valoir sur l’achat d’un billet standard du Paris Container Day (100€ au lieu de 140€).

IMPORTANT : les places étant limitées, merci de confirmer votre inscription en récupérant votre place gratuitement sur « Eventbrite » via ce lien.

Une journée à Devoxx, l’avis des Xebians

$
0
0

Des Xebians ont pu assister à plusieurs conférences lors de Devoxx France qui s’est tenu du 17 au 19 avril dernier. Retrouvez dans cet article leurs retours !

Mercredi

University

Highway to Elm – pour un meilleur front-end

Jordane Grenat persiste et signe sa passion pour le langage Elm avec cette très bonne université.

Après des informations sur sa syntaxe, son histoire, puis lors d’un live coding d’une petite application web, on a pu voir Elm en action. Pas (trop) de JS bashing, mais une mise en avant de ses faiblesses et des réponses apportées par Elm (exclure la possibilité de run time exception, limitation des dépendances,…).
On a eu une démonstration des différents types de programmes Elm existants permettant de répondre à différents besoins : application web de base, gestion ou non de side effects et gestion du routing. Le live coding a également couvert la gestion des vues en HTML, la façon d’interagir depuis une application Elm vers du JS et inversement et les appels HTTP.
C’était une bonne université, j’ai beaucoup elmé :)

Développer et déployer sur Kubernetes comme un Googler

Au cours de l’université, David Gageot nous présente certains services ou outils de Google (Open Source ou disponibles sur GCP) pour démontrer

Comment déployer et faire tourner une application sur Kubernetes comme un Googler

Dès le début j’apprends une petite chose que je ne savais pas, on peut monter en un clic un cluster K8 sur sa machine avec Docker Desktop (cool).

KNative
Est une solution Open Source qui permet de packager son application dans Docker et la déployer en mode serverless, soit grâce au Service Cloud Run (complètement managé), soit sur GKE (en installant KNative par dessus), soit sur n’importe quel Kubernetes avec KNative installé dessus. Cela rend l’application très portable et évite le vendor lock-in.

Skaffold
Permet de simplifier la configuration et le déploiement d’un conteneur dans Kubernetes. Il propose une configuration en fonction d’un Dockerfile et génère le fichier yaml. En une ligne de commande, cela permet de builder, tagger, déployer et voir les logs d’une application jusqu’au run dans Kubernetes. Il peut même être lancé en mode « watch ». Dès que le code est changé, toutes les étapes jusqu’au run s’exécutent automatiquement. En fonction de la configuration et du langage on peut plus ou moins optimiser les traitements faits pour avoir une boucle de feedback hyper rapide (c’est moins vrai pour Java 😜). Ex. pour un site statique, il est capable de voir le fichier modifié, le copier et l’injecter dans le conteneur qui tourne sur un cluster Kubernetes (à chaud). Il est également capable de faire du port forwarding pour tester une application via un navigateur lancé en local. Très bien pour le développement mais pas pour la production bien sûr.

Jib
Plugin maven / graddle pour construire des images Docker sans aucune config et qui gère également un cache pour éviter de refaire certaines étapes de construction de l’image Docker si ça n’a pas d’incidence sur le livrable (changement de commentaires dans le pom, etc.).

Dive
Petit outil pour inspecter en détail les fichiers ajoutés/modifiés dans chaque layer d’une image Docker.

Cloud Code (Beta)
Série d’extensions pour IDE (VS Code ou JetBrain), pour faciliter le développement avec K8. Cela permet, via l’IDE, de se connecter à un cluster K8 et d’interagir avec grâce à une interface graphique.
Il est également possible de créer des projets « squelettes » pour différents langages avec une configuration Docker d’exemple ou des templates yaml de configuration K8 prêts à l’emploi.
Il est facile depuis l’IDE d’avoir les mêmes fonctionnalités qu’avec Skaffold. L’utilisateur bénéficie de l’autocomplétion et de la validation des fichiers yaml de configuration K8.
Enfin, cela offre aussi une façon de pouvoir débugger dans l’IDE une application qui tourne sur un K8.

Cloud Build
C’est un service cloud de CI/CD sur GCP. Skaffold peut piloter Cloud Build pour gérer… le build. Mais l’inverse est également possible.

Tekton
Permet de gérer des pipelines pour le build grâce à une norme de fichier yaml qui a pour ambition de devenir le format standard. CloudBees & Co vont pousser pour pouvoir convertir les Jenkins files en Tekton.

Istio
David Gageot nous montre comment l’utiliser pour rajouter différents types d’objets customs à K8 afin de configurer des comportements au niveau des pods :

  • Introduire des erreurs pour certains services.
  • Gérer des retry à un niveau global sur tous les microservices d’un cluster.
  • Configurer les ingress K8 pour gérer le routing (UI/microservices/ …) et ainsi éviter une façade embarquée avec l’UI et faire une gateway.

On ressort avec une bonne vision d’ensemble d’outils pouvant nous faciliter la vie au quotidien. Chaque outil était présenté grâce à des démos qui m’ont permis de bien comprendre ce qu’il faisait même si Kubernetes n’est pas directement dans mon domaine d’expertise.

Deep Learning pour le traitement du Langage avec Pytorch

Une présentation très complète pour tous les amateurs de machine learning et de deep learning qui travaillent sur des données textuelles. Le Speaker a commencé par des explications sur des concepts basiques de machine learning et du traitement de texte, ce qui a permis de mettre tout le public au même niveau de connaissance pour la suite de la présentation. Pour la première démo il a implémenté une solution simple, la vectorisation des mots disponibles dans le corpus et l’implémentation d’un modèle basique de machine learning (la régression logistique).

Lors de la deuxième partie du talk, le modèle a été amélioré en utilisant des techniques de plus en plus avancées : représentation de données textuelles avec Spacy grâce avec les word embeddings des frameworks Glove et Fasttext. Ensuite, il a fait une brève introduction sur la modélisation Deep Learning, en particulier les paramètres du réseau (la fonction de loss, la fonction d’activation etc.) et les couches LSTM. La précision du modèle a augmenté avec l’implementation Pytorch, l’utilisation du word embedding et d’un réseau de neurones simple,.

Pourquoi utiliser Pytorch pour la modélisation Deep Learning ? Selon le Speaker, l’avantage principale est la possibilité de gérer des batchs de différentes tailles. Ce qui n’est pas possible avec les autres frameworks en Python.

Quarkus: Pourquoi & Comment faire une appli Java Cloud Native avec Graal VM

Lors de cette Université Emmanuel Bernard et Clement Escoffier nous ont présenté Quarkus. Pour bien expliquer les motivations pour Quarkus, Emmanuel fait un historique rapide de la JVM et de ce qui fait ses défauts dans les paradigmes de déploiement apportés par les plateformes de cloud et la containerisation.

Cette évolution tend à standardiser les applications de demain sous forme de multiples composants autonomes appelés service, micro service ou fonction. Chaque composant, peut avoir un cycle de vie et des technologies différentes.

Cette vision met l’écosystème JAVA ( JVM, Hotspot) en difficulté car il a été conçu pour être une surcouche au système d’exploitation. Ces concepteurs ont pris parti de délaisser la performance de démarrage pour privilégier la performance à long terme. Les développeurs Java parlent de chauffer la JVM.

Les problèmes de lenteur au démarrage sont dus à la compilation, à la vérification du bytecode, aux chargements des classes, à l’exécution de tous les blocs de code “static” et au lancement de la JVM en elle même. La JVM possède une empreinte mémoire excessive en particulier pour de petites applications.

C’est là qu’intervient GraalVM. Graal VM c’est une réécriture complète du compilateur C2 en Java. A l’inverse de C2, Graal génère un code machine qui fonctionne dans une micro VM appelée “Native Image” (également connu sous le nom de code “Subtract VM”). Cette compilation “ahead-of-time” apporte de nouvelles contraintes tel que closed-world assumption. Quarkus est un framework inspiré de Play Framework et reprenant des briques de l’écosystème open source Java. Il est capable de générer, via GraalVM, des applications exécutables sur Native Image.

Tools-in-Action

TensorFlow 1.x n’est plus, Vive TensorFlow 2.0

Dans ce talk, Alexia Audevart, Google Developer Expert Machine Learning, nous a fait un panorama sur les nouveautés de TensorFlow 2.0. Ces changements ont pour but, d’améliorer la productivité des développeurs.
Elle nous a expliqué comment le framework Keras a été introduit dans TensorFlow pour rendre plus facile le développement de nouveaux modèles.
Avec TensorFlow 2.0 Alpha, il est déjà possible d’utiliser Gpu ou Tpu d’une manière beaucoup plus transparente au niveau de la codification de l’algorithme.
Il existe deux modes de codification:

  • Le mode fonction, très proche de python, qui permet de debugger beaucoup plus facilement.
  • Avec Autograph, il est possible de migrer ce mode de fonctionnement vers le mode traditionnel de Graph.

C’était un parcours très intéressant sur les améliorations du framework, qui sera prochainement en version stable.

 

Jeudi

Conférence

10 choses que j’aurais aimé savoir avant d’utiliser Spark en production

Après avoir travaillé plusieurs années avec Spark, les Speakers ont constaté que les configurations par défaut ne sont pas toujours optimales. Dans ce talk, ils nous donnent 10 conseils pour que nos applicatifs soient plus efficaces :

  1. RDD vs DataFrames vs DataSets : avant de commencer à travailler avec Spark, il faut choisir parmi les trois structures de données. RDD est recommandé pour les données non-structurées ou semi-structurées, alors que le Dataframe et le Dataset sont préférés pour les données structurées sur lesquelles nous voulons faire des aggregations. Avantage supplémentaire pour le Dataset : le type des colonnes est connu et vérifié lors de compilation.
  2. Data Serialisation Format : pour la structure RDD, n’utilisez pas la serialisation Java par défaut, mais plutôt Kryo, qui s’avère plus rapide, plus compacte et plus facile à configurer. Pour les DataSets et DataFrames il est recommandé d’utiliser la serialisation tungsten.
  3. Storage Formats : évitez les formats csv ou json et préférez les formats binaires : parquet, avro. Voici leurs avantages :
    1. Apache Parquet : possède une compression élevée, à choisir si l’on requête beaucoup sur quelques colonnes.
    2. Apache Avro : est rapide lors de l’écriture, possède la fonctionnalité schema evolution. Il est donc à utiliser si le schema de nos données peut évoluer.
  4. Broadcast Join : le join est l’opération la plus coûteuse pour Spark. Pour la rendre plus efficace, on peut augmenter le timeout du broadcast dans la configuration. Cependant pour les données de moins de 10Mb, le broadcast est fait automatiquement.
  5. Hardware Tuning: il est nécessaire de bien connaitre les paramètres de notre cluster : la RAM, les Cores, Yarn… Si vous n’avez pas ces connaissances, vous risquez une utilisation excessive des ressources et par conséquent des performances médiocres. Vous pouvez utiliser la fonctionnalité Dynamic Allocation pour ajuster des ressources automatiquement en cours d’utilisation.
  6. Level of parallelism/partitions : augmentez le nombre de partitions. Pour les fichiers compressés utilisez la méthode repartition.
  7. Garbage Collector tuning: modifiez les configurations des spark driver et executor en ajoutant des extraJavaOptions.
  8. Errors : pour gérer les problèmes de mémoires, augmentez le nombre de partition ou réduisez le nombre d’exécuteur.
  9. Data skew : si vos données ne sont pas distribuées de manière uniforme sur les partitions vous risquez de rencontrer des problèmes. Les solutions possibles sont : de repartitioner les données, de ne pas utiliser la colonne asymétrique pour faire le join.
  10. Data Locality : tuner les configurations de la fonctionnalité Data Locality pour accélérer le temps de traitement de vos jobs Spark.

Et pour mieux gérer l’ensemble des configurations de vos jobs Spark, les Speakers recommandent d’utiliser l’outil Dr Elephant.

Introduction to Face processing with computer vision

Dans ce talk Gabriel Bianconi, de Scalar Research, nous a fait une introduction à la reconnaissance facial. Il a structuré son talk en cinq sections pour nous expliquer les techniques de traitement de visages :

  • Qu’est-ce que la reconnaissance de visages et les tâches associées au traitement du visage : ce sont des techniques qui permettent de localiser des visages dans des photos, les identifier et les manipuler.
  • Review des images par ordinateur : pour l’ordinateur, une image est une matrice multidimensionnelle.
  • Introduction au machine learning et à la vision par ordinateur : il y a deux approches pour faire de la vision par ordinateur, la plus traditionnelle, consiste à extraire les features d’une image pour inférer un modèle. La deuxième, utilise deep learning (réseaux de neurones).
  • Techniques de traitement de visages et exemples :
    • Histogram Oriented Gradients (HOG) qui consiste à découper les images en sub-areas, chacune identifiée par 16 gradients représentatifs.
    • Neural Networks, en particulier, les Convolutional Neural Networks (CNN).
  • Traitement de visages dans la pratique : Gabriel nous a présenté différentes libraries et frameworks spécialisés dans l’exécution des tâches communes au traitement d’image: TensorFlow, OpenFace, dlib, FaceNet et FaceRegognition. Pour faire du traitement de visage à l’échelle, il a aussi mentionné Ms Azure Face Api, Aws Rekognition et Face++

Mob programming

Les deux Speakers nous ont parlé de leurs experiences en mob programming, les avantages ainsi que les pièges.

Le mob programming est une approche de développement dans laquelle toute l’équipe de développeurs travaille sur le même écran pour résoudre un problème. Il est important lors des sessions mob que tous les membres de l’équipe participent de manière active. Une fois le cadre des sessions mob établi, les avantages sont nombreux. Premièrement, cela permet de réduire voire d’éliminer la dette technique cumulée dans le passé. Toute l’équipe est engagée dans la tâche en cours, chacun devient le garde de fou de la qualité du code développé. Deuxièmement, toute l’équipe est synchronisée, il n’y a pas donc pas de risque de truck factor, ni de décalage des tâches à cause des absences des membres de l’équipe. De plus, les décisions technologiques et d’implémentations sont prises collectivement. Cela permet de récolter plusieurs idées, d’évaluer plusieurs possibilités et aussi de responsabiliser toute l’équipe.

Ce qu’il faut veiller et éviter ce sont les débats trop longs et fatigue. Pour cela, les Speakers conseillent de faire tourner une rôle de facilitateur du mob. Il a pour objectif de cadrer le déroulement de la session mob en incluant des pauses.

Un outil à tester pour faciliter vos mob programming : Mobster.

Master Tooling for Containers with DevOps

Dans ce talk, Jessica Deen, Microsoft Cloud Developer Advocate nous a parlé des outils et bonnes pratiques DevOps pour Kubernetes et la gestion des Containers.

Jessica a parlé du conflit historique entre Dev et ops. D’un côté les Dev qui d’un côté ont besoin de développer, migrer et déployer plus rapidement et de l’autre, les Ops recherchent la sécurité, la stabilité et la robustesse. La culture DevOps a commencé avec le besoin de réunir les deux mondes pour travailler ensemble, communiquer et collaborer.

DevOps, c’est l’union des personnes, des processus et du produit pour livrer de la valeur.

Parmi les bonnes pratiques DevOps, elle a mentionné :

  • L’infra as code,
  • CI/CD,
  • L’automation,
  • Le monitoring.

Ensuite, Jessica a énuméré les avantages apportés par l’utilisation des conteneurs. Ils nous permettent d’écrire du code une seule fois et de l’exécuter partout. Puis, elle nous a présenté une démo d’un jeu écrit en JavaScript et en Go, containérisé et déployé sur un cluster Kubernetes. La démo a servi d’exemple, pour nous présenter des outils qui facilitent la mise en place des bonnes pratiques mentionnées précédemment.

  • build → Docker
  • package → helm(package manager de facto de Kubernetes), Kubernetes. Repository: Jfrog Artifactory
  • deploy → Kubernetes
  • tests → Selenium

Pour elle, le succès de Kubernetes vient de la culture DevOps dans laquelle il a été développé.

Elle a fini par mentionner des bonnes pratiques pour Kubernetes :

  • Build de petits conteneurs
  • Utiliser des namespaces
  • Utiliser Helm Charts
  • Contrôler l’accès à la base de rôles (RBAC)
  • Implémenter des health checks
  • Établir des limites pour les Web Requests

Modern Java: Change is the Only Constant

Lors de cette présentation, Mark Reinhold, l’architecte en chef du groupe “Java Platform” chez Oracle revient sur les récentes évolutions de l’écosystème et celles à venir.

Il explique pourquoi Java doit évoluer et comment il va évoluer ces prochaines années. Il compare le nombre de JEP (Java Enhancement Proposals) clôturées par la version 8 avec la somme des JEP clôturées par les versions 9, 10 et 11 : environ 90 dans les deux cas.

“La vitesse d’innovation reste inchangée, c’est la vitesse de diffusion de ces évolutions qui change”, assure Mark. Il propose une belle métaphore de la situation : vous devez choisir entre une pilule bleue ou rouge. La pilule bleue, c’est une montée de version légère tous les 6 mois. La pilule rouge correspond une montée de version conséquente tous les 3 ans.

L’autre message que Mark a voulu faire passer concerne la fermeture des API Internes (Sun, intern). Pour le moment un avertissement est affiché à la compilation mais très rapidement la compilation ne sera plus possible.

 

Vendredi

Conférence

L’Intelligence Artificielle pour tous

Comment éviter les biais du passé dans les algorithmes de l’IA ?

Les deux
développeuses, Rachel Orti et Mélanie Rao, ont commencé leur talk par une présentation de l’histoire de l’IA. Elles ont montré les différents types d’apprentissage automatique et les librairies que les développeurs peuvent utiliser pour tester l’IA. La puissance de l’IA et les risques liés à une mauvaise utilisation ont augmenté ces dernières années.

Un point important qu’elles notent, c’est la nouvelle réglementation RGPD. En particulier, le droit de l’explicabilité des modèles ainsi que le fait que les algorithmes d’IA ne peuvent pas prendre des décisions en se basant sur des données personnelles.

Elles proposent plusieurs outils qui permettent d’éviter le biais dans les modélisations :
IBM AI Fairness 360, Google
What-If Tool,
Audit AI de
Pymetrics. Les outils proposés permettent de modifier les données en entrée de l’algorithme pour que les prédictions et les analyses ne soient pas discriminatoires pour les groupes ethniques, ou le genre…

Cette présentation qui ne m’a pas laissée indifférente. Est-ce que les données historiques brutes, qui démontrent les comportements négatifs sur une partie de la population, sont réellement biaisées ? Quelles sont les performances des algorithmes qui utilisent des données sensibles versus les autres ?

Les algorithmes de machine learning se basent sur les données passées, décorrélées des préjudices (au moins quand la machine les traite). Si l’on souhaite traiter le biais introduit dans les données, il faut le faire sur l’ensemble des données, indépendamment du caractère personnel. L’objectif de la plupart des projets d’IA est d’avoir les meilleures prédictions possibles. Notre algorithme va donc faire le maximum de croisements de données pour arriver à bien prédire les phénomènes. Ceux qui permettent par exemple, d’aider les médecins à classifier les maladies, de lutter contre les fraudes ou la criminalité. Mais ce qui est sûr, nos projets data science doivent respecter la vie privée ainsi que la réglementation européenne.

Pépite #14 – Les script phases de Cocoapods

$
0
0

Depuis la version 1.4.0, CocoaPods nous permet de définir des script phases directement depuis notre Podfile.

Dans cette pépite nous allons voir brièvement comment les utiliser avec deux outils populaires que sont SwiftLint et SwiftGen, et comment pousser leur utilisation un peu plus loin.

SwiftLint

target :myApp do
  pod SwiftLint
  pod ...

  script_phase {
    :name => '🚨 SwiftLint',
    :script => '"${PODS_ROOT}/SwiftLint/swiftlint"',
    :execution_position => :before_compile
  }
end

La script phase pour SwiftLint est la plus simple qui puisse être créée : on définit un script « 🚨 SwiftLint » qui sera executé avant la compilation de notre code.

A l’éxécution, le binaire « swiftlint » situé dans le répertoire des pods (PODS_ROOT) est appelé. Et c’est tout. Aucune dépendance pour l’exécution, ce script sera ré-exécuté à chaque fois que l’on lancera la compilation de notre code.

Faites attention à bien échapper les variables d’environnement dans la commande que vous exécutez en utilisant des guillements double comme dans l’exemple.

SwiftGen

target :myApp do
  pod SwiftGen
  pod ...

  swiftgen {
    :name => '🛠 Generate resources',
    :script => '"${PODS_ROOT}/SwiftGen/bin/swiftgen" config run --config .swiftgen.yml && touch .swiftgen.yml"',
    :execution_position => :before_compile,
    :input_files => ['.swiftgen.yml'],
    :output_files => ['Generated/Assets.generated.swift', 'Generated/Fonts.generated.swift']
  }

Le principe est le même que pour SwiftLint, mais nous avons ajouté deux paramètres supplémentaires :
input_files, qui permet de définir les fichiers dont l’exécution de la commande dépend ;
output_files, qui définit les fichiers générés par la commande (obligatoire depuis Xcode10).

A partir de CocoaPods 1.7.0, vous pouvez aussi utiliser {input/output}_file_lists pour passer des xcfilelist en entrée/sortie.

Going deeper

Ici l’idée est d’aller un cran plus loin et de définir nos script phases dans un fichier séparé, que nous allons appeler Phasesfile.

Phasesfile
module Phase
  def self.swiftlint
    {
      :name => '🚨 SwiftLint',
      :script => '"${PODS_ROOT}/SwiftLint/swiftlint"',
      :execution_position => :before_compile
    }
  end

  def self.resources
    {
      :name => '🛠 Generate resources',
      :script => Script.swiftgen,
      :execution_position => :before_compile,
      :input_files => ['.swiftgen.yml'],
      :output_file_lists => ['.swiftgen.outputs.xcfilelist']
    }
  end
end

module Script
  def self.swiftgen
    <<~EOS
      "${PODS_ROOT}/SwiftGen/bin/swiftgen" config run --config .swiftgen.yml
      touch .swiftgen.yml
    EOS
  end
end 

Il nous suffit ensuite de les invoquer dans notre Podfile :

load 'Phasesfile'

target :myApp do
  script_phase(Phase.swiftlint)
  script_phase(Phase.resources)

  pod SwiftLint
  pod SwiftGen
  pod ...
end

Deux avantages à cette approche :

1. Notre fichier Podfile reste de taille raisonnable

2. Il est facile de porter nos phases dans un autre projet en copiant le fichier Phasesfile, ou en l’important via un Github Gist par exemple.

 

Vous savez maintenant tout sur les script phases de CocoaPods qui sont un ajout intéressant, et qui permettront un jour de ne plus avoir à versionner son .xcodeproj !

 

Viewing all 1865 articles
Browse latest View live