Projet réalisé avec Gridslab en 2019. Le projet consiste à afficher les niveaux de pollution de l'air ambiant sur des panneaux lumineux en bord de route. Les messages affichés sur les panneaux changent en fonction du niveau de pollution de l'air (bon/moyen/mauvais) et sont administrables depuis un backoffice dédié.

Petit tour de ce qu'il y a sous le capot...

Contexte

Chaque panneau est composé d'un écran et d'un capteur. Chaque capteur remonte les données mesurées brutes sur le cloud du fabriquant qui les met à disposition via des APIs web. Le backend de l'appli récupère les de chaque panneau qui n'a pas été mis à jour depuis plus de 5 minutes et les expose aux panneaux via une API Rest. Les panneaux, eux, viennent choper les infos les concernant toutes les 5 minutes en HTTP sur notre backend.

C'est certainement plus clair avec un schéma, alors voilà:

mqs_overview

La logique interne aux panneaux est par conséquent assez simple: on polle un backend régulièrement, et s'il y a quelque chose à afficher de différent (on est passés d'un seuil bon à moyen par exemple), on affiche un nouveau contenu.

J'aime bien ce projet parce que l'architecture est en fin de compte assez simple mais il y a pleiiiiiin de choses à dire dessus !

Technique

Back

J'ai longtemps hésité sur le type de techno à utiliser pour réaliser le backend, en fin de compte ce sont les timings qui m'ont fait rester sur du connu: Le backend est une application symfony (https://symfony.com/)qui utilise api-platform (https://api-platform.com/) pour la partie REST.

Je voulais éviter au maximum d'avoir des tâches planifiées à éxécuter et baser mes actions sur des événements ("le panneau X passe du statut bon à mauvais", etc). Aussi, j'ai implémenté une seule commande symfony dont le role est de venir récupérer la liste des panneaux qui doivent être mis à jour à l'instant T basé sur des règles très simple (pas mis à jour depuis X minutes). Pour chaque panneau trouvé, on publie un message dans un exchange RabbitMQ. Les consumers derrière récupèrent les infos du panneau (data provider associé au panneau, etc) et appliquent la stratégie à utiliser pour savoir s'il faut mettre à jour le panneau.

Il existe également un backoffice qui permet aux utilisateurs de suivre le statut de leurs panneaux depuis une IHM web. Ce backoffice est une SPA développée avec VueJS et utilise les APIs REST. Je m'étais débrouillé pour implémenter tous les endpoints et être capable de réaliser toutes les actions via postman histoire de challenger mes endpoints. J'ai vraiment pu me focaliser sur le back puis en tant voulu sur le front.

Tous les environnements sont conteneurisés. J'ai longtemps utilisé Rancher 1.6 pour effectuer les déploiements mais je suis repassé sur du docker-compose au moment ou Rancher est passé à du full K8S. Tout simplement pas la possibilité de me former sur K8S dans des délais raisonnables à l'époque !

Devices

J'ai pensé le firmware en sachant pertinemment que dans la V1, on ne pourrait pas faire d'upgrades OTA ou de prise de contrôle à distance. Les contraintes que je me suis imposé vis à vis du firmware sont les suivantes:

  • Le panneau doit démarrer tout seul
  • en cas de reboot, tout redémarre tout seul
  • en cas de problème que le panneau ne saurait gérer seul, on affiche une icone de maintenance.
  • si le panneau marche mais que c'est le back qui s'est cassé la gueule, le panneau doit savoir gérer en attendant que le back redevienne up.

En gros, l'image que j'avais en tête, c'était que j'envoyais un satellite dans l'espace et qu'il devait marcher sans aide extérieure.

Le firmware tourne sur un petit PC embarqué dans le panneau. On exécute un linux sur lequel j'ai provisionné tout les logiciels via un playbook ansible. Celui ci va effectuer plusieurs choses:

  1. installer toutes les dépendances logicielles
  2. provisionner les scripts
  3. désactiver tout un tas de trucs inutiles
  4. configurer l'appli avec quelques variables (id du panneau, API Key, url de l'API...)

En l'état, c'est un peu chiant parce que j'ai besoin de démarrer le PC pour le provisionning et que du coup jje provisionne à chaque déploiement, mais comme tout est automatisé la procédure prend 10 minutes. Et ça, c'est vraiment confort. J'ai des plans pour passer sur quelque chose de bien plus abouti à base de buildroot (https://buildroot.org/) ou je pourrai me permettre de builder un ISO. J'aurais ainsi qu'à flasher l'ISO, puis injecter les variables spécifiques au panneau. Mais ça demande pas mal de temps de R&D pour arriver à quelque chose d'abouti !

Une fois le provisionning terminé, on a un linux qui démarre en mode plein écran sur l'appli.

Infrastructure / Monitoring

L'appli est déployée sur un serveur de Gridslab. Tous les services sont conteneurisés. On a jamais eu à le faire, mais elle est prête pour un déploiement "privé" (c'est le genre de chose que j'aimerais beaucoup gérer)

