Sommaire
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.
Le développeur backend “Cloud Native”
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 :
- Le FaaS (Function as a Service) : associé à un pattern de nano-service, il permet de ne pas réserver des ressources pour une tâche peu utilisée ou sans Qualité de Service (QoS),
- Le CaaS (Container as a Service) : associé à un pattern de micro-service, il est très utilisé actuellement pour offrir une scalabilité rapide et une densification du Système d’Information (en particulier dans une approche FinOps),
- Le PaaS (Platform as a Service) : associé à un pattern de service, il permet aux développeurs de déployer une application dans le Cloud sans forcément monter en compétence dessus.
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 :
- Pour scaler uniquement ce qui est utilisé dans le service et ne pas instancier du code non utilisé
- Pour pouvoir itérer rapidement - un micro-service doit pouvoir être re-développé en deux sprints
- Pour ne définir qu’un objet métier et être humainement compréhensible - principe KISS (Keep It Small and Simple)
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 :
- Automatiser la création et destruction des dépendances de son code
- Automatiser les exports et imports de ses données (Plan de Reprise d’Activité)
- Toujours s’assurer de l’approche stateless (suppression / recréation de son instance ou multiples instances accessible via un loadbalancer)
- Mettre en place des alertes et des dashboards pour son application
- Mettre en place des tests d’intégration, de performance et des sanity check (en plus des tests unitaires)
- Privilégier les outils headless pour les intégrations
- Faire des déploiements réguliers dans une plateforme de développement dans un environnement Cloud
- Mettre en place des endpoints “health” et “live” pour s’assurer de la bonne santé de son application et indiquer quand elle est bien démarrée
- Architecturer son code pour optimiser la scalabilité, le redé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.
Cas client
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 :
- Tous les développements soient versionnés dans GitHub en utilisant le semantic versionning.
- La chaîne de CI du client est sur CircleCI.
- Le client utilise déjà un “Identity Provider” qui fonctionne sur le protocole “OpenId Connect”. Il faudra donc utiliser ce provider existant.
- Bien sûr l'ensemble des besoins métiers sont également définis
Besoins des développeurs
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 :
- lancer les tests unitaires
- lancer un scan des CVE
- builder l’application
- envoyer le container sur Google Container Registry ou un package dans Google Cloud Storage (GCS)
- stocker le swagger et les résultats du build (code coverage, …) dans GCS
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 à :
- Des documents statiques, un nginx fera l’affaire
- Un webservice REST, vous pouvez utiliser un projet appelé json-server
- Un stockage en mode objet via fake-gcs-server pour GCS ou Minio pour S3
Besoins de l’infrastructure
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é.
- Pour le monitoring, Prometheus est l’outil utilisé dans le cadre des applications déjà en place - l’operator Prometheus a été installé pour cela
- Les logs des Pods sont remontés par Fluentd vers un centralisateur
- La traçabilité est assurée par un Jaeger s’appuyant sur un ElasticSearch
- L’alerting est fait par des “AlertManager” - également installés par l’Operator Prometheus
Organisation du repository
En résumé l’organisation d’un repository applicatif doit être :
- .circleci... le pipeline de CI pour l’application, le mock et le templating Helm
- src/... code source de l’application
- mock/... code source du mock
- mock/Dockerfile...
- docs/... la documentation du service - technique et métier
- tests/... les tests d’intégration / de performances...
- helm/... le packaging helm
- terraform/... le code d’infrastructure
- dbmigrate/... pour la migration de la base de donnée
- Dockerfile
- docker-compose.yaml... pour lancer le projet localement (build et run)
- deploy.sh
- rollback.sh
Réponse technique aux demandes clients
Ici nous allons voir les réponses aux demandes du client et le choix du développeur backend.
Développement de l’application
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 :
- Golang : idéal pour un microservice REST dans un conteneur, il est léger et assez facile de prise en main
- Python et Flask : probablement le langage commun entre tous vos développeurs, il est indispensable pour faire de l’IaC. Avec Flask il existe de très nombreux raccourcis pour simplifier vos développements
- Ruby et Sinatra : loin de la lourdeur de Ruby on rails, c’est une bonne solution si vous aimez Ruby ;-) Certaines librairies Ruby sont uniques et indispensables pour diminuer le temps de développement
- Java et Spring : si vous avez l’habitude de l'écosystème Java, Maven… probablement le langage le plus riche en librairies
- NodeJS et express : l’implémentation de javascript “côté serveur”. Intéressant pour des développeurs “fullstack”.
- DotNet Core : pour les utilisateurs Microsoft, il est compatible avec les conteneurs et promet de bons résultats de performance.
- Rust : probablement l’outsider de Golang - à suivre…
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.
Protocole de connexion
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.
gRPC
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.
Chiffrement des flux
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.
Cache
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.
Observabilité
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 :
- un Prometheus installable via le Custom Resource Definition (offert par le Prometheus Operator),
- un ensemble de PrometheusRule définissant les alertes des microservices,
- des ConfigMap contenant des dashboard Grafana.
Performance et scalabilité
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.
Conclusion
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 ?