June 3, 2023

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:

Architecture globale de Appclacks

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 :)

Tags: devops cloud

Add a comment








If you have a bug/issue with the commenting system, please send me an email (my email is in the "About" section).

Top of page