Tous les services remontent dans une instance de sentry.io (https://sentry.io/), ce qui m'a permis d'identifier facilement quelques bugs à la marge dans les premiers mois. Dans les premiers mois du projet j'étais chez Datadog car je voulais absolument avoir le suivi des logs et quelques métriques à portée de main sans avoir à faire du SSH et des tail... et c'était la solution la plus simple et rapide à mettre en place. Aujourd'hui, j'ai mis en place une prometheus et un loki qui me permettent d'utiliser l'offre managed de grafana cloud (et d'économiser des ronds au passage).

J'ai un alerting basique sur le fonctionnement de la plateforme via des systèmes externes. A vrai dire, toutes les briques de monitoring sont externalisées.

Coté déploiement, j'ai des playbooks ansible qui gèrent un docker compose et redémarrent les services nécessaires. C'est assez drôle parce que depuis, j'ai commencé à toucher à k8s et aux helm charts, et je me rends compte à quel point ce que j'ai fait à l'époque via ces playbooks pourrait être carrément mieux géré avec ces outils.

Les APIs sont accessible au travers d'un Traefik 2 qui se charge du passage en SSL et le renouvelle automatiquement.

Axes d'amélioration

Le fait que chaque panneau vienne chercher les informations à afficher toutes les 5 minutes peut clairement être amélioré. J'ai expérimenté différentes options à base de MQTT qui marchent vraiment bien mais ça sous-entendait également des coûts plus élevés. En tout cas, c'est clairement dans cette direction que je souhaite aller si le projet continue d'évoluer

Utiliser MQTT pour la communication entre le back et les panneaux

Je l'ai déjà mentionné plus haut, ça me semble être la chose la plus intéréssante à faire. En plus de faire péter des endpoints HTTP qui ne servent plus, on pourrait publier des messages pour ordonner aux panneaux d'effectuer certaines actions uniquement quand c'est nécessaire : on serait véritablement dans une architecture orientée événements et on aurait encore moins de latence entre le moment ou un panneau est mis à jour coté back et le moment ou le panneau actualise son affichage.

Mieux encore, si on intègre des fabriquants de capteurs qui exposent leur données via le hardware, on pourrait faire évoluer le firmware pour qu'il aille récupérer ces données, afficher le bon seuil sans intervention du back, et eventuellement publier le nouveau seuil sur le back. Les panneaux seraient ainsi résilients à une panne du back (qui en l'état est un gros SPOF, même si en deux ans d'exploitation a quasiment tourné comme une horloge !)

J'ai épluché la doc de l'Azure IoT hub ou encore L'IoT hub de scaleway qui me semblaient être les composants les plus intéréssants pour du managé. Je voulais pas avoir à gérer un serveur MQTT avec toutes les problématiques que je voulais gérer, alors que dans le cas de l'IoT Hub de scaleway, on a tous les éléments pour faire de l'IOT de façon sécure via du mTLS par exemple.

Créer un artefact pour le firmware

Aujourd'hui, Chaque firmware est provisionné via ansible. ça marche très bien, j'ai confiance dans ce process, mais on est pas à l'abri. Je préfererais builder une image une fois, et la déployer sur le hardware, un peu comme une image docker en fin de compte. Le provisionning d'un panneau serait encore plus rapide (là je sais pas trop ce que je pourrais faire pour le rendre encore plus rapide), mais surtout plus sécurisé: pas de risque qu'une dépendance à télécharger ait disparu en cours de route ou n'importe quoi d'autre.

Déployer la partie backend sur du K8S (managé)

ça me permettrait de bénéficier de beaucoup de choses dont je ne peux pas tirer profit aujourd'hui

Améliorer le process d'intégration de panneaux

C'est un autre gros chantier que j'aimerais mettre en place: déléguer la conception de la partie hardware/firmware à des prestas tiers. j'aimerais exposer des APIs qui permettent de créer un panneau, qui retourneraient l'ID du panneau, l'URL de la gateway pour les events, et completement déléguer cette partie.

Même sans parler d'intégrateur tiers, voici ce que ça me permettrait de faire:

  1. on demande au back de l'appli de générer un nouveau panneau via l'API. L'API nous retourne l'ID du panneau, une clé d'accès à l'API (ou une clé pour se connecter au broker, si on parle de messages MQTT) et le endpoint à utiliser
  2. On lance la procédure de provisionning (si on a l'ISO linux prébuild, ben on flash l'ISO sur le PC
  3. On configure le panneau avec les infos obtenues en 1.

J'aimerais beaucoup mettre ça en place, je pense que ça montrerait un niveau plutôt sympa d'automatisation

En résumé

C'est pas trop à moi de dire ça mais au final je trouve que j'ai réussi à mettre en place une architecture ou les différents composants communiquent via des contrats qui sont bien définis, ou chaque composant a ses spécificités (contraintes liées à l'embarqué coté Panneaux, contraintes cloud coté back), qui se maintient bien et encaisse plutôt bien un certain volume de panneaux: quelques tests de charge ont été effectués sur un environnement dédié mais qu'il faudrait refaire avec des scénarios réalistes et sur une longue période pour que ce soit vraiment significatif.

Je suis content de la CI. Sur les derniers développements, j'ai pris le temps d'ajouter une couverture de tests fonctionnelle via des tests d'API. Désormais, sur chaque nouvelle merge request, on déploie un environnement (en gros une review app), on charge des fixtures, on exécute les tests fonctionnels et on peut tester via une URL spécifique l'environnement en question. Quand la MR est fermée, on le supprime.

mqs_ci

OK, sur ce pipeline, y'a pas la review app.

Coté IoT, le firmware fait le taff et même si y'a beaucoup de choses à améliorer, la liste est clairement identifiée. J'aimerais bien trouver des pistes pour gérer des upgrades OTA; ça passerait potentiellement via une refacto du firmware pour devenir une appli electron et utiliser le framework squirrel (https://github.com/Squirrel).

Enfin bref, je suis assez fier du résultat !