Dapr - a serverless runtime for distributed applications

A microservice architecture allows you to tackle scalability problems, high availability and quick time-to-market settlement. However, your teams need to be warned about this type of architecture. Indeed, microservices needs high development rigor and good dependency management, and so maintain multiple applications with microservices can be heavy and complex.

Dapr is a tool for reducing this kind of complexity induced by distributed applications. It’s part of this sort of tools, like Kubernetes, that abstract all the interactions between your dependencies and the external world of your IS. The key difference between Dapr and Kubernetes is that the last one acts at the system level, while Dapr acts on the application level. It implements a complete runtime where your applications can run safely, whatever the language, and where they access to multiples APIs facilitating some aspects of a complex microservice architecture: inter-service calls, queue management, state management …

A little about architecture

Dapr is all about abstractions. Indeed, every microservice platform needs classic elements like state management or Pub/Sub mechanisms, and Dapr is here to simplify interactions between your applications and these services. Dapr is based on Docker images, and so it encapsulates some of the famous infrastructure services like Redis or Service Bus to be pluggable with applications. These tools can be reached with standard HTTP and/or gRPC calls from within applications with your favorite languages.

Dapr run as a standalone process that interacts with your application to perform some actions. For example, if you want to store some objects as a persistent state for your application, you will first call the Dapr API (HTTP or gRPC), and Dapr will subsequently call the right system with the state management system and store your object. Here is the architecture process when you run Dapr in local mode :

Here is the version with Kubernetes :

The Dapr platform is built around the concept of building blocks. A building block is a single unit feature usable by your application to perform some actions. For example, state management is a building block. Here is the list of building blocks available of the moment of the writing of the article (version 0.6) :

  • Service Invocation : service-to-service invocation through method calls with retries. Each service can be automatically discovered by Dapr ;
  • State Management : key/value pairs store including Azure CosmosDB, AWS DynamoDB or Redis ;
  • Publish and Subscribe Messaging : Publishing events and subscribing to topics ;
  • Resource Bindings : resource bindings and triggers with event-driven approach ;
  • Distributed tracing : easy logging and event tracing through all Dapr workflows ;
  • Actors : actor model pattern implementation that make concurrency simple with method and state encapsulation. Actors are orchestrated by Dapr with many capabilities like timer, activation/desactivation and reminders.
  •  

In local mode, each running service launches a Dapr runtime process aside of the application, and use a Redis Docker image to use state management and Pub / Sub building block.

At the same time, Dapr can be configured to run in Kubernetes. Dapr will configure his services to run into Pods, and runs as a side-card container in the same pod as the service. This container will provide notifications of Dapr components updates.

A Dapr project structure

Dapr is pluggable with any projects : you only need to install the Dapr CLI, and it will automatically encompass your application into the Dapr runtime. The main commands of the CLI are :

  • init : initializes the Dapr installation into your local machine, or Kubernetes if –kubernetes is specified ;
  • run : runs the specified application into the Dapr runtime ;
  • stop : stops the specified application ;
  • list : lists all applications that are currently running ;
  • invoke : calls a specified API exposed by the service ;
  • publish : publishes an event to the pub/sub mechanism (whatever it is configured into the runtime, Dapr completely abstracts this part) ;
  •  

When you launch the command dapr run in your local environment, Dapr automatically creates YAML config files for 2 standard building blocks:

  • pubsub.yaml : a Redis component with Pub / Sub functionality ;
  • statestore.yaml : a Redis cache for store management.

The Pub/Sub config file looks like this

What does it say ? It tells Dapr to create a Redis cache configuration for pub/sub commands, configures the cache to be reachable at localhost:6379 with no password. This configuration should only be used in local mode. These few lines of code are enough for Dapr to create your building block. Of course, this configuration would be improved in future development of Dapr.

The second file looks like this :

The configuration is almost the same, but it includes the actorStateStore section. The parameter indicates Dapr that the store management mechanism should be used like an actor building block. This means that only one thread can be active inside an actor object's code at any time. Turn-based access greatly simplifies concurrent systems as there is no need for synchronization mechanisms for data access.

These simple config files allows your application to use state management and pub/sub building blocks through Dapr mechanisms. For example, into the same project, we have:

  • A Node project which acts as a server for simple CRUD operations;
  • A Python project as a client which calls the server each second for posting data;

The project can be found into the Dapr sample repository : https://github.com/dapr/samples.

To launch your Node app, you only have to use this command :

dapr run --app-id nodeapp --app-port 3000 --port 3500 node app.js

The app-id allows Dapr to identify your app with a unique id, the app-port is the port which Dapr can access your app through, and the port option defines the port of Dapr. After all the options, you just have to specify the command to launch your app, here is node app.js.

To check if your app is up and running, you can use an HTTP client (Curl, Postman …) and run this query :

POST http://localhost:3500/v1.0/invoke/nodeapp/method/neworder

You can directly use a Dapr command to post your command :

dapr invoke --app-id nodeapp --method neworder --payload "{\"data\": { \"orderId\": \"41\" } }"

Your app generates logs when it receives a new HTTP call, and Dapr relays the logs into your console. When you launch the command, you can see your app logs appear :  

