Il n’y a pas de règle “absolue” dans le choix d’une stack technique, il faut s’adapter au besoin du client mais surtout du projet. Il faut donc lire cet article comme un guide dans la mise en place d’une stack idéale. Enfin il faudra remettre en question ces choix avec le temps et la sortie de nouveaux outils.
Pour poser un cadre à cet article, nous verrons donc les demandes d’un client fictif puis les réponses que le développeur doit apporter.
On définit par “Cloud Native” ce qui est conçu pour et avec les outils du “Cloud”. C’est une définition bien vague qui est limitée par la diversité des fournisseurs d’outils Cloud. On pourrait également définir ses applications comme celles tirant le mieux parti du fournisseur de Cloud sur lequel elles sont déployées.
On peut par contre définir plusieurs catégories à destination des développeurs :
Je mets volontairement de côté le IaaS et la création d’images immutables, le cas d’utilisation devient limité à un passage au Cloud de monolithes en “lift and shift” ou à des applications nécessitant des ressources importantes et spécifiques.
Les backends dits “Cloud Natives” doivent respecter les contraintes suivantes :
Stateless : c’est un standard de facto pour permettre une scalabilité en in et out - et l’utilisation du serverless
Une taille limitée :
En résumé, le backend doit être “twelve factor”.
Pour développer des applications “Cloud Native” il faut aussi prendre des habitudes de développement :
En conclusion, le développeur doit pouvoir prendre du recul sur son application et pouvoir comprendre la roadmap du Product Owner, ainsi que la vision amenée par l’architecte et le socle construit par son équipe opérationnelle.
Ces rôles ont pour objectif final d’améliorer “l’expérience de développement” afin d’assurer le maintien dans l’équipe des compétences et d’accélérer le Time To Market et l’innovation.
Dans ce cas, le client a déjà organisé ses applications autour d’une architecture composée de microservices et nanoservices. Il déploie ses services dans Google Cloud Platform. Ses applications sont, pour la plupart, conteneurisées et lancées via GKE mais il a également des CloudFunctions pour des APIs peu appelées ou à faible QoS et des instances GCE pour deux services ayant de forts besoins de CPU et RAM.
Dans le cadre d’un nouveau projet, il veut faire appel à une équipe de développeurs externes pour une API particulière.
Lors de l’étude du besoin métier, il apparaît que certaines fonctions de l’API doivent être déployées en tant que fonctions et d’autres via des microservices.
Etudions le cahier des charges que le client a déposé avec la demande.
Il est demandé que :
Pour requêter les nouveaux microservices, il doit y avoir une documentation utilisable par les développeurs. En tenant compte des modifications fréquentes de l’API, le client demande :
de la documentation dans un répertoire “docs” à la racine de chaque repository Git. Ainsi la documentation sera versionnée avec le même outil que le code.
la création automatisée d’un fichier OpenAPI compatible avec Swagger lors du build de l’application.
Dans chaque repository, il doit y avoir également la configuration de l’outil de Continuous Integration permettant de :
Le client a pour objectif de reprendre la main à moyen terme sur le développement. Pour reprendre la main, les équipes auront également besoin de lancer la stack en local sur leurs postes. Il faudra leur fournir un fichier docker-compose fonctionnel pour cela. Le fichier permettra de lancer le service localement ainsi que tous les mocks nécessaires. Ainsi un développeur ne maîtrisant pas encore ce service pourra commencer sa tâche sans un travail fastidieux de découverte de la stack nécessaire et des versions compatibles.
Dans ce même objectif, l’ensemble des services pourront être lancés dans un package Docker pour être utilisable “en local” facilement. Seul certains services non mockable Cloud seront utilisables via des plateformes “sandbox” pour les développeurs.
Les tests de performance doivent également être inclus via un outil. Dans ce cadre, il vous faudra tenir X utilisateurs pendant Y minutes en ne dépassant pas x CPU et y Go de RAM.
Dans le cadre d’une architecture microservices, il n’est pas nécessaire d’installer l’ensemble des services dans toutes les plateformes pour effectuer les développements.
Chaque service devra donc offrir un mock sous la forme d’un conteneur. Ce mock sera buildé par la CI et versionné avec les même tags que le service.
Le choix de la technologie de mock dépend du service que l’on veut mocker, ainsi si l’on veut accéder à :
Le projet est entièrement automatisé, Terraform est utilisé pour la création de l’infrastructure GCP et l’équipe utilise Helm (dans sa version 3) pour déployer ses applications.
Dans le répertoire de l’application, l’équipe devra également mettre un répertoire “helm” contenant les templates et le fichier “values.yaml” par défaut. La documentation sera dans le répertoire “docs”.
S’il y a besoin de la création d’éléments d’infrastructure - topic PubSub ou autre - un répertoire “terraform” sera créé également pour mettre les nouveaux éléments. Les “remotes states” des autres layers Terraform sont disponible dans GCS.
Pour le versionning de la base de données, il est attendu un script permettant de migrer et de revenir en arrière.
Une infrastructure a également besoin d’observabilité.
En résumé l’organisation d’un repository applicatif doit être :
Ici nous allons voir les réponses aux demandes du client et le choix du développeur backend.
L’intérêt des microservices est de pouvoir choisir son langage en fonction du besoin. Voici quelques exemples de langages que vous pourriez utiliser :
Il y en a tellement d’autres ! En fonction du cas, vous pourrez choisir le plus adapté au besoin.
La diversité des langages de développement - ou fractionnement - peut faire peur si l’on se positionne dans l’objectif d’une maintenance corrective ou même pour garder une équipe pluridisciplinaire. Il y a un difficile équilibre à trouver autour du nombre de stacks techniques dans une infrastructure.
Le client a besoin de service REST et Json pour ses besoins mais la communication interne entre nos services se fera en gRPC et Protobuf.
Pour permettre une requête extérieur à notre système, des gRPC Gateways seront mis en sidecar de nos applications.
Pour commencer avec gRPC nous devrons créer des “.proto” décrivant le protocole de communication, ces fichiers nous permettront également de définir le contrat d’interface.
Par exemple :
syntax = "proto3";
package test;
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
import "protoc-gen-swagger/options/annotations.proto";
option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
info: {
title: "Users";
version: "1.0";
contact: {
name: "Test project";
url: "https://github.com/wescale";
email: "sebastien.lavayssiere@wescale.fr";
};
};
schemes: HTTPS;
consumes: "application/json";
consumes: "application/x-foo-mime";
produces: "application/json";
produces: "application/x-foo-mime";
};
service UsersService {
rpc GetUser (User) returns (User) {
option (google.api.http) = {
get: "/v1/users/{id}"
};
}
rpc GetUsers (google.protobuf.Empty) returns (Users) {
option (google.api.http) = {
get: "/v1/users"
};
}
rpc CreateUser(User) returns (User) {
option (google.api.http) = {
post: "/v1/users",
body: "*"
};
}
rpc UpdateUser(User) returns (User) {
option (google.api.http) = {
patch: "/v1/users/{id}",
body: "*"
};
}
}
message User {
string id = 1;
string firstname = 2;
string lastname = 3;
string email = 4;
}
message Users {
repeated User list = 1;
}
Grâce à ce descripteur de protocoles, nous serons compatibles avec la plupart des langages ci-dessus et nous pourrons également convertir, toujours grâce au projet “grpc-gateway” nos fichiers descripteurs “.proto” en “json” interprétable par Swagger.
gRPC s’appuyant sur http2, il est facile de chiffrer les flux sur TLS.
Pour tester le principe dès le poste local, nous utiliserons CFSSL, un outil de CloudFlare dont nous avions déjà parlé ici.
Dans le cadre des déploiements dans Kubernetes, il est également possible d’utiliser CFSSL d’après la documentation de Kubernetes.
Pour pouvoir accélérer nos appels, et décharger en partie la base de données, il nous faut stocker des objets, de préférence en Json pour pouvoir les lire indépendamment du langage.
Pour cela, le choix est fait d’utiliser Redis avec un module supplémentaire “ReJson”.
Pour créer des clusters de Redis facilement, nous utilisons un Operator Kubernetes, dans ce cas : “KubeDB” permet de monter un cluster déjà monitoré par Prometheus.
L’ensemble de ces stacks proposent une librairie Prometheus qui répond au besoin de monitoring.
Les métriques métiers seront ajoutées en fonction du besoin métier.
En attendant la sortie d’OpenTelemetry, le tracing sera fait par OpenTracing.
Enfin il sera fourni dans le packaging Helm :
Les tests de performance seront réalisés par artillery.io, je vous conseille l’excellent article sur la question.
Grâce aux métriques Prometheus et aux remontées des tests de performance, il est possible de piloter les HorizontalPodAutoscaler de Kubernetes. Pour faciliter la mise en place vous pouvez utiliser ce projet.
Grâce à l’architecture microservices et aux nouveaux outils, il est demandé aux développeurs de se projeter dans une vision globale de leurs applications. La frontière entre outils de production et de développement est chaque jour moins franche et cela ne peut qu’amener le développeur dans l’état d’esprit “DevOps”.
Le “DevOps” est le prolongement pour la production de l’agilité qui a été intégré au sein des équipes métiers, produit et de développement. Il transforme les équipes de production en fournisseur d’un service “socle” pour l’ensemble des métiers de l’entreprise. Sur ce socle les équipes construisent les produits.
Il est question régulièrement de développeurs “FullStack” dans les annonces d’emplois. Dans ce cadre-là, on entend “développeur frontend et backend”.
Mais le vrai développeur FullStack n’est-il pas celui qui code l’application, son infrastructure et son déploiement ?