Toute l'actualité devOps dans le média Mcorbin
Staff engineers et plus: l'impact transverse veut-il dire dispersion ?
On entend maintenant depuis quelques années de plus en plus parler d’entreprises Françaises mettant en place des career track expertes (ou IC pour individual contributor). On parle souvent d’impact transverses dans ces rôles, mais il a t-il pas un risque de se disperser en faisant cela ? Individual Contributor Comme dit précédemment de nombreuses boîtes Françaises (notamment des boîtes tech type scale-up) mettent en place des "career tracks" pour définir clairement les évolutions possible de leurs employés, avec deux chemins possibles: devenir manager (gérer une équipe, puis gérer un département…) ou devenir expert (staff, senior staff, principal engineer). On va s’intéresser dans cet article à la seconde catégorie. Le but de cet article n’est pas de décrire comment fonctionnent les careers tracks dans la tech, lisez cet article d’Hugo Lassiège avant cet article si vous n’êtes pas familiers avec ces concepts. Disclaimer Comme souvent, notamment sur les sujets de ce type, ce blog est l’occasion pour moi de mettre par écrit des idées qui me trottent dans la tête et de déclencher des discussions intéressantes. Comme on dit souvent, Views are my own, not my employer, et mes articles sont le fruit de toutes sortes d’expériences, discussions, remises en question…actuelles ou passées, et ne reflètent pas forcément comment je travaille au quotidien ;) Impact Le mot "impact" est souvent mentionné lorsqu’on parle de devenir staff engineer ou plus. On s’attend à ce que vous ayez un impact dans votre équipe puis au delà: à l’échelle de la tech, de toute l’entreprise, voir que vous rayonnez à l’extérieur. On trouve beaucoup d’articles expliquant comment devenir staff: "trouver" des sujets transverses sur lesquelles travailler, se faire remarquer… presque parfois pour pour moi à l’excès où le processus de promotion est presque devenu un jeu où il faut "grind" pour monter (mais c’est un autre sujet). De mon côté c’est en partie ma frustration qui me porte, de "il y a des quick win à faire sur ce truc" à "cette techno/pratique que toute la boîte utilise ne devrait pas exister dans tous les mondes du multivers, allons régler son compte à cette dinguerie". Vous avez donc un champ d’action de plus en plus large, avec en parallèle une attente sur faire monter en compétence des gens sur toutes sortes de sujets, savoir débloquer des situations diverses et variées, mettre en place des indicateurs pour que les gens puissent suivre ce qu’il se passe (si j’avais envie de troller, ce qui n’est pas du tout mon genre, je dirai que créer des dashboards Grafana avec du vert et du rouge est peut être le meilleur moyen de rendre des chefs heureux). On vous demandera d’aligner les gens et les équipes pour mettre tout le monde d’accord, savoir prioriser, communiquer avec le management, aller aider dans des incidents et post mortems… Des journées bien chargées en somme. Dispersion Des journées bien chargées, et assez diverses. Enormement de staff engineer et plus le disent: ils font moins de "tech pure" qu’avant. Ceux côté dev vont diront même peut être "c’est devenu bien rare que je code". Ce sont des personnes qui vont plus être là en support, pour coordonner et conduire des changements, pour proposer une certaine vision tech. Vous ne faites au bout d’un moment plus partie d’une équipe, vous êtes en dehors. Mais comment développer l’expertise technique si vous ne pratiquez plus ? Prenons l’exemple fictif d’Yvain, SRE dans une équipe s’occupant de la stack d’observability de l’entreprise (et chevalier sur son temps libre). Yvain a une très grosse expertise sur certains sujets liés au monde de monitoring et a un fort impact dans son équipe et ailleurs grâce à la performance des outils mis en place (tracing, logging, SLO, whatever). Yvain a aussi la chance d’avoir pas mal baroudé dans ses boîtes précédentes et donc a des connaissances assez larges qui lui permettent d’intervenir sur mal de sujets d’autres équipes. Rapidement Yvain va passer Staff engineer, et plus ensuite. Il commence donc à travailler sur toutes sortes de problématiques transverses et interagit avec beaucoup d’équipes sur toutes sortes de sujets. D’ailleurs, il n’a plus trop le temps à consacrer à l’équipe d’où il vient à la base, ce n’est plus vraiment son rôle même. Un jour, Yvain vaque à ses occupations habituelles de staff et se pose une question. Oui, il réussit à porter des sujets transverses, mais quid de son expertise précédente ? Aujourd’hui, ce n’est plus lui qui travaille directement sur l’évolution de la stack d’observability de l’entreprise, pourtant il était très bon à ça et il sait qu’il y avait beaucoup plus à faire pour arriver à l’état de l’art. Mais ce n’est plus son rôle, au mieux il peut donner un coup de main à l’occasion. Est ce que le risque n’est pas d’envoyer vos meilleurs éléments dans des rôles où l’excellence technique et leur capacité délivrer leur est finalement moins utile, car accès sur l’accompagnement et l’alignement (et la politique associée) ? Est ce qu’on ne devrait pas au contraire encourager des individus à devenir de vrais experts reconnus sur leurs sujets (ce qui demande énormément de pratiques et veille au quotidien), et ensuite avoir un impact transverse grâce à l’excellence des solutions qu’ils mettent en place dans un scope précis ? Est ce que l’impact "vertical" n’est pas plus intéressant que l’impact "horizontal" en sautant de sujet en sujet ? Est ce que le discours "fini d’aller mettre les mains dans le cambouis à partir d’un certains niveau" n’est pas en réalité contre productif ? Signé: un senior staff.
Staff engineers et plus: l'impact transverse veut-il...
On entend maintenant depuis quelques années de plus en plus parler d’entreprises...
Source: mcorbin
DevOps: recréer les silos pour une meilleure efficacité ?
DevOps, casser les silos dans les entreprises, faire collaborer équipes pour que tout le monde travaille dans la joie et la bonne humeur… On vise tous ça non ? Mais est-ce que parfois recréer les silos ne serait pas la solution pour une meilleure efficacité ? Casser les silos Je ne vais pas m’attarder sur cette partie, on connait la chanson. On fait du DevOps pour casser les silos car "les ops veulent de la stabilité alors que les dev eux veulent développer et innover le plus rapidement possible." Cette définition est assez incomplète selon moi et laisse entendre que les ops ne savent pas innover ou que les dev ne sont que bons à pisser du code sans soucis de qualité, et à force de la répéter les gens peuvent intégrer ces définitions. Bref, on casse les silos en créant des OKR communs, en facilitant la communication et en faisant intervenir l’ensemble des équipes lors de la conception d’un nouveau projet. Si vous me suivez depuis un moment vous connaissez probablement ma position sur tout ça. Je milite depuis longtemps pour rapprocher les dev et les ops, sur le travail en commun mais aussi sur les pratiques et connaissances techniques: des dev qui gèrent de A à Z leurs services (déploiements en production et astreintes inclus) et ont des connaissances qui ne s’arrêtent pas au code, et des ops qui incluent l’ingénierie logicielle dans leur quotidien en ayant une vraie approche produit pour l’infrastructure (voir un de mes talks sur le sujet, et sans scripts shell svp). Mais donc, pourquoi recréer les silos ? Le déséquilibre Dev et Ops Une notion revient souvent quand on parle de SRE: le fait de pouvoir scale fortement des équipes de développement sans avoir à recruter à la même vitesse des gens côté infrastructure. Le monde de l’infrastructure a énormément évolué ces dernières années (produits de plus en plus performants, automatisation, cloud…) et il est tout à fait possible de gérer des productions assez conséquentes (j’inclus dans "production" la gestion de l’infrastructure et de l’abstraction qu’on construit pour le reste de la tech) avec des équipes réduites. Il est donc très commun d’avoir un ratio par exemple de 1/15, 1/20 ou plus encore entre SRE et le reste du département tech, alors que le scope porté par les SRE peut être extrêmement vaste. Comme dit précédemment ce déséquilibre est bon signe: ça veut dire que l’infra tourne bien et est assez efficace pour que la tech "scale" sans l’impacter. Mais cela peut aussi vite créer un déséquilibre: quand la petite équipe essaye d’influencer d’une manière ou d’une autre la plus grosse, c’est David contre Goliath. A l’inverse, une mauvaise pratique semblant peu dérangeante pour la grosse équipe au niveau local peut fortement impacter la petite qui elle se retrouve en bout de chaîne. Vous allez me dire que dans ce cas on manque d’alignement et on ne travaille pas aux mêmes objectifs, et c’est vrai. Mais c’est là où la réalité rattrape la théorie. Il arrive en effet que cette différence d’objectif soit assumée, et forcer les gens à travailler ensemble dans ce contexte ne peut conduire qu’à de la frustration. L’exemple du cloud Imaginons que votre infrastructure soit hébergée dans le cloud, chez Amazon Web Service (AWS) par exemple. AWS vous fournit des produits que vous pouvez ensuite consommer, via du tooling ou une interface web. C’est à vous de définir comment vous allez l’utiliser. Vous pouvez très bien ignorer l’ensemble des bonnes pratiques du cloud ou fournies par AWS (dans sa documentation par exemple), démarrer n’importe quoi, saturer des services… AWS s’en fiche complètement ! Eux se contentent de fournir le produit, le tooling, et un cadre: Un SLA pour chaque produit et des garanties sur la tolérance aux pannes (régions, availability zones…) Des quotas, que de soit en nombre de ressources ou sur la ressource en elle même (par exemple sur une machine virtuelle, la capacité réseau en MB/s, un nombre maximum de requêtes DNS par seconde, des IOPS limitées pour le stockage…) Un support en ligne pour répondre aux questions, et d’autres moyens de communication clairs pour annoncer les nouvelles fonctionnalités, les incidents ou autre Des experts qui peuvent intervenir ponctuellement pour répondre à des problématiques précises du client Ensuite, vous êtes libre. Vous assumez les conséquences de vos choix, qu’ils soient bons ou mauvais (incidents, facture lourde…). Est ce que ce ne serait pas le bon modèle en interne à appliquer dans certains cas ? Est ce que votre équipe infrastructure ne devrait pas être considérée comme une autre entreprise, complètement décorrélée du reste, et fournissant un produit mais très spécifique (avec ce que cela implique en terme de conception produit et discussion avec les "clients finaux") ? Est ce qu’un découplage total dev et ops n’est pas en fait la solution aux problèmes d’alignement et d’objectifs contraires ? Est ce que ce modèle peut vraiment marcher sur le long terme ? Est ce que peut être ce n’est pas ça le vrai platform engineering ? Je ne répondrai pas à ces questions, à vous de me dire ;)
DevOps: recréer les silos pour une meilleure efficacité...
DevOps, casser les silos dans les entreprises, faire collaborer équipes pour que...
Source: mcorbin
Réflexion sur les microservices: avantages, inconvénients, patterns, complexité accidentelle
Les avantages des microservices sont souvent énoncés mais c’est également une approche posant de nombreux challenges au quotidien. Au final, est ce que ça vaut le coup ? Sommaire Introduction Exploitation en production Communication, transaction, données Bus de message Rapide introduction à Kafka Transactions Découpler les actions Outbox Pattern Réconciliations Saga idempotence Accès aux données Duplication Désynchronisation Reconstruction Compaction et stream first Mauvais pattern de duplications Mauvais découpage des domaines Découpage pour la scalabilité et la tolérance aux pannes Dupliquer ou non les données Envoyer des emails Kafka Stream Appels synchrones Testing Reconstruire le monolithe Environnement de dev local Environnement à la demande Cloud Une histoire de confiance Découpler, tests inclus Un problème de lead time et de complexité accidentelle Ralentir Organiser Conclusion Bonus: authentification Introduction Cela fait plus de 10 ans (je pense ?) que l’on voit les microservices déferler dans nos SI. Le cloud et les outils d’orchestration de conteneurs ont également accompagnés cette montée en puissance des microservices au cours des années, simplifiant en partie leurs gestions. Les microservices apparaissent souvent comme une solution pour avoir de petites équipes travaillant de manière décentralisée. Le Domain Driven Design (DDD) est également une méthode de conception souvent mise en avant dans les microservices, où chaque service ou groupe de service sera responsable d’un domaine métier, domaine qui peut donc être assigné à une équipe dédiée. La théorie nous dit aussi que chaque service doit avoir sa propre base de données, car chaque service est le "propriétaire" de la donnée de son domaine: c’est lui la source de vérité unique de cette donnée. Sur le papier, tout ça est très intéressant. On a donc des services (et donc équipes, loi de Conway oblige) faiblement couplés, avec des dev travaillant en autonomie sur des bases de code réduites et spécialisées, et donc plus faciles à maintenir. Les services peuvent également être conçus dans des langages de programmations différents, et monter en charge de manière indépendante. Bien sûr, there ain’t no such thing as a free lunch. Et il est tentant d’ignorer les nombreux problèmes apportés par les microservices, ou de ne leur apporter qu’une solution partielle. Je donnerai dans cet article ma vision sur le sujet, en décrivant quelques patterns (ou antipatterns) que j’ai eu l’occasion de rencontrer au cours de ma carrière de manière la plus simple et objective possible. Je ne vais pas parler tant que ça de découpage d’un monolithe. C’est un sujet très important mais de nombreuses personnes le font déjà mieux que moi. Regardez par exemple le talk de Julien Topçu et Josian Chevalier sur le sujet: Et l’avantage d’un blog, c’est que cela me permet également de relire mes propres articles quelques années après et de voir comment ma vision des choses a évoluée ;) Exploitation en production Gérer le déploiement et l’orchestration de plusieurs centaines de services en production (et dans les autres environnements: QA, Staging…) peut vite devenir un challenge. Heureusement, l’outillage a énormément évolué ces dernières années. Kubernetes, our Lord and Savior, aide par exemple énormément grâce à ce qu’il fournit nativement (tolérance aux pannes, service discovery, gestion du firewalling, health checks, rolling restart, load balancing…). Ce n’est pas la seule solution mais dans tous les cas il est nécessaire d’avoir un moyen standardisé de gérer le cycle de vie d’un service de la CI à la production. Si chaque service doit avoir sa propre base de données (ou autre dépendance infra dont nous parlerons plus tard), il faut aussi être capable de provisioner et gérer dans le temps tous ces composants, pour chaque microservice. Bien sûr, c’est aussi le cas pour un monolithe: on n’imagine pas un monolithe sans tolérance aux pannes ou sans pipeline de déploiement continu (voir un de mes articles sur le sujet). D’ailleurs, les microservices permettent de déployer en production des changements plus petits, plus souvent, avec un "blast radius" plus faible en cas d’incident. Cela peut également être assez appréciable. Néanmoins, le debugging s’en trouve complexifié. Une requête HTTP d’un client peut facilement "passer" par plusieurs services (de manière synchrone ou asynchrone), et bien sûr planter à chaque étape. Cela peut être dû à un problème logiciel (bug dans un des services) ou tout simplement à cause d’un problème de l’infrastructure sous-jacente. Le microservice gérant vos utilisateurs doit par exemple envoyer des requêtes au service d’envoie d’emails, alors que dans un monolithe tout se ferait dans le même process. Des outils comme le tracing distribué (cliquez, vous verrez c’est intéressant :D) deviennent donc obligatoires pour tenter de comprendre ce qu’il se passe dans le système d’un point de vue macro. Ajoutez des métriques et des alertes sur vos services, configurez des SLO (plus facile qu’à dire qu’à faire, en réalité c’est une approche difficile à faire adopter). J’ai plusieurs articles sur le sujet des métriques si ça vous intéresse. On en arrive vite à rajouter une certaine complexité sur l’infrastructure et les services à cause des microservices: service mesh pour tenter de créer une infrastructure réseau plus robuste (c’est souvent l’inverse qui arrive), circuit breakers pour protéger des services ou dépendances en cas de pannes, retries à divers endroits… Bref, comme dit Murphy, "Anything that can go wrong will go wrong". Une certaine maturité technique sera nécessaire pour aboutir à une plateforme robuste. Le platform engineering est également obligatoire. Vos SRE devront travailler main dans la main avec vos développeurs pour que chaque équipe puisse travailler en autonomie sans avoir les ops dans le chemin critique. Cela est vrai également si vous ne faites pas de microservices d’ailleurs. J’avais fait un talk sur le sujet que je redonnerai dans une version complètement retravaillée prochainement: Une solution pour résoudre les problèmes de fiabilité (et surtout de cascading failures, où la panne d’un composant cause la panne d’autres services en cascade) est de tenter de découpler totalement les services. Serait-il possible d’avoir des services totalement autonomes et donc pouvant continuer de fonctionner en cas de panne partielle de système ? En théorie oui, mais nous verrons ça dans la section suivante. Communication, transaction, données On retrouve généralement dans l’informatique deux grands moyens de faire communiquer deux services entre eux: Synchrone: un service appelle un autre service via un protocole comme HTTP (ou gRPC par exemple) et s’attend à une réponse immédiate. Si le service cible n’est pas disponible, une erreur se produit. Asynchrone: un service pousse un événement un message dans un bus de message et un autre service consommera (éventuellement) ce message un jour pour faire quelque chose avec. Ces définitions sont bien sûr un peu trop généralistes. Des outils comme Nats supportent par exemple des patterns de type request-reply via des queues asynchrones (on simule donc du synchrone), ce qui peut être utile dans certains cas. Les appels synchrones créent un couplage entre l’appelant et l’appelé. Comme dit précédemment, si le service cible n’est plus joignable, le service appelant ne pourra pas fonctionner tant que la cible n’est pas restaurée. Ceci peut éventuellement être un problème (ou pas, nous y reviendrons). D’autres approches émergent pour tenter de découpler les services. Bus de message On voit dans ce nombreuses implémentations des microservices l’utilisation d’outils comme Apache Kafka pour distribuer des événements entre services, et donc tenter de les découpler. Rapide introduction à Kafka Kafka est un bus de message permettant à des consommateurs de s’abonner à des topics. Un topic est composé de plusieurs "queues" de messages appelés partitions. Lorsqu’un message est produit dans un topic, il est assigné à une partition en fonction d’une clé de partitionnement (généralement une valeur du message, comme par exemple l’ID de la ressource représentée dans l’événement). Cela est très utile car on a comme ça une garantie d’ordre: tous les messages d’une partition sont consommés en mode FIFO par les consommateurs et tous les événements pour une entité donnée seront consommés dans l’ordre d’arrivée. Dans cet exemple, un service produit des événements concernant des actions sur des utilisateurs (création de compte, mise à jour, suppression). Ces événements sont envoyés dans un topic composé de 2 partitions, partitionnées par ID de l’utilisateur. Les consommateurs consomment le topic. Chaque consommateur (aussi appelé consumer group dans le langage Kafka) lira l’entièreté des messages du topic. Les messages dans Kafka ne sont pas supprimés après lecture mais après un TTL (7 jours par exemple), ce qui permet de relire à tout moment d’anciens événements si nécessaire (tant qu’ils sont plus récents que le TTL). N’hésitez pas à googler si vous voulez en savoir plus sur Kafka, ça vaut le coup et il y a des tonnes de contenus sur le sujet. Fin Un service peut par exemple réagir sur la mise à jour d’une entité qu’il gère (de son domaine) en publiant un événement de domaine (une projection de la donnée du servicex). D’autres services, dont le service producteur n’a pas connaissance, peuvent consommer à ces événements et déclencher des actions en fonction de ce qu’ils reçoivent. A l’inscription d’un utilisateur le microservice responsable de la création du compte publiera par exemple un événement UserCreated (contenant les informations de l’utilisateur) qui pourra être consommé par un autre service qui déclenchera lui même une action. On peut aussi envoyer via le bus de message des actions (commandes) à réaliser par d’autres services, par exemple "Envoie cet email à l’adresse foo@example.com" qui sera ensuite consommé par un service chargé d’envoyer des emails. Ici, le découplage entre le producteur et les consommateurs est total. En cas de panne d’un consommateur, ce n’est pas grave: à son redémarrage il pourra reconsommer les événements envoyés par le producteur (avec du retard), ce dernier continuant de fonctionner normalement. Comme dit précédemment, c’est aussi un bon moyen d’avoir une communication 1 → N entre services, plusieurs consommateurs parallèle sur les mêmes événements étant possibe. Transactions Dans ce genre de système, l’ordre des événements est très important. En effet, il existe de très nombreux cas où le résultat d’une application d’une série d’actions sur une entité donnée dépend de l’ordre de ces actions. Les outils comme Kafka permettent de garantir l’ordre des messages en fonction de la clé de partitionnement des messages. Mais c’est là où les problèmes peuvent commencer également à survenir. Il est courant d’avoir des cas de figures où un service: A besoin d’écrire dans une base de données. Doit ensuite, une fois l’action réalisée, publier un événement dans un bus de message pour annoncer l’action aux autres services. On a donc en résumé besoin de réaliser une transaction entre deux systèmes. TL:DR: ce n’est pas possible à faire simplement et on peut rapidement se retrouver dans des cas où par exemple la donnée est écrite, mais l’événement non publié. Cet événement non publié ne sera pas donc propagé au reste du système, ce qui peut causer des problèmes d’incohérences. Comment résoudre ce problème ? Découpler les actions Découplons les deux actions avec un service intermédiaire, pour que chaque service ne fasse qu’une action transactionnelle et non deux: Dans 1), on a notre situation initiale: Le service A exécute les deux actions l’une après l’autre, sans garantie de transactionnalité. Dans 2), on utilise un pattern un peu différent: Le service A écrit d’abord dans le bus de message. Ce message est lu par Service B (qui pourrait même être Service A lui même qui reconsomme ses propres messages pour éviter de maintenir un autre service) qui lui écrit dans les base de données. On voit dans la seconde solution qu’on a gagné: nous n’avons plus cette transaction entre deux systèmes, au prix d’une complexité un peu supérieure. La solution 2) contient pourtant un inconvénient majeur: la possibilité de problèmes de type read-after-write. Imaginons le cas suivant: Un utilisateur décide de changer son nom dans l’application: Service A reçoit la requête, et pousse un événement de type Utilisateur Foo change son nom en Bar. Le service A n’a ici aucune idée de quand l’action sera réellement exécutée par le Service B (il n’a même pas connaissance de ce service), mais il répond quand même à l’utilisateur "l’action est effectuée". Si l’utilisateur rafraichit sa page avant que service B ait consommé l’événement et réalisé l’écriture en base, l’ancien nom sera toujours visible. Bizarre non ? Et que se passe t-il si Service B est en fait en panne et que le message met une heure à être consommé ? Bref, c’est problématique mais dans certains cas où le read-after-write n’est pas un problème cela peut être suffisant. Mais on reparlera de ce pattern plus loin ;) Outbox pattern Une autre technique possible est d’utiliser l’outbox pattern. Il est extrêmement bien décrit dans cet article donc je n’irai pas dans les détails. L’idée est de ne plus avoir à écrire la donnée dans deux systèmes à la suite (base de données et bus de message), mais d’écrire la donnée ET l’événement à envoyer dans une même transaction au même endroit: dans la base de données. L’événement sera écrit dans une table spécifique qui sera ensuite lue régulièrement par un système externe, et publié dans notre bus de message. On a cela grâce aux propriétés transactionnelles des bases de données (notamment SQL) la garantie que les deux actions seront faites, ou annulées. Mais jamais le cas intermédiaire. Que vaut ce pattern ? Ca marche, mais le coût de mise en oeuvre est important. Des outils populaires comme Debezium (très utilisé aussi pour un autre pattern appelé Change Data Capture) sont généralement utilisés mais ils demandent souvent une connaissance fine du fonctionnement d’une base de données (replication des WAL par exemple), et peuvent être assez sensibles à gérer en production. Here be dragons. Réconciliations Un autre pattern courant dans les systèmes distribués est de travailler avec des boucles de réconciliations. J’ai eu l’occasion de travailler presque 4 ans chez un cloud provider utilisant ce pattern. C’est également un pattern utilisé par énormément d’outils, notamment dans le monde de l’infrastructure (Kubernetes est un exemple). Dans un système distribué, tout peut arriver: problèmes d’infrastructures divers, problèmes réseau, bug dans une dépendance, crash d’un service… Le gros risque de tout ça est comme décrit précédemment l’interruption d’une action en cours de traitement. Pire, il faut pouvoir faire revenir à l’état nominal du système. Je vois encore trop d’équipes réconciliant des données "à la main" dans ce type d’architectures distribuées, car les applications n’ont aucun mécanisme de reprise. Et ce mécanisme peut être les boucles de réconciliations. Un très grand nombre de systèmes peuvent se définir en terme de "workflow": une série d’actions à exécuter pour faire converger une entité dans un état voulu. Dans notre exemple précédent, ces actions étaient au nombre de deux: écrire dans la base de données et publier un message dans le bus de message. Mais en réalité, il est assez commun d’avoir des workflow plus complexes: écrire dans une base de données, appel HTTP vers un service d’un partenaire, traitement de la réponse et nouvelle écriture en base, publication d’un message… Pourrions-nous construire un système spécifiant que lorqu’une action est acceptée par le système, on ait la garantie qu’elle va jusqu’au bout de son exécution même en cas de problème ? Oui. Dans mon expérience précédente, toutes les actions sur une entité (un enregistrement en base de données) pouvaient être décrites sous forme de machines à états finis déclenchant des workflow représentés par des arbres syntaxiques abstraits. J’ai apprécié cette manière de concevoir des services backend et les systèmes étaient extrêmement robustes. Qu’est ce que tout ça veut dire ? Nous utilisions une librairies (disponible d’ailleurs sur Github) pour définir simplement une série d’actions à réaliser. Ces actions pouvaient être appelées l’une après l’autre ou en parallèle (pour accélérer l’exécution), le tout de manière très flexible et avec des choses comme la gestion d’erreur ou le retry inclus. On voit dans cet exemple une série de 5 actions, dont deux sont réalisées en parallèle. L’ordre des actions est ici numéroté. Lors d’une action sur le service (appel HTTP, événement Kafka…) l’entité pouvait optionellement changer d’état ("destroying", "updating", "upgrading… ce qui pouvait être aussi utile pour interdire des actions parallèles sur une entité en vérifiant son état) et une réconciliation était déclenchée. Si cette dernière plantait, elle était retentée plus tard (immédiatement, ou X secondes…c’était configurable). En fin de réconciliation l’entité repassait dans un état nominal ("running" par exemple). Du tooling nous permettait de suivre en temps réel l’exécution de chaque étape d’un job et de forcer la réconciliation d’une entité. Ces slides (notamment à partir de la 47) décrivent briévement ce pattern, qui fonctionne très bien dans beaucoup de cas. On entend d’ailleurs de plus en plus parler de workflows et de "durable execution" en ce moment, qui est un concept assez similaire mais avec une vraie gestion de la temporalité. La grosse hype du moment dans ce domaine est Temporal, que je n’ai malheureusement toujours pas eu le temps de tester. Je vous conseille fortement de cliquer sur le lien et de jeter un coup d’oeil, ces patterns fonctionnant aussi dans des architectures classiques. L’idée à retenir de tout ça: construisez des systèmes capables de s’auto réparer sans actions de votre part. Saga Tout ça me fait aussi penser au pattern Saga. Le problème est toujours le même: comment exécuter une transaction distribuée (à voir ensuite si l’on souhaite vraiment des transactions entre services), et voir comment rollback en cas d’échec. Certains "frameworks" implémentant ce pattern sentent un peu le "framework d’entreprise™" mais il y a toute une littérature intéressante sur ce sujet. L’idempotence L’idempotence est obligatoire dans ce type de système. On parle d’idempotence lorsque la même action répétée plusieurs fois conduit au même effet. Pourquoi est-ce une notion critique en microservice ? Car la majorité des systèmes de ce type fournissent des garanties de type "at least once". Si le même événement est envoyé deux fois par un producteur dans un bus de message (à cause d’un bug, d’un retry…), les consommateurs le recevront deux fois mais doivent produire le même résultat que si il n’avait été envoyé qu’une fois. Même chose pour des requêtes HTTP. Cela est assez facile à faire dans certains cas, plus difficile pour d’autres. Attacher un UUID d’idempotence aux actions peut être une solution (de nombreux SaaS dont Stripe fonctionnent comme ça par exemple), mais cela force les consommateurs à d’une manière ou d’une autre stocker et vérifier ces ID à chaque message reçu, et à gérer leurs expirations. Accès aux données The elephant in the room. Comme dit précédemment, l’état de l’art en microservice énonce que chaque service doit avoir sa propre base de données. Ce service doit être le seul à modifier cette donnée, et doit être la source de vérité pour l’état de cette donnée. Duplication Sauf qu’on se retrouve parfois dans des cas où un service a besoin d’une donnée d’un autre service. Prenons l’exemple d’un site de e-commerce, et l’exemple de duplication de données que l’on retrouve dans la majorité des architectures microservices (et que j’ai vu dès le début de ma carrière): la recherche. Un service peut par exemple être responsable de la gestion des produits, et un autre de la recherche. Le service produit pourrait utiliser une base SQL classique, et le service de recherche un outil comme Elasticsearch. Lorsqu’un nouveau produit est créé ou supprimé dans le service produit, ou modifié, un événement est émis par ce service dans le bus de message (le pattern Change Data Capture peut s’avérer également utile ici pour fiabiliser la production des événements). Le service de recherche consomme cet événement et met à jour sa propre base de données. Ceci est intéressant: les deux services peuvent fonctionner (et scale) en totale autonomie. Une panne du service de recherche n’affectera pas le service produit, et vice versa. Les caractéristiques de ce type de duplication de données doivent pourtant être bien comprises. Ici, l’intérêt est évident: faire de la recherche demande des technologies dédiées, des données ayant une forme différente par rapport à celles du service source (champs spécifiques, structure différente…) et il fait sens de découpler les deux fonctionnalités. Un peu de duplication est donc conseillé, mais il ne faut pas en abuser et il faut toujours se poser la question du pourquoi on duplique, de comment, et si une solution alternative est possible. En effet, de nombreux cas limites existent avec cette approche. Désynchronisation Les données dans le service dupliquant les données (ici la recherche) peuvent être en désynchronisation avec le service producteur pour plusieurs raisons. Peut être par exemple que le service producteur a échoué à produire des messages, et je vous garantis que ça va arriver même avec la meilleure volonté du monde, il y aura des bugs dans votre système. Peut être également que la propagation entre le service producteur et consommateur prendra plus de temps que prévu, et donc que les mises à jours arriveront avec 10, 20, 30… secondes de retard. Bref, vous ne serez jamais sûr que les données sont complètement synchronisées des deux côtés. Est-ce acceptable pour votre fonctionnalité ? On peut considérer pour la recherche que quelques résultats incorrects (description non mise à jour, article qui vient d’être ajouté mais qui n’apparait pas encore dans les résultats…) peut être "acceptable": à vous de définir vos SLO (service level objective) sur vos données. Dans d’autre cas, ça ne l’est pas. Imaginez la catastrophe si par exemple un utilisateur ayant son compte suspendu puisse continuer d’utiliser un sous-ensemble de votre service car sa suspension n’a pas été propagée à l’ensemble du système ? Les conséquences ne seraient sûrement pas les mêmes. Reconstruction Les données dupliquées doivent pouvoir être reconstruites car comme dit précédemment elles seront désynchronisées de manière subtile avec le temps. Si on reprend notre exemple de la recherche, peut être également qu’un nouveau champ a été ajouté dans la base de données du producteur et que l’on souhaite l’indexer dans le moteur de recherche. Ce process de reconstruction doit en plus fonctionner sans causer de downtime que ce soit d’un côté ou de l’autre. Et il faut bien sûr pouvoir également construire cette duplication une première fois quand le nouveau service est créé. Poser vous la question de comment faire avant d’implémenter le pattern. Une solution simple pourrait être de déclencher d’une manière ou d’une autre une action sur le service producteur pour reproduire la totalité des événements représentant sa base de données dans le bus de message, événements qui pourront être reconsommés par le service de recherche qui mettra à jour ses enregistrements. Selon le volume de données, ça peut piquer. Mais pour des cas simples (quelques millions ou dizaines de millions d’enregistrements) cela peut être suffisant. Le process peut être optimisé selon les besoins: utilisation d’un microservice de reindexation dédié, utilisation d’un replica de la base de données du producteur pour extraire les données… Bref, à voir en fonction du volume. Attention aussi à ne pas impacter d’autres services en faisant ça: rappelez vous qu’en microservice plusieurs consommateurs peuvent généralement écouter la même queue d’événement, créez en une dédiée à la réindexation pour éviter que l’ensemble des consommateurs aient à reconsommer un volume énorme d’événements. Avoir un peu d’outillage pour pouvoir très simplement réindexer une entité peut également être utile. Vous découvrez qu’un produit est mal indexé ? Un simple appel POST /reindex/<id> pourrait déclencher sa réconciliation. Attention aussi à la gestion des suppressions. Rater un événement de création ou de mise à jour n’est pas grave, il suffit de le reproduire depuis la source pour le réindexer correctement. Mais que se passe t-il si une donnée n’existe plus dans la source mais existe toujours dans la destination car l’événement de suppression a été perdu ? Cela montre aussi les avantages d’une réindexation complète malgré les challenges techniques qu’elle amène. Pour la petite anecdote, j’ai travaillé en 2017 sur un projet à base de microservices (dans Kubernetes), Kafka pour le bus de message, et avec chaque service ayant une base de données SQL dédiée. C’était un projet en grand groupe, pas encore en production mais il y avait quand même une soixantaine de personnes travaillant dessus (~40 dev de mémoire) et on avait déjà un environnement de QA utilisable. Les architectes avaient fait le choix de dupliquer de très nombreuses tables métier entre microservices via Kafka, un peu comme dans l’exemple de la recherche mais pour des données assez génériques et sans réelles transformations de ces dernières. On avait donc la majorité des services avec des duplications locales de données d’autres services, très souvent pour de mauvaises raisons (on y reviendra), bien sûr désynchronisées, et c’était devenu un bordel sans nom au point qu’il fallait régulièrement "reset" toutes les bases (supprimer toutes les données) pour repartir d’un état propre car au bout d’un moment l’utilisation normale de la plateforme devenait impossible ! Heureusement, c’était pas en prod (et j’ai quitté le navire avant^^) Cas classique où la solution technique a été mise en place sans analyse et stratégie pour ensuite être "scale" à l’ensemble des équipes qui ont toutes reproduites les mêmes erreurs sans trop de poser de questions. Cette cascade avait été réalisée par des professionels, ne faites pas ça chez vous. Compaction et stream first Depuis le début de cet article, je parle du bus de messages comme un moyen d’échanger des événements métiers ou des commandes entre plusieurs services. Mais on peut aller plus loin. Et si il était possible d’utiliser le bus de message comme une base de données ? Les données représentant des entités (les produits par exemple) n’expireraient jamais mais resteraient indéfiniment dans le bus, prêtes à être reconsommées à tout moment. C’est possible sur des technologies comme Kafka avec la compaction. J’étais comme beaucoup un peu hypé par le stream processing il y a une dizaine d’années. On avait depuis quelques années des outils comme Apache Storm, ou Spark Streaming qui ouvraient la voie à une nouvelle approche du traitement de données par rapport au batch (qui se rappelle de MapReduce ?). Puis est arrivé Apache Kafka (je me rappelle avoir travaillé avec la version 0.8 en 2015 de mémoire), Je n’ai jamais stoppé d’utiliser cette technologie depuis. J’avais un peu décrit le fonctionnement de Kafka en début d’article mais je n’ai pas parlé de la compaction. Mon premier contact avec cette feature fut chez Exoscale où elle était utilisée pour gérer les zone DNS (voir cet article de pyr, mon ancien CTO). Clairement le genre de features où lorsqu’on vous l’explique pour la première fois vous êtes comme ça: Comme dit en début d’article, Kafka garde les messages qu’on lui envoie durant une certaine durée configurable (TTL), comme par exemple 7 jours. Après ça, elles sont tout isimplement supprimées. Sauf avec des topics en mode compaction. Avec la compaction, Kafka gardera dans son topic le dernier événement pour chaque clé unique de message. Rappelez vous ce que j’écrivais en début d’article: lorsqu’on envoie un message dans Kafka, on choisit une clé pour ce dernier. Imaginons que nous envoyons dans Kafka les détails d’un utilisateur à chaque changement de ce dernier (création, mise à jour ou suppression). On aura donc une série d’événements dans Kafka contenant ces informations, chaque événement ayant comme clé l’ID de l’utilisateur. En mode Kafka "classique", les services consommateurs consomment ces événements, puis après quelques jours (valeur du TTL) ils seront supprimés de Kafka. En mode compaction, le dernier événement (le plus récent) pour chaque utilisateur sera gardé (voir la doc officielle sur ce sujet). Les consommateurs consomment les changements du topic consommé au fil de l’eau. A chaque mise à jour d’une entité, le changement est toujours propagé aux consommateurs. Sauf qu’ici, toutes les données de vos utilisateurs sont disponibles dans le topic. Grâce à la compaction, la taille du topic reste limitée (=~ le nombre d’utilisateurs dans votre système dans cet exemple). Il est également possible de configurer un consommateur pour retraiter l’entièreté du topic (et donc l’entièreté des données) ! Revenons à l’exemple du service Produit et du service Recherche décrit précédemment: avec un topic compacté, reindexer les données dans le service de Recherche devient facile: il faut juste lui dire de reconsommer le topic depuis le début. Je vous conseille fortement de le talk Turning the database inside-out du célèbre Martin Kleppman décrivant une architecture orientée streaming. L’implémentation "habituelle" de ce pattern est la suivante: Un service a une base de données "classique" et est le propriétaire de ces données En cas de mise à jour, il écrit dans sa base et pousse un événement dans le topic compacté La suppression fonctionne de la même manière: il faut pousser un événement spécial (de type tombstone) dans Kafka pour supprimer une clé d’un topic compacté, sinon l’entité restera pour toujours dans le topic. Pensez y, c’est obligatoire rien que pour des raisons légales (RGPD). Du soft-delete côté base de données peut être très intéressant pour faire re-converger ce topic (mais vous la base de données sera plus grosse). Ici se pose encore une fois la question de la qualité des données du topic compacté. Si un service écrit dans sa base mais ne pousse pas le message, le topic n’est pas mis à jour. On revient au problème (et solutions) décrites plus tôt dans l’article. Martin Kleppman va même plus loin: pourrions-nous faire une architecture 100 % orientée streaming, où la source de vérité n’est pas la base de données locale de chaque service mais le topic Kafka ? Dans cet exemple simplifié, deux topics avec la compaction servent à créer 2 "materialized views". Les écritures se font directement dans les topics, et des consommateurs (non représentés ici) se chargent de reconstruire des vues dans des base de données depuis les nouveaux événements. C’est intéressant car ici la source de vérité est Kafka, et chaque vue peut être facilement reconstruire "from scratch". On limite fortement les problèmes de désynchronisations par rapport à la solution précédente. La difficulté ici est d’intégrer ce modèle au monde d’aujourd’hui, notamment HTTP. Quand un utilisateur exécute une action côté frontend, il s’attend à avoir une réponse rapidement. On veut éviter également les problèmes de type read-after-write comme décrit en début d’article, donc répondre "OK" au client alors que la donnée n’est pas propagée aux vues (donc invisible côté client) est problématique. Une solution pourrait être d’attendre dans les services (comme dans le Service User) qu’un callback produit par un service consommateur arrive avant de répondre au client ( je link encore une fois la doc de Nats pour faire cela par exemple). Mais que faire si le callback ne vient jamais ? On ne peut pas répondre au client "réessaye", l’action est probablement toujours en cours dans le bus. On ne peut pas lui dire "OK", si il change son prénom, rafraîchit la page et voit toujours la même donnée, il ne va pas comprendre. Pour des systèmes critiques il vaut probablement mieux écrire dans la base de données avant. Par contre, pour du service interne où on a la main sur le client (contrairement à un browser web), ça peut marcher, notamment si l’on est pas intéressé par recevoir une réponse immédiate. Dernier conseil: lisez absolument le livre Designing Data Intensive Applications dont Martin Kleppman est l’auteur, il est génial. Mauvais pattern de duplications Parfois, les équipes dupliquent les données entre services pour les mauvaises raisons. Il devrait y avoir une règle universelle quand on fait des microservices qui ressemblerait à quelque chose comme On tourne 7 fois son clavier dans ses mains avant de dupliquer de la donnée. Mauvais découpage des domaines Erreur classique mais fréquente. Vous pensiez avoir bien identifié votre domaine, en fait vous aviez tort. Vouloir créer des microservices "trop petits" est je pense une erreur courante, notamment lors du découpage d’un monolithe. Découper à la tronçonneuse, c’est facile, mais quand on se rend compte de la boulette c’est trop tard et il faut passer du temps à re-fusionner des services ensemble (et on sait tous qu’il y a de grandes chances qu’on vous dise "ah non on a déjà fait l’effort de découper, on va pas encore refaire !"). Parfois, l’erreur n’est pas un bug mais une feature: "oui je sais c’est le même domaine mais moi je veux coder en Rust et le service existant est en PHP donc je ne veux pas contribuer dessus", mais là on est plus dans un problème organisationnel que technique. Bref, faites attention à ça. Découpage pour la scalabilité et la tolérance aux pannes Un cas que j’ai déjà rencontré dan
Réflexion sur les microservices: avantages, inconvénients,...
Les avantages des microservices sont souvent énoncés mais c’est également...
Source: mcorbin
Karpenter: le futur de la gestion des workers et de l'autoscaling sur Kubernetes
Contrairement à ce que l’on pourrait penser, je suis rarement "hypé" par des outils. Mais parfois ça arrive, et c’est le cas pour Karpenter qui est en train de révolutionner la gestion des noeuds sur Kubernetes. Le monde avant Karpenter Les conteneurs (pods) gérés par Kubernetes tournent sur des workers (généralement des machines virtuelles). Gérer ces workers peut rapidement devenir pénible pour plusieurs raisons. Sur la majorité des offres Kubernetes dans le cloud (mais aussi sur des implémentations on prem) une abstraction "nodepool" existe, représentant un groupe de machines similaires. On choisit par exemple pour un nodepool les caractéristiques des machines à lancer (nombre de CPU, mémoire, image), la configuration réseau (firewalling, réseau privé), la taille du disque root, et toutes sortes d’options selon le cloud provider (IAM, clés SSH, configuration de cloud init…). Toutes les instances du Nodepool sont donc identiques. Une fois un nodepool créé, il est possible de configurer sa taille (et donc le nombre de workers à déployer pour ce nodepool) et de la changer à tout moment (manuellement ou via des outils d’autoscaling). Le problème qui se pose rapidement est la multiplication du nombre de nodepools à gérer, par exemple: Avoir des nodepools avec des types d’instances (CPU, mémoire, disques, AMI, génération d’instances…) différents dans le but de faire tourner certains workloads gourmands sur des noeuds dédiés: un nodepool avec des machines 4CPU/16GB de mémoire, un autre 8CPU/32GB… Avoir des nodepools utilisant des instances de type "spot" (comme par exemple chez AWS): dans ce cas de figure on veut maximiser l’utilisation d’instances spot pour faire baisser la facture cloud tout en démarrant des instances classiques en cas de non disponibilité des spot. Faire du multi availability zone (AZ): cela peut demander la création d’un nodepool par availability zone pour chaque type de nodepool. On se retrouve donc rapidement avec 3, 6, 10… nodepools par cluster et cela devient difficile à gérer: il faut les gérer via de l’infra as code (Terraform par exemple) et donc de manière assez statique et il faut les mettre à jour un par un lors de mise à jour de clusters ou d’AMI. Faire des économies via un placement intelligent des pods devient également compliqué car tout doit être fait "à la main": je me rappelle par exemple lorsqu’on avait de multiples groupes de machines du type "memory optimized", "high memory optimized" dupliqués par AZ pour faire tourner certaines applications gourmandes: on avait toujours des noeuds sous utilisés. Gérer le cycle de vie des noeuds peut également être assez pénible: On utilise généralement le cluster autoscaler de Kubernetes pour l’autoscaling des noeuds. Il fonctionne globalement bien mais est assez basique dans son fonctionnement. Lorsqu’un noeud est supprimé (downscaling, mise à jour…) il faut d’abord drain les pods sur le noeud et seulement ensuite le supprimer. Sur le cloud et notamment lorsqu’on utilise des instances spot on veut aussi parfois préventivement supprimer un noeud qui risque d’être réclamé par le cloud provider dans un futur proche. Sur AWS cela se fait via une gestion d’événements poussés dans une queue SQS et l’utilisation d’un composant supplémentaire à déployer (AWS node termination handler). AIlleurs, c’est souvent à vous de construire l’outillage pour le faire. Karpenter Karpenter est un outil open source développé par AWS pour répondre à la problématique de gestion des workers dans Kubernetes. L’outil s’est longtemps limité à la gestion de noeuds sur AWS, mais son architecture modulaire (une partie "core" et une "plugin" pour les intégrations spécifiques à chaque cloud provider) et la qualité de l’outil font qu’il est en train de prendre de l’ampleur. L’outil autrefois géré sur le compte github d’AWS a maintenant rejoint la communauté Kubernetes (sigs autoscaling), et Microsoft Azure vient également de le rendre disponible sur son cloud. Pour moi, Karpenter est le futur de la gestion de noeuds sur Kubernetes, et va dans les prochaines années complètement remplacer cluster-autoscaler. Je vais expliquer pourquoi en expliquant rapidement comment fonctionne Karpenter sur AWS. En pratique Karpenter est un operator Kubernetes et doit donc s’installer sur le cluster (via helm par exemple), ou du moins communiquer avec l’apiserver pour pouvoir gérer les noeuds. AWS sortira d’ailleurs Karpenter en tant qu’Addons complètement géré par AWS dans les mois à venir. Il se configure ensuite via des CRD spécifiques comme n’importe quel operator Kubernetes. Sur AWS, on utilisera les ressources EC2NodeClass (spécifiques à AWS) et NodePool (générique). Je présume que sur Azure EC2NodeClass serait remplacé par une autre CRD, et que dans le futur chaque cloud (ou outil déployant du Kubernetes on prem avec une intégration Karpenter) aura sa CRD dédiée. EC2NodeClass Voici un exemple d’EC2NodeClass appelé example: apiVersion: karpenter.k8s.aws/v1beta1 kind: EC2NodeClass metadata: name: example spec: amiFamily: AL2 blockDeviceMappings: - deviceName: /dev/xvda ebs: encrypted: true volumeSize: 70Gi volumeType: gp3 metadataOptions: httpEndpoint: enabled httpPutResponseHopLimit: 1 httpTokens: required role: karpenter-example-iam-role securityGroupSelectorTerms: - id: sg-1cb731bc101ab1e3f - id: sg-2a3fa43a111ab1a54 subnetSelectorTerms: - tags: Name: kubernetes-* tags: Name: example-karpenter environment: production managed-by: karpenter userData: | #!/bin/bash echo "optional cloud init user data" Cette ressource représente la configuration AWS d’un worker: l’AMI à utiliser: ici, on se contente de spécifier la famille d’AMI à utiliser (AL2, Amazon Linux 2 ici). Karpenter prendra automatiquement l’AMI la plus récente pour la version du cluster Kubernetes cible, mais il est également possible de spécifier l’image exacte à configurer. La configuration du disque racine (taille, chiffrement, storage class…). Des options liées au serveur de metadata de l’instance. Le role à attacher à l’instance (sur AWS on configure généralement très finement les permissions de chaque machine notamment sur EKS). Les security groups (règles de firewalling) à attacher à l’instance. Il est possible de choisir ces security groups par ID (ce que je fais dans cet exemple), par nom, par tags (et donc d’inclure tout ceux avec un tag donné), ou une combinaison de tout ça. Les subnets de réseaux privés (VPC) à utiliser pour la machine (là aussi la sélection peut se faire via différents critères). Des tags à attacher aux machines. Des user data pour avoir si besoin une configuration cloud init spécifique. Comme dit précédemment tout ceci est assez spécifique à AWS mais on retrouvera des choses similaires dans d’autres implémentations futures de Karpenter sur l’autres plateformes. En conclusion, vous définissez ici à quoi doit ressembler la partie "infra" de vos noeuds via un simple fichier YAML. Voyons maintenant la partie NodePool. NodePool La CRD NodePool représente un groupe de noeuds et permet de lier une NodeClass avec une gestion de cycle de vie des noeuds. Si le scheduler de Kubernetes n’arrive pas à éployer un pod par manque de capacité, Karpenter sélectionnera un NodePool qui par sa configuration pourrait accueillir le pod et et créera un nouveau noeud. Karpenter s’occupera également de tout l’aspect autoscaling (donc cluster autoscaler n’est plus nécessaire) et comme nous allons le voir, de bien plus. Voici un exemple de NodePool: apiVersion: karpenter.sh/v1beta1 kind: NodePool metadata: name: example spec: disruption: consolidationPolicy: WhenUnderutilized expireAfter: 24h0m0s limits: cpu: "400" template: metadata: labels: purpose: example spec: nodeClassRef: name: example requirements: - key: karpenter.k8s.aws/instance-category operator: In values: - c - m - r - key: kubernetes.io/arch operator: In values: - amd64 - key: karpenter.sh/capacity-type operator: In values: - spot - on-demand - key: kubernetes.io/os operator: In values: - linux La première chose très intéressante dans la configuration d’un NodePool est la partie disruption. Ici, la partie consolidationPolicy indique à Karpenter d’optimiser continuellement le placement des pods sur le cluster pour utiliser le moins de ressources (et donc de noeuds) possibles. Rien que ça est génial et permet de faire baisser de manière non négligeable votre facture cloud, et d’avoir un taux d’occupation de vos noeuds Kubernetes très important ! Encore mieux, Karpenter fera également en sorte d’utiliser les noeuds disponibles sur le cloud les moins chers, mais j’en reparlerai juste après. L’option expireAfter permet d’ajouter un TTL sur les noeuds. Ici, les noeuds seront automatiquement recyclés (supprimés et recréés) toutes les 24 heures. Cela apporte de nombreux avantages: Les noeuds se mettent à jour tout seul, que ce soit en cas de nouvelle version de l’image (AMI) référencée par la NodeClass, ou en cas de mise à jour du cluster Kubernetes à une nouvelle version. On a une vraie approche "infrastructure immuable" sur la gestion des noeuds, qui sont vraiment des ressources jetables et remplaçables à tout moment. Cela force tous les utilisateurs du cluster Kubernetes (dev comme ops) à construire des programmes et des architectures pouvant supporter un recyclage permanent des noeuds. Lorsque l’on fait un recyclage des noeuds que tous les 3 mois par exemple, on a tendance à ne pas voir certains problèmes applicatifs sous-jacents: applications tolérant mal le drain de ses pods, problèmes réseaux transitoires causant de la perte de traffic… Tout péter toutes les 24H met très rapidement en évidence des problèmes et donc force les gens à les corriger, ce qui améliore grandement la résilience des applications de manière générale. D’ailleurs ce pattern est clairement génial, qu’on soit sur Kubernetes ou non. La partie limits de la spec permet de mettre une limite sur la capacité totale du NodePool. Ici, la somme des CPU des machines gérées par ce NodePool ne pourra pas dépasser 400. C’est intéressant pour éviter de scale à l’infini un NodePool. La configuration template.metadata contiendra des labels (et annotations si besoin) à propager aux noeuds créés. Enfin, la partie template.spec.requirements permet de définir des conditions pour les noeuds qui seront provisionnés. Dans cet exemple, que des machines des classes d’instances c, m, et r d’AWS (via karpenter.k8s.aws/instance-category), des instances amd64 (via kubernetes.io/arch), des instances spot ou on demand (via karpenter.sh/capacity-type) et enfin des instance Linux (kubernetes.io/os) pourront être démarrées par ce NodePool. Il existe de nombreux autres requirements possibles qui sont listés dans la documentation de Karpenter. On peut par exemple ajouter une taille maximum (en terme de CPU/mémoire) ou minimum pour les noeuds. Cette CRD permet également de configurer des taints sur les noeuds gérés par un NodePool, ainsi que la configuration de Kubelet. J’avais expliqué précédemment que Karpenter essaye d’optimiser le déploiement des noeuds pour que cela coûte le moins cher possible via la consolidation. Dans cet exemple, Karpenter essayerait par exemple de démarrer en priorité des instances "spot", ou bien tentera en permanence de créer des workers optimisés pour les workloads. La chose importante à garder en tête est les noeuds d’un NodePool Karpenter peuvent avoir différentes tailles (ce qui n’est généralement pas le cas sans Karpenter). Si Karpenter détecte que le plus rentable en terme de prix (facture cloud) est d’avoir un noeud avec 64 GB de mémoire et 16 CPU et 3 noeuds de 32 GB de mémoire et 8 CPU pour les pods tournant sur le cluster, le tout faisant partie d’une certaine catégorie d’instances, c’est ce qu’il déploiera. Au bout d’un moment, peut être qu’il remplacera les 3 noeuds par 2 noeuds plus gros, en spot par exemple, car moins cher. L’infrastructure est donc très dynamique car en permanence modifiée pour optimiser la facture. Vous n’avez donc pas à créer beaucoup de Nodepool Karpenter comme c’était le cas avant pour les Nodepool "non Karpenter" (où on créeait un NodePool par taille de macine à déployer sur le cluster). On se contente de laisser Karpenter créer les meilleures instances (bonne taille, coût le moins cher…) pour nos workloads, et des pods très disparates en terme d’utilisation de ressources cohabiterons sans problème sur le même NodePool. Drift Une fonctionnalité intéressante des NodePool Karpenter est également la gestion du drift, qui se configure dans la partie disruption de la CRD. En cas de changement de configuration d’un NodePool ou NodeClass, si les workers actuels (ou un subset d’instances) ne sont plus valides en terme de configuration, Karpenter les recréera automatiquement car il détectera que l’instance n’est plus valide. Après, si vous mettez expireAfter à 24 heures comme dans cet exemple, juste attendre peut être suffisant: après un jour toutes les instances seront forcément mises à jour. On peut également tout simplement supprimer un noeud de manière sûre via kubectl delete node grâe à Karpenter: ce dernier s’occupera de drain le noeud et d’en démarrer un nouveau si nécessaire. Précautions à prendre Comme dit précédemment, Karpenter passera son temps à supprimer et recréer des noeuds (TTL sur les noeuds, consolidation, drift…). Il faut donc que les applications tolèrent ça mais c’est de toute façon obligatoire d’avoir des applications "cloud native" sur Kubernetes, ou même ailleurs. Il faudra également configurer correctement des PodDisruptionBudget sur vos pods. Sans cela, vous n’aurez pas de garantie sur le nombre minimum de replica "up" pour par exemple un déploiement. Karpenter comprend nativement toutes les primitives de Kubernetes (PDB, affinity et anti affinity, Topology Spread Constraints…). Enfin, ajouter l’annotation karpenter.sh/do-not-disrupt: "true" sur un pod indiquera à Karpenter de ne jamais supprimer le noeud où tourne le pod. Il ne faut bien sûr pas ajouter cela sur un pod tournant en continu (type déploiement), sinon le noeud ne pourra jamais se mettre à jour. Mais elle est très utile sur les pods générés par des Jobs (et donc aussi CronJob): cela permettra à Karpenter d’attendre la fin d’un job avant de supprimer un noeud, et de ne pas l’interrompre en plein milieu. Conclusion Il y a eu pour moi un avant et un après Karpenter dans mon utilisation de Kubernetes. C’est comme dans le cochon, tout est bon dans le produit. Je pense qu’il va vraiment prendre de l’ampleur dans la communauté Kubernetes ces prochaines années et qu’avoir une intégration Karpenter sera vraiment un différentiateur entre offres Kubernetes, notamment sur le cloud mais pas que: pourquoi ne pas écrire des plugins Karpenter pour l’on prem également ? Lien vers la doc officielle de Karpenter si vous souhaitez creuser le sujet
Karpenter: le futur de la gestion des workers et de...
Contrairement à ce que l’on pourrait penser, je suis rarement "hypé" par...
Source: mcorbin
Le dilemme du cloud souverain
Le fait que de nombreuses startups Françaises, comme par exemple Olvid récemment, choisissent des solutions cloud US fait couler beaucoup d’encre. Mais ont-elles le choix ? Est ce que les certifications type SecNumCloud sont la solution ? Découvrez mon avis sur le sujet. Le cas Olvid Olvid est une startup fournissant une application de messagerie sécurisée et entièrement chiffrée. On entend beaucoup parler d’elle en ce moment car le gouvernement Français a demandé à ses ministres d’utiliser cette application et non des messageries moins sécurisées comme WhatsApp. Cette recommandation du gouvernement a généré un débat dans le monde tech car Olvid utilise Amazon Web Service (AWS) pour l’hébergement de sa plateforme. De nombreuses personnes considèrent donc par défaut cette application comme non sécurisée. Il faut savoir qu’Olvid a publié une FAQ sur le sujet, justifiant le choix d’AWS par le fait que c’est le cloud provider répondant le mieux à leurs besoins et que l’application chiffre les messages de bout en bout. Aucun intermédiaire n’est donc en mesure de lire les communications. Bien sûr, ces justifications ne suffisent souvent pas aux "experts Twitter". De mon côté, je fais confiance aux ingénieurs développant la solution. Et c’est normal: les types développant et auditant l’application ont des doctorats en crypto d’écoles réputées, donc c’est pas moi, misérable ver de terre de la crypto, qui vais aller les contredire en disant "HAHA vous utilisez AWS, je suis sûr que vous avez pas pensé au man in the middle". Un peu de sérieux. On voit aussi des gens se posant la question de ce choix d’AWS. Pourquoi ne pas utiliser un cloud provider Français ? Comme dit précédemment, Olvid dit avoir fait ce choix pour des raison pragmatiques d’efficacité. Choix que je comprends également. Le cloud Français La France est en retard sur le cloud. Ce n’est pas une opinion, c’est un fait. Nos cloud providers locaux, malgré leurs efforts, ont du mal à rattraper ce retard. Les fondations sont toujours en construction dans un monde où les clients s’attendent à beaucoup plus. J’aurai d’ailleurs largement préféré que les cloud providers Français essayent d’innover sur des sujets spécifiques (SDN, Edge, offre Kubernetes avancée, base de données, Observability etc. comme peuvent le faire des entreprises comme Cloudflare, Tailscale, Confluent, Grafana…) plutôt que d’implémenter des offres généralistes calquées sur les cloud US (souvent en moins bien) 10 à 15 ans après eux. J’étais personnellement optimiste sur le fait que des offres locales puissent rattraper leur retard il y a quelques années sur de l’IaaS/PaaS généraliste, mais on voit année après année que ça ne marche pas. Comme je le dis souvent, le cloud est un énorme accélérateur de productivité pour les entreprises, notamment les startups/scale-up. Et aujourd’hui ces entreprises ne peuvent pas se permettre de sacrifier leur efficacité et fiabilité dans des marchés où la concurrence est souvent mondiale et où il est très dur de s’imposer, encore plus pour des startups européennes avec un marché fragmenté en de nombreux pays. Reprenons le cas d’Olvid: est ce que la boîte en serait où elle en est aujourd’hui si elle n’était pas sur AWS ? Et nos autres scale-up et licornes, très souvent utilisatrices de solutions US ? C’est là que se trouve le dilemme du cloud souverain: doit-on sacrifier un pan non négligeable de l’économie tech Française sur l’autel de la souveraineté, en forçant les entreprises à utiliser des produits ne répondant pas à leurs besoins ? Et si la réponse est oui, comment survivre à cette forte baisse de productivité alors que la concurrence étrangère aura elle accès aux meilleurs produits du marché ? On le voit d’ailleurs avec SecNumCloud: on commence à imposer à certaines entreprises d’avoir leurs applications hébergées sur des offres SecNumCloud. Et là aussi soyons honnête: personne ne souhaite quitter des offres US pour aller sur les offres SecNumCloud actuelles (Outscale ou des offres type VMWare OVH). C’est quoi le but, planter les quelques boîtes Françaises qui ont réussi à décoller en leur mettant encore plus de bâtons dans les roues ? Et même si d’autres offres devenaient SecNumCloud dans le futur, ça fait pas rêver. J’ai d’ailleurs l’impression que certains cloud providers font "all-in" sur cet aspect, comme si ils avaient abandonnés l’idée de faire des produits qui seront utilisés car de qualité. Je trouve cette stratégie assez dangereuse: certes cela avantagera les cloud locaux mais handicapera fortement le reste de l’écosystème tech. Et je trouve la stratégie de "blaming" permanent des gens utilisant des solutions US extrêmement pénible (surtout quand ça vient de gens en plein Dunning-Kruger) Prendre son temps Il ne sert à rien de vouloir aller trop vite sur ces sujets. Je pense que la majorité des tech Français aimeraient utiliser des cloud locaux, mais ils ont aussi la responsabilité de faire des choix ne mettant pas en danger la santé de leurs entreprises. Vouloir imposer certains acteurs alors que le produit n’est pas prêt ne peut qu’avoir des conséquences désastreuses pour la tech Française, qui n’a vraiment pas besoin de cela dans le contexte actuel.
Le dilemme du cloud souverain
Le fait que de nombreuses startups Françaises, comme par exemple Olvid récemment,...
Source: mcorbin
Kubernetes, Cloud, Observability... Quand l'écart technique et de pratiques tue le débat
Pourquoi certaines technologies créent des débats sans fins et souvent stériles ? Je pense que c’est parce que l’écart s’est trop creusé entre les différents partis. Et je vais commencer cet article par une anecdote. Il y a de ça quelques années, je présentais sur Twitter un de mes projets open source, projet évoluant dans le monde du monitoring/observability. Une personne "très connue" de la communauté tech Française est rapidement venue troller en disant que le projet était inutile, que des tonnes d’alternatives existaient déjà, et qu’il fallait à un moment arrêter de réinventer la roue. Je n’étais bien sûr pas d’accord avec ces affirmations pour de nombreuses raisons mais lors du débat qui a suivi une chose m’a marqué: j’étais en totale déconnexion en terme de technique et pratiques avec mon interlocuteur. Je décrivais mon projet en expliquant ce qu’il apportait en terme de bonnes pratiques de monitoring par rapport à d’autres produits, et donc parlais de blackbox vs whitebox monitoring, de labeling, de formats d’expositions, de service discovery, de l’intêret d’avoir des outils "API first", de hot reload… là où mon interlocuteur était bloqué à "on n’a jamais fait mieux que Nagios pour monitorer des infras". Bref, Dunning-Kruger tambourinait très fort à la porte. La discussion s’est évidemment enlisée car au final toutes les pratiques "modernes" de monitoring n’était il paraît que de la hype inutile. Un pattern courant Cela ne vous rappelle rien ? Des sujets sur le cloud et le Kubernetes sont également victimes de ce phénomène. Trop hypes, trop chers, inutiles, "de toute façon je fais mieux en roulant mon infra sous les aisselles"… Le point commun de tout ça ? Expliquer l’apport en terme de pratiques et de fiabilité technique en apportant des faits ne permet plus de convaincre, car la communauté tech est disloquée en plusieurs mondes difficilement réconciliables. J’ai beaucoup essayé sur ce blog ou ailleurs (podcast, conférences…) de parler d’abord pratiques: qu’est ce que ça veut dire avoir une prod de qualité ? Comment on rend des développeurs autonomes dans leurs tâches quotidiennes sur la production, sans avoir des ops dans leurs pattes ? Comment on arrive à déployer 10, 20, 50, 100… fois par jour en prod de manière fiable ? Comment lutter contre l’alert fatigue et avoir une gestion d’incidents efficace ? Qu’apporte une approche "self-service" dans un département tech ? Comment on arrive à ne (presque) plus faire de run ? Comment je fiabilise au maximum ma prod en terme de tolérance aux pannes et sécurité sans avoir à restreindre les fonctionnalités de la plateforme ? Je pourrai rajouter de nombreuses choses à cette liste. Toute cette vision pourrait se résumer à "comment produire de la qualité en tant qu’ops pour tirer la boîte vers le haut". Il y a un travail organisationnel et d’alignement pour porter ce type de vision mais faire les (bons) choix techniques est également important notamment sur les projets non triviaux. J’insiste sur le "non triviaux" pour ceux au fond de la salle qui me sortiront le fameux "oui mais moi j’ai juste un wordpress à déployer". Cool, dans ce cas là n’importe quelle solution fera l’affaire. L’ops est un milieu conservateur Les ops adorent généralement critiquer les développeurs et notamment les différentes "hypes" du moment des dev: nouveaux frameworks, outils, pratiques. Et c’est vrai qu’il y a parfois de l’abus dans certains écosystèmes. Mais il faut également remercier les développeurs pour cela. Le monde de l’ops est un milieu extrêmement conservateur (même si cela s’améliore avec la nouvelle génération), où les nouvelles technologies et pratiques sont souvent vus avec méfiance voir haine. Et dans de nombreuses boîtes c’est clairement les dev (souvent des dev ayant également un pied dans l’infra, soyons honnête) qui ont réussi à pousser de nouvelles manières de bosser. Quand en 2023 vos ops sont toujours à se demander si il faut faire du conteneur en production, voir débattent encore de l’intérêt de systemd, la meilleure chose à faire est en effet de les bypass dans le but de retrouver de la productivité. Et c’est ce conservatisme que l’on retrouve dans les longs "débats" tech dans les réseaux sociaux. Je me rappelle il y a longtemps un manager infra disant quelque chose comme "Des machines virtuelles à la demande? J’en ai jamais eu besoin dans le passé je vois pas pourquoi on investirait là dedans". Pourtant l’apport aurait été énorme pour les projets (qui eux étaient demandeurs), mais son manque d’expérience des pratiques modernes ne lui permettait même pas d’entrevoir ce que cela allait apporter. Remplacez machines virtuelles par cloud, Kubernetes ou autre. Combien "d’anti cloud" ou "d’anti Kubernetes" ont réellement une expérience en production avec ? Je me suis par exemple rendu compte récemment que j’avais débattu pendant des années avec des gens sur le sujet de Kubernetes alors qu’ils ne l’avaient en fait jamais utilisé. Certains vont probablement me dire "oui mais le contexte c’est important, certains contextes techniques sont difficiles etc". Et c’est vrai. Et j’ai travaillé dans le passé dans ce type de contexte. Mais jamais je ne serais aller faire la leçon à tout ceux qui m’ont permis de progresser en leur disant "Ah non, vu que moi je bosse dans une organisation broken personne n’a le droit de faire mieux", voir, comme on me l’a déjà dit "ce que tu décris est impossible, tu mens sur tes pratiques". Je reste quelqu’un d’optimiste. Je pense que globalement tout le monde est capable de progresser dans le bon contexte, et on a tous des expertises différentes. Je suis nul sur des sujets comme la virtualisation, le stockage, ou le choix de hardware. On ne me verra jamais aller tenter d’imposer mon point de vue sur ces sujets avec des personnes bossant dessus depuis des années, par contre j’écouterai avec attention ce qu’ils ont à dire si je me retrouvais à travailler sur ce type de technologie. Savoir où s’arrêtent ses connaissances est je pense très important en tant qu’ingénieur. Mais je suis aujourd’hui assez fatigué de discuter avec des gens qui ne souhaitent ni écouter ni progresser mais qui par contre ont des convictions fortes sans ni l’expérience ni l’expertise pour les soutenir. Je pense pourtant qu’il est important de ne pas se décourager et de continuer démonter le FUD sur les réseaux pour une seule raison: les sysadmin/SRE juniors. Un junior peut vite se laisser influencer par certains discours "anti hype", où l’ops est vu comme quelqu’un à contre courant, incarnation du pragmatisme qui peut tout faire à coup de scripts bash et insensible au "marketing" (le mot marketing incluant tout ce qui est sorti après 2005). Dommage de laisser quelqu’un commencer sa carrière avec cet état d’esprit. Cette vision passéiste (et anti modernité) doit disparaître, et c’est aussi notre rôle de faire en sorte que cela arrive.
Kubernetes, Cloud, Observability... Quand l'écart...
Pourquoi certaines technologies créent des débats sans fins et souvent stériles...
Source: mcorbin
Tracing avec Opentelemetry: pourquoi c'est le futur (et pourquoi ça remplacera les logs)
Le tracing, c’est génial mais souvent sous-exploité aujourd’hui dans notre industrie. Venez découvrir pourquoi vous devez mettre des traces dans vos applications. Cet article est second article de ma série sur l'observability. Retrouvez le premier article sur les métriques ici. C’est quoi le tracing ? Le tracing est une technique permettant de suivre au sein d’un même service ET également entre services (donc dans un système distribué) le "parcours" d’une action. Exemple: Une client HTTP envoie une requête à une application appelée App A App A reçoit la requete, déclenche une autre requête vers une base de données, puis envoie également une requête HTTP à une application App B App B reçoit la requête, fait également une requête à une base de données, et renvoie une réponse à App A App A retourne la réponse au client initial On voit ici qu’une action utilisateur (la requête HTTP) a déclenché de nombreuses actions côté applicatif. Il est souvent difficile de suivre dans les systèmes d’aujourd’hui, notamment microservices, ce type d’actions. C’est là que le tracing intervient: il nous permet de suivre avec détails ce qu’il se passe sur nos systèmes. Opentelemetry est une implémentation standard du tracing. Le fait que ce soit un standard est très important. Traditionnellement les APM (cloud type Newrelic, Datadog…) fournissaient leurs propres agents et SDK pour ce type de besoins. Avec Opentelemetry votre code applicatif n’est plus lié à un vendor particulier et donc n’est pas "lock-in". Si vous utilisez Opentelemetry pour les traces, vous aurez la possibilité de facilement changer de technologie de stockage (cloud ou on premise) sans changer votre application, notamment grâce à l’Opentelemetry collector qui est un composant pouvant recevoir et ensuite transférer les traces à différents systèmes Tout un écosystème s’est développé autour d’Opentelemetry et des traces et l’outillage aujourd’hui est très bon dans tous les langages "mainstream". Des gestionnaires de traces comme Grafana Tempo vous permettent par exemple d’avoir de très belles représentations d’une action sur un système distribué (image tiré de cet article). D’autres visualisations sont possibles comme des "services map" montrant sous forme de graphe les communications entre services. Traces et Spans On va donc générer des traces composées de spans pour suivre nos requêtes et actions. La documentation officielle d’Opentelemetry explique bien le concept que je vais résumer ici. Prenons l’exemple précédent. Il va être possible avec Opentelemetry de mesurer le temps d’exécution de chaque étape du parcours de notre action. On veut donc mesurer le temps d’exécution de la requête par le client HTTP original. Puis le temps passé dans l’application A, où l’on peut mesurer le temps de traitement de la requête HTTP par le serveur, qui sera lui même composé du temps d’exécution de la requête SQL et de l’appel HTTP à l’application B. Même chose pour l’application B où l’on mesurera le temps de traitement de la requête HTTP reçue et de la requête SQL exécutée. Voici une représentation de cela, avec le temps en abscisse: Il serait également possible de représenter cette série d’action comme un arbre: On voit ici que chaque action a comme noeud suivant l’action (ou les actions) suivante (et inversement ces actions ont comme parent l’action précédente). On pourrait également représenter l’exécution de la toute première requête HTTP (HTTP Client) en JSON: { "name": "HTTP Client", "context": { "trace_id": "0x5b8aa5a2d2c872e8321cf37308d69df2", "span_id": "0x5fb397be34d26b51" }, "start_time": "2023-08-19T17:52:58.114304Z", "end_time": "2023-08-19T17:55:98.114304Z", "parent_id": null, "attributes": { "http.route": "/foo", "http.request.method": "GET", "http.response.status_code": 200, "server.port": 443, "url.full": "https://app-a.mcorbin.fr/foo" } } En fait, ce JSON est la représentation d’une span opentelemetry. Comme dit précédemment une trace permet de suivre l’exécution d’actions liées entre elles. Une trace est tout simplement une liste de spans partageant le même trace_id. Dans cet exemple, ma span a une trace_id égale à 0x5b8aa5a2d2c872e8321cf37308d69df2. Elle a une également une span_id (0x5fb397be34d26b51, permettant de l’identifier de manière unique). Ma span a également une date de début et de fin (start_time et end_time). La span démarre avant l’exécution de l’appel HTTP, et se termine lorsque la réponse est reçue. On peut comme cela déduire le temps d’exécution total ce cette action. Vient ensuite le champ parent_id. Rappelez-vous de la représentation en arbre des actions: ici, c’est la première action à être exécutée donc la span n’a aucun parent: c’est ce qu’on appelle la root span. Viennent ensuite les attributes dont nous allons parler dans la section suivante. Spans et attributes Une span avec juste un nom (comme HTTP Client) et un temps d’exécution serait inutile: il serait impossible d’en faire quelque chose et de comprendre l’action réalisée. C’est là qu’interviennent les attributes. Dans la span représentée précédemment, les attributes seront liés à la requête HTTP réalisée: route, méthode, status de la réponse, port, url cible… cela permet de comprendre immédiatement la requête qui a été exécutée. Représentons maintenant la span suivante de notre arbre: le serveur HTTP recevant la requête dans l’application A: { "name": "HTTP server", "context": { "trace_id": "0x5b8aa5a2d2c872e8321cf37308d69df2", "span_id": "0x8ad397bae4d26489" }, "parent_id": "0x5fb397be34d26b51", "start_time": "2023-08-19T17:53:01.114304Z", "end_time": "2023-08-19T17:55:50.114304Z", "attributes": { "http.route": "/foo/:id", "http.request.method": "GET", "http.response.status_code": 200, "url.path": "/foo/123456", "url.scheme": "https", "server.port": 443, "server.address": "app-a.mcorbin.fr", "client.address": "10.36.1.2", "client.port": 39874 } } Regardons déjà les ID de trace et de span. Le trace_id est le même que la span HTTP CLient: en effet, les deux spans font partie de la même série d’action (de la même trace). Le span_id est par contre unique à la span. Ici, le parent_id n’est pas null: il est égal à 0x5fb397be34d26b51 qui est la valeur de la span précédente (HTTP Client). Cela est logique car c’est bien cette span qui est parente dans l’arbre. Les attributs donnent également ici des informations sur la requête HTTP reçue. On peut voir quelques similitudes avec la span HTTP Client. Conventions de nommage On a vu l’importance des attributs pour donner du contexte aux spans. Dans notre exemple du début d’article on aurait par exemple une span pour la requête SQL du service A. Cette span ressemblerait probablement à quelque chose comme: { "name": "SQL query", "context": { "trace_id": "0x5b8aa5a2d2c872e8321cf37308d69df2", "span_id": "0x1bc187ba44dc6aea" }, "parent_id": "0x5fb397be34d26b51", "start_time": "2023-08-19T17:53:08.114304Z", "end_time": "2023-08-19T17:53:49.114304Z", "attributes": { "db.name": "users", "db.statement": "SELECT id, email, password FROM users WHERE =", "db.operation": "SELECT" } } Ici les attributs seront spécifiques à SQL et nous permettent aussi de facilement identifier l’action réalisée. Il est important de standardiser les attributs. Nous allons en effet pouvoir faire des recherches sur les traces. Cela serait très difficile si un service nommait un attribute db.statement et l’autre database.statement: il faut des conventions de nommage. Le standard Opentelemetry fournit déjà une liste de noms et types d’attributs (let attributes sont typés) standards. Vous pouvez trouver cette liste ici. Tous les attributs techniques "classiques" (en lien avec des technologies comme les base de données, les cloud providers, des protocoles comme HTTP ou gRPC) sont déjà standardisés. Des librairies existent également pour construire ces attributs standards (comme la lib semconv en Golang). Il est également très important de standardiser vos attributs métiers. Avoir des attributs techniques attachés aux spans est intéressant, mais il est encore plus intéressant d’attacher du contexte métier à vos spans. Cela peut être des attributs comme <company-name>.user.id, <company-name>.organization.id ou tout autre attribut métier important. Rappelez vous que les traces sont intéressantes notamment pour découvrir des problèmes de performance: peut être allez-vous vous rendre comme que c’est toujours les requêtes venant d’un utilisateur spécifique qui sont lentes grâce à un attribut contenant son ID. Sans cet attribut, l’investigation du problème pourrait être beaucoup plus lente. Resource Il est commun dans une span d’avoir également un bloc resource contenant des attributs communs à une application: { "resources": { "service.name": "App-A", "service.version": "0.0.1" } } Ces attributs sont souvent utilisés par les base de données de traces pour stocker ensemble les spans d’un même service, car toutes les spans émises par une instance d’un service donné auront les mêmes ressources. Scope Un autre bloc présent dans une span est le bloc scope. Voici par exemple le contenu de scope pour une span émise par le package Golang otelgin, permettant d’ajouter le support d’Opentelemetry au framework Gin: { "scope": { "name": "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin", "version": "0.42.0", "attributes": {} } } Cela permet de voir que cette span a été générée par cette librairies. La documentation officielle d’Opentelemetry explique bien cela. Status Il est également possible d’ajouter à une span un status avec un message permettant de très rapidement filtrer les spans en erreur. Une span représentant une erreur de validation de requête HTTP pourrait avoir comme status: { "status": { "message": "Invalid HTTP Body", "status": "error" } } Events, et pourquoi les traces peuvent remplacer les logs On arrive à un concept que j’adore dans le tracing: les events. On a étudié précédemment des exemples de spans représentant une action avec une date de début, une date de fin et des attributs. Il est également possible d’ajouter à une span des messages arbitraires avec un timestamp fixe associé et des attributs: ce sont les events. On peut voir un event comme un log, c’est exactement le même principe. Reprenons notre exemple de serveur HTTP recevant une requête. Il est commun d’avoir dans des handlers HTTP des logs applicatifs de ce type: logger.Infof("received request from user %s to perform action Foo", userID) logger.Infof("user %s is in trial mode", userID) // traitement de la requête logger.Infof("successfully performed action Foo for user %s", userID) Ces logs sont il est vrai de faible qualité (c’est juste pour l’exemple) mais on voit souvent ce genre de patterns avec de nombreux logs ajoutés "au cas où" et loggant toute sorte de choses en production. Mais si vous faites du tracing, ces logs peuvent complètement disparaître: L’user ID et mode peuvent être simplement attaché à la span comme un attribute Des events attachés à la span peuvent remplacer les logs Un exemple: { "name": "HTTP server", "context": { "trace_id": "0x5b8aa5a2d2c872e8321cf37308d69df2", "span_id": "0x8ad397bae4d26489" }, "parent_id": "0x5fb397be34d26b51", "start_time": "2023-08-19T17:53:01.114304Z", "end_time": "2023-08-19T17:55:50.114304Z", "attributes": { "user.mode": "trial", "user.id": " 86a7d17f-ab35-4312-88ea-9414a15e450b", "http.route": "/foo/:id", "http.request.method": "GET", "http.response.status_code": 200, "url.path": "/foo/123456", "url.scheme": "https", "server.port": 443, "server.address": "app-a.mcorbin.fr", "client.address": "10.36.1.2", "client.port": 39874 }, "events": [ { "name": "successfully performed action Foo", "timestamp": "2023-08-19T17:55:20.114304Z", "attributes": { "user.id": " 86a7d17f-ab35-4312-88ea-9414a15e450b", } } ] } Comme vous pouvez le voir, un log est devenu un event attaché à ma span et le reste des attributs (user.id et user.mode). Et ça, c’est GENIAL. Pourquoi c’est genial ? Reprécisons pourquoi le tracing est intéressant: suivre des actions entre services. Les logs sont très souvent utilisés pour ça également. Qui n’a jamais utilisé les logs pour suivre le parcours d’une requête dans une application, ou même entre applications via des recherches plus ou moins complexes ? En ayant des attributs métiers dans mes spans ET en ajoutant des events lorsque j’ai besoin d’avoir un timestamp exact associé à un message, j’ai le meilleur des deux mondes: Je bénéficie de tout l’écosystème des traces pour suivre le parcours de mes actions Je peux, une fois ma requête identifiée, avoir facilement tous les "logs" (events) associés à cette requête car ils seront tout simplement attachés aux spans? C’est un énorme gain de temps. Je n’ai pas à jongler entre différents outils pour essayer de trouver une information. Du moment que j’ai un ID de trace (ou que je peux le retrouver via une recherche sur un attribut), j’ai accès à toutes les informations sur cette trace (depuis tous mes services) d’une manière unifiée. Je n’ai plus à galérer à corréler des logs entre eux, ma trace et mes spans le font pour moi ! Je prévois dans le futur une fusion des outils de gestion de traces et de logs. Pourquoi garder les deux alors que les traces font déjà un boulot LARGEMENT meilleur tout en étant un format standard ? Je pense que si je devais reconstruire un système d’information "from scratch" aujourd’hui je: Garderai les logs pour les erreurs et certains logs très importants (type logs "légaux" à conserver). Et encore: le SDK Opentelemetry permet d’attacher des erreurs aux spans sous forme d’events donc c’est également super d’avoir les erreurs attachées aux spans. Tout le reste: des traces avec des attributs et events de qualité. Je suis sûr qu’on pourrait même faire une sorte de "log level" pour les traces, ajoutant certains events à des spans en fonction d’une configuration globale, comme un logger. Sampling Cela me permet de rebondir sur le sujet du sampling. On entend toujours dire lorsqu’on parle des traces "conserver 100 % des traces est trop coûteux en stockage car trop de volume", et donc qu’il faut faire du sampling (par exemple, ne conserver que 5 % des traces). Je ne pense pas que ce soit une bonne idée. Certains outils comme Grafana Tempo ont pour objectif de pouvoir garder 100 % des traces en les stockant sur S3 pour baisser les coûts. Mais le lien entre traces et logs est aussi important. Les logs aussi sont souvent coûteux à stocker (je vois les utilisateurs Elasticsearch sourire en lisant ça). Pourtant, des applications qui loggent comme des porcs sans que personne ne lève un sourcil ne posent aucun problème dans de nombreuses entreprises, et personne ne veut faire du sampling sur les logs car cela baisse les capacités d’investigations de problèmes. Pourquoi ne pas voir les traces comme un remplaçant des logs et donc avec un transfert des coûts depuis les logs vers les traces ? Au final, l’information est quasiment la même, les deux sont des hashmaps clé/valeur. Ne vous dites pas "j’aurai 40TB de traces en plus de mes 40TB de logs", mais plutôt "j’aurai 40TB de traces mais plus que 2TB de logs, et une meilleure capacité à investiguer des problèmes". C’est vraiment vers ça que doit pour moi se tourner l’industrie (et les gens construisant des outils de gestion de logs). Pour aller plus loin Il y a d’autres champs disponibles dans les spans. Les Links permettent de lier des spans entre elles même si elles font partie d’autres traces (donc d’avoir aussi des relations entre traces et non seulement entre spans d’une même trace). Le champ Kind permet d’ajouter une valeur spéficiant si l’émetteur est un client, un serveur, ou consumer de messages (d’une queue de message par exemple)… Bref, la définition des spans est vaste et le mieux est d’explorer la documentation. Propagators Les traces permettent donc de suivre entre systèmes des actions. Mais comment réaliser cela quand par exemple un client envoie un requête HTTP à un serveur, ce serveur publiant ensuite un message dans une queue de message (ou Kafka par exemple) qui sera ensuité consommé par un autre service ? Nous voulons pouvoir suivre l’action jusqu’à sa fin. C’est là que les propagators interviennent: c’est une convention pour passer des informations de tracing entre systèmes. Le plus connu est le propagator Trace context: c’est un standard du W3C. Dans le cas d’un client HTTP faisant un requête sur un serveur, les informations de la span courante côté client seront passées au serveur via un header HTTP appelé Traceparent, par exemple: Traceparent: 00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01 Le format est <version>-<trace-id>-<parent-span-id>-<flags> Le serveur récupèrera ces informations et les utilisera pour ses propres spans (notamment en mettant dans trace_id l’ID de la trace et dans parent_id l’ID de la span parente du client). Pour de l’asynchrone, pareil: les informations de la trace seront passées dans le message et récupérées par le consommateur. Quand vous utilisez Kafka, il est courant d’utiliser les headers du protocol Kafka pour cela. Il est très important de ne jamais "casser la chaine" du tracing ou alors le contexte est perdu, et votre trace incomplète. Cela demande une certaine discipline dans le code applicatif pour être sûr que le contexte est toujours passé entre fonctions et à chaque I/O. Si un service intermédiaire n’implémente pas le tracing et casse la chaine, c’est game over. Générer des métriques depuis les traces ? Pourquoi as-ton besoin de traces quand on a des métriques ? Car il n’est pas possible avec des traces brutes d’avoir une vue aggrégée du comportement d’un système. Les traces permettent d’avoir accès aux détails sur la performance d’une requête unique: cela est impossible avec des métriques type Prometheus. Les métriques fournissent des informations comme par error le nombre de requêtes par seconde sur un service, et du nombre d’erreur (error rate). Elles vont fournir aussi des quantiles pour mesurer la latence (p50, p99…). Et c’est sur des métriques que l’on pourra définir des SLO et calculer par exemple des choses comme un error budget ou burn rate (peut être que je ferai un article sur ce sujet plus tard). Néanmoins, tout cela pourrait théoriquement être dérivé depuis les traces. Si vous gardez 100 % de vos traces, il est possible de recalculer tout ce que vous voulez depuis ces traces. L’outillage pour réaliser cela "at scale" est un peu limité pour le moment. Je montrerai dans un prochain article comment réaliser cela en utilisant Mirabelle, un outil que j’ai conçu et sur lequel je viens de rajouter le support d’Opentelemetry traces. Cela n’est pas forcément une idée nouvelle. A l’époque de protocoles comme statsd les métriques étaient dérivées d’événéments. L’industrie a ensuite changé son fusil d’épaule avec des outils comme Prometheus (et du pull pour récupérer les métriques) mais je pense que l’on va revenir dans les prochaines années à des systèmes orientés "push", basés sur Opentelemetry, et j’espère les traces. Je rêve d’un monde où j’ai juste à générer des traces pour remplacer la majorité de mes métriques et de mes logs. Conclusion Je pense que le tracing est le futur de l’observabilité. Je n’ai rien d’autre à dire pour conclure cet article ;)
Tracing avec Opentelemetry: pourquoi c'est le futur...
Le tracing, c’est génial mais souvent sous-exploité aujourd’hui dans...
Source: mcorbin
Les développeurs doivent-ils apprendre/connaître l'infra (docker, cloud, Kubernetes...) ?
Je tenterai dans cet article de répondre à cette question. Tout a commencé sur un réseau social bien connu où un développeur conseillait à ses camarades d’apprendre les notions d’infrastructures (docker, cloud, Kubernetes). Il en est ressorti des discussions intéressantes et ça m’a donné envie de reparler du sujet. Apprendre l’infrastructure est toujours intéressant De manière générale apprendre de nouveaux trucs est toujours intéressant. Je pense personnellement que les meilleurs profils du marché sont des profils ayant une forte expertise dans plusieurs domaines, ce qui leur permet d’avoir une vue d’ensemble des problématiques de l’entreprise. Cela facilite également grandement la capacité à proposer puis implémenter une vision technique et à faire le pont entre différentes équipes. C’est un peu mon cas où j’ai toujours travaillé à la frontière du dev et de l’ops et donc suis capable d’intervenir et accompagner sur les deux tableaux selon les besoins. Par exemple, je considère aujourd’hui qu’il est important qu’un Sysadmin/DevOps/SRE (le nom du job est un détail) doit savoir coder. Quand je dis doit savoir coder, je ne dis pas être capable de faire du shell dégueulasse, je dis être en capacité d’être mis dans une équipe de dev et de s’en sortir dans un temps raisonnable, et donc de maîtriser à minima les bases de l’ingéniérie logicielle (design patterns, concurrence et parallélisme, tests et mocks, maîtrise basique du DDD). Soyez rassuré si vous êtes dans ces métiers mais manquez de compétences en développement: cela s’apprend. Dans mon entreprise nous formons en interne au développement par exemple. C’est aussi là que l’on se rend compte qu’avoir des équipes avec des profils venant des deux mondes est intéressant (nous recrutons également des profils purs dev que nous formons à l’infrastructure). Mais revenons aux développeurs. Apprendre l’infra sera donc en effet un gros plus pour eux. J’adore personnellement ce genre de profils. Mais apprendre la data (analyse, machine learning, stockage…) serait également un gros plus, ou bien apprendre la programmation système ou kernel, ou le réseau… La connaissance n’est jamais perdue et on ne peut de toute façon pas être expert en tout. Donc apprendre l’infrastructure est intéressant mais ce n’est pas obligatoire. Il y a pourtant certaines bases à maîtriser. Gérer son app du dev à la prod Je suis convaincu qu’une entreprise produisant du logiciel (je parle du type d’entreprise que je connais: services en ligne, sites web, solutions SaaS, services Cloud, logiciels édveloppés et utilisés en interne type SI grands groupes…) ne peut être efficace que si les développeurs peuvent travailler en autonomie du développement à la production jusqu’à l’exploitation. Qu’est ce que cela veut dire ? Les équipes infrastructure ne doivent pas être un point de blocage ou de friction lorsqu’un développeur doit déployer en production. Une mise en prod doit devenir quelque chose de simple à réaliser, qui peut être fait de manière répétable et à tout moment. Cela ne veut pas dire qu’on va donner les accès admin de la production aux dev: on va au contraire faire en sorte qu’un déploiement soit une procédure standard et automatisée. Dans ma boîte actuelle on déploie ~80 fois en production par jour, heureusement que je ne suis pas contacté à chaque fois pour valider le déploiement, hein ? Par exemple, à chaque fois qu’une nouvelle release est réalisée pour une application, ou qu’un commit est merge sur la branche principale, la plateforme d’intégration continue peut lancer automatiquement (ou via un action manuelle d’un dev comme cliquer sur un bouton) le déploiement. Ici, les dev n’ont pas accès à la prod: ils ont accès à une abstraction permettant de déclencher l’action de mise en production de manière fiable. De la même manière un rollback doit être une simple action accessible également aux dev. Il est également important ici pour les dev de connaître la façon dont le déploiement va se réaliser, notamment son déroulement, quel que soit le type d’infrastructure utilisé (PaaS, Kubernetes…): On a très souvent plusieurs instances d’une application en production pour la tolérance aux pannes, et on met à jour généralement ces instances une par une à la nouvelle version, en arrêtant le déploiement en cas de soucis. Cela permet d’éviter les downtime en cas de problème. Lorsqu’une ancienne version de l’application est arrêtée, la plateforme de déploiement envoie généralement un signal (SIGTERM) à l’application pour lui dire de s’éteindre. L’application doit capter ce signal et se stopper proprement, sans perdre aucune requête HTTP (par exemple celles en cours de traitement): l’ordre d’arrêt des composants internes de l’application est donc importante (il serait dommage de stopper son threadpool de base de données alors que le serveur HTTP reçoit toujours des requêtes). L’application doit également démarrer proprement: les applications exposent habituellement un endpoint HTTP /health indiquant qu’elle est prête à accepter du traffic réseau. Là aussi l’ordre de démarrage des composants de l’application est imoortant. Il est courant d’exécuter des changements de schémas de base de données au démarrage des applications: il faut garder en tête qu’il faut pouvoir rollback. On voit dans ces quelques exemples que l’infrastructure a un impact sur la conception de l’application et que cette dernière doit avoir un certain comportement pour fonctionner sans problème. Il est extrêmement courant d’avoir des applications perdant des requêtes HTTP (donc retournant des erreurs aux clients finaux) during les mises à jour par exemple car les contraintes infrastructures ne sont pas comprises. De même, si l’application a un problème en production (par exemple une application HTTP commence à retourner des erreurs 500), ce ne sont pas les équipes infrastructure qui doivent être alertées en premier. Pourquoi ? Car ce sont les équipes de dev qui ont la connaissance de l’application. En tant que SRE, que puis-je faire si un bug applicatif cause un problème dans une application sur lequel je n’ai jamais travaillé, voir dont je ne connais ni le langage ni le métier ? Il n’y a donc aucun intêret à avoir un intermédiaire "ops" qui se contentera de toute façon de retransmettre l’alerte aux équipes dev concernées, cela fera perdre du temps à tout le monde. Cela veut dire que les dev doivent aussi maîtriser la partie observability de leurs applications: Compréhension de la différence entre logs, métriques, et traces, de quand et comment les utiliser (types de métriques, labels, cardinalité, logs structurés, fonctionnement du tracing…). Capable de mettre en place des métriques techniques et business et de définir des SLO et alertes dessus. Capacité à aller explorer ces informations (dashboards, outils de recherche de logs…) et de savoir en autonomie investiguer des problèmes liés à leurs applications. Capable de comprendre l’impact d’une panne d’un composant système externe (base de données, autre service…) sur son application et de prévoir des mécanismes pour que l’application réagisse correctement (éviter des états inconsistants dans la base de données, réconciliation…). Cela ne veut pas dire qu’il n’y a pas des équipes ops/SRE en support, qui seront là pour mettre en place les outils nécessaires pour rendre cela possible et accompagner si besoin les équipes sur ces bonnes pratiques. Bref, on voit que de nombreuses choses liées à l’infrastructures doivent être compréhensibles pour les développeurs. J’aurai également pu parler de pipelines de CI où là aussi une certaine autonomie est attendue pour définir les étapes de tests, lint, build et pour investiguer des jobs en echec. Une histoire de niveau Bien sûr, un dev junior ne peut pas maitrîser tout ça. Mais au bout de plusieurs années de dev maîtriser ces sujets devient essentiel. Au délà de la partie métier (compréhension du besoin du client, organisation…) et technique pure dev (architecture logicielle), les profils senior et plus doivent élargir leurs horizons et comprendre comment les applications s’intègrent dans le SI au sens large. Travailler sur des architectures orientées service ou des systèmes distribués que l’on retrouve de plus en plus (message bus, base de données NoSQL, ou même sur les communications inter services) demandent d’avoir des compétences transverses. J’aurai également pu citer une maitrîse basique du réseau (HTTP/gRPC, TCP, TLS, UDP, DNS, base du routage, load balancing…) comme autre compétences importantes, voir des compétences systèmes ou hardware sur des applications demandant de très hautes performances (pattern d’accès au disque, epoll…). Je pense que ce sont les équipes qui ont intégrées ces éléments dans les compétences des développeurs qui travaillent le plus efficacement aujourd’hui. Mais je le répète, les dev ne remplacent pas les ops, on ne demande pas aux dev ici de déployer des clusters Kubernetes ou de configurer des machines virtuelles, mais d’avoir la maîtrise totale de leurs applications. Références J’ai déjà produit pas mal de contenu sur le sujet que je repartage ici: Mon talk "Rendez vos développeurs autonomes sur la production" Kubernetes et manifests YAML: trop bas niveau pour les dev ? L’important n’est pas la technologie mais la plateforme 2022: bash n’est toujours pas une bonne idée pour l’administration système Conclusion Avoir certaines compétences infra est non seulement utile mais indispensable. Pas besoin d’être un expert de Linux, du cloud ou de l’orchestration de conteneurs pour être développeur, mais des bases sont attendues et si des gens veulent aller plus loin c’est très bien aussi: l’industrie a besoin de ces profils hybrides ayants des expertises variées.
Les développeurs doivent-ils apprendre/connaître...
Je tenterai dans cet article de répondre à cette question.
Tout a commencé...
Source: mcorbin
Observability: tout ce que vous avez toujours voulu savoir sur les métriques
Je présenterai dans cet article "techno-agnostic" (aucune techno citée) les différents types de métriques que vous pouvez retrouver dans une application, et expliquerai comment les utiliser. Cet article est le premier d’une j’espère longue série sur l’observability qui traitera en profondeur du sujet. Attendez vous prochainement à d’autres articles sur les logs, les traces, les SLO/error budget/burn rate, l’alerting, le monitoring "blackbox" et toutes les bonnes pratiques associées. Retrouvez l’article sur Opentelemetry et le tracing ici. Qu’est ce qu’une métrique ? Une métrique est une mesure et des informations associés à une date précise (un timestamp). Prenons l’exemple d’une métrique représentant le nombre de requêtes HTTP reçu par un serveur web. Je l’appellerai http_requests_total. Cette métrique sera donc incrémentée à chaque fois que le serveur HTTP reçoit une nouvelle requête. Imaginons maintenant que nous puissons regarder et noter de manière régulière la valeur de cette métrique. Cela donnerait peut être (Pour plus de simplicité, je ferai toujours commencer le temps à la valeur 0 dans mes exemples) : Table 1. Valeurs de http_requests_total timestamp valeur 1 10 11 70 21 90 Au temps 1, la valeur de la métrique est 10. Au temps 11, elle est de 70 et au temps 21 elle est de 90. On en déduit donc que notre serveur HTTP a reçu 60 requêtes (70 - 10) entre les temps 1 et 11, et 20 requêtes (90 - 70) entre les temps 11 et 21. Prenons un autre exemple: une métrique pourrait par exemple remonter la consommation mémoire (RAM) d’un serveur. Elle s’appellera ici memory_used et sa valeur sera en mégabyte: Table 2. Valeurs de memory_used timestamp valeur 1 1500 11 3000 21 2900 Notre serveur utilisait donc 1500 MB de mémoire au temps 1, 30000 MB au temps 11, et 2900 au temps 21. Nos métriques peuvent donc représenter plusieurs choses mais l’idée est toujours la même: une mesure associée à un temps. Labels Tout cela est bien pratique, mais il est très souvent nécessaires d’avoir plus de détails sur les métriques. Reprenons notre métrique http_requests_total : comment faire si je souhaite compter le nombre de requêtes par url cible ou par méthode HTTP par exemple ? Si mon serveur web héberge un blog, j’aimerai bien avoir le nombre de de requêtes par article de blog dans le but de connaître mes articles les plus populaires. Cela est possible en rajoutant des labels à la métrique. Les labels sont des dimensions supplémentaires attachées à une métrique. Je vais ici en ajouter trois: url : l’adresse de la page demandée à mon serveur web method : la méthode HTTP de la requête Voici à quoi pourraient ressembler des observations de cette métrique pour par exemple un blog culinaire présentant des recettes de pâtisseries: Table 3. Valeurs de http_requests_total avec des labels timestamp url method valeur 1 /paris-brest GET 40 3 /eclair GET 10 11 /paris-brest GET 60 13 /eclair GET 15 Ces valeurs nous montrent qu’au temps 1, la page paris-brest avait eu 40 visites, puis 60 au temps 11. La page eclair avait elle 10 visites au temps 3, puis 15 au temps 13. La méthode HTTP ici est toujours GET. Nous verrons des exemples plus complexes dans la suite de cet article. On appelle généralement série une combinaison possible des valeurs des labels pour une métrique donnée. Nous avons dans cet exemple deux séries pour la métrique http_requests_total: url="/paris-brest", method="GET" url="/eclair", method="GET" Cela nous amène à la notion de cardinalité et du choix des labels. Cardinalité On appelle cardinalité le nombre de série pour une métrique. La cardinalité était donc de deux dans notre exemple précédent. Imaginons le même site web mais avec ce coup ci 50 recettes différentes, et que l’on autorise en plus les utilisateurs à voter pour une recette (dans le but de pouvoir classer les recettes par popularité par exemple) en exécutant une requête de type POST sur l’url de la page. GET /paris-brest permettrait par exemple aux utilisateurs de récupérer la recette du Paris Brest, et POST /paris-brest de voter pour cette recette. On a donc 50 pages (50 recettes), et 2 méthodes (GET et POST) par recette. Notre cardinalité est donc de 50 * 2 soit égale à 100. Rajoutons un label à notre métrique: le nom de la machine (host) hébergeant le serveur web. Il est en effet courant d’avoir une application hébergée sur plusieurs serveurs pour par exemple avoir de la tolérance aux pannes. Voici par exemple deux séries ayant les mêmes labels à part celui nommé host: Table 4. Exemples de séries timestamp url method host ̀ valeur 1 /paris-brest GET server_1 40 1 /paris-brest GET server_2 10 Quelle serait la cardinalité de la métrique si l’on hébergeait le blog culinaire sur 4 serveurs différents ? Elle serait de 50 * 2 * 4 soit 400 (50 pages, méthodes HTTP GET ou POST, et les 4 serveurs). choix des labels et explosion de la cardinalité Il est important de choisir correctement les labels d’une métrique: Les labels doivent être pertinents et avoir du sens pour la métrique. Comme nous le verrons plus loin dans cet article la majorité des bases de données pour stocker des séries temporelles permettent de requêter les métriques en fonction de leurs labels. Avoir un label url sur une métrique HTTP est donc logique car il sera très utile de pouvoir filter les métriques sur ce label. Il faut éviter d’avoir une cardinalité trop importante. Une erreur classique faite par de nombreux développeurs est de stocker l’ID aléatoire (uuid généralement) généralement associé à une requête HTTP dans un label: cela veut dire que chaque requêtes HTTP sur le serveur web créera une nouvelle série. Cette série n’aura qu’une mesure, car une nouvelle requête en créera une nouvelle. Il y a d’autres pièges à éviter sur les labels. Reprenons notre label url sur notre serveur HTTP. Certaines URL peuvent être variables, par exemple une API web pourrait contenir un ID d’utilisateur comme par exemple /user/:id, la partie :id étant variable et contenant un ID associé à chaque utilisateur. Il est important dans ce cas d’utiliser comme label pour url la valeur /user/:id sans remplacer l’ID à chaque requête, et non par exemple /user/1, /user/2… ce qui créerait une série pour chaque utilisateur de la plateforme. Utiliser l’url avec la variable non remplacée ne créera qu’une série quel que soit la valeur de la variable. Certains labels peuvent être identiques à de nombreuses séries. Les organisations ont très souvent plusieurs environnements: production, pré-production (staging), développement… Il est très intéressant d’ajouter ce label aux métriques pour pouvoir facilement les distinguer, comme pour par exemple avoir des politiques d’alertes différentes entre un environnement de production et de développement. Toutes les métriques de production pourraient par exemple avoir un label environment=production. D’autres labels génériques de ce type peuvent aider à classifier les séries. Il est également important d’avoir une cohérence sur le nommage des labels. Il serait dommage d’avoir la moitié des métriques avec env=production et l’autre moitié avec environment=production par exemple. Certains outils permettent de faire du relabeling (renommer les labels de certaines métriques) mais se poser ce genre de questions dès la mise en place du monitoring reste important. Types de métriques Il existe différents types de métriques. Il vous faudra choisir le bon type selon ce que vous voulez mesurer et calculer. Compteurs Un compteur (counter) est tout simplement une métrique comptant quelque chose. C’était par exemple le cas de la métrique http_requests_total présentée précédemment. Cela veut dire dans le cas de cette métrique que mon serveur web va incrémenter la série correspondante (en fonction des labels) à chaque requête HTTP reçue. Ces compteurs par série vont donc seulement s’incrémenter en permanence. Compter des choses est utile mais il est souvent plus intéressant de calculer un taux (rate) par seconde. Voici par exemples 3 valeurs pour une même série avec le calcul du rate pour chaque valeur. Table 5. Calcul du rate http_requests_total timestamp url method compteur rate (req/sec) 1 /paris-brest GET 40 11 /paris-brest GET 80 (80-40)/10 = 4 21 /paris-brest GET 180 (180-100)/10 = 8 31 /paris-brest GET 210 (210-180)/10 = 3 On voit que le rate est calculé en soustrayant la valeur actuelle du compteur par sa valeur précédente, le tout divisé par l’interval de temps. En effet, entre ma métrique au temps 1 et celle au temps 11, 10 secondes se sont écoulées. La valeur de la métrique au temps 1 était de 40, et celle au temps 11 était de 80. Il y a donc eu 40 requêtes (80 - 40) entre ces deux valeurs. On en déduit donc que l’application a reçue en moyenne 4 requêtes par seconde pendant cet interval de temps. Appliquer cette méthode à chaque nouvelle valeur reçue permet de calculer le rate au fil du temps. Réinitialisation du compteur Les applications gardent généralement leurs métriques en mémoire. Cela veut dire que les métriques sont réinitialisées en cas de redémarrage de l’application par exemple. Il est possible dans ce cas d’obtenir un rate négatif. Table 6. Calcul du rate http_requests_total timestamp url method compteur rate (req/sec) 1 /paris-brest GET 40 11 /paris-brest GET 80 (80-40)/10 = 4 21 /paris-brest GET 10 (10-80)/10 = -7 On voit dans cet exemple que la valeur de la métrique est de 80 au temps 11, et -7 au temps 21. En effet, la valeur de la métrique est passée de 80 à 10, ce qui peut arriver si l’application a redémarrée pendant cet interval de temps. Une solution peut être par exemple de filtrer les valeurs négatives, ces dernières étant de toute façon incorrectes. Jauge Un autre type de métrique est la Jauge (Gauge). Cette métrique représente tout simplement une valeur arbitraire. On peut s’en servir pour compter le nombre d’éléments dans une queue de message par exemple. Notre programme pourrait générer une métrique toutes les 10 secondes contenant le nombre d’éléments dans la liste à cet instant. Table 7. Valeur de ma jauge timestamp valeur 1 10 11 8 21 15 C’est sur ce genre de besoins (nombre d’éléments dans une liste, une queue, une table d’une base de données…) que l’on rencontrera le plus souvent ce type de métriques. Quantiles et histogrammes Les quantiles (souvent appelés également percentiles) sont très courants dans le monde du monitoring lorsqu’on souhaite monitorer les performances d’une application. Reprenons notre exemple de blog culinaire. Nous pourrions avoir envie de mesurer le temps de chargement des pages de notre site. Nous allons donc devoir mesurer le temps des requêtes côté serveur et utiliser ces mesures pour avoir une aperçu des performances de notre serveur HTTP. C’est ici que les quantiles entrent en jeux. Les quantiles vont s’appliquer sur nos mesures et servent à découper en deux partie cet ensemble de mesures, par exemple: q50 (quantile 50, souvent appelé 50 également): ceci est la médiane. Ici, 50 % des requêtes HTTP ont un temps d’exécution inférieur à la valeur associée à mon quantile, et 50 % ont un temps d’exécution supérieur. q75: 75 % des requêtes HTTP on un temps d’exécution inférieur à la valeur associée à mon quantile, et 25 % ont un temps d’exécution supérieur. q99: 99 % des requêtes HTTP on un temps d’exécution inférieur à la valeur associée à mon quantile, et 1 % ont un temps d’exécution supérieur. q1: ceci sera tout simplement la valeur maximale (la requête la plus lente) de mon serveur HTTP. Prenons par exemple ce jeu de données représentant les durées d’exécution des requêtes sur mon serveur HTTP en millisecondes: 550, 300, 1000, 2000, 450, 1300, 1400, 200, 300, 400, 900, 1200, 800, 350, 500 Nous n’avons dans cet exemple que 15 valeurs pour faciliter l’exemple mais en réalité vous pourriez réaliser ce calcul sur plusieurs milliers si nécessaire. Une des premières choses que l’on pourrait faire est de trier ces valeurs: 200, 300, 300, 350, 400, 450, 500, 550, 800, 900, 1000, 1200, 1300, 1400, 2000 Calculons maintenant le q50 sur ces valeurs (la médiane): nous voulons donc trouver la valeur au centre de notre distribution et donc avant le même nombre de valeurs avant et après cette valeur. Ceci est assez simple dans notre cas: nous avons 15 valeurs, donc la médiane sera la 7eme valeur de notre liste triée. Nous aurons en effet 6 valeurs inférieures, et 6 valeurs supérieures. La valeur du q50 est donc de 500. De la même façon, nous voulons pour le q75 trouver la valeur ayant 75 % de valeurs inférieures (soit 15 * 75/100 = 11,25 que l’on arrondira à 11), et 25 % supérieures (donc 4) La 11eme valeur de notre liste est 1000, et donc notre q75 sera égal à cette valeur. Notre jeu de données est petit donc la même procédure appliquée au q99 nous donnera la valeur 2000 qui est aussi la valeur maximale. Les quantiles sont donc bien utiles car ils permettant d’avoir rapidement une information pertinente sur par exemple des performances d’applications. Cela permet également d’énoncer des objectifs de performances clairs, comme par exemple 99 % de mes requêtes doivent s’exécuter dans un temps inférieur à une seconde. Dans des cas réels avec de grands jeux de données (des milliers de requêtes par exemple) on peut même aller jusqu’à calculer le q99.9 ou q99,99 si besoin. Histogrammes La méthode précédente pour calculer des quantiles est intéressante car elle permet de calculer exactement la valeur du quantile. Elle a également un défault: l’ensemble des valeurs doivent être disponibles pour réaliser le calcul. Cela peut être problématique lorsqu’on veut calculer des quantiles sur un grand nombre de valeurs qui devront donc être stockées de façon unitaire. Une autre solution pour calculer les quantiles est d’utiliser un histogramme dans le but de calculer une valeur approchée du quantile mais sans avoir à stocker l’ensemble des données. La première chose à faire est de choisir les intervalles (aussi appelés bucket) de notre histogramme. Nous reprendrons comme exemple ici le temps de traitement de requêtes par un serveur HTTP, avec ce temps en millisecondes. Une pratique courante dans le monde du monitoring serait d’utiliser des intervalles démarrant tous à 0 et de compter le nombre de requêtes ayant un temps d’exécution inférieur à une valeur donnée. La liste de nos mesures est dans cet exemple la même que précédemment: 200, 300, 300, 350, 400, 450, 500, 550, 800, 900, 1000, 1200, 1300, 1400, 2000 Comptons maintenant le nombre de valeurs dans différents intervalles, par exemple combien de requêtes ont un temps d’exécution dans l’intervalle 0-100, 0-200, 0-400… Table 8. Intervalles de l’histogramme Minimum (toujours 0) Maximum (inclus) Total (nombre) 0 100 0 0 200 1 0 400 5 0 600 8 0 1000 11 0 1400 14 0 2200 15 0 Infini 15 On a donc 0 requête ayant un temps d’exécution entre 0 et 100 millisecondes, 1 entre 0 et 200 millisecondes, 5 entre 0 et 400 millisecondes etc. On remarque que cette manière de faire permet de ne pas avoir à garder l’ensemble des mesures: il suffit lorsqu’une nouvelle mesure est réalisée d’incrémenter tous les intervalles nécessaires. On remarque également que le nombre de valeur dans chaque intervalle est en augmentation constante, ce qui est logique car les valeurs précédentes sont inclus dans chaque intervalle vu que l’on recompte à chaque fois le nombre de valeurs dans l’intervalle depuis 0. Le dernier intervalle est intéressant: il compte le nombre de valeurs de 0 à Infini, et contiendra donc toujours le total des valeurs. Ces informations permettent de calculer simplement une valeur approximative d’un quantile, comme par exemple le q50: Il y a 8 intervalles différents, et 15 mesures dans ces intervalles. Nous savons que nous recherchons la métrique au centre de notre distribution, et que nous voulons calculer la médiane: Nous commençons donc par réaliser le calcul suivant: 0.5 * 15 = 7.5. Nous recherchons donc où se trouve cette valeur (7.5) dans notre histogramme. On recherche l’intervalle juste après cette valeur: dans notre cas, c’est dans l’intervalle [0, 600] car sa valeur est de 8. La valeur de l’intervalle précédent étant de 5 nous pouvons en déduire que le quantile se trouve dans cet intervalle (car 5 < 7.5 < 8). Nous calculons maintenant le nombre de valeurs présentes entre cette intervalle ([0, 600]) et le précédent ([0, 400]. Nous souhaitons donc répondre à la question combien de mesures ayant une valeur entre 400 et 600 avons nous ? Le résultat est 8 - 5 et est donc égal à 3. Comme dit au début de cet article, nous allons calculer une valeur approximative pour notre quantile. Nous savons que notre quantile se trouve quelque part dans l’intervalle [400, 600] (qui couvre une durée d’exécution de 200 millisecondes), et que nous avons 3 valeurs dans cet intervalle. Rappelez vous que l’on recherche la durée théorique pour la valeur 7.5 calculée précédemment. Nous réalisons l’opération (7.5 - 5) / 3 = 0.833. Nous soustrayons ici la valeur recherchée à la valeur associée à la borne inférieure (400) de notre intervalle, que nous divisons ensuite par le nombre de valeurs dans l’intervalle (3). Nous multiplions le résultat précédent par la durée de l’intervalle: 200 * 0.833 = 166.6. Nous pouvons décrire ce calcul de la façon suivante: j’ai un intervalle de taille 200 le point recherché se trouve au pourcentage 0.833. Nous ajoutons la borne inférieure de notre intervalle à ce résultat: 400 + 166.6 = 566.6. Ceci est le résultat final et la valeur approximative de notre quantile (et la médiane dans cet exemple). Ce calcul peut aussi se résumer au fait de tracer une droite entre les coordonnées [400, 5] et [600, 8] et de rechercher la valeur associée à 7.5 sur cette droite. Notre résultat approximatif est différent du résultat réel (qui est de 500). Il faut garder en tête que ce type de calculs fonctionne mieux sur de plus gros jeux de données. Mais ce résultat nous donne dans tous les cas une idée de la performance de notre application, et c’est ce qui est le plus important. Connaître la performance de notre application à la milliseconde près n’est pas utile dans de nombreux contextes. Il vaut mieux pouvoir obtenir rapidement et simplement (en utilisant peu de capacités de stockage et de calcul) une valeur approximative mais proche de la réalité que de toujours vouloir une valeur exacte mais qui peut se révéler difficile à calculer. Pull vs Push et stockage Nos systèmes émettent donc des métriques. Pour les exploirer, il faut pouvoir les requêter et donc les stocker. Il existe deux mondes lorsqu’il s’agit pour une application de diffuser ses métriques dans le but de les stocker: le mode push et le mode pull. Le push Dans ce mode, l’application pousse les métriques vers une base de données temporelle (ou vers tout autre système de stream processing pouvant éventuellement servir à filtrer, modifier, enrichir la métrique, ou faire backpressure). Ce mode a un certain nombre d’avantages: Un endpoint unique à connaître pour les applications pour pousser les métriques: cela permet une énorme facilité en terme de configuration applicative et réseau (règles de firewalling en sortie seulement vers une destination unique). Possibilité d’ajouter facilement des composants intermédiaires comme dit précédemment pour faire du stream processing ou absorber des pics de charge en ayant un composant "buffer" entre l’application et la base de données temporelle. Haute disponibilité et scaling très facile (load balancing, déduplication du traffic entre plusieurs base de données/services cloud par exemple) Le push est plus facile à scale et beaucoup plus flexible en ajoutant un système intermédiaire de type "message broker" entre l’application et les systèmes externes. N’importe qui peut comme ça consommer les métriques sans impacter les autres. Bref, le push, c’est bon, mangez en, malheureusement c’est plus forcément "à la mode" pour la gestion de métriques. Le pull Une technologie apparue il y a quelques années a proposé une approche différente et été vite adoptée pour différentes raisons: le pull. Dans ce mode, c’est l’outil stockant les métriques qui va aller chercher (pull) les métriques en envoyant directement des requêtes à l’application. Cela veut dire que l’application doit les exposer, via HTTP dans notre cas. Une application pourrait par exemple exposer un endpoint HTTP /metrics retournant dnas cet exemple la valeur associée à l’instant T de la requête aux différentes métriques configurées (ici des compteurs): healthcheck_total{id="f7b4dba8-9626-436e-a0d2-670e862c650a", name="appclacks-website", status="failure", zone="fr-par-1"} 1 healthcheck_total{id="f7b4dba8-9626-436e-a0d2-670e862c650a", name="appclacks-website", status="success", zone="fr-par-1"} 2789 healthcheck_total{id="f7b4dba8-9626-436e-a0d2-670e862c650a", name="appclacks-website", status="failure", zone="pl-waw-1"} 1 healthcheck_total{id="f7b4dba8-9626-436e-a0d2-670e862c650a", name="appclacks-website", status="success", zone="pl-waw-1"} 2789 La base de données temporelle fera périodiquement (toutes les 30 secondes par exemple) une requête, associera à la valeur des métriques le timestamp de la requête, et stockera ça dans sa base. L’approche pull demande une énorme logique en service discovery et a une plus grosse complexité réseau que le push. Cela force également toutes vos applications à exposer un endpoint HTTP servant les métriques. Au final, les deux approches fonctionnent. Je vous recommande un autre de mes articles sur le sujet si vous voulez avoir plus de retours sur le pull vs push. Calculs côté client ou côté serveur Dernière partie de cette article, le calcul côté client ou côté serveur. J’ai expliqué précédemment comment calculer par exemple des rate, quantiles… à partir de métriques brutes. Il faut savoir que parfois, certaines librairies applicatives de gestion de métriques calculent ces valeurs pour vous. Cela veut dire que votre application ne va pas exposer à la base de données temporelle les données brutes (par exemple, la valeur d’un compteur ou les buckets d’un histogramme) mais directement un nombre de requête par seconde, es quantiles (p99, p75…). Cela peut sembler intéressant de prime abord car il n’y a aucun calcul à réaliser côté base de données temporelle, mais il y a beaucoup d’inconvénients à ne pas avoir accès aux métriques brutes: Si les données sont pré-calculées côté application, il n’est plus possible d’aggréger ensemble les métriques de plusieurs instances (replicas) d’une même application. Il est en effet commun de calculer des quantiles pour une instance d’une application, mais aussi pour toutes les instances d’une application ensemble. Cela permet de visualiser la latence par instance de l’application (utile pour voir si une instance a des caractéristiques de performances étranges par rapport aux autres) mais aussi de voir la latence globale pour toutes les instances de l’application. Ce dernier calcul ne peut se faire que en ayant accès aux métriques brutes et en faisant la somme de chaque bucket de chaque instance de l’application. Je n’ai présenté dans cet article que quelques exemples de calculs à réaliser sur les métriques. En réalité, il y en a de nombreux autres que vous retrouverez dans vos base de données temporelles et qui sont également indispensables pour monitorer vos applications. Si vous n’avez pas accès aux données brutes, ces calculs seront irréalisables. Conclusion Les métriques sont indispensables pour monitorer correctement des composants applicatifs ou d’infrastructure. Bien choisir ses métriques (type, labels…) est important et est la clé pour construire une solide plateforme d’observability.
Observability: tout ce que vous avez toujours voulu...
Je présenterai dans cet article "techno-agnostic" (aucune techno citée) les différents...
Source: mcorbin
L'architecture d'Appclacks de A à Z
Je travaille depuis un moment sur un service SaaS de monitoring: Appclacks. Je détaillerai dans cet article l’architecture complète de la plateforme, du dev à la prod. Le produit On m’a avec raison fait remarquer que les articles mi-sérieux mi-troll devenaient redondants, donc repartons sur un article un peu plus technique. Appclacks est une solution SaaS de monitoring de type "blackbox", permettant d’exécuter des health checks sur vos services web (requêtes HTTP(s), TLS, DNS, TCP, vérification d’expiration de certificats…). Les health checks sont réalisés depuis plusieurs endroits dans le monde (2 actuellement, 3 prochainement). J’essaye toujours de créer des produits dont moi même je serai utilisateur, souvent car je suis frustré par les solutions existantes. Construire un SaaS complet d’une qualité professionel en solo est aussi un bon challenge et me permet également d’expérimenter. Appclacks a plusieurs avantages par rapport à la concurrence: Outillage "state of the art": API, CLI, Provider Terraform, un opérateur Kubernetes (avec CRD maison) est en préparation (déjà fonctionnel mais le dépôt est encore privé). Pas de clickodrome. Compatible Prometheus pour récupérer les métriques des health checks: configurez votre Prometheus pour scrape l’API d’Appclacks ! Un accent fort sur l’open source. L’outil exécutant les health checks, Cabourotte, est open source. D’autres suivront. Fonctionne on premise ! Déployez Cabourotte chez vous et venez faire du service discovery via Appclacks pour monitorer vos endpoints internes, eux même gérés via CLI/Terraform/… ! Tout n’est pas encore finalisé, le produit n’est pas totalement "prod ready" mais on en est pas loin. N’hésitez pas à tester en vous inscrivant à l’alpha (lien dispo en fin de page ici). Mais comment faire tout ça de manière fiable quand on travaille sur le produit en mode "side project" ? L’architecture On voit qu’on a besoin ici de plusieurs choses: Une API comme point d’entrée à toutes les actions sur la plateforme et une base de données pour stocker les informations. Un composants pour stocker les métriques des health checks côté Appclacks et les rendre disponibles aux clients. Ce sera ici Prometheus. Pouvoir déployer des instances de Cabourotte, servant à exécuter des health checks, un peu partout dans le monde si besoin. Chaque instance doit également savoir quel sous ensemble de health check à exécuter pour pouvoir scale la plateforme horizontalement. J’utilise aujourd’hui exclusivement Scaleway (la partie Cloud) pour l’hébergement. Voici un schema d’architecture global: Je vais maintenant expliquer l’architecture en détail. L’infrastructure Provisioning Toute l’infrastructure Scaleway est déployée via Terraform. Le provider Scaleway est de qualité, je n’ai pas eu de problèmes avec. L’API, Prometheus, et Cabourotte sont déployés sur des machines virtuelles dédiées. Pour Cabourotte, le déploiement se fait en France et en Pologne pour, comme dit précédemment, avoir des health checks exécutés depuis différents endroits (chaque health check configuré sur Appclacks sera exécuté de ces deux localisations). Quoi ? mcorbin n’utilise pas Kubernetes malgré sa propagande importante sur le sujet ? On en reparlera en fin d’article. J’utilise ici beaucoup les offres managés de Scaleway. Pour PostgreSQL, pour envoyer des emails, et le nouveau service cockpit qui me sert de stockage long terme pour Prometheus et pour pouvoir consulter mes métriques (et les métriques Scaleway) via Grafana. Rien à redire sur ces services managés pour le moment, j’en suis satisfait. Cockpit avait quelques instabilités pendant la beta mais maintenant ça a l’air de marcher beaucoup mieux. C’est très agréable d’avoir accès à toutes les métriques internes de Scaleway via Grafana, ceux qui ont déjà utilisé AWS Cloudwatch comprennent la douleur qu’est la gestion des métriques chez certains cloud providers. Bravo Scaleway pour ce produit. Scaleway a par contre plusieurs limitations persistantes pénibles, comme le fait que les instances dans un réseau privé avec DHCP puissent changer d’IP (incroyable sur le cloud) ou sur la gestion des security groups (pas possible d’utiliser un autre security group en source/target par exemple). Mais je fais avec. Déploiement La configuration des différentes machines virtuelles et softwares (que ce soit ceux cités précédemment ou des outils comme node_exporter) se fait avec Ansible, lancé depuis mon poste^^. Aucun rôle, just des playbooks par composants pour simplifier la maintenance à l’extrême. Par contre Ansible c’est lent sans la fibre ;'( L’API L’API, le coeur du réacteur, est codée en Go (avec Echo comme framework HTTP) et reçoit toutes les requêtes vers Appclacks. C’est aussi elle qui gère l’authentification. Elle est stateless et peut donc scale horizontalement. D’ailleurs, concernant l’authentification, j’ai fait au plus simple: elle se fait par token (sauf quelques endpoints d’administrations comme changer son mot de passe, ou bien créer un token ^^) et il est possible d’attacher à un token la liste des appels à autoriser. Cela permet par exemple, si vous utilisez comme décrit précédemment Prometheus en interne pour scrape les métriques fournies par Appclacks, de ne permettre que l’appel GetHealthchecksMetrics sur le token fourni à Prometheus. Bref, l’API reçoit des requêtes et intéragit avec PostgreSQL pour la gestion des données. Lorsque vous créez un health check, récupérez les résultats des health checks, listez vos tokens… on passe par l’API et par PostgreSQL. J’ai choisi dès le début d’extraire tous les types (struct Golang) utilisés par l’API public dans un dépôt Github à part (go-types) ce qui me permet de les réutiliser directement dans le client Golang (utilisé notamment par la CLI, le provider Terraform, l’operator Kubernetes). Vous pouvez remarquer sur les types de l’API les différents tags json, query… utilisés ensuite par Echo pour désérialiser les requêtes, et les tags validate utilisés pour la vérification des données (via la lib Go validator). J’aime cette approche avec les types publics. Le prochain chantier est d’ailleurs de générer la spec OpenAPI depuis ces types, mais l’écosystème Golang sur le sujet laisse à désirer. Le code est organisé dans dans les grandes lignes en mode "DDD", ce qui facilite grandement son organisation et l’écriture de tests (me permettant de mock facilement mes repositories si nécessaire par exemple). Les migrations de base de données sont exécutées au démarrage de l’application via une lib Go. L’application log dans stdout en JSON (avec le logger zap) et expose un certain nombre de métriques (latency/rate/error des requêtes et réponses http, de certains clients…) au format Prometheus. Je n’ai pas encore de traces car aucun endroit pour les stockers, mais j’espère en avoir à terme. Emails Le service transactional email de Scaleway est top. J’ai configuré une clé d’API avec leur nouveau système d’IAM n’autorisant que l’envoi d’email. L’API utilise ensuite la lib net/smtp de Golang avec les creds de Scaleway et tout fonctionne comme prévu. Peut être que je ferai un article sur le sujet à l’occasion. Cabourotte Un défi était ensuite de configurer les différentes instances de Cabourotte exécutant les health checks des utilisateurs. J’avais un cahier des charges assez strict: Lorsqu’un utilisateur crée un health check, il devait commencer à s’exécuter très rapidement Cabourotte doit pouvoir scale horizontalement: je veux pouvoir faire exécuter les health checks des clients depuis plusieurs instances au sein d’une même région. Par exemple, mais en gardant la garantie qu’un health check n’est exécuté qu’une fois par région. Que la solution puisse fonctionner ensuite sur du multi région pour le support de multiples PoP, sur plusieurs cloud providers si besoin. Voici la solution retenue. Prober ID Chaque instance de Cabourotte dans une région donnée (France par exemple) se voit attribuer un ID (un peu comme un statefulset Kubernetes): 0, 1, 2… L’API connait ensuite le nombre total d’instances de Cabourotte par région: 3 par exemple. Détail important: bien qu’utilisant des UUID comme clés primaires, chaque health check créé sur Appclacks se voit attribuer un autre ID aléatoire au format integer. C’est important pour la suite. Si nous avons 1000 health checks à exécuter dans une région, nous voulons une répartition à peu près stable entre instances, proche de 333.. Cabourotte supporte déjà du service discovery, c’est ce qui permet de le faire tourner chez vous mais branché sur Appclacks. J’ai donc réutilisé ce système pour assigner les health checks aux instances de Cabourotte gérées par le SaaS. Chaque instance est configurée pour récupérer via un endpoint API interne (path/credentials spécifiques) les probes à exécuter. Le endpoint est de type /probers/discovery?prober-id=1, on voit que le prober ID décrit précédemment est passé en paramètre. Que fait donc l’API à partir de ça ? Une simple requête SQL similaire à "SELECT * FROM healthcheck WHERE random_id%<nombre_total_prober>=<prober_id> AND enabled=true": On sélectionne les health checks Mais seulement ceux où le résultat du modulo entre le nombre total d’instances Cabourotte dans la région et l’entier aléatoire assigné à chaque health check est égal à l’ID du prober (= de Cabourotte) demandant sa liste de health check à exécuter. On ne garde que les health checks qui sont été activés par l’utilisateur (Appclacks supporte la désactivation d’un health check sans le supprimer si nécessaire). Prenons un exemple avec 3 health checks ayant comme ID aléatoire 46, 190, 27. Si nous n’avons qu’une seule instance Cabourotte par région, voici ce que ça donne: Healthcheck random ID Prober ID Placement 46 0 46 mod 1 = 0 ? Vrai 190 0 190 mod 1 = 0 ? Vrai 27 0 27 mod 1 = 0 ? Vrai Si l’instance Cabourotte 0 demande à l’API donne moi mes health checks à exécuter, tous les health checks seront retournés. Normal, on a qu’une instance de Cabourotte qui tourne. Voyons maintenant le comportement si nous avons deux instances de Cabourotte. On s’attend à ce que les health checks soient répartis entre ces instances. Rappelez vous du calcul effectué (WHERE random_id%<nombre_total_prober>=<prober_id>). Ici le nombre de total de prober est 2. Healthcheck random ID Prober ID Placement 46 0 46 mod 2 = 0 ? Vrai 46 1 46 mod 2 = 1 ? Faux 190 0 190 mod 2 = 0 ? Vrai 190 1 190 mod 2 = 1 ? Faux 27 0 27 mod 2 = 1 ? Faux 27 1 27 mod 2 = 1 ? Vrai Ici, le prober 0 gèrera les health checks 46 et 190, le prober 1 le health check 27. On voit que grâce au modulo chaque instance récupère seulement un sous ensemble de health checks à exécuter. Si un nouveau health check est créé, celui ci sera automatiquement démarré au prochain "refresh" du service discovery de l’instance Cabourotte correspondante. Scale les probers est donc facile: il me suffit de rajouter des instances et de modifeir le nombre total d’instance dans l’API pour que les health checks se reconfigurent de manière équitable entre ces instances, et surtout la solution a le mérite d’être très simple. Elle est sûrement améliorable notamment pour éviter un déplacement important de health checks en cas d’ajout d’une instance Cabourotte (consistent hashing) mais c’est vraiment de l’optimisation prématurée surtout vu ma volumétrie actuelle. Je n’ai également aucune idée de la scalabilité de faire du modulo directement dans postgreSQL mais j’ai le temps de voir venir. Cabourotte pousse ensuite le résultat des health checks dans l’API, qui sont ensuites récupérables par les utilisateurs. Métriques Comme dit en début d’article, une instance Prometheus peut directement scrape l’API d’Appclacks pour récupérer les métriques des health checks exécutés par le SaaS. Il faut donc stocker ces métriques et les rendre récupérable. Prometheus récupère directement les métriques de Cabourotte sur le endpoint /metrics des prober. Lorsque les instances Prometheus des utilisateurs (ou quand appclacks healthcheck metrics get est exécuté) ciblent l’API d’Appclacks, cette dernière se contente de récupérer dans Prometheus les métriques de l’organisation les demandant. Oui, une simple query Prom du type (sum by (name, name, le, zone, id) (healthcheck_duration_seconds_bucket{owner="%s"})) or (sum by (name, name, status, zone, id) (healthcheck_total{owner="%s"})), où owner est un label indiquant l’organisation envoyant la requête (déduite depuis le token d’authentification). Les données sont ensuites renvoyées au format texte de Prometheus en réponse. Je n’ai aujourd’hui qu’une instance de Prometheus mais je prévois d’en mettre rapidement une deuxième, en actif/actif et avec un load balancing pas trop bête côté client, dans le but d’avoir une forte tolérance aux pannes sur ce composant. Comme dit précédemment, tout part également en remote write dans Scaleway Cockpit (métriques systèmes et applicatives incluses). Service discovery Une des propriétés intéressantes lorsqu’on branche Cabourotte installé on premise au service discovery d’Appclacks est la possibilité de passer des labels pour sélectionner les health checks à récupérer (et donc à exécuter) par Cabourotte. En effet, comme la majorité des ressources d’Appclacks, il est possible d’attacher à leurs créations des labels (de simples clés/valeurs). Elles sont stockées dans PostgreSQL au format jsonb (labels jsonb dans la table SQL). Ici pas de magie malheureusement, pour l’instant je me contente d’itérer de manière un peu degueulasse dans le code pour ne retourner que les "match" pour des labels donnés. Site vitrine J’utilise Dorik pour le site vitrine appclacks.com[appclacks.com]. Pour les pas doués du CSS comme moi c’est très bien et j’en suis content. Evolutions futures Résultats sur S3 Tous les résultats des health checks sont stockés pour l’instant dans PostgreSQL dans une table nommée healthcheck_result. Ca prend un max de place. Prenons des chiffres fictifs: 1000 healthchecks 3 régions Exécutés toutes les 30 secondes. 1000*3*2 = 6000 insertions par minute, donc 8640000 par jour. Alors bien sûr ça passe, Postgres n’a aucun soucis à gérer ça. Mais je prévois à moyen terme d’historiser les résultats dans S3 et de ne garder en base que quelques semaines maximum (voir moins) de rétention pour économiser du stockage et éviter que cette table devienne trop grosse. Du sharding serait également une possibilité Kubernetes Gérer des machines virtuelles est pénible. J’aimerai migrer toute l’infrastructure (sauf les instances de Cabourotte déployées de manière autonomes) sur Kubernetes. Je n’utilise aujourd’hui pas l’offre Kapsule de Scaleway car elle ne répond pas à mes exigences pour de la production (pas de réseau privé, pas de support des security groups ce qui la rend de facto inutilisable dans un contexte d’infra as code, kubelet exposé sur internet). Quand Scaleway aura amélioré son produit je basculerai dessus: cela me permettra de gérer beaucoup plus simplement qu’aujourd’hui l’infrastructure, et de manière plus fiable. Release de l’operator Kubernetes Il est prêt, plus qu’à faire la CI et la documentation :) Kube builder a été utilisé pour générer tout le boilerplate. Browser monitoring J’aimerai rajouter à moyen terme du monitoring via démarrage d’un vrai navigateur web (type Selenium) pour etoffer l’offre. C’est aussi pour ça que je veux basculer sur Kubernetes: cela me donnerait une plate-forme de base pour gérer des cronjobs Selenium de manière très simple par exemple. Interface web Je suis une quiche en HTML/CSS/Javascript et je prévois de le rester. Je préfère me focus sur du tooling "state of the art" comme dit précédemment. Conclusion Monter tout ça était (et est toujours) bien fun, on verra où ça va dans les années à venir :)
L'architecture d'Appclacks de A à Z
Je travaille depuis un moment sur un service SaaS de monitoring: Appclacks. Je...
Source: mcorbin
Le Tech radar de 2023
Quelles sont les technologies en vogue ou à éviter en 2023 ? Infrastructure management, cloud provider, langages de prog, Observability… Nos experts vous donnent leurs avis dans cet article écrit à la va vite ! Le radar Lien direct Infra management ADOPT Terraform est forcément indispensable en 2023. Bon, en réalité l’outil ne scale pas, il vaut mieux juste l’utiliser pour bootstrap vos clusters Kubernetes mais c’est le meilleur outil pour faire cela. Cloud init fait toujours le café pour faire 2-3 trucs dégueulasses (qu’on sait généralement pas trop où mettre) au boot d’une machine, un indispensable aujourd’hui. Ansible, à réserver pour les rares choses qui ne tournent pas sur Kubernetes (type certains VPN) et où une machine virtuelle est nécessaire. Faites des CLI internes, c’est cool TRIAL Nix: la hype du moment, c’est bien cool pour gérer des machines de manière déclarative, par contre ne pas utiliser si vous n’avez pas la fibre chez vous (ou alors faut pas avoir à installer un paquet en urgence à 3H du matin parce que l’astreinte a sonnée). HOLD Puppet: c’est fait en Ruby, tout est dit. Shell scripts: l’interdiction du shell pour l’administration système est en bonne voie, ne pariez pas là dessus. Procedures in PDFs: un classique de nos grands groupes, on évite également. Cloud providers ADOPT AWS: Nobody Ever Got Fired for Buying AWS: un classique. Pas forcément le cloud le plus user friendly (IAM, VPC peering…) mais ça marche bien, c’est très stable, et le catalogue de produit vous permettra de faire tourner les prods les plus modernes dessus. Exoscale: le meilleur cloud provider européen, what else ? Netlify: super choix pour du site statique, avec un peu de FaaS autour si besoin. Cloudflare: Le king du CDN/Anti DDoS et qui innove en permanence (cloudflare workers, R2…) TRIAL Scaleway: encore pas mal de produits moyens (par exemple l’offre Kubernetes) mais ça vaut dans le bon sens. Leurs dernières sorties (Observability, Transactional email…) sont top. On y croit. Scalingo: le PaaS qui monte. On attend maintenant quelques features avancées autour du réseau et la fameuse certification SecNumCloud, à suivre de près. Openstack: le boss du cloud privé. HOLD OVH: pour du bare metal avec le vrack, ça passe. Pour la partie cloud on oublie (offre disparate, outillage terrible…). Clever Cloud: beaucoup trop limité en fonctionnalité pour le moment pour pouvoir être utilisé pour de nombreux projets. No Cloud: aucune excuse pour ne plus faire du cloud en 2023 (j’inclus le cloud privé). CI & CD ADOPT ArgoCD: le meilleur outil de l’écosystème Kubernetes, un indispensable qui fait le café. Vos développeurs adoreront redémarrer leurs applications via l’interface quand ça mem leak ! Github actions: simple et efficace go race -race ./… && golangci-lint run && docker build && docker push: en réalité, est ce qu’on a vraiment besoin de plus dans une CI ? Arrêtons avec la complexité accidentelle. TRIAL Jenkins: les papys font de la résistance ! Avec du job DSL et du Jenkins pipeline partout ça fait le boulot. Flux: je le mets pour faire plaisir aux copains mais l’UI d’ArgoCD est trop bonne pour que Flux puisse tenir la comparaison, désolé ! HOLD Gitlab CI: qui s’est dit que c’était une bonne idée de faire un système de CI turing complet avec du YAML qui embarque du bash inline ? Pour lancer du one-liner, pourquoi pas, mais forcez vous à l’utilisez aucune autre fonctionnalités. Programming languages ADOPT Clojure: fonctionnel, persistent & immutable data structure, super écosystème (merci la JVM): coder en Clojure augmente votre productivité, c’est prouvé ! Golang: le langage des dégueulasse mais l’outillage, la stdlib, l’écosystème, les perfs et le fait que ça build en binaire statique fait de lui un très bon choix pour votre prochain projet. Java: je l’ai déjà dit, la JVM est probablement la meilleure plateforme pour écrire des apps en 2023 (et bientôt loom !). Les dernières évolutions de Java en font un super langage. Faites juste attention à ne pas utiliser les bloatwares comme Spring, Hibernate etc…. TRIAL Kotlin: Java avec quelques features en plus, indispensable pour le dev mobile. Python: franchement il était pas loin de passer dans la catégorie suivante rien qu’à cause des systèmes de build qui changent tous les deux jours. Mais si vous arrivez à build votre projet, ça fait le job, surtout en ajoutant des type hints. Zig: pas stable mais prometteur. Rust: difficile d’ignorer Rust en ce moment même si j’ai jamais vu un langage aussi usine à gaz/compliqué/si difficile à lire. A garder pour remplacer C mais c’est tout. NUKE IT FROM ORBIT (only way to be sure) Une catégorie spéciale ici ! Bash: la section Shell scripts a tout dit, à faire disparaître en urgence de votre production. Ruby: un langage où le moindre serveur web recevant 3 requêtes par secondes vous demandera une quantité astronomique de ressource, le tout propulsé par un framework (rails) inventé par le malin ! Perl: on a tous cru dans perl 6 (ou pas). C/C++: même les experts mondiaux qui en font depuis 40 ans arrivent à faire des use after free, heap overflow… dans des codes type sudo/openssl. Donc inaccessible pour les moldus comme nous. k8s infrastructure ADOPT KEDA: indispensable pour l’autoscaling depuis de nombreuses sources, comme par exemple des métriques Prometheus. Cluster autoscaler: la base pour autoscale les noeuds des clusters kube. Horizontal pod autoscaler: la base pour scale les pods, à coupler avec KEDA. Karpenter: le futur d’AWS EKS, qui simplifiera grandement la gestion de vos noeuds Kubernetes. TRIAL Cluster API: a regarder si vous devez vous diriger si vous devez gérer de nombreux clusters kubernetes chez vous. Kubevirt: gérer ses machines virtuelles de manière déclarative via Kubernetes, pourquoi pas ? HOLD Crossplane: l’idée est bonne mais au final ça fait un peu usine à gaz. Installer le provider AWS avec ses > 900 CRDs causera également quelques soucis à vos clusters. Service Mesh: une complexité démentielle pour un gain qu’on cherche encore. On oublie pour le moment sauf cas particuliers. MAPE K: peut être le futur de la gestion d’infrastructure mais on peine à trouver des implémentations. Load balancers ADOPT Traefik: la star pour faire de l’ingress sur Kubernetes. Stable même avec des milliers d’ingress, bonnes perfs, des métriques sympa par défaut… à utiliser. HAproxy: le choix par défaut pour du load balancing hors Kubernetes. TRIAL Envoy: le challenger, tout le monde en parle mais par contre personne ne l’a jamais vu en production. Caddy: parfait pour servir des fichiers statiques. HOLD nginx: qui comprend vraiment le format de configuration de l’outil ? F5: $$$ Observability ADOPT Opentelemetry: surtout pour les traces, c’est le futur ! Mettez en partout, remplacez vos logs par des events attachés aux spans ! Prometheus: J’ai jamais aimé le pull mais faut se rendre à l’évidence, ça marche et l’écosystème est là. Loki: Parfait pour vos logs surtout si vous voulez pas payer trop cher vu que tout part sur s3. Elasticsearch: le gestionnaire de logs historique, ça marche toujours bien surtout on premise. Tempo: Probablement l’outil le plus intéressant pour stocker vos traces. Grafana: L’outil de base (le seul ?) pour vos dashboards. Thanos: Simple et à installer et vous permettra de stocker vos métriques Prometheus sur le long terme. TRIAL Victoria metrics: Prometheus mais avec de meilleures performances, à tester ! Timescale: stocker vos métriques dans postgresql, pourquoi pas ? A voir selon la volumétrie que vous avez. Mimir: Comme Thanos, à tester notamment en cas de forte volumétrie. Appclacks: le futur du blackbox monitoring il parait. HOLD statsd: plus d’actualité cloudwatch: UX terrible à tous les niveaux Nagios & fork: le paradigme du monitoring a complètement changé depuis cette époque. Databases ADOPT Postgres: ça fait tout (SQL, json, full text search, timeseries comme montré précédemment…), le projet évolue à toute vitesse, un très bon choix ! MariaDB: marche très bien, rien à redire. MongoDB: une base au format document, ça peut servir. MongoDB Atlas est pour moi la meilleure offre DB cloud du marché actuellement en terme d’UX/fonctionnalités. Cassandra: la meilleure base orientée colonne, super notamment pour faire du multi data center facilement. Aurora: Propriétaire mais ça scale ! S3: Utiliser S3 comme base de données, de plus en plus fréquent pour les logs, métriques… peut être bientôt pour le reste également ? TRIAL MySQL: Toujours très présent donc doit être cité RocksDB: Ca a de grosses perfs et c’est fun ! HOLD Oracle: vous savez pourquoi Microservices ADOPT Pizza teams: On aime quand les équipes ont un ownership fort sur leurs services et domaines. Contract testing: obligatoire quand on fait du microservice ? Feature flags: Release early, release often: les features flags sont l’outil de base pour réaliser cela. TRIAL Keep the good old monolith: et pourquoi pas si le code est bien conçu ? DDD: permet d’éviter le couplage, facilite les tests… un bon outil. HOLD Distributed monolith: un classique on-demand environments: une mauvaise idée même si certains essayent de vous convaincre du contraire :D Bus and queues ADOPT Kafka: meilleur outil depuis des années pour faire des architectures orientées événements, du stream processing, faire buffer pour vos logs/métriques… Il s’opère également très bien et est dispo partout en mode SaaS. SQS: simple d’utilisation et fait le job quand on a juste besoin d’une queue de message. TRIAL NATS: Le streaming "cloud native", à voir où ça va. RabbitMQ: Bien mais pénible à opérer surtout si vous ne savez pas lire les stacktraces Erlang. HOLD Sidekiq: c’est du Ruby donc ça partage ses caractéristiques. Lean ADOPT Lean: on ne peut plus s’en passer. Des tonnes de pratiques utiles au quotidien. Self service/product approach: On fournit des services internes de qualité en écoutant nos utilisateurs et en les rendant autonomes au quotidien. J’en parle ici. Dev oncall on production: you build it, you run it. La qualité augmente bizarrement très vite quand on reçoit les alertes de ses propres services. Blameless incidents: Les incidents c’est de la faute de tout le monde et on ensemble bosse pour que ça se ne reproduire plus. No backlog: Un backlog, pourquoi faire ? De toute façon au bout de 3 mois votre backlog ne sera plus valide. Autant ne plus en avoir. TRIAL http://programming-motherfucker.com/: tout est sur le lien. HOLD Certifications: ça sert à quoi ? ITIL & SAFE: bien mais seulement pour les consultants spécialisés. Agile & Scrum: on veut du Lean. Security ADOPT IAM: la base de la base, notamment sur le cloud. Dommage que ce soit pas dispo sur ce nombreux acteurs Français. RBAC: si vous faites du Kubernetes, vous allez y passer. Kyverno: parfait pour valider vos manifests Kubernetes, faire de l’audit, et ajouter une couche supplémentaire de sécurité. TRIAL OPA: On préfère Kyverno ici mais ça fait aussi le job, notamment si vous maitrisez Rego. HOLD Databases exposed on internet: mauvaise idée il y a 15 ans, mauvaise idée aujourd’hui. Conclusion A vous de faire vos choix !
Le Tech radar de 2023
Quelles sont les technologies en vogue ou à éviter en 2023 ? Infrastructure management,...
Source: mcorbin
Kubernetes multi cluster et multi région en GitOps avec ArgoCD
Je présenterai dans cet article comment gérer les applications et ressources déployées sur plusieurs clusters Kubernetes, déployés dans plusieurs datacenters, avec ArgoCD. ArgoCD ArgoCD est un outil de déploiement continu pour Kubernetes, permettant de configurer en GitOps vos clusters. Cet outil est capable de gérer tout type de ressources Kubernetes (sans aucune limitations, CRD incluses). Il supporte également très bien l’écosystème Kubernetes standard pour gérer les manifests de déploiements (Helm, Kustomize… mais étendre l’outil avec ses propres outils est également possible), et dispose égalemement d’une très bonne interface utilisateur. ArgoCD est l’outil que je recommande le plus aujourd’hui pour les utilisateurs de Kubernetes. Nous commencerons dans cet article par expliquer rapidement le fonctionnement d’ArgoCD, puis nous déploierons 3 clusters Kubernetes sur Exoscale. SKS, l’offre Kubernetes as a service d’Exoscale, est actuellement la meilleure offre "managed Kubernetes" européenne, meilleure en tout point de vue par rapport à celles de Scaleway et OVH. C’est aussi celle la plus facile et rapide à déployer et à administrer. Mais il serait tout à fait possible de réaliser l’architecture de ce tutoriel sur un autre cloud provider, on premise, ou même en mode "multi cloud" pour une meilleure tolérance aux pannes ! C’est une des forces de Kubernetes: c’est disponible partout. Un cluster sera utilisé pour déployer ArgoCD. Les deux autres seront eux gérés par l’instance ArgoCD installée sur le premier cluster. Chaque cluster sera déployé dans un datacenter séparé, dans des pays différents. Je trouve cette approche intéressante pour plusieurs raisons: ArgoCD pilote l’ensemble des clusters et permet donc d’avoir une gestion de configuration de ces clusters centralisée, notamment grâce aux ApplicationSet que je présenterai plus loin. Toutes les ressources déployées dans tous les clusters seront par exemple visibles dans l’interface d’ArgoCD. Le cluster où ArgoCD tourne peut être très simple (avec seulement ArgoCD et pour un cas réel de production quelques outils de monitoring). Cela donne un cluster facile à administrer, facile à reconstruire, et peu coûteux. Cela peut avoir l’air d’un Single Point of Failure mais perdre ArgoCD n’est pas très grave: toutes les applications continuent de tourner, seulement les mises à jour seront impossibles. Si le cluster est complètement reconstructible en quelques minutes (ce qui est largement faisable sur le cloud), ArgoCD n’est plus vraiment un SPOF. Vous pouvez même si vous le souhaitez avoir un cluster Kubernetes passif avec ArgoCD préinstallé mais désactivé (0 replica), pour pouvoir à tout moment le redémarrer en cas de perte du cluster principal. Votre source de vérité sera de toute façon Git, le cluster ArgoCD sera complètement stateless. On voit sur cette image que seul le cluster Kubernetes à Genève aura ArgoCD d’installé. Il pilotera les deux autres clusters installés dans deux autres datacenters (Zurich et Vienne), et pourra également être utilisé pour se configure soit même. Avec cette architecture, des load balancers devant vos clusters Kubernetes et du DNS, vous pouvez facilement avoir des applications fortement tolérantes aux pannes, en actif/passif entre régions et clouds ou même en actif/actif si votre architecture le permet. Je vais maintenant présenter très brièvement les différentes CRD d’ArgoCD. Pour plus d’informations, lisez la documentation de l’outil. Project La première ressource importante d’ArgoCD est AppProject. Un AppProject est une abstraction permettant de regrouper des Applications (présentées juste après) ensemble. Les AppProject permettent d’exprimer des choses comme "dans le namespace monitoring de Kubernetes, je n’ai le droit que de déployer que les applications venant du dépot gît dont l’URL est github.com/mon-org/monitoring.git, et j’autorise seulement ces types de ressources Kubernetes (deployment, service, ingress…) à être déployés". Un exemple: apiVersion: argoproj.io/v1alpha1 kind: AppProject metadata: name: example-project namespace: argocd spec: sourceRepos: - '*' destinations: - namespace: '*' server: '*' clusterResourceWhitelist: - group: '*' kind: '*' Ce projet appelé example-project autorise tout. SourceRepos, la liste des dépôts Git autorisés à être déployés pour ce projet, contient *. On pourrait la remplacer comme dit précédemment par les addresses des dépôts autorisés à être déployés. destinations permet de configurer dans quelles namespaces et servers (clusters kubernetes) les applications peuvent être déployées. Enfin, clusterResourceWhitelist permet de spécifier quelles types de ressources Kubernetes sont autorisées. Application Une Application représente un groupe de ressources Kubernetes gérées par ArgoCD: apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: guestbook namespace: argocd spec: project: default source: repoURL: https://github.com/argoproj/argocd-example-apps.git targetRevision: HEAD path: guestbook destination: server: https://kubernetes.default.svc namespace: guestbook Cette application s’appelle guestbook. Le paramètre source dans la spec permet de référencer un dépôt Git contenant les fichiers Kubernetes à déployer. Le paramètre destination indique sur quel cluster les déployer, et dans quel namespace. Ici c’est le cluster où ArgoCD est déployé qui est ciblé. Ici, ArgoCD récupérera les fichiers présents dans https://github.com/argoproj/argocd-example-apps, dans le dossier guestbook (le paramètre path de la source), et les déploiera sur le cluster Kubernetes local (où ArgoCD tourne, accessible sur https://kubernetes.default.svc) et dans le namespace guestbook Dans cet exemples de simples fichiers sont déployés. La ressource Application supporte aussi des outils comme Helm, ou Kustomize. ApplicationSet On voit que la partie destination permet déjà de déployer sur plusieurs clusters Kubernetes depuis une même instance d’ArgoCD. Cela est pratique mais a un désavantage: si vous souhaitez déployer la même ressource sur 3 clusters, vous allez devoir définir 3 applications, chacunes avec une destination différente. C’est là que les ApplicationSet interviennent. Voici un exemple d’ApplicationSet: apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet metadata: name: guestbook namespace: argocd spec: generators: - clusters: {} template: metadata: name: '{{name}}-guestbook' spec: project: "default" source: repoURL: https://github.com/argoproj/argocd-example-apps/ targetRevision: HEAD path: guestbook destination: server: '{{server}}' namespace: guestbook On voit qu’une ApplicationSet est assez similaire à une Application: la partie template contient d’ailleurs la définition d’une Application. L’ApplicationSet va en fait être utilisée pour générer des ressources de type Application en fonction de générateurs (generators dans la configuration). Dans cet exemple, on utilise le generateur clusters qui génère par défaut une Application ArgoCD pour chaque cluster configuré dans ArgoCD (la partie suivante montrera comment configurer des clusters). La ressource ApplicationSet permet également de templatiser en fonction du générateur utilisé la configuration des Applications générées. Par exemple ici: La variable {{name}} contiendra le nom du cluster cible de l’application générée. La variable {{server}} contiendra l’URL du cluster cible. Si j’ai par exemple deux clusters configurés dans ArgoCD, l’un appelé cluster1 ayant pour URL cluster1.com, et l’autre appelé cluster2 ayant pour URL cluster2.com, cette ApplicationSet me génèrera 2 applications dont les noms seront cluster1-guestbook et cluster2-guestbook et dont les destinations seront les URL correspondantes. Il existe de très nombreux générateurs, permettant d’automatiser la génération d’applications ArgoCD en se basant sur de nombreux critères (clusters, Pull Requests ouvertes sur un projet Git, en fonction de fichiers, en fonction d’une simple liste de variables…). Les ApplicationSet sont un très bon outil pour éviter de passer son temps à réécrire plusieurs fois des Applications quasiment identiques. Les ApplicationSet autorisent aussi depuis peu des rollout progressifs, pour mettre à jour des clusters un par un dans un ordre pré-déterminé. Cas pratique Création des clusters Comme dit précédemment je vais utiliser Exoscale pour ce tutoriel. Vous pouvez reproduire l’exercice sur un autre cloud également avec quelques variations mais je vous recommande vraiment d’essayer l’offre d’Exoscale qui est de bonne qualité, et qui permet d’avoir dans son mode starter un contrôle plane Kubernetes gratuit. La documentation d’Exoscale vous explique comment créer un cluster grâce à sa CLI. Vous aurez besoin aussi de kubectl pour interagir avec Kubernetes. Le script suivant vous permettra de créer les 3 clusters comme montré dans la première image (dans 3 régions différentes). A noter qu’en production il vaudrait mieux créer un security group par cluster pour plus de sécurité, mais dans ce contexte de POC ce n’est pas trop grave de partager le même: #!/bin/bash set -e ## Création des règles réseaux exo compute security-group create sks-argo-tuto exo compute security-group rule add sks-argo-tuto --description "NodePort services" --protocol tcp --network 0.0.0.0/0 --port 30000-32767 exo compute security-group rule add sks-argo-tuto --description "SKS kubelet" --protocol tcp --port 10250 --security-group sks-argo-tuto exo compute security-group rule add sks-argo-tuto --description "Calico traffic" --protocol udp --port 4789 --security-group sks-argo-tuto ## Création des 3 clusters ### Premier cluster dans la région ch-gva-2 qui hébergera ArgoCD exo compute sks create argocd --zone ch-gva-2 --service-level starter --nodepool-name argocd --nodepool-instance-prefix argocd --nodepool-size 3 --nodepool-security-group sks-argo-tuto ### Second cluster dans la région ch-zrh-1 (anciennement appelée ch-dk-2) exo compute sks create zrh1 --zone ch-dk-2 --service-level starter --nodepool-name zrh1 --nodepool-instance-prefix zrh1 --nodepool-size 3 --nodepool-security-group sks-argo-tuto ### Troisième cluster dans la région at-vie-1 exo compute sks create vie1 --zone at-vie-1 --service-level starter --nodepool-name vie1 --nodepool-instance-prefix vie1 --nodepool-size 3 --nodepool-security-group sks-argo-tuto Récupération des kubeconfig Nous allons maintenant récupérer des fichiers Kubeconfig root pour chaque cluster dans le but de pouvoir intéragir avec eux via kubectl: exo compute sks kubeconfig argocd kube-admin --zone ch-gva-2 --group system:masters > argocd.kubeconfig exo compute sks kubeconfig zrh1 kube-admin --zone ch-dk-2 --group system:masters > zrh1.kubeconfig exo compute sks kubeconfig vie1 kube-admin --zone at-vie-1 --group system:masters > vie1.kubeconfig Vous devriez maintenant pouvoir par exemple lister les noeuds de vos clusters, avec kubectl --kubeconfig <kubeconfig-file> get nodes, par exemple kubectl --kubeconfig argocd.kubeconfig get nodes. Installation d’ArgoCD Nous allons maintenant installer ArgoCD sur le cluster Kubernetes de la zone ch-gva-2: kubectl --kubeconfig argocd.kubeconfig create namespace argocd kubectl --kubeconfig argocd.kubeconfig apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml Pour ce tutoriel nous désactivons pour plus de facilité l’authentification d’ArgoCD. ArgoCD supporte une gestion fine des utilisateurs et permissions (et supporte également des protocoles comme OIDC), donc n’hésitez pas à jeter un oeil à la documentation officielle. Exécutez tout d’abord kubectl --kubeconfig argocd.kubeconfig -n argocd edit configmap argocd-cm pour désactiver l’authentification. Rajoutez le block suivant dans le YAML affiché, sauvegardez et fermez votre éditeur: data: users.anonymous.enabled: "true" Faisons la même chose pour une seconde configmap via kubectl --kubeconfig argocd.kubeconfig -n argocd edit configmap argocd-rbac-cm, pour rendre admin l’utilisateur par défaut. data: policy.default: role:admin Rappel: ne faites pas ça sur un "vrai" cluster, ou même un cluster de test où ArgoCD est exposé sur internet ! Je ne vais pas configurer d’ingress controller dans ce tutoriel l’accès à ArgoCD se fera via kubectl port-forward. Lancez kubectl --kubeconfig argocd.kubeconfig port-forward svc/argocd-server -n argocd 8080:443 et ouvrez votre navigateur sur localhost:8080: une fois le certificat auto signé accepté vous devriez pouvoir accéder à l’interface d’ArgoCD. Configuration des clusters ArgoCD permet par défaut de déployer des applications sur le cluster où il est installé mais pas ailleurs. Nous allons donc reconfigurer les clusters dans ArgoCD. Cela se fait en créant un secret Kubernetes par cluster. Nous utiliserons ici directement les certificats contenus dans les kubeconfig générés précédemment pour s’authentifier aux clusters distants (pour le cluster local, l’authentification se fera par défaut via le service account d’ArgoCD). Voici la configuration à appliquer, en remplaçant les valeurs server et les certificats attendus par chaque cluster par ce que vous avez dans vos kubeconfig: --- apiVersion: v1 kind: Secret metadata: name: cluster-gva2 namespace: argocd labels: argocd.argoproj.io/secret-type: cluster type: Opaque stringData: name: gva2 server: https://kubernetes.default.svc --- apiVersion: v1 kind: Secret metadata: name: cluster-zrh1 namespace: argocd labels: argocd.argoproj.io/secret-type: cluster type: Opaque stringData: name: zrh1 server: "<contenu de server dans zrh1.kubeconfig>" config: | { "tlsClientConfig": { "caData": "<contenu de certificate-authority-data dans dans zrh1.kubeconfig>", "certData": "<contenu de client-certificate-data dans dans zrh1.kubeconfig>", "keyData": "<contenu de client-key-data dans dans zrh1.kubeconfig>" } } --- apiVersion: v1 kind: Secret metadata: name: cluster-vie1 namespace: argocd labels: argocd.argoproj.io/secret-type: cluster type: Opaque stringData: name: vie1 server: "<contenu de server dans vie1.kubeconfig>" config: | { "tlsClientConfig": { "caData": "<contenu de certificate-authority-data dans dans vie1.kubeconfig>", "certData": "<contenu de client-certificate-data dans dans vie1.kubeconfig>", "keyData": "<contenu de client-key-data dans dans vie1.kubeconfig>" } } Note Dans le cas d’Exoscale, la bonne pratique serait de créer un ClusterRole dédié pour ArgoCD pour éviter l’utilisations de system:masters pour l’authentification, et d’utiliser par exemple des certificats avec un TTL maîtrisé. La commande exo compute sks kubeconfig permet en effet de spécifier un TTL. Voir mon article sur le TLS et l’authentification dans Kubernetes pour plus de détails. Une fois les valeurs remplacées, et le fichier appliqué via kubectl --kubeconfig argocd.kubeconfig apply -f <fichier>, les clusters devraient être visibles dans l’interface d’ArgoCD à l’adresse https://localhost:8080/settings/clusters: Déploiement d’un ApplicationSet Déployez maintenant l’ApplicationSet suivant pour tester le setup, toujours via kubectl --kubeconfig argocd.kubeconfig apply -f <fichier>: --- apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet metadata: name: guestbook namespace: argocd spec: generators: - clusters: {} template: metadata: name: '{{name}}-guestbook' spec: project: "default" source: repoURL: https://github.com/argoproj/argocd-example-apps/ targetRevision: HEAD path: guestbook destination: server: '{{server}}' namespace: guestbook syncPolicy: syncOptions: - CreateNamespace=true automated: prune: true selfHeal: true ArgoCD devrait vous générer automatiquement 3 applications, chacunes sur un cluster différent, le tout en fonction de ce qui est stocké dans le dossier guestbook sur le répertoire github https://github.com/argoproj/argocd-example-apps/. Elles seront également visibles dans l’interface. On remarque que le nom de chaque application est bien <cluster>-guestbook, par exemple vie1-guestbook. Rappelez vous que les générateurs des ApplicationSet vous permettent de filtrer si besoin sur quelles clusters les applications doivent être déployées, via des labels par exemple. Dans le setup actuel, ArgoCD créera automatiquement l’application pour chaque nouveau cluster ajouté, ou la supprimera si un cluster est supprimé. Chaque changement dans Git sera répercuté sur l’ensemble des clusters automatiquement également (grâce à l’option syncPolicy.automated, vous pouvez également choisir de "sync" manuellement les applications). Vous pouvez également vérifier sur vos clusters avec kubectl que l’application a bien été déployée, par exemple avec kubectl --kubeconfig vie1.kubeconfig get po -n guestbook. Un dernier point avant la conclusion: rien ne vous empêche d’utiliser les Application ET les ApplicationSet ArgoCD en parallèle selon les besoins. Conclusion ArgoCD est un outil puissant, qui peut grandement simplifier le multi region ou le multi cloud Kubernetes. On voit encore une fois ici l’avantage de Kubernetes: l’outillage est là pour répondre à des problématiques assez complexes, et ça fonctionne très bien.
Kubernetes multi cluster et multi région en GitOps...
Je présenterai dans cet article comment gérer les applications et ressources...
Source: mcorbin
DevOps
Définition
Le DevOps est une approche de développement logiciel qui vise à améliorer la collaboration entre les équipes de développement (Dev) et d'exploitation (Ops) au sein d'une organisation. Le terme "DevOps" est une contraction de "Development" (développement) et "Operations" (exploitation). L'objectif principal du DevOps est d'accélérer le cycle de développement, de déploiement et de mise en production des logiciels tout en assurant une plus grande fiabilité et une meilleure qualité.
Les principaux aspects du DevOps sont les suivants :
- Collaboration : Le DevOps encourage une communication et une collaboration étroites entre les équipes de développement et d'exploitation. Cela aide à éliminer les silos organisationnels et à favoriser une compréhension mutuelle des objectifs et des contraintes de chaque équipe.
- Automatisation : L'automatisation est au cœur du DevOps. Les tâches répétitives et manuelles sont automatisées autant que possible, ce qui permet de réduire les erreurs humaines, d'accélérer les processus et de garantir une cohérence dans les déploiements.
- Intégration continue (CI) : Dans le cadre du DevOps, les développeurs intègrent fréquemment leur code dans une base commune. Chaque intégration est automatiquement testée, ce qui permet de détecter rapidement les erreurs et de les corriger.
- Livraison continue (CD) : La livraison continue consiste à automatiser le processus de déploiement des applications. Les modifications apportées au code sont automatiquement déployées dans un environnement de test, puis dans l'environnement de production lorsque les tests sont concluants.
- Surveillance et rétroaction : Le DevOps implique une surveillance continue des performances de l'application en production. Les données de surveillance aident à détecter les problèmes rapidement et à prendre des mesures correctives. De plus, les commentaires des utilisateurs sont pris en compte pour améliorer constamment l'application.
- Sécurité : La sécurité est un aspect essentiel du DevOps. Les pratiques de sécurité sont intégrées dès le début du processus de développement, et des contrôles de sécurité sont automatisés dans le pipeline de livraison continue pour détecter les vulnérabilités rapidement.
En adoptant le DevOps, les organisations visent à accélérer leur capacité à fournir des logiciels de haute qualité tout en réduisant les risques et les coûts associés aux déploiements. Cette approche favorise également une culture de collaboration, d'amélioration continue et d'agilité au sein de l'entreprise.