The code below shows how the Node app in interacting with Dapr to store the order. It only calls the Dapr URL (http://localhost:3500/v1.0/state/statestore) and posts the payload to be stored into the Redis cache.

With a simple HTTP call you can achieve a state management operation through the Dapr abstraction layer.

After checking that our Node application is working correctly, we can run the Python program. This small application calls the Node app periodically to persist state into Redis. The Python application uses the call service mechanism of Dapr to call the Node service through this URL :

http://localhost:3500/v1.0/invoke/nodeapp/method/neworder

Open a new command prompt and use this command to launch the Python app (Python should be installed into your machine):

dapr run --app-id pythonapp python app.py

If you return to the first prompt, you’ll see new logs coming from the Node application telling that new orders are created and stored into the Redis cache.

Thanks to Dapr, we are able to make a communication between 2 microservices through a common abstraction layer. In conclusion, Dapr allows you to :

  • Integrate easily an application into a complex microservices architecture;
  • Use standard communication channels between services;
  • Operate services with common building blocks (state management, pub/sub …);
  • Wrap your services into small atomic process units easily switchable;
  • Use as many languages as you want;
  • What about Tye ?

Tye is a tool that makes developing, testing, and deploying microservices and distributed applications easier. Project Tye includes a local orchestrator to make developing microservices easier and the ability to deploy microservices to Kubernetes with minimal configuration. The OSS project is here : https://github.com/dotnet/tye.

Tye focuses on .NET project and makes easier for you to launch .NET microservices in your local environment with a single command line :

tye run

This tool uses your SLN project file to find all your projects and run it together. You can launch a project separately (inside the project folder) or all together with your SLN file. Tye uses a separate YAML config file if you want to run this tool into your CI/CD pipeline. For example, if you use the command line tye init, the tool generates this file tye.yaml :

This file references all your project based on your SLN file. The YAML allows you to specify more settings apart of the SLN file, like the registry you want to use.

The schema for the YAML file can be found here : https://github.com/dotnet/tye/blob/master/src/schema/README.md.

Tye fits perfectly with Dapr as a local orchestrator of your microservices. Dapr is here to perform a loosely-coupled architecture,  meanwhileTye is here to orchestrate the launching, debugging and deployment of all your services. Inside the Tye Github project you can find a Dapr sample. This project includes a YAML file with all the .NET project referenced. Inside this file, you noticed the section about Dapr :

With only these lines, Tye launches all your projects through the Dapr command line.

The Tye dashboard shows you all the containers launched, and you can see that Dapr container are launched aside of your services containers.

The redis acts here as a pub/sub and state management component.

 

Conclusion

Theses two projects are currently under development and should not be used for production environment. But this looks promisingWe can use Dapr to easily simplify our architecture and build very atomic loose-coupled microservices, and in the same time we have Tye to help us with our local development, debugging and deployment. All these tools can be used separately or together, but they definitely help us as developers for microservices development.

 

Christophe Gigax

Technical Leader Microsoft et Angular  - MVP Visual Studio & Development Technologies

Les Durables Functions – Devenez un expert de l’orchestration dans Azure

Le Cloud Microsoft est aujourd’hui l’un des cloud publics incontournables avec AWS, et propose une panoplie déjà bien complète de service Serverless au service du développeur. Pour rappel, le Serverless est une technique permettant de pousser du code instantanément sur une infrastructure totalement gérée de A à Z par le prestataire externe et mise à l’échelle automatiquement selon la demande. Le développeur n’a ainsi plus besoin de se soucier de la puissance des serveurs ou même du système sous-jacent. Ce genre de service proposé optimise également les coûts des applicatifs puisque les entreprises paient selon la consommation via un plan de facturation ‘Pay-as-you-go’. Si le service ne fait rien, aucune facturation ne sera émise.

Azure propose ce type de service via les Azure Functions. En C#, Javascript, Java ou encore Python, le développeur est en mesure d’héberger du code fonctionnel en quelques minutes sans se préoccuper de l’infrastructure sous-jacente. Microsoft a poussé le concept plus loin encore en proposant un SDK permettant d’orchestrer des fonctions afin de concevoir des applications Serverless plus complètes et avec plus de maîtrises : les Durable Functions. Cet article présentera d’abord les concepts généraux sur lesquels reposent les Durable Functions, puis nous décortiquerons le SDK afin d’explorer ses possibilités. Enfin, nous expliquerons divers patrons de conception relatifs à ces concepts.

La pattern Actor Model

Le principe d’un acteur est simple, il faut le voir comme étant une unité logique de votre application. Ensuite, le pattern repose sur la notion de messages. Cette notion de message est très importante, car c’est ce qui permet d’avoir des acteurs suffisamment indépendants pour fonctionner tout seul. On peut voir les acteurs comme étant des fonctions ou encore des unités de traitement type processus système. Les acteurs ont la capacité de :

  • Stocker des données propre à chaque acteur ;
  • Recevoir des messages depuis d’autres acteurs ;
  • Envoyer des messages vers d’autres acteurs ;
  • Créer de nouveaux acteurs ;

C’est avec ces règles simples que l’on construit un cadre d’exécution sain et terriblement efficace. L’autre force de ce patron de conception est que les messages sont envoyés et livrés de manière asynchrone, c’est-à-dire que deux acteurs différents peuvent tout-à-fait traiter des messages de manière parallèle sans s’attendre l’un et l’autre. Cependant, chaque acteur ne peut traiter qu’un seul message à la fois.

 

 

Le principe de l’acteur permet ainsi une mise à l’échelle efficace, puisque le parent va ainsi créer autant d’acteur enfant qu’il a besoin pour traiter sa requête. Le parent va ensuite gérer sa propre exécution en fonction des retours des différents enfants.

Attention cependant, le pattern Actor model n’est pas adapté à tous les cas d’usages, par exemple si vous avez besoin de traitement synchrone dans votre application. Si tel est le cas, cela veut probablement dire que vous n’avez tout simplement pas besoin de ce genre de pattern. Ensuite, ce cadre d’exécution n’est pas particulièrement adapté pour l’exécution via transaction, et notamment pour les rollbacks. En effet, imaginons une transaction bancaire avec retour en arrière si une erreur se produit, il devient alors difficile et fastidieux avec de simple retour de message de traiter ce cas d’usage si plusieurs acteurs ont déjà été mis en œuvre lors de la transaction. Heureusement, pour ces points manquants, divers Framework et SDK intègre des mécanismes permettant de pallier à ces manques (par exemple Akka).

Nous venons de voir les concepts généraux autour du pattern Actor model. Ces bases sont essentielles à la compréhension du fonctionnement des Durable Functions. Nous allons maintenant explorer d’autres patrons de conception, reposant sur le principe des acteurs, et pouvant être mis en œuvre via les Durable Functions dans Azure.

Dans les profondeurs du SDK

Au sein du SDK Durable Functions, il existe plusieurs types de fonctions que le développeur va devoir créer afin de les orchestrer correctement :

  • Client function : c’est le point d’entrée de votre solution d’orchestration. C’est elle qui va lancer le premier orchestrateur et ainsi lancer tous vos autres processus ;
  • Orchestrator function : cette fonction représente le cœur de l’orchestrateur de votre système. C’est cette dernière qui s’occupe de lancer les fonctions les unes après les autres, ou en parallèle, ou de conditionner leurs lancements et ainsi de suite… Les orchestrateurs sont des fonctions C# simple, mais un certain nombre de bonnes pratiques doivent être respectés. Par exemple, aucun appel HTTP doit être effectué dans cette fonction. L’ensemble des préconisations se trouve ici : https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-code-constraints ;
  • Activity function : plus petite unité de traitement et indépendante du système orchestré. Ces fonctions sont les tâches qui sont orchestrés les fonctions d’orchestrations. Ces fonctions ne sont pas limitées comme le peuvent être les fonctions d’orchestrations. C’est donc elles qui doivent effectuer les traitements HTTP ou les calculs nécessitant plus de mémoire ou de CPU ;

Entity function : opération de lecteur / écriture de l’état durable de l’orchestration. Lors du traitement de l’orchestrateur, l’application a besoin de stocker de l’état en mémoire de manière transverse à tous les acteurs. Ces fonctions sont là pour assurer l’atomicité de l’opération vers un stockage sécurisé et prévu pour les Durable Functions.

Chacune des fonctions sont activables selon un binding Azure Functions bien précis.

Depuis l’orchestrateur, on retrouve la classe DurableOrchestrationContext (ou IDurableOrchestrationContext pour la V2) et on y retrouve les méthodes suivantes :

  • CallActivityAsync : permet d’appeler une Activity Function avec son nom et des paramètres si besoin ;
  • CallActivityWithRetry : appel une Activity Function avec possibilité de relancer si jamais la fonction à échoué ;
  • CallSubOrchestrator[WithRetry] : appel un autre orchestrateur lui permettant lui aussi d’orchestrer des fonctions à sa guise. Cette méthode possède également sa version avec les options de relances ;
  • WaitForExternalEvent : attends qu’un événement externe à l’orchestrateur se produise. Ces événements sont notamment lancés via la méthode RaiseEventAsync dans les fonctions sous-jacentes.

Ces méthodes sont la base de l’orchestration avec les Azure Functions. Avec ceci, on retrouve plusieurs binding Azure Functions nous permettant d’activer ces fonctions :

  • [OrchestrationClient] ou [DurableClient] : binding de démarrage permettant d’initier une fonction d’orchestration. Le nouveau trigger [DurableClient] (Azure Function 2.x) permet également d’activer des fonctions d’entité ;
  • [OrchestrationTrigger] : binding d’orchestration permettant à la fonction de démarrer des acteurs ;
  • [ActivityTrigger] : binding d’activation d’une fonction d’acteur appelé par un orchestrateur ;

[EntityTrigger] : binding d’activation d’une fonction entité permettant de modifier de l’état persistent dans l’application ;

Un peu de code

La première fonction à créer est le client d’orchestration. Ce dernier va s’activer selon un événement externe (appel HTTP, Service Bus …) et va lancer le premier orchestrateur.

L’Azure Function ci-dessus est déclenché suivant un appel HTTP de type GET sans authentification particulière. Elle accepte également un binding [DurableClient] permettant d’injecter un objet de type IDurableOrchestrationClient afin de lancer l’orchestration. La fonction StartNewAsync permet de lancer la fonction d’orchestration HelloWorld.

Nous allons ensuite développer notre fonction d’orchestration. Cette dernière va nous permettre de lancer plusieurs fonctions d’activité selon notre cas métier.

 

La fonction effectue 2 actions principales :

  • Récupération du paramètre d’entrée via GetInput ;
  • Lancement d’un acteur appelé SayHello via la méthode CallActivityAsync ;

C’est ici que le développeur va intégrer sa logique d’orchestration avec des conditions et des boucles selon ses cas métiers. On peut notamment imaginer de lancer plusieurs acteurs en parallèle si besoin : chaque acteur devenant une Azure Function, le développeur profite alors d’une puissance de calcul et de traitement quasi infini.

Une fois l’acteur lancé, ce dernier va récupérer le paramètre et renvoyer un résultat sous forme de chaîne de caractère modifié.

Depuis un client orchestrateur, il est également possible d’appeler une fonction d’entité pour modifier l’état de l’application. La fonction ci-dessous modifie l’état d’une valeur placé sous l’effigie d’une entité Counter.

Ensuite, la fonction modifiant l’état peut prendre plusieurs formes :

Une fonction analysant l’opération souhaité et utilisant des méthodes GetState et SetState pour modifier l’état ;

  • Une classe définissant une propriété représentant l’état et les opérations comme des méthodes.

La version avec la classe paraît la plus robuste en termes de typage, mais la première version permet des scénarios plus génériques si besoin.

Avec l’ensemble de ces fonctions et leurs particularités, le développeur est en capacité de proposer une solution entièrement Serverless et orchestré avec finesse afin de répondre aux besoins métier. La puisse du Cloud vient ensuite s’ajouter à ce type d’architecture pour proposer une solution totalement scalable et encaissant la charge de manière très réactive.

Durable Functions et patterns

Le premier type d’architecture possible est le Function chaining. Ce dernier permet de lancer des fonctions dans un ordre bien précis afin d’orchestrer leurs exécutions. La sortie d’une fonction sera le paramètre d’entrée de la suivante, et ainsi de suite.

A noter : tous les schémas de cette section sont également disponibles sur la documentation officielle de Microsoft

Le code est ainsi très simple et enchaîne simplement les fonctions les unes après les autres.

Le deuxième type d’architecture est le l’opposé de celui vu à l’instant : la parallélisation. Avec Les Durable Function, il est aisé de lancer plusieurs acteurs de manière parallèle afin de profiter de toute la puissance du Cloud pour accélérer le traitement. Le SDK permet ensuite d’attendre que toutes les tâches aient terminés avant de continuer.

Le code ci-dessous illustre le schéma.

La clé ici est l’attente de tous les acteurs via la méthode Task.WhenAll. Par la suite, le code récupère le résultat de chaque tâche via la propriété Result de chaque Task.

Lors d’un traitement long, il est commun de développer un mécanisme permettant de suivre la progression du traitement afin de savoir précisément s’il est encore en cours de fonctionnement ou s’il a terminé.

Les Durable Functions intègrent déjà ce genre de fonctionnement. En effet, lorsque chaque orchestrateur démarre, il est possible de récupérer plusieurs points d’entrées, sous la forme d’URL, et ainsi en les appelant on récupère des informations relatives à l’état de l’orchestrateur. Une fois l’orchestrateur lancé, il faut utiliser la méthode CreateCheckStatusResponse afin de récupérer ces informations. Par exemple, l’URL suivante permet de récupérer le statut d’un orchestrateur avec l’ID b79baf67f717453ca9e86c5da21e03ec lancer dans l’application myfunc.

https://myfunc.azurewebsites.net/runtime/webhooks/durabletask/b79baf67f717453ca9e86c5da21e03ec

Dans le même esprit que la surveillance de l’orchestration, il est possible de créer une fonction qui vient scruter l’état d’un orchestrateur afin de lancer les actions en conséquence. Par exemple, si l’orchestrateur tombe en erreur, la fonction de surveillance pourrait lancer une alerte et ainsi notifier une administrateur qu’une erreur s’est produite

Ce pattern permet d’automatiser la surveillance des processus longs et de réagir en conséquence.

Ensuite, il est également possible d’attendre une notification externe avant de continuer le processus d’orchestration. En effet, le SDK intègre un mécanisme permettant de notifier l’orchestrateur d’un événement externe. On peut imaginer une interaction humaine via une interface Web ou autre. 

Cette notification s’effectue via un appel HTTP vers l’Azure Function. Par exemple, l’URL suivante lance un événement intitulé ApprovalEvent, et permet ainsi à l’orchestrateur de continuer son processus.

http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}/raiseEvent/ApprovalEvent

L’attente de ce type de notification se fait via la méthode WaitForExternalEvent depuis l’objet IDurableOrchestrationContext. On lui passe alors en paramètre le nom de l’événement attendu, ici ApprovalEvent.

Task<bool> approvalEvent = context.WaitForExternalEvent<bool>("ApprovalEvent");

Enfin, le dernier type d’architecture possible est l’agrégation de données entrantes dans l’application. Une Azure Function peut récupérer des données en entrées de manière massive puis peut utiliser une Durable Entities pour mettre à jour l’état de l’application au fur et à mesure que les données arrivent.

Les Durable Entities vont s’occuper de gérer l’accès concurrentiel de manière efficace afin de na pas perturber le flux de traitement. L’Azure Function ci-dessous écoute les événements d’un Event Hub puis agrège le flux dans une Durable Entites intitulé Counter.

Les Durable Function sont construites à partir du Durable Task Framework disponible en open-source sur GitHub : https://github.com/Azure/durabletask.

Conclusion

Les Durables Functions permettent au développeur de construire des applications avec traitement long et de les gérer de manière efficace au travers du SDK. Ce dernier élimine la majorité des difficultés rencontrés et permet au développeur de se concentrer uniquement sur son code afin d’apporter au mieux la valeur ajouté métier dont il a besoin.

Ce paradigme de programmation se fond totalement dans l’infrastructure Cloud Azure via les Azure Function. Cette parfaite intégration permet aux applications de disposer d’une puissance de calcul très grande et ainsi de répondre à de fortes charges. Cette scalabilité est un atout majeur en faveur de ce type d’architecture. Cependant, il n’enlève en rien la responsabilité du développeur de construire un algorithme prêt à être mis à l’échelle, car dans le cas contraire l’utilisation de ce type de SDK peut s’avérer être une vraie plaie.

Christophe Gigax

Technical Leader Microsoft et Angular  - MVP Visual Studio & Development Technologies

Pourquoi .NET Core ?

Afin de bien comprendre pourquoi .NET Core a été créé, il est important d'analyser et de comprendre chaque morceau dans ce Framework. En effet, chacun à une place bien précise dans l'architecture de ce dernier : runtime, librairie de classe, outils … Comprendre les dépendances entre eux permet de comprendre en quoi ce Framework est si modulable et puissant.

La modularité prend une place prépondérante avec .NET Core car ce doit être un Framework qui fonctionne sur un très grand nombre de plateforme : mobile, desktop et tablette avec UWP, Web  et Cloud avec ASP.NET, IoT avec Windows 10 IoT Core. Selon le runtime, le Framework doit d'adapter afin d'utiliser uniquement les librairies qui l'intéressent, c'est pourquoi la grande majorité de ces dernières sont déployés via des paquets NuGet, optimisant la gestion des versions, la centralisation des paquets et la diffusion du code.

Ce chapitre est l'occasion de faire le tour des différentes briques logicielles de .NET Core : le runtime, les librairies, les outils … et voir comment ces composants font de .NET Core un Framework aussi puissant et multiplateforme.

Les librairies de CoreFX

L'appellation de CoreFX fait principalement référence à l'ensemble des classes de bases disponible sous le namespace System.* et certaines extensions de Microsoft.*. Nous les connaissons plutôt sous le nom de Base Class Library, ainsi nous pouvons dire que CoreFX est le BCL de .NET Core. L'ensemble de ces classes sert de socle solidaire et bas-niveau afin que d'autres composants, tels que ASP.NET, puisse construire le Framework par-dessus. La caractéristique importante de CoreFX est que la majorité des APIs disponible sous .NET Core le sont également avec le .NET Framework classique. On peut facilement rapprocher les 2 en déclarant que CoreFX est un fork de la BCL du .NET Framework.

Le rôle principal de cette BCL est de faire en sorte que les APIs disponibles soit exécutables sur le plus grand nombre de plateforme possible. Prenons l'exemple de la cryptographie qui utilise des librairies du système pour fonctionner. Ainsi, l'implémentation de la cryptographie en .NET Core est assez délicate, puisque selon le système sous-jacent, il est possible qu'une même fonctionnalité soit disponible sur Windows mais pas sur macOS. L'énorme travail ici des équipes de Microsoft est de faire en sorte qu'un maximum d'algorithme soit compatible sur les plateformes, cependant ce n'est pas toujours le cas. Nous retrouvons ci-dessous un tableau récapitulatif des algorithmes RSA disponibles :

 

 

Lorsqu'une API n'est pas supportée par la plateforme, le code lève une exception de type PlatformNotSupportedException. De ce fait il faut bien comprendre qu'en fonction des APIs utilisés, si ces dernières sont étroitement dépendantes du système, une exception peut être levé car non supporté par CoreFX. Heureusement, les APIs les plus communes (collection, sérialisation, async, console, système de fichier …) sont entièrement supportés sur toutes les plateformes.

Le runtime historique via CoreCLR

Le runtime de .NET Core est CoreCLR, et est basé sur le même code de base du CLR classique et historique que le .NET connait. Initialement, CoreCLR était le runtime de Silverlight, et conçu spécialement pour fonctionner de manière multiplateforme, notamment Windows et MacOS. CoreCLR fait maintenant partie intégralement de .NET Core, et représente une version simplifiée du CLR classique. Son grand avantage est le côté multiplateforme, notamment depuis l'ajout du support de Linux pour répondre aux besoins de .NET Core. Le runtime est également une machine virtuelle incluant le JIT permettant d'exécuter du code à la volée et aussi le Garbage Collector garantissant la bonne gestion de la mémoire.

Rendu Open Source depuis le début de l'année 2015, l'objectif de CoreCLR est de fournir un environnement d'exécution multiplateforme, c'est pourquoi le code du runtime est un mélange de C# et C++. Le projet utilise CMake, un outil également multiplateforme, permettant de builder le projet sur Windows, Linux et Mac. Le diagramme ci-dessous présente la répartition des quelques 2,6 millions de lignes de code contenu dans CoreCLR :

 

 

Le CoreCLR rejoint ainsi CoreFX en tant que grand projet Open Source de Microsoft. La grande différence entre les 2 se trouve au niveau de l'intégration de code C++ dans CoreCLR : ce projet a besoin de plus d'outils de compilation afin de le faire fonctionner.

Le futur via CoreRT

Le projet CoreRT, encore en développement, est un runtime dédié à .NET Core permettant d'accroître ses performances. Tout d'abord, CoreRT permet de faire de la compilation native, évitant ainsi l'étape du code intermédiaire que .NET à connu pendant longtemps. Cela veut dire que les performances de la compilation et de l'exécution d'un projet sont accrues. Ensuite, ce compilateur permet de faire de l'AOT via RyuJIT (nouveau compilateur pour CoreRT), c’est-à-dire qu'il est capable, via un système de métadonnées, de parcourir le code source et de détecter ce qui est utilisé et ce qui ne l'est pas. Il va ensuite générer un seul exécutable ne contenant que ce qu'il a besoin, et va épurer au maximum les dépendances du projet lors de la compilation. Le déploiement d'un tel livrable est également bien allégé.

En parallèle, on parle également de .NET Native pour le workflow général de compilation optimisé, utilisant CoreRT de manière sous-jacente et totalement transparente. Les avantages de CoreRT sont les suivants :

  • ● Une compilation native générant qu'un seul binaire final, incluant les dépendances, le code managé et le runtime ;
  • ● Démarrage plus rapide des applications puisqu'elles sont directement compilés en natif ;
  • ● Nouveaux scénarios de déploiement puisque ce dernier devient plus simple (via un seul ou via un conteneur Docker).
  •  

Mono savait déjà faire de la compilation AoT native, et UWP via .NET Native. Cependant, afin de faire tourner des applications .NET à la fois sur Windows, Linux et Mac, Microsoft avait besoin d'une nouveau CLR, c'est pour cela que CoreRT a été créé. Le code étant Open Source, on peut identifier les composants principaux du code :

 

 

On peut voir que le composant le plus utilisé est System.Private.CoreLib, écrit en C#. L'intérêt ici de ce langage est qu'il est managé, évitant ainsi au maximum les fuites mémoires. Les composants natifs sont écrits en C++. Nous pouvons constater le runtime intègre également un compilateur (via les composants ILCompiler.XXX et ILVerification). Son rôle est le suivant :

  • Scan des modules et des classes afin de déterminer l'arbre de dépendance entre eux ;
  • ● Génération des métadonnées via Reflection ;
  • ● Compilation du code IL (via RyuJit, Web Assembly ou C++) ;
  • ● Génération des méthodes compilés.
  •  

Le runtime CoreRT s'appuie sur plusieurs services permettant de faire fonctionner le moteur d'exécution, qu'ils soient écrits en C# ou en code natif C/C++. On retrouve :

  • ● Le Garbage Collector (37k lignes de code) ;
  • ● La gestion d'exception ;
  • ● La gestion de la stack.
  • ● L'allocation mémoire ;
  • ● Le débogage et le profiling ;
  • ● L'interfaçage avec l'OS ;
  • ● La gestion des processus et des tâches.
  •  

Si nous prenons un projet un projet Hello World simple, le découpage du binaire ressemble à ceci :

 

 

On constate qu'un programme simple (4 MB au final) embarque beaucoup de fonctionnalités provenant à la fois de CoreFX et CoreRT. Cette analyse met en lumière également les parties de l'application fonctionnant via du code managé et celles où c'est le code natif qui prend le relais (typiquement le GC).

Au final, nous avons identifier les différents composants de .NET Core lui permettant de fonctionner et de proposer autant de fonctionnalités riches pour le développeur. La plupart des couches basses ont été réécrites et modularisé, ce qui permet aujourd'hui d'avoir une plateforme performante tant au niveau de la rapidité d'exécution que de l'architecture adopté.

Christophe Gigax

Technical Leader Microsoft et Angular  - MVP Visual Studio & Development Technologies

Comment concevoir son architecture microservice ?

L'architecture microservice

L'architecture microservice est un type d'architecture logicielle émergente qui tente de répondre aux problématiques des applications dites "monolithiques" Celles-ci concernent le développement fastidieux, le manque de flexibilité, les difficultés de déploiement ou encore l'absence de scalabilité. Le principe des microservices est simple : on décompose l'application en plusieurs applications plus petites et plus autonomes qui gravitent autour d'un Business Domain. L'intérêt des microservices est de se recentrer sur le métier et les problèmes fonctionnels que l'application tente de résoudre.

 

Caractéristiques d'un microservice

Les microservices ont l'avantage d'être plus petits et donc plus flexibles en termes de développement, de déploiement et de maintenabilité. Chaque microservice se doit de répondre à un aspect métier que l'on appelle "Contexte Délimité" ("Boundary Context" en anglais). L'enjeu de l'architecture microservice est de s'assurer que la délimitation en domaine fonctionnel est suffisamment pertinente et cohérente pour assurer des microservices aussi petits que possible mais suffisamment autonomes.

La taille des microservices est en fait le piège de cette architecture. Il paraît évident que plus les microservices sont petits, plus il sera facile de les gérer. Cependant, on peut s'apercevoir très vite que certains microservices communiquent beaucoup entre eux car ils ont souvent, voir systématiquement, besoin des informations de l'autre pour fonctionner. Si un tel comportement est observé, cela veut dire que deux microservices ne doivent former qu'un.

L'analyse architecturale préalable d'une solution en microservice doit aboutir à la séparation du Business Model initial en plusieurs contextes délimités, les plus petits et autonomes possible. Dans ce contexte, la cohésion doit être le maître mot. En effet, chaque contexte doit être suffisamment cohérent avec lui-même afin d'éviter le surplus de communication avec les autres.

Le concept Domain Driven Design

Le concept Domain Driven Design (appelé DDD) a été introduit pour la première fois par Eric Evans en 2004 dans son livre "Domain-Drive Design: Tackling Complexity in the Heart of Software". Aujourd'hui, cette oeuvre est répandue mondialement et je vous conseille fortement sa lecture intéressante.

Le DDD est une méthode mettant en œuvre l'ensemble des concepts cités ci-dessus. Lors de la phase d'analyse de l'architecture du projet, le DDD permet le découpage en contexte délimités en identifiant les sous-domaines, qui deviennent par la suite les microservices.

Lors de la conception des microservices, le code est souvent divisé en couches. Cette division en couches est à la fois conceptuelle et technique. Cela permet aux développeurs de mieux reconnaître les différentes parties de l'application en séparant d'une manière bien précise les entités qui composent le code. Ces couches sont des abstractions logiques destinées à une meilleure compréhension du code.

 

Voici comment nous pourrions définir les différentes parties représentées ci-dessus :

  • ● Application : contient la partie opérationnelle du microservice en exposant le métier et le Domain model vers l'extérieur. Cette couche contient ainsi tout le paramétrage et le code nécessaire afin que le microservice puisse exister et fonctionner dans un environnement qui est le sien. Cette partie ne connaît pas le business, et s'occupe simplement de coordonner les interactions entre les autres couches et/ou le monde extérieur ;
  • ● Domain model : représente les concepts propres au business que le microservice peut traiter dans le système d'information. L'état propre du service est représenté dans les entités de cette couche, et représente clairement le cœur du business du microservice ;
  • ● Infrastructure : les données sont stockées en base de données (ou autre).

Les dépendances entre les différentes couches sont importantes et ne doivent pas se faire de manière aléatoire. Dans un premier temps, la couche Domain model ne doit pas avoir de dépendance avec les autres couches. La librairie doit rester neutre, elle est exploitée dans les autres librairies en tant que dépendance. Ensuite, la couche Infrastructure n'a qu'une seule dépendance vers le Domain model. Cela paraît évident puisque c'est elle qui est responsable de la persistance des données. Les modèles permettent de symboliser les entités de la base de données. Enfin, la couche Application possède des dépendances vers les autres parties car :

  • Elle a besoin des services disponibles dans la partie Infrastructure afin d'effectuer les actions nécessaires pour traiter les requêtes entrantes et répondre aux besoins métiers ;
  • Elle a besoin des modèles du Domain model pour faire transiter / créer / mettre à jour les données ;

La couche Application peut être considérée comme l'orchestratrice des autres couches.

L'importance des entités

 

La notion de Domain model étant très importante, il est indispensable de bien découper ses contextes afin d'avoir une cohérence dans les entités. Une entité, si son identifiant est le même au travers de plusieurs microservices, peut être partagée dans tout le système, mais pas forcément sous le même modèle. Par exemple, une entité Acheteur et une identité Facture vont dépendre de 2 microservices créés spécialement pour permettre de les gérer tous deux de manière indépendante. Le microservice Acheteur va reposer sur un modèle très complet de l'acheteur. Cependant, le microservice Facture n'a pas besoin d'un modèle aussi complet : l'identifiant de l'acheteur suffit, mais d'autres propriétés peuvent intervenir en cas de besoin. Le contexte de chaque microservice influe sur son Domain model.

Une autre règle est très importante en DDD :

Chaque entité du Domain model doit contenir les données et les comportements qui lui sont propre selon son contexte.

Dans un premier temps, cela veut dire qu'on ne créé pas de modèle dit DTO (Data Transfer Objects), mais des modèles POCO (Plain Old CLR Object) qui contiennent du comportement. Selon Martin Fowler et Eric Evans, précurseurs du DDD, utiliser des DTO avec classes de services donnerait des anti-patterns comme du code spaghetti ou des scripts transactionnels. On parle alors de domaine anémique. Pour des microservices simples (type CRUD), cela pourrait suffir mais dans le cadre d'un système complexe avec plusieurs microservices, la bonne pratique est d'utiliser des modèles POCO, c’est-à-dire des modèles qui contiennent les méthodes permettant de gérer ses données (ajout, modification, etc).

Martin Fowler explique clairement cette différence dans plusieurs de ses articles :

https://martinfowler.com/bliki/AnemicDomainModel.html et https://martinfowler.com/eaaCatalog/domainModel.html

De ce fait, au sens de Martin Fowler et Eric Evans, les entités doivent ressembler à ceci :

Il est cependant tout à fait possible d'avoir des entités sans méthodes. Cela peut arriver dans le cas d'entités enfants très simples où le microservice n'a pas besoin de beaucoup de complexité.
Plusieurs débats tournent autour du modèle anémique et beaucoup de gens pensent que c'est un anti-pattern. Au final, cela dépend vraiment du contexte de votre projet et du microservice. Pour un microservice très simple, un modèle anémique est certainement ce qu'il vous faut. Néanmoins, plus la complexité va s'agrandir, plus il est judicieux de construire des modèles de type POCO afin de regrouper au même endroit les règles métiers. Un modèle riche ne sera qu'un plus pour la conception d'un système avec DDD et permettra aux différents microservices de garder leur pérennité sur le long terme.


L'approche DDD est tournée vers le domaine métier, et sa représentation dans le code tend à former un système d'information répondant aux problèmes clients de manière optimale. Les notions de contexte délimités, de couches d'abstraction, d'entité POCO et de modèle anémique constituent les facettes de cette méthodologie qu'il faut appréhender lorsqu'une architecture microservice est adoptée.

 

Christophe Gigax

Technical Leader Microsoft et Angular  - MVP Visual Studio & Development Technologies

Passer la certification Azure Developer Associate

Si comme moi vous vous intéressez à la plateforme de Microsoft, il y a de grandes chances que vous vous soyez interrogés sur les certifications. Pourquoi se certifier ? Par quelle certification commencer ? Qu’allez-vous en retirer ? Etc… Je me suis longtemps posé ces questions et j’ai récemment sauté le pas. Je viens d’obtenir la certification Azure Developer Associate, et voilà pourquoi je l’ai passée.

La certification

La certification Azure Developer Associate s’obtient après le passage de l’examen AZ-203 Developing Solutions for Microsoft Azure. Elle permet de valider les compétences en conception et développement de solutions de cloud computing sur Azure. Elle repose principalement sur les service PaaS, SaaS et Serverless de la plateforme. Elle garantit l’expertise des compétences sur une panoplie complète de sujets sur le Cloud, et plus spécifiquement, en développement et architecture d’applications cloud computing.

Les points visés

Cette certification traite principalement des sujets autour de l’hébergement des applications, du stockage des données autour de SQL Serveur, Compte de stockage, CosmosDB … On parle aussi d’indexation, de messaging, d’infrastructure avec les machines virtuelles, un peu de réseau et beaucoup de sécurité avec Azure Active Directory notamment. Enfin, la plupart des questions tournent également autour de l’outil Azure CLI et des commandes Powershell qui permettent de créer et gérer les ressources Azure.

L'importance du cloud aujourd'hui

Le Cloud est aujourd’hui omniprésent et répond à bien des problématiques : scalabilité, performance, sécurité, haute disponibilité, etc...Toutes les grandes entreprises et grosses applications du quotidien (Netflix, Facebook...) se basent sur un Cloud public, nous ne pouvons pas y échapper. La bonne connaissance des services Cloud et de ses immenses possibilités permet de maîtriser les développements et les architectures misent en place dans les applications.

Se certifier sur le cloud

Le développement Cloud va devenir de plus en plus recherché du fait des enjeux économiques et financiers qu’il représente sur le marché. Un développeur certifié va se démarquer des autres sur ce point, et l’entreprise va ainsi lui faire confiance pour mener à bien les projets. La reconnaissance de son expertise lui sera précieuse pour faire les bons choix dans sa carrière.

Être certifié, pour soi mais surtout pour nos clients

Aujourd’hui, être reconnu comme professionnel certifié, c’est important aux yeux de nos clients. Versusmind offre une gamme de services autour du cloud et celle-ci repose sur des professionnels directement adoubés par Microsoft par le biais de leur organisme de certification. Pour notre crédibilité, être certifié, c’est fondamental. Cela nous permet d’obtenir la confiance de nos clients et par la suite de leur garantir un niveau de qualité élevé dans nos prestations.

Poursuivre sa certification

La prochaine étape, après cette certification, est la reconnaissance Architecte Azure sur la plateforme de Microsoft. Cette certification reconnaît les compétences en architecture avancées Cloud computing. Cela renforcera votre expertise sur le sujet dans le futur.

À propos de l’auteur

Je suis Christophe Gigax, Technical Leader Versusmind & Microsoft MVP.
Chez Versusmind depuis 1 an et demi, j’ai commencé comme tech lead et je travaille aujourd’hui en premier lieu sur les projets Agence à forte valeur ajoutée. Je réalise aussi des missions d’expertise sur du .NET, du Angular et également du Azure. J’ai également monté la formation de A à Z sur ASP.NET Core et je délivre cette formation dans le cadre de Versusinstitute.

 

Christophe Gigax

Technical Leader Microsoft et Angular  - MVP Visual Studio & Development Technologies

Choisir Kubernetes comme orchestrateur

Pourquoi Kubernetes ? Quels sont les avantages à utiliser un orchestrateur ?

Docker, microservices et Kubernetes

Docker est une technologie nous permettant de construire des applicatifs encapsulés dans des conteneurs et exportables simplement d'un environnement à un autre. Nous commençons donc à construire des sortes d'unités applicatifs, manipulables très facilement, se lançant et s'arrêtant avec une simple commande Docker. Cette simplicité de gestion des conteneurs nous amène à construire de plus en plus d'applicatifs sous Docker et à concevoir un nouveau type d'architecture : les microservices.

Les microservices nous permettent de concevoir des applications plus petites, focalisées sur un domaine fonctionnel bien précis de l'ensemble du système. Au vu de la légèreté des conteneurs, l'objectif des microservices est de faire fonctionner plusieurs conteneurs côte à côte afin d'encaisser au maximum les charges induites par l'utilisation du système d'informations.

La gestion de plusieurs conteneurs devient ainsi une problématique beaucoup plus grande que par le passé. Peut-on faire fonctionner plusieurs conteneurs différents côte à côte ?  Comment gérer la création / suppression des conteneurs de manière automatique selon la charge ? Afin de répondre à ces problématiques, Google a rendu open-source l'un des outils qui permettait justement à l'entreprise de gérer ses conteneurs en interne sur lesquels fonctionnaient ses applications : Kubernetes.

Kubernetes, une plateforme d'orchestration

Kubernetes est une plateforme d'orchestration open source de conteneurs permettant entre autres d'automatiser le déploiement et la gestion d'applications multi-conteneurs scalable. Kubernetes fonctionne de pair avec Docker : votre application fonctionne dans une image Docker. Cette image va être gérée par Kubernetes afin de s'assurer que l'image, répliquée X fois selon les usages, fonctionne correctement.

L'orchestrateur est cependant compatible avec n'importe quelle technologie de conteneur conforme au standard Open Container Initiative. Docker a également développé son propre orchestrateur : Swarm. Cependant, Kubernetes est devenu plus populaire et semble avoir été adopté plus largement par la communauté.

L'architecture de Kubernetes est représentée par le diagramme ci-dessous :

Le nœud master

Un cluster Kubernetes est constamment composé d'un nœud master, les autres nœuds sont des nœuds enfants qui seront pilotés par le nœud master. Le nœud master est composé des outils Kubernetes suivants :

  • API Server : également appelé kube-apiserver, l'API Server est le composant central de gestion du cluster Kubernetes. Il permet en exposant des API REST de manipuler le cluster et ses nœuds via les fichiers de configuration. Il permet également de stocker l'état du cluster dans le stockage etcd ;
  • Controller Manager : composant de Kubernetes permettant de 'surveiller' le cluster, et d'agir si nécessaire. C'est lui qui permet au cluster de maintenir son état de fonctionnement, c’est-à-dire l'état décrit dans le dernier fichier de configuration. Il vérifie que les nœuds fonctionnent toujours et que le cluster respecte le bon nombre de réplica d'un service déployé au sein de celui-ci ;
  • Scheduler : entité qui va planifier le déploiement d'un pod sur un nœud. Le planificateur va prendre en compte beaucoup de paramètres pour décider sur quel nœud il faut déployer le pod : les ressources serveurs et logiciels, les contraintes, les spécifications d'affinités, les interférences …
  • etcd : stockage persistent de type NoSQL avec clé/valeur permettant à Kubernetes de connaître à tout moment l'état supposé du cluster.

Les différents types d'objets

Il est ensuite possible de communiquer avec l'API Server via une interface en ligne de commande ou via une interface graphique. Ensuite, Kubernetes manipule ce qu'on appelle des objets. Il en existe plusieurs types, et il est important de bien connaître la terminologie :

  • Pod : c'est l'entité la plus unitaire de Kubernetes, la plus petite et la plus simple. Un Pod représente un processus en cours d'exécution dans le cluster. Nous pouvons comparer cela à Docker : les images sont à Docker ce que les Pods sont à Kubernetes. Un Pod encapsule une application (ou plusieurs selon certains cas, mais non conseillé) et possède une ressource stockage, une adresse IP et des options qui dictent comment le Pod doit se comporter. Il représente une unité de déploiement et correspond au final à une seule instance d'une application dans Kubernetes ;
  • Service : un service est un ensemble de Pod regroupés sous une même enseigne logique : les labels. Les services deviennent une abstraction des Pods permettant à Kubernetes de savoir quels Pods sont concernés par quelle facette fonctionnelle de votre application ;
  • Volume : un Pod a une durée de vie indéterminée. S'il tombe, Kubernetes va recréer ce Pod à l'identique selon la configuration indiquée. Ainsi, tous les fichiers de l'ancien Pod sont perdus. Les volumes apportent une solution permettant de faire perdurer les fichiers, mais la durée de vie reste celle du Pod. Le volume permet cependant de conserver les fichiers même si des conteneurs sont tombés dans le Pod ;
  • Namespace : les namespaces permettent de diviser un cluster afin de faire fonctionner plusieurs environnements à l'intérieur (développement, production …). Cette division est une division logique, et permet par exemple de diviser les ressources à l'intérieur du cluster selon le namespace choisi.

Les modes de modification du cluster

Kubernetes permet 2 modes de modification du cluster : le mode déclaratif par opposition au mode impératif. Le mode impératif demande à l'administrateur du cluster d'écrire les commandes qui vont changer le cluster. Par exemple, les commandes ci-dessous permettent de créer un namespace, un quote, un déploiement et un service.

L'administrateur exécute lui-même les commandes, et doit ensuite lui-même s'assurer que le cluster maintient le bon état. Par exemple, si un Pod tombe, c'est à lui de relancer le pod avec les commandes appropriées. De plus, une fois le cluster dans l'état final, il est difficile de se souvenir de l'état désiré du cluster, il n'y a pas de source of truth.

Cette technique comporte bien des soucis notamment en termes de maintenabilité de l'état du cluster. C'est pourquoi il existe la méthode déclarative. Cette méthode permet d'utiliser des fichiers de configuration au format au YAML afin de piloter le cluster. Ces fichiers YAML sont établis par l'administrateur du cluster, et sont ensuite soumis à Kubernetes via la commande suivante :

Avec cette commande, Kubernetes va vérifier l'état du cluster en fonction de ce fichier de configuration. S'il trouve une différence, Kubernetes va automatiquement lancer les commandes nécessaires afin de faire correspondre l'état du cluster avec le fichier de configuration. Ensuite, cette configuration est stockée de manière persistante dans etcd. Cette méthode possède plusieurs avatanges :

  • C'est Kubernetes qui s'occupe de lancer les commandes en fonction des différences trouvées. L'administrateur décrit l'état du cluster au final et non plus les commandes qui amènent à cet état ;
  • L'état est sauvegardé et donc Kubernetes connaît constamment l'état du cluster ;
  • Kubernetes surveille le cluster et compare avec l'état souhaité soumis avec ses fichiers de configuration. Il va donc lancer les commandes nécessaires si une anomalie devait se produire.

Gérer un cluster Kubernetes grâce aux fichiers de configuration

Les fichiers de configuration semblent donc être la meilleure solution afin de gérer un cluster Kubernetes. Ces fichiers au format YAML doivent respecter un schéma bien particulier afin d'être compris par Kubernetes. Il est important de rappeler que YAML est une version dérivée de JSON. Cela veut dire que vous pouvez aussi très bien utiliser des fichiers au format JSON pour piloter le cluster. Pour tester Kubernetes en local sur votre machine, il est possible d'installer Minikube (disponible sur Mac, Linux et Windows). Cet outil permet de simuler des nœuds dans des machines virtuelles et ainsi tester vos déploiements : https://kubernetes.io/docs/setup/minikube/.

L'exemple ci-dessous présente la création d'un Pod via un fichier de configuration YAML :

Les fichiers Kubernetes commencent toujours par apiVersion. Ceci permet de cibler la version du schéma Kubernetes que vous souhaitez. Ensuite, on indique quel type d'objet Kubernetes on souhaite créer via l'attribut kind. Ici, nous voulons un Pod. Une bonne pratique est de constamment rajouter des métadonnées à ses objets. Dans le cas ci-dessus, on rajoute un name et un label. Dans la section spec, le fichier décrit les images docker que Kubernetes doit télécharger. Dans ce cas, on utilise une image nginx et une image personnalisée de l'application que l'on souhaite faire fonctionner dans le Pod. Une fois ce fichier bien conçu, il suffit de lancer la commande suivante :

La commande create absorbe le fichier pod.yaml et l'envoi à Kubernetes pour la création du Pod. La commande suivante permet de suivre l'avancée de la création du Pod :

Au bout d'un moment, le Pod passe en statut Running, confirmant que la création s'est bien déroulée avec succès.

Grâce aux fichiers de configuration, il est très facile de piloter et maintenir un cluster Kubernetes. Les architectures micro-services doivent se doter d'orchestrateur afin de garantir une haute disponibilité des applicatifs. Kubernetes est un excellent choix dans ce sens notamment par sa robustesse et sa flexibilité de gestion. Aujourd'hui mondialement répandu, ce projet Open Source est un parfait exemple d'outil développé par la communauté et qui sert au plus grand nombre.

Christophe Gigax

Technical Leader Microsoft et Angular  - MVP Visual Studio & Development Technologies

Les bénéfices de Docker

À quoi sert Docker ? Quels sont les avantages d'utiliser Docker plutôt que la virtualisation ? Comment l'intégrer dans nos applications ?

Docker, et la conteneurisation de manière générale, sont devenus pratiques courantes et résolvent bien des problèmes depuis l'avènement de ces technologies : déploiement, isolation des applicatifs, environnement homogène… Si votre application fonctionne sur votre machine avec des conteneurs, elle fonctionnera assurément sur vos environnements de déploiement.

La technologie des conteneurs se rapprochent de la virtualisation tout en étant beaucoup plus légère : un conteneur n'embarque pas forcément un système d'exploitation. De par cette caractéristique très importante, les conteneurs sont ainsi plus appropriés pour rendre les applications portable (d'une machine à une autre ou d'un Cloud à un autre) que les VM.

La technologie Docker

À l'origine, Docker est une technologie issue du monde Open Source. L'entreprise de la Silicon Valley exploite en effet plusieurs composants du noyau Linux afin de concevoir ses conteneurs (LXC, Libvirt …).

Lancé par le français Solomon Hykes, Docker permet de faciliter les déploiements d'application et la gestion du dimensionnement de l'infrastructure. Cette technologie s'appuie sur une brique d'API standard (LXC sur Linux et Windows Server Container sur Windows) afin de fournir une couche d'abstraction permettant aux conteneurs de s'exécuter sur n'importe quelle machine. Docker à l'avantage d'être bien plus léger qu'une machine virtuelle. Le lancement d'un conteneur est également plus rapide, ce qui en fait une solution privilégiée pour le déploiement de ses applications.

Docker ne fait pas qu'aider les entreprises dans leurs déploiements, il permet également d'accélérer les évolutions des écosystèmes Cloud, et ça Microsoft, Amazon ou encore Google l'ont bien compris. En effet, avec les conteneurs, il est très facile de déployer les services Cloud on-demand. Par exemple, il est judicieux de conteneuriser les bases de données (MySQL, SQL Server …) afin de les déployer très rapidement selon la demande des utilisateurs.

Une réponse concrète à certains scénarios de développement

Cette technologie apporte une réponse concrète à certains scénarios de développement. Par exemple, vous souhaitez tester une nouvelle version d'un Framework sur votre applicatif ? Très simple ! Docker est fait de plusieurs images Docker qui s'empilent pour créer votre image Docker, il suffit donc de remplacer la couche souhaitée par la nouvelle version afin d'essayer votre application. Avec une VM, cela devient compliqué de changer de version car il faut désinstaller, puis installer la nouvelle version. Le temps perdu peut être conséquent.

Il en va de même pour la mise en production des applicatifs. Avec Docker, si l'application fonctionne sur votre machine, vous serez certain qu'elle fonctionnera sur l'environnement de production. Docker permet d'encapsuler toutes les dépendances relatives au système d'information, et ceci tout en conservant des systèmes sous-jacents propres (votre machine de développement par exemple). Vous n'avez pas besoin d'installer les dépendances, tout est embarqué dans le conteneur.

Le grand avantage de Docker est donc sa portabilité. Il est tellement portable qu'il est possible de faire tourner des images Docker sur des objets connectés. À partir du moment où des terminaux embarquent des noyaux Linux (Raspbian par exemple sur des Raspberry), il est possible de faire fonctionner Docker sur des objets connectés. Docker propose également depuis longtemps des outils Open Source afin de manipuler des conteneurs et tester des architectures sur des objets connectés : LinuxKit.

Une limite persiste tout de même avec Docker : il n'est pas possible de faire fonctionner des images créées avec Linux sur Windows, et réciproquement. Cependant, les équipes de Docker travaillent activement avec les équipes de Microsoft pour pallier à ce manque (notamment avec LinuxKit car il faut un minimum de noyau Linux pour faire fonctionner ce genre d'image Docker).

Installer Docker

La documentation de Docker explique très clairement comment installer Docker sur Linux et Windows. Sur Linux, c'est une série de commandes à lancer. Pour Windows, il faut installer Docker for Windows permettant ainsi de disposer de la technologie sur son poste. Une fois installée, on peut tester le CLI via la commande suivante :

> docker info

Cette commande donne accès à la version courante de Docker, mais également à toutes les images et les conteneurs du poste de travail. Afin de mieux tester l'installation, il suffit de lancer la commande suivante, qui va lancer une image très simple de test :

> docker run hello-world

L'image n'étant pas sur votre machine (si c'est la première fois), Docker va télécharger l'image et lancer le conteneur. Un message apparaît alors dans la console montrant que l'installation s'est bien passée :

La commande suivante permet de vérifier les images qui ont été téléchargées sur votre machine :

> docker image ls

Ensuite, cette commande permet de lister les conteneurs qui sont en cours de fonctionnement sur votre machine :

> docker container ls --all

Enfin, pour arrêter un conteneur, il suffit de lancer la commande suivante :

> docker stop NOM_DU_CONTENEUR

Les images

Un conteneur se construit à partir d'une image. Mais à partir de quoi se construit une image ? Docker se base sur des fichiers intitulés Dockerfile pour construire les images. Un Dockerfile contient une série d'instructions que Docker va exécuter afin de construire l'image finale qui va être stockée dans un registre d'image Docker. Le site Docker contient son propre registre public : https://hub.docker.com/.

Vous pouvez vous-même publier sur ce registre. Beaucoup de grandes compagnies comme Microsoft ont déjà poussé des images Docker sur ce registre afin que le grand public puisse les utiliser. Sinon, vous pouvez toujours installer un Hub privé sur vos serveurs, ou encore utiliser des services clé-en-main disponibles sur Azure ou AWS pour constituer vos Hub privés.

Une image est constituée de couches successives d'autres images Docker. Ainsi, lors de la constitution de son image Docker, on va commencer par s'appuyer sur une autre image afin de bénéficier des commandes lancées précédemment dans cette image. Dans un fichier Dockerfile, la ligne ci-dessous permet de s'appuyer sur une image comportant déjà le Framework .NET Core en version 2.2 :

FROM microsoft/dotnet:2.2-aspnetcore-runtime AS base

La commande FROM permet cette succession de couche d'image Docker. À partir de là, notre image est capable de lancer des projets .NET Core car l'image microsoft/dotnet:2.2-aspnetcore-runtime a déjà installé le runtime .NET Core.

Analyse du Dockerfile

Visual Studio 2017 intègre un template de projet avec un Dockerfile déjà intégré permettant de démarrer rapidement sur un projet ASP.NET Core avec Docker. Nous allons décortiquer le Dockerfile généré afin de mieux comprendre ce que fait Docker :

Les trois premières lignes permettent d'exposer le port 80 du conteneur vers l'extérieur. Cela veut dire que lorsque l'application .NET Core va fonctionner, son port 80 va automatiquement être exposé vers l'extérieur, garantissant les échanges entre le monde extérieur et l'application .NET Core fonctionnant à l'intérieur du conteneur.
Les lignes suivantes permettent de copier le .csproj du projet dans le conteneur Docker afin de restaurer les paquets Nuget :

Ensuite, les 3 lignes suivantes permettent de copier tout le code source afin de lancer la commande dotnet build à l'intérieur du conteneur :

Enfin, Docker va lancer une publication via la commande dotnet publish, puis va lancer l'applicatif qui en résulte :

Quelques remarques par rapport à ces dernières lignes :

  • FROM build AS publish permet de n'utiliser que l'image avec le SDK .NET Core, c’est-à-dire les commandes de build. Ceci est une micro-optimisation car l'image est plus petite que si on prenait l'image avec tout le SDK .NET Core ;
  • FROM base AS final permet d'utiliser uniquement l'image avec le runtime .NET Core pour lancer le projet.

Syntaxe personnalisée

Docker possède sa petite syntaxe personnalisée pour effectuer quelques commandes dans un Dockerfile. La documentation est très bien fournie à ce sujet : https://docs.docker.com/engine/reference/builder/.

Une fois votre fichier complété, il suffit de lancer la commande suivante dans le dossier qui contient le Dockerfile :

docker build .

Une fois lancé, on peut s'apercevoir que Docker lance les commandes inscrites dans le Dockerfile sous la forme d'étapes qui s'enchaînent dans la console :

Si vous lancez une deuxième fois la commande, on peut s'apercevoir que Docker réutilise le résultat de la dernière build sur chaque couche déjà utilisée. C'est ici toute la puissance de Docker : ce système de cache permet de rapidement construire des images qui peuvent au final être conséquentes.
Au final, Docker indique que la build s'est bien passée. Nous n'avons pas donné de nom à notre image, mais Docker se charge de donner un identifiant unique pour pouvoir la retrouver.

Cette image est pour le moment sur votre ordinateur, et peut être lancée à tout moment via la commande suivante :

docker run f0568f9b129c

Nous pouvons constater que l'application se lance comme si on l'avait lancée sur le PC, mais ici le serveur fonctionne dans le conteneur Docker.

C'est ici toute la force de Docker. Il permet d'encapsuler l'exécution d'une application, et ainsi de la rendre portable de machine en machine. Notre application fonctionnant sur notre machine de développement, nous sommes certains que cette application fonctionnera sur un environnement différent supportant Docker (pré-production, production …).

Christophe Gigax

Technical Leader Microsoft et Angular  - MVP Visual Studio & Development Technologies

Retour de Hackathon: Projet de gestion des files d'attentes dans les services d'urgences

Le projet que notre équipe a retenu est le suivant : Comment répondre à l’incompréhension en salle d’attente dans les services d’urgences ? Les patients en service d’urgence sont souvent dans l’incompréhension : pourquoi d’autres personnes sont-elles prises en charge avant eux ? Pourquoi l’attente est-elle aussi longue ? Autant de question pour lesquelles le patient en salle d’attente n’a pas de réponse. Pour résoudre ces problématiques, nous souhaitions développer une application Web reliée à un système en salle d’attente. Celui-ci afficherait un ordre de passage anonyme intégrant un code couleur selon l’urgence de l’intervention.

Pour répondre à cette problématique, nous avons choisi de concevoir une plateforme SaaS avec une architecture Microservice. Le modèle SaaS nous permet de déployer rapidement le système dans tous les services d’urgence qui en aurait besoins. De plus, l’architecture Microservice nous permet d’être plus souple au niveau des technologies utilisés (stockage ou code), de la mise à jour des services et de l’hébergement. Vous trouverez ci-dessous la liste des technologies que nous avons utilisez pour mettre en œuvre notre solution :

  • ASP.NET Core Web API pour les APIs de la plateforme ;
  • Entity Framework Core pour le requêtage en base de données ;
  • Angular pour la partie front ;
  • Azure pour l’hébergement ;
  • SQL Database pour le stockage des comptes utilisateurs ;
  • Azure Table Storage pour le stockage des patients et de leur statut ;
  • Azure API Management pour la traçabilité et la sécurité des APIs exposées ;
  • Optical Character Recognition pour la détection de carte vitale ;

Notre application s’articule autour de 3 écrans principaux. Le flux d’échange de l’information est le suivant :

  • Formulaire de saisie des informations pour le patient, indiquant la où se trouve la douleur et quel est son degré d’intensité. Un numéro unique lui est alors attribué pour suivre son avancement dans la chaîne décisionnel ;
  • Envoi des infos au médecin du service afin qu’il évalue le degré de sévérité de l’urgence (urgent, peu urgent, pas du tout urgent …) selon les informations saisies par l’utilisateur. Il dispose tout de même du numéro du patient pour demander des informations si besoin ;
  • Liste des numéros de patient (pour la confidentialité) afin qu’il puisse suivre son avancement dans la salle d’attente.

Le premier écran est donc le formulaire de saisie. Nous avons utilisé Angular Material pour les composants graphiques, et nous avons rajouté un peu d’intelligence dans le formulaire : le patient peut prendre en photo sa carte vitale afin de préremplir ses informations. Cela permet notamment d’aller plus vite lors de l’arrivée de patient. Pour faire la détection de texte, nous utilisons OCR dans Azure qui sait analyser une photo et nous renvoyer les textes de la photo.
Le deuxième écran concerne la liste des fiches patients en attente de validation par le médecin. Cette liste se met automatiquement à jour lorsqu’un patient à validé son formulaire. Cela est possible grâce à SignalR et les WebSockets qui nous permettent de communiquer en temps réel entre le client Angular et le serveur ASP.NET Core.

Enfin, le troisième écran permet de suivre anonymement la liste d’attente avec les numéros et la couleur qui indique la sévérité de l’urgence.
Au niveau technique, voici l’architecture retenu pour notre solution SaaS :

Nos Web Services sont les suivants :

  • Authentification : connexion à un compte utilisateur (représentant le service d’urgence) via des identifiants. Ces derniers sont stockés dans une base SQL classique requêté via Entity Framework Core. Si l’authentification est correcte, le serveur renvoi un jeton JWT permettant au client de s’authentifier sur le reste de la plateforme ;
  • Data : service de gestion des données des patients. Nous avons utilisé Azure Table Storage pour stockés ces infos pour 2 raisons : c’est du NoSQL et comme nous n’avons pas de liens entre les données, cela nous convenait largement. Ensuite le mécanisme de partitionnement des données via une clé unique nous permet de gérer le côté SaaS de la plateforme au plus près des données. Cette clé unique, appelé PartitionKey, est ainsi le liant fort entre la plateforme SaaS et les données puisque chaque client de la plateforme va devoir rajouter cette clé dans toutes ses requêtes. Cet identifiant est donné lors de l’authentification.
  • OCR : service de reconnaissance de Carte Vital. Cette API accepte un fichier en entré, puis va l’envoyer au système de reconnaissance d’image dans Azure pour effectuer le traitement. L’API va ensuite traiter le résultat et le renvoyer proprement au client.

Le tout respecte les normes REST pour la communication entre le client et la plateforme au travers de Azure API Management. Ce service nous permet de centraliser les APIs des microservices au sein d’un même service de gestion d’API. De ce fait, cela nous permet de monitorer les services, de contrôler les flux entrant et sortant, de rajouter des restrictions et des quotas si besoin. Le client n’a plus qu’à connaître un seul domaine pour requêter la plateforme, et non un domaine par service.
L’hébergement est de l’Azure pour assurer la haute disponibilité de la plateforme SaaS. Nous aurions aimé aussi intégrer Azure Service Fabric comme orchestrateur de nos services afin de garantir un bon niveau de service lors de la monté en charge de la plateforme. Malheureusement nous manquions de temps et de recul sur la fonctionnalité pour le finaliser jusqu’au bout.
Bravo à toute l’équipe pour la réalisation de ce projet innovant quasiment prêt pour une production au bout de 48 heures de développement : Allan, François, Jordan, Pierre et Christophe.

Christophe Gigax

Technical Leader Microsoft et Angular  - MVP Visual Studio & Development Technologies

Azure – Introduction au Serverless

Au sein d’une architecture classique d’entreprise, les systèmes d’informations sont généralement hébergés sur des serveurs, connectés entre eux via des réseaux puis exposés au grand public via Internet. Ce modèle de conception a perduré pendant des décennies, et a permis aux hommes de développer des systèmes de plus en plus complexes afin de satisfaire aux besoins métiers divers et variés.
Les équipes de développement font ainsi faces à plusieurs problématiques d’ordres classiques tels que les bugs et la qualité, mais malheureusement aussi ils doivent côtoyer les problèmes d’infrastructures, de serveurs, de machines virtuelles ou encore de réseau qui peuvent très rapidement devenir une vraie plaie pour l’équipe.
Le concept de Serverless peut paraître bien flou aux premiers abords. Que veut dire Serverless ? Quel est la place des machines dans tout cela ? A qui est délégué la gestion des serveurs ? Quel est le rôle du développeur dans tout cela ?

La notion de Serverless

La première notion importante du Serverless est que la gestion des serveurs et des machines est complètement déléguée au fournisseur Cloud. Le développeur ne se préoccupe que de son code et de son workflow d’exécution, et le fournisseur s’occupe d’héberger cela sur des machines appropriées et au bon endroit tout en garantissant une haute disponibilité du code qui est hébergé. Ainsi, la gestion de la scalabilité de l’application est également automatiquement gérer par le fournisseur Cloud : plus il y a de code a exécuté, plus le fournisseur va s’occuper de « scaler » la logique métier pour suivre la cadence.
Ensuite, la deuxième notion importante est le coût d’exécution du code : si aucune demande n’est effectuée sur l’application Serverless, aucune facturation n’est opérée sur le client. Cela permet assez facilement de réduire de 50% (selon les cas) les coûts d’infrastructure, ce qui n’est pas négligeable. Finalement, le client ne paie que lorsque l’application est en activité.
Enfin, la troisième notion est le mode d’exécution du Serverless : par événement. Vu que l’application ne fonctionne pas tout le temps, il faut bien des déclencheurs pour « réveiller » le code hébergé. C’est là que les événements rentrent en jeu, et il en existe approximativement 200 dans Azure : ajout d’un fichier, appel à une API, envoi d’un Tweet, réception d’un e-mail, déclenchement d’une CI/CD et bien plus encore. Ces déclencheurs sont vraiment le liant fort d’une application Serverless, et continuellement de nouveaux sont ajoutés à la plateforme pour enrichir l’offre.

Les offres Serverless chez Azure

La plateforme Cloud de Microsoft est particulièrement bien fournie en termes de technologie Serverless. Dans cette section nous allons faire un tour d’horizon des offres Azure qui sont Azure Functions, Logic App et Event Grid avec un exemple pour illustrer.

Azure Functions

Les Azure Functions représentent un concept dans Azure permettant de lancer du code C#, F#, JavaScript ou TypeScript au déclenchement d’une action. Le développeur n’a pas à se soucier d’où et comment sont code est hébergé : il est déclenché selon un événement particulier, puis effectue une action afin d’interagir avec la plateforme Cloud : écriture dans une base de données, retour d’une valeur au format JSON … La documentation fournie tous les déclencheurs possibles selon la version d’Azure Functions : https://docs.microsoft.com/fr-fr/azure/azure-functions/functions-triggers-bindings.

Le gros avantage des Azure Functions est la scalabilité et la souplesse que peut apporter ce genre de solution car il est bien plus facile de mettre à l’échelle une fonction d’une application toute entière. Une Azure Functions doit rester courte et concise et ne représente qu’un seul traitement. S’il le faut, la plateforme peut comporter plusieurs Azure Functions qui s’enchaînent pour former un workflow (si le workflow est conséquent, il vaut mieux coupler cela avec Azure Logic App). 
Sur le portal Azure, lorsqu’on souhaite créer une Azure Functions, ce dernier nous propose plusieurs scénarios prédéfinis. Le premier (Webhooks + API) nous donne la fonction suivante :

Cette fonction récupère simplement un paramètre dans l’URL intitulé name et le renvoi dans la réponse avec un code HTTP 200. S’il n’existe pas dans l’URL, la fonction va chercher dans le corps de la requête. Sinon, la fonction renvoi un code d’erreur.
En cliquant sur le bouton Run au-dessus du code, le développeur peut tester sa fonction en modifiant la requête entrante : en-tête, corps, verbe, paramètre d’URL … Le développeur peut également récupérer un lien afin de tester sa fonction via l’URL du site. En étudiant plus précisément la fonction, on se rend compte qu’elle est sauvegardée dans un fichier run.csx. L’extension .csx indique que les Azure Functions sont simplement des scripts C# qu’on exécute à la volée selon les besoins.
Sur la gauche du code, on peut voir que les Azure Functions regorgent de fonctionnalités.

  • Functions : Azure Functions classique avec des options de déclenchement et de monitoring ;
  • Proxy : fonctionnalités capables de créer plus rapidement des APIs à partir d’Azure Functions, Cela permet notamment d’agréger plusieurs fonctions en un seule API ;
  • Slots : options de déploiement en silo pour les Azure Functions (prod, staging …).

Le développeur a ainsi accès à un paramétrage plutôt complet pour ses fonctions. Les IDE Visual Studio et Visual Studio Code permettent également de développer des Azure Functions localement ou en mode remote selon les besoins.

Azure Logic App

L’offre Logic App de Azure permet de connecter l’application Cloud de diverses manières et avec des centaines de connecteurs différents (Office 365, Twitter, Dropbox …) et ceci avec 0 ligne de code. Logic App est vraiment une solution d’intégration pour la plateforme Cloud, et le principal concept est le workflow : on enchaîne des étapes qui vont chacune pouvoir interagir avec les centaines de connecteurs disponibles pour lancer une action, un code, une Azure Function … Il est même possible de mettre des conditions et des boucles. Sur le même principe que les Azure Functions, une Logic App se déclenche lors d’un événement.
Au niveau des actions possibles, il en existe des centaines également. Par exemple, vous pouvez demander aux cognitives services d’analyser un texte afin de déterminer l’humeur du message. Ou alors vous pouvez envoyer un mail, un SMS, un message dans Teams ou Slack, et tout ceci sans une seule ligne de code.
Dès la création d’une Logic App, Azure nous propose les déclencheurs les plus populaires que le développeur peut utiliser. En dessous Azure nous propose des templates de Logic App les plus communs.

Prenons le déclencheur ‘When a http request is received’. Azure va générer pour nous une URL lorsque nous allons sauvegarder l’application. On se retrouve alors dans un designer où l’on peut facilement rajouter de nouvelles étapes, tout ceci sans code. Il est également possible de rajouter un schéma JSON pour le corps de la requête HTTP afin d’obliger l’appelant à mettre des paramètres.

Ensuite, nous allons rajouter une étape d’envoi de mail. Le workflow de notre application est donc le suivant : lorsque que l’URL est appelée, envoi d’un mail avec le paramètre dans le corps du mail. L’étape de l’envoi de mail est la suivante :

Avec un utilitaire comme Postman, il suffit de simuler un envoi sur l’URL fourni par Azure, et constater qu’un email est envoyé à chaque fois que l’URL est requêtée.

Le développeur a accès ainsi à des milliers de scénario possible selon son workflow et les déclencheurs qu’il va utiliser.

Azure Event Grid

Avec Azure Functions et Azure Logic App, les applications basées sur événements peuvent grandir et l’architecture devient alors trop complexe pour assurer une maintenabilité optimale. L’offre Azure Event Grid intervient afin de simplifier la gestion des événements dans ce genre de situation.

L’objectif de Event Grid est de proposer une fonctionnalité capable de centraliser les événements tout en garantissant le minimum de latence possible entre la ressource qui a émis l’événement et l’application qui s’est branché dessus, pour atteindre le quasi temps réel. Event Grid met en œuvre également des fonctionnalités telles que :

  • Routage intelligent des événements ;
  • Filtrage des événements ;
  • Connexion multi-application ;

Il existe 2 notions importantes dans Event Grid : les Topics et les WebHooks. Les Topics sont des regroupements d’événements, permettant ainsi de les catégoriser. Un émetteur va alors envoyer des événements à ces topics, et peut également répondre à des événements relayer par les topics. Les WebHooks sont simplement des callbacks HTTP qui vont permettre de répondre aux appels envoyés aux topics.
Dans le portal, lorsqu’on créé un Event Grid Topics, Azure nous génère une URL qui va pouvoir être utilisé par les autres ressources Azure pour publier des événements.

Ensuite, il suffit de rajouter des Subscriptions. Avec notre Logic App précédente, il suffit de mettre l’URL de la Logic App en Endpoint afin qu’elle réponde lorsque le Topics est appelé, et le tour est joué !

Conclusion

Le Serverless dans Azure possède un grand nombre d’avantage : interconnexion, réactivité, souplesse, scalabilité. Les piliers de la programmation Serverless restent indéniablement les événements générés par les différentes ressources et / ou systèmes gravitant autour de Azure, et offre ainsi des capacités infinies tant sur le plan de l’intégration avec des systèmes d’entreprises que des scénarios possibles.

Christophe Gigax

Technical Leader Microsoft et Angular  - MVP Visual Studio & Development Technologies

Protractor – A l’assaut des tests E2E avec Angular

Les tests restent un vaste sujet sur lequel chaque projet et chaque développeur devrait réellement se tourner. Il existe évidemment plusieurs types de test. Par exemple, les tests unitaires sont là pour tester et valider un morceau de code bien particulier pour une fonction. Ils sont faciles à écrire, et ils garantissent un comportement bien précis du système à un instant T. Toute refactorisation ou modification du code doit garantir que les anciens tests fonctionnent toujours afin de s’assurer qu’on a rien casser.
Cependant, pouvant nous garantir que tous les cas sont bien testés ? Que se passe-t-il lorsque des cas d’utilisations croisent plusieurs fonctionnalités différentes de l’application ? Quel est le comportement de l’application lorsque les performances sont mauvaises ? Comment tester dans mon cas qu’une modification dans le système a bien été répercuté 3 écrans plus loin ? Pour répondre à ces questions, les tests unitaires ne suffisent plus car leur périmètre est bien trop restreint. C’est là que Protractor vient nous aider avec les tests end-to-end.
Les tests end-to-end (ou E2E) sont particulièrement utiles lorsqu’on veut tester des fonctionnalités très complètes où le développeur a besoin d’une grande partie de l’application. Leur but est notamment de tester certains modules complexes et critiques de l’application (enchaînement de plusieurs pages, interaction avec de multiples composants …) afin de garantir que chaque nouvelle version de l’application conserve le comportement souhaité. En pratique, les tests E2E vont venir simuler des interactions utilisateurs avec un navigateur : clique sur un bouton, écriture dans une zone de saisie, vérification de contenu … Pour cet article, nous allons nous baser sur une nouvelle application Angular généré à partir de Angular CLI.

Vous avez dit Protractor ?

Protractor est un Framework de test E2E développé pour Angular et AngularJS. C’est un superset de Selenium et se base sur le même driver que ce dernier afin d’envoyer des commandes au navigateur. Le développeur va pouvoir écrire des tests (en Typescript) et des commandes comme si c’était l’utilisateur final qui manipule le navigateur. Il va ainsi pouvoir :

  • Ecrire dans des champs de saisie ;
  • Cliquer sur des boutons ;
  • Rechercher des éléments dans le DOM par leur contenu ;
  • Faire bouger la souris à des endroits bien précis.

Le Framework a été spécialement conçu pour Angular, cela veut dire qu’il intègre des APIs et des mécanismes uniques permettant de tester des éléments spécifiques à Angular sans effort particulier. De plus, nous savons qu’Angular effectue souvent des opérations asynchrones (appel API, détection de changement …) et Protractor est capable d’attendre n’importe quel tâche avant de continuer l’exécution du test.

La configuration de Protractor

Tout d’abord, voyons ce que Angular CLI a généré pour nous au niveau du fichier protractor.conf.js. Vous l’aurez compris, ce fichier permet de configurer Protractor lors de son lancement.

Les différentes options sont les suivantes :

  • allScriptsTimeout : définis le temps d’attente de Protractor pendant que Angular fait des choses en arrière-plan ;
  • specs : chemin vers les fichiers .spec de test de Protractor. Les chemins peuvent contenir des glob patterns afin de simplifier les écritures ;
  • directConnect : option afin que Protractor communique directement avec le navigateur sans passer par le driver Selenium. Cette fonctionnalité fonctionne avec Chrome ou Firefox, permettant ainsi que les scripts se lancent plus vite au démarrage ;
  • baseUrl : URL sur laquelle Protractor va envoyer les commandes ;
  • framework : framework de test utilisé par Protractor (par défaut Jasmine) ;
  • jasmineNodeOpts : paramétrage du framework de test. Dans l’exemple ci-dessus :
    • Les couleurs seront affichés dans le terminal ;
    • Les tests échoueront après 30s d'inactivité ;
    • L'affichage des résultats a été modifiés.
  • onPrepare : fonction exécuté acant le lancement des tests. Dans l'exemple, on indique quel fichier de configuration le transpilateur TypeScript doit utiliser. Ensuite, on ajoute un reporteur pour l'affichage des résultats à Jasmine.

Lancement des tests

Après avoir analyser notre configuration, pourquoi ne pas lancer nos tests ? Il suffit de lancer la commande suivante :

Cette commande d’Angular CLI va lancer Protractor et les tests associés. Dans la console, on voit que l’outil compile le projet, le lance et exécute les tests en retournant le résultat dans le terminal.

Note : on peut noter au passage que Protractor a bien utiliser directement le driver de Chrome, et non le driver de Selenium pour communiquer avec le navigateur.

Regardons maintenant d’un peu plus près le fichier de test app.e2e-spec.ts.

Les mots-clés importants ici sont :

  • describe : définit un jeu de un ou plusieurs tests qui sont regroupés sous un seule libellé ;
  • beforeEach : définit la fonction qui va être appelé avant chaque exécution de test. Il existe également beforeAll, afterEach et afterAll.
  • it : définit un test qui va être exécute par le moteur.

Ceci suffit à Protractor pour détecter les tests à exécuter.
Un pattern est régulièrement utilisé dans les tests E2E : le Page Objects pattern. Ce pattern est très simple : il encapsule les informations de la page qui va être testé dans un objet TypeScript afin de les manipuler plus aisément : recherche d’élément dans la page, saisie de texte dans une zone de saisie, clique sur un bouton … Cette technique a plusieurs avantages :

  • Réutilisation de l’objet dans d’autres au besoin ;
  • Simplification et meilleure lisibilité du test. Le code du test ne fait qu’utiliser des expect ;
  • Lors d’un changement dans le test, il suffit de changer qu’à un endroit le code pour répercuter les changements dans tous les tests.

Dans notre exemple, le Page Object utilisé fait 2 choses :

  • navigateTo : navigation vers la page concerné ;
  • getParagraphText : il récupère le contenu de l’élément HTML suivant le sélecteur app-root h1.

Quelques cas plus complexes

Pour aller plus loin dans nos tests E2E, nous allons tester de la saisie et des cliques sur des boutons afin de valider que notre application fonctionne correctement. Rajouter les morceaux de code suivant.

app.component.html

app.component.html

Note : n’oubliez pas d’importer FormsModule dans AppModule.

Le code rajouté est très simple et met en œuvre plusieurs fonctionnalités d’Angular comme :

  • clique d’un bouton ;
  • boucle sur tableau ;
  • binding bidirectionnel via ngModel ;
  • binding unidirectionnel.

Commençons par étoffer notre Page Object afin de manipuler notre page plus facilement. Nous allons rajouter les méthodes suivantes :

  • addName : ajout d’un nom dans notre collection ;
  • removeName : suppression d’un nom ;
  • sendKeysToInputNameByIndex : écriture dans une zone de saisie. Comme nous avons une boucle, l’index va nous permettre d’identifier précisément dans quelle zone nous voulons écrire ;
  • getTextContenetByIndex : récupère le contenu du texte bindé à la variable name.text, toujours selon un index précis (car il peut y en avoir plusieurs).
  • getNumberOfInput : récupère le nombre total de zone saisissable.

Nos cas de tests seront les suivants :

  • cas 1 : ajout d’un nom, écriture dans la zone de saisie et vérification que le nom est correct ;
  • cas 2 : ajour d’un deuxième avec un nom différent, vérification que le nom est correct et qu’il est différent du premier ;
  • cas 3 : suppression d’un nom, vérification qu’il n’en reste plus qu’un ;

Dès la première lecture, les tests sont très lisibles et compréhensibles grâce notamment à notre Page Object. Quelques remarques :

  • Les méthodes d’actions de notre Page Object retournent tout le temps les actions effectués par Protractor (getText(), sendKeys() …). Cela est dû au fait que ces méthodes retournent des Promises, et Protractor sait les attendre automatiquement si leur exécution est répercuté à la fonction racine du test (donc le it) ;
  • Les tests sont dépendants les uns des autres. En effet, le premier test rajoute le premier nom et teste que ce dernier est valide. Pour le deuxième test, on ne rejoue pas tout le scénario du premier test car cela serait trop fastidieux. Les scénarios sont donc liés et donc les it sont liés entre eux. Il est donc important de conserver des tests liés dans des describe consistent, et ce sont les describe qui doivent être indépendant les uns des autres.

Après lancement on peut voir le résultat positif de nos tests.

Conclusion

Les tests E2E sont là afin de tester des cas complexes dans l’application : nos cas de tests auraient facilement pu être écrit en tests unitaires. Cependant, ils illustrent assez bien l’utilisation de l’outil sur des cas simple. Cependant, il est clair que l’écriture des E2E n’est qu’une étape avancée dans le testing Angular : il faut d’abord s’assurer d’avoir écrit tous les tests unitaires avant de s’attaquer aux tests E2E. De plus, les tests E2E peuvent être chronophage en temps de maintenabilité car selon les cas ils peuvent vitre devenir instable. Il faut donc prendre soin et y apporter un soin tout particulier si on veut qu’ils restent efficaces.

Christophe Gigax

Technical Leader Microsoft et Angular  - MVP Visual Studio & Development Technologies