URL: https://linuxfr.org/users/julieng-2/journaux/du-serverless-au-faas-et-du-golang-c-est-l-ete Title: # Du serverless au FaaS - et du Golang -, c'est l'été Authors: JulienG Date: 2022年09月07日T20:43:18+02:00 License: CC By-SA Tags: serveur, cli, serverless, faas et golang Score: 12 Bonsoir, ## 0. Introduction L'été, sa chaleur écrasante et ses travaux périscolaires : cette année, je m'étais donné deux objectifs, la définition et la mise en œuvre d'une (petite) solution _FaaS_ (_function as a service_) et l'apprentissage de Go. Difficile de faire de l'architecture de SI si on ne se contente que de la définition de l'ouvrage, et depuis des années j'entends du bien (et aussi beaucoup de mal) sur le langage de Google : alors autant tester soi-même, l'un et l'autre. Je n'avais pas prévu d'en faire un article, mais lorsqu'il y a quelques jours (à date de rédaction de ces lignes), un article a attiré mon attention : "_2022, l'année de l'explosion du serverless_" ([article du JDN](https://www.journaldunet.com/web-tech/cloud/1514083-2022-l-annee-de-l-explosion-du-serverless/)), cela m'a motivé. Car voilà quelques années, j'avais eu l'occasion d'écrire une thèse pro sur des sujets similaires. [La présentation de la soutenance](https://www.slideshare.net/nothus1/penser-son-systme-dinformation-apprhender-ses-volutions-230728506) y fait directement référence, en associant FaaS et RDF comme deux des outils modernes et indispensables pour produire des traitements automatiques hautement distribués et porteur de sens partagés entre les organisations. C'est dire si le sujet me passionne. Pour autant le code que j'ai produit n'a pas d'autre perspective qu'un travail de meilleure compréhension du sujet - et accessoirement d'avoir un outil pour mes propres besoins, sur mon serveur de développement. Je donne plus loin le lien vers le dépôt, sous licence MIT. De plus, c'est pile dans l'actualité : nous parlerons de mise à l'échelle, particulièrement ici de réduction vers 0 du coût des processus. En cette période de "fin d'abondance", c'est bienvenu... Enfin et comme vous pouvez vous en doutez, les lignes que vous lisez comme le travail de développement m'ont demandés quelques heures au carré. Malgré tout le soin que j'ai apporté à l'écriture de cet article et au reste, j'ai la bonne certitude d'avoir laissé de (grosses ?) boulettes. Merci de votre modération dans vos commentaires (je suis [très] ouvert à une critique quand elle fait plus de trois mots !). Le sujet _serverless_ que j'aborde n'est pas le plus simple et, reconnaissons-le, le plus limpide. Il est soumis à l'interprétation et donc à nos propres biais. C'est une discipline "jeune" où l'aspect commercial agit beaucoup sur la communication et la formation qui en est faite (pour le meilleur et le pire). Je mets ici le terme jeune entre guillemets car certaines approches sont des héritages lointains, qui ont parfois des dizaines d'années. Mais cet article est déjà considérablement trop long pour en faire l'exégèse... ------------------------------------------------------------------------------ ## 1. _Serverless_, Ce que c'est ### Du concept global La définition n'est pas facile, car le concept reste globalement flou. Évidemment c'est le concept est un trompe-l’œil : il y a ___toujours___ un serveur quelque part. Sa gestion est simplement et totalement transparente pour l'équipe de développement, qui n'assure pas la gestion d'un parc d'infrastructure. Au-delà, c'est la gestion même d'un "service" (c'est-à-dire un service _rendu au client_) qui est également géré par celui qui l'héberge l'applicatif. Pour l'organisation qui porte le développement (entreprise, association, projet ouvert, etc.), cela a plusieurs avantages : - généralement il y a la fonction "_scale-to-zero_", c'est-à-dire que l'applicatif ainsi hébergé, s'il n'y est pas utilisé, ne consomme pas de ressources vives (seulement son stockage). La facture en cas de non-utilisation est donc soit particulièrement réduite, soit nulle ; - le déploiement est facile, au sens où l'on utilise généralement une API Web ou via une ligne de commande simple et courte ; - la sécurité, la sûreté reposent en partie sur le service sous-jacent, même si les fonctions d'authentification, de protection contre les attaques de force brute ou contre les déni de service, ne sont pas toujours compris (voir point suivant) ; - la facture se fait au "hit", à l'appel à l'applicatif... ... Ce qui est là un des principaux défauts, dû à la gestion économe des ressources : le temps de démarrage. Du fait que l'applicatif est éteint (parfois "mis en pause" mais généralement le coût n'est alors pas nul pour l'hébergement), le premier appel porte le coût de démarrage. Ce coût peut être très important comme nous le verrons plus loin. Il faut donc avoir des stratégies efficaces de démarrage, que l'applicatif doit porter en son sein, notamment par exemple, en l'optimisant avec un tampon local important lorsque c'est possible. Autre point possiblement problématique, c'est la montée en charge ou mise à l'échelle (la fameuse "_scalabilité_"). Lorsque le développeur poussera son applicatif, il ne saura pas ni combien de noeuds sont en usage - ni même s'il y en aura plusieurs -, et selon quelle configuration, son infrastructure sera à un instant donné. Si le tampon doit être "unique", au sens où toutes les instances doivent partager le même, il faut donc prévoir un service dédié type _memcached_ ou _redis_. C'est là des principaux points que j'énonce mais loin d'être exhaustif. Si l'approche vous tente et que vous ne dépassez pas le cadre d'une organisation à quelques personnes pour une charge anecdotique ou presque, mieux vaut se tourner sur une solution _serverless_ très légère, mono-noeud par principe, car vous ne gagnerez que les avantages. Bien évidemment, si vous n'êtes pas dans ces situations, avoir une part de _serverless_ dans votre architecture n'est pas sans impact voire peut-représenter un risque majeur. D'autant que les solutions actuelles de _serverless_ "à destination des développeurs" et notamment celle qui me semble la plus (re)connue OpenFaas, fonctionne quasi-systématiquement avec un "gros machin" comme Kubernetest (même si des contre-exemples existent [ici](https://blog.alexellis.io/faas-containerd-serverless-without-kubernetes/) pour l'explication et [là](https://github.com/openfaas/faasd) pour le dépôt). C'est-à-dire qu'on a une mise en oeuvre loin d'être évidente pour aboutir à cette abstraction tant désirée. Pour l'avoir testé sur une infrastructure personnelle - un Rancher à deux fournisseurs de VM distincts -, c'est vite inommable en terme de débogue... ### Du particulier A cet instant de l'article, vous vous direz peut-être "_serverless n'est pas FaaS !_" et globalement je serais d'accord avec vous. En effet, vous pouvez parfaitement utiliser un service quelconque ([DaaS](https://fr.wikipedia.org/wiki/Data_as_a_service) ou [SaaS](https://fr.wikipedia.org/wiki/Software_as_a_service)), afin de pousser votre logique métier quelque part (ici, le "_as a service_" est le support à cette logique métier). Vous ne payerez au mieux qu'à concurrence de la consommation réelle ou au pire au forfait et, parfois, l'adaptation d'une équipe de développeur sans aucune notion d'infrastructure (hélas ça existe), peut alors s'intégrer dans ces types de services sans (trop) douleur. Cependant vous risquez d'être extrêmement limité dans le format du code exécuté, de sa taille et de sa complexité. Ou alors avoir de l'[IaaS](https://fr.wikipedia.org/wiki/Infrastructure_as_a_service) ou du [PaaS](https://fr.wikipedia.org/wiki/Platform_as_a_service) : c'est une abstraction moins forte, qui permet d'avoir facilement un environnement prêt à l'emploi - mais la notion de serveur à configurer reste encore largement présente. En résumé, tout dépend de votre _volonté d'abstraction_ de la réalité physique des bits sur un bout de plastique couvert de silicium et autres métaux. Ce n'est une frontière entre tout et rien : c'est une échelle parfois assez subtile. Chacun des concepts que j'ai énoncé, recouvre une portion plus ou moins importante de la couche ultime que constitue le _serverless_. La frontière de ce dernier (me) semble comme pour les autres concepts, une frontière poreuse où l'on répond à un besoin, il est donc normal, notamment pour le point évoqué du PaaS, que les deux concepts se chevauchent par endroit. Pourtant ils sont bien dissemblables et c'est [Cloudfare](https://www.cloudflare.com/fr-fr/learning/serverless/glossary/serverless-vs-paas/) qui porte - avis personnel -, le meilleur exemple : > "_L'informatique sans serveur peut être comparée au fait de tirer de l'eau d'un robinet, l'eau représentant la puissance de calcul. Dans une maison moderne, on peut ouvrir un robinet à tout moment pour obtenir toute l'eau dont on a besoin. Le PaaS s'apparente davantage à un distributeur d'eau avec un service de livraison de bonbonnes. Il est toujours possible d'obtenir autant d'eau potable que nécessaire, mais il ne suffit pas d'ouvrir le robinet ; le consommateur doit demander au fournisseur de lui livrer davantage de bonbonnes si la demande augmente. Dans les deux cas, un tiers s'occupe des services en « backend » c'est-à-dire en arrière plan - en purifiant l'eau, en l'acheminant l'eau au bâtiment, etc. - mais seule l'eau du robinet peut être dosée avec précision, à la demande et en temps réel._> _(...) Pour poursuivre avec la métaphore de l'eau, les consommateurs qui utilisent l'eau du robinet paient exactement la quantité d'eau qu'ils consomment. De même, la facturation de l'informatique sans serveur est extrêmement précise, et les développeurs ne paient que ce qu'ils utilisent. Certains fournisseurs d'informatique sans serveur ne facturent aux développeurs que le temps précis d'exécution de leurs fonctions, jusqu'à quelques fractions de seconde pour chaque instance individuelle de chaque fonction. D'autres fournisseurs facturent selon le nombre de demandes._> _Les consommateurs qui utilisent un distributeur d'eau et qui se font livrer des bonbonnes d'eau ne paient également que ce qu'ils utilisent, mais ils paient à la bonbonne et non au mètre cube. De la même manière, certains fournisseurs de PaaS ne facturent aux développeurs que ce que leur application utilise. Cependant, la facturation est loin d'être aussi précise que pour l'informatique sans serveur. D'autres fournisseurs de PaaS facturent leurs services au mois. Les développeurs peuvent généralement définir la quantité de puissance de calcul qu'ils souhaitent obtenir. Cependant, cette décision est prise à l'avance et n'est pas une réaction aux augmentations et aux diminutions de l'utilisation en temps réel._> _Cette différence ne signifie pas nécessairement que l'architecture sans serveur est toujours plus abordable. De même qu'il devient rapidement très cher de laisser couler un robinet d'eau en permanence, les applications web dont le flux d'utilisation est important et constant sans grande fluctuation peuvent être coûteuses à faire fonctionner en utilisant l'informatique sans serveur._" ### De l'usage réel Une fois ces considérations aqueuses passées, reste à formaliser le _[FaaS](https://en.m.wikipedia.org/wiki/Function_as_a_service)_. Imaginez que le _serverless_ est l'ensemble des technologies pour faire une abstraction (quasi-)totale du sous-jacent technique et d'infrastructure. Le _FaaS est une modalité du _serverless_ : c'est la technique (et souvent un _framework_ pour accompagner le déploiement du code) permettant au développeur non seulement de faire abstraction de la partie hébergement, mais aussi même dans le cas extrême, d'où provient la charge de travail. Le périmètre d'action est alors très réduit : vous produisez une fonction qui sera appelée dans un processus que vous ne maîtrisez pas. Mais dont la mise à l'échelle, la sécurité et la sûreté, la gestion en temps réel, est porté pour vous. Du fait que nous sommes au XXIe siècle, les appels sont principalement (voire exclusivement dans de nombreux cas), portés seulement par le réseaux. Revenons à OpenFaas quelques instants et [le premier exemple](https://docs.openfaas.com/tutorials/first-python-function/) donné : ```python3 import requests import json def handle(req): result = {"found": False} json_req = json.loads(req) r = requests.get(json_req["url"]) if json_req["term"] in r.text: result = {"found": True} print json.dumps(result) ``` ... et c'est tout. Le reste se résume en un fichier de pré-requis et un fichier de déclaration de cette fonction. En deux-trois commandes, vous envoyez ça dans un coin de l'univers, en supportant une charge virtuellement sans limite (ou seulement celui de votre compte en banque). Si vous n'avez pas d'activité, aucune charge pour la fonction n'existe (pas de processus en activité) : c'est le _scale-to-zero_, le graal du _Green (Washing) IT_. Ici OpenFaas a assuré plusieurs fonctionnalités essentielles pour nous : - l'écoute du réseau sur un serveur où l'instance d'OpenFaas tourne, - la récupération de la requête entrante, - son éventuelle répartition de charge vers d'autres instances OpenFaas, - le contrôle de l'intégrité de la requête (_parse_) - la détection de la fonction appelée puis éventuellement, le contrôle de sécurité pour l'appel, - le démarrage d'un nouveau conteneur pour la prendre en charge (ou l'envoi à un conteneur existant), - l'envoi d'une requête à ce conteneur dans un format attendu, - la réception par le processus au sein du conteneur de la requête, - le lancement la fonction `handle` définie plus haut, _(... ici le traitement par la fonction ...)_ - la récupération d'erreur (`try / except`) ou du retour normal, - l'envoi du processus vers l'extérieur du contenur, - la récupération du retour par l'instance d'OpenFaas, - le contrôle de l'intégrité du retour, - la production d'un retour pour le client final, - l'envoi de ce retour au client final. Rien que ça - et je vous ai épargné la réalité des appels de signaux, des traitements réseaux, etc. On ne mesure parfois pas bien, et moi le premier, la quantité absolument folle d'énergie, de travail, de synergie qu'il faut pour arriver à produire un tel système informatique. Le nombre incalculable de choses qui peuvent mal tourner. Je trouve ça vertigineux. Bref aparté : si vous connaissez le paradoxe de la veste en laine, ce vertige ne doit être que plus grand encore (sinon courrez voir [la chaîne de Mr Phi](https://monsieurphi.com/2017/06/19/adam-smith-grain-de-philo-5/))... On atteint d'un certain côté à une division du travail quasi-parfaite entre des compétences diverses ; faisant perdre l'aspect "généraliste" de l'informatique des premiers temps, que tant de passionnés comme moi apprécient... ### Des considérations L'aspect intéressant du _FaaS_ au-delà d'une baisse toujours plus grande de la barrière d'entrée dans le développement (notamment pour les profils junior), est aussi le contrôle laissé par le développeur à son infrastructure pour optimiser ses propres moyens. Comme vous ne vous préoccupez pas du processus qui porte la "fonction" ni "où" ce processus est exécuté, l'infrastructure est (et doit) être en capacité de rationaliser sa consommation de ressources : bref, elle va couper tout ce qui est inutile. On en revient à la partie aqueuse : on coupe le robinet pendant qu'on se brosse les dents. On "_scale-to-zero_" (on réduit l'échelle en bon français !). Le _FaaS_ n'existe pourtant pas de manière unique, comme si on abordait systématiquement le problème avec une même approche. D'autant que j'ai passé ici sous un relatif silence l'aspect "coût en temps" qui est le point noir général de la solution. Le _FaaS_ peut-être vu alors avec deux principales applications : - un processus qui existe tant qu'il y a de l'activité, et qui est éteint lorsque celle-ci s'arrête (avec un délai modifiable). On se rapproche là considérablement d'un serveur classique. L'avantage : le temps de démarrage est porté seulement par la première requête et qu'un cache partagé entre plusieurs requêtes peut exister. L’inconvénient : il est délicat de définir, voire totalement contre-productif d'avoir un délai d'extinction fixe (rarement un SI se comporte avec une grande régularité et si tel est le cas, il existe d'autres solutions bien plus simples que le FaaS...) ; - un processus déclenché par requête, comme on peut l'avoir avec FastCGI. L'avantage est alors une plus grande isolation entre les requêtes, ainsi qu'un coût réellement nul s'il n'y a pas de requête (le processus s'arrête à la fin du traitement de sa requête). Le coût de démarrage est souvent sensible dans le coût temporel total et il est porté individuellement par l'ensemble des requêtes. Évidemment ces applications négligent volontairement dans l’approche, un aspect essentiel : il faut _toujours_ un processus d'écoute actif, qui déclenchera l'une ou l'autre. Il n'y a pas de magie : si un requête arrive, du client final ou d'une balance de charge, il faut pouvoir être en écoute et répondre, mettre en œuvre la solution. Pour cela, plusieurs solutions existent. Cela que je propose plus loin, avec _systemd_, permet une réduction possible de 100% de la charge sur un OS pour la solution de _FaaS_ : elle est elle-même allumée et éteinte par le système d'exploitation suivant l'activité. Du reste je n'ai pas poussé l'exemple (et le vice) jusqu'à éteindre mon ordinateur - mais vous imaginez bien qu'avec une balance de charge extérieure, on peut sans difficulté éteindre jusqu'aux machines physiques. Le _FaaS_ permet alors de réduire pour une entreprise, de réduire sa dépense au seul coût de possession. La solution de _FaaS_ pour être tout à fait professionnelle ou à tout le moins complète, dispose donc nécessairement de plusieurs composants et règles de gestion, pour faire coller la partie développement logi(ciel/que) à l'infrastructure. ------------------------------------------------------------------------------ ## 2. Le cas d'étude ### La présentation générale : FAASS - Function As A (Simple) Service Le dépôt est ici : [https://github.com/JGarderon/faass](https://github.com/JGarderon/faass). Il contient une seule branche, et j'ai posté [une version `alpha2`](https://github.com/JGarderon/faass/releases/tag/alpha2). Je n'ai pas de "plan sur la comète" - si ça intéresse quelqu'un, je suis prêt à échanger et à aider à l'usage, mais ce travail est pensé et prévu comme un exercice d'entraînement et pour un usage véritablement personnel (remarquez, Linus disait ça aussi... ;)). A ce jour, je prends en charge aujourd'hui deux types différents de "route", qui sont les appels possibles : - une "fonction" (_function_), qui est exécuté dans un conteneur dédié. C'est véritablement du _FaaS_. Le mécanisme derrière, s'appuie sur _stdin_/_stdout_ pour la communication avec le serveur. C'est un choix motivé pour ne pas à réinventer la route côté processus piloté : on se met d'accord sur une procédure d'échange (ici un corps en deux parties, chacune avec une taille définie par un entier déclaré à leur début). Donc pas besoin d'un framework d'exécution comme dans OpenFaas ; - un "service" (_service_), qui est un conteneur lancé et arrêté en fonction des besoins. C'est donc moins dans la logique _FaaS_ que celle du _serverless_. Ici l'échange se fait en HTTP sur un port défini dans la configuration. Le service est arrêté passé une période d'inactivé et rédémarré lorsqu'un client revient. Forcément en fonction de la situation, il sera plus ou moins efficace d'utiliser l'un ou l'autre : - pour des traitements par lot, la version "service" est efficace car elle restera active sans discontinuité durant le temps nécessaire ; - par contre pour un appel très occasionnel, la version "fonction" est suffisante. Autre élément à prendre en compte : la partie "fonction" est la seule qui permet de déclarer via API, un script ou un exécutable qui sera lancé au sein du conteneur. Le service ne dispose qu'un point de montage vers le dossier temporaire. Si vous souhaitez tester les performances dans les mêmes conditions que les miennes, voici un squelette de ma commande locale : ```bash ab -n 1500 -c 50 -s 2 http://127.0.0.1:9090/lambda/{la route à tester} ``` ### La mise en route A cette heure, je n'ai pas eu le temps d'étoffer les tests fonctionnels, donc ne vous inquiétez pas du retour anecdotique de la console : ```bash julien@julien-Vostro-7580:~/Developpement/faasd/test-go-faas$ ./build-and-test.sh --- --- --- start tmp path existent cache path existent example function existent --- --- --- build 2022年09月06日 23:27:28,188 - general - SPECIAL - FaasS = Function as a (Simple) Service --- Created by Julien Garderon (Nothus) from August 01 to September 06, 2022 MIT Licence --- This is a POC - Proof of Concept -, based on the idea of the OpenFaas project /!\ NOT INTENDED FOR PRODUCTION, only dev /!\ 2022年09月06日 23:27:28,188 - general - OK - all is ready for work ! 2022年09月06日 23:27:28,189 - general - INFO - build asked 2022年09月06日 23:27:28,488 - general - OK - build finished 2022年09月06日 23:27:28,488 - general - SPECIAL - ending of script --- --- --- functional tests 2022年09月06日 23:27:28,612 - general - SPECIAL - FaasS = Function as a (Simple) Service --- Created by Julien Garderon (Nothus) from August 01 to September 06, 2022 MIT Licence --- This is a POC - Proof of Concept -, based on the idea of the OpenFaas project /!\ NOT INTENDED FOR PRODUCTION, only dev /!\ 2022年09月06日 23:27:28,612 - general - OK - all is ready for work ! 2022年09月06日 23:27:31,784 - build_and_run - OK - execute test 'test_lambda_url_notfound_get' (pass) 2022年09月06日 23:27:31,785 - build_and_run - OK - execute test 'test_lambda_url_invalid_get' (pass) 2022年09月06日 23:27:31,787 - build_and_run - OK - execute test 'test_home_get' (pass) 2022年09月06日 23:27:32,853 - build_and_run - OK - execute test 'test_lambda_services_get' (pass) 2022年09月06日 23:27:33,708 - build_and_run - OK - execute test 'test_lambda_functions_get' (pass) 2022年09月06日 23:27:34,404 - build_and_run - OK - run without error 2022年09月06日 23:27:34,404 - general - SPECIAL - ending of script --- --- --- stop ``` _Même si certains de vos tests passent en rouge sur votre poste, l'applicatif a déjà été construit et est donc prêt à l'emploi. Du fait de la puissance des machines hôtes, avec les délais de connexion, le script de tests peut partir en erreur (faux positif)._ Vous aurez ensuite besoin d'un certificat auto-signé pour démarrer l'applicatif (pour l'instant c'est obligatoire, je rajoute prochainement cet aspect comme option dans la configuration). De [nombreux tutoriels](https://doc.ubuntu-fr.org/tutoriel/comment_creer_un_certificat_ssl) existent... Tout est prêt pour le lancement. Commençons simplement par... l'arrêt via ctl+c : ```bash julien@julien-Vostro-7580:~/Developpement/faasd/test-go-faas$ ./faass INFO: 2022年09月04日 12:13:46 main.go:68: UI path found : /home/julien/Developpement/faasd/test-go-faas/content INFO: 2022年09月04日 12:13:46 main.go:75: Authorization secret found ; API active ^CINFO: 2022年09月04日 12:13:48 main.go:127: interrupt received ; shutting down WARNING: 2022年09月04日 12:13:48 server.go:23: Shutdown ListenAndServeTLS terminated INFO: 2022年09月04日 12:13:48 main.go:138: process gracefully stopped ``` Si vous souhaitez repartir de zéro pour la configuration voir d'un environnement vierge (effacez `./conf.json`), `faass` le fera pour vous : ```bash julien@julien-Vostro-7580:~/Developpement/faasd/test-go-faas$ ./faass -prepare WARNING: 2022年09月04日 12:16:19 utils.go:74: Unable to create environment for UI contents "/home/julien/Developpement/faasd/test-go-faas/content" : mkdir /home/julien/Developpement/faasd/test-go-faas/content: file exists ; pass ``` Il créera ainsi l'arborescence locale si nécessaire. A partir de maintenant, je vous conseille d'avoir trois consoles ouvertes : - la première pour `./faass` (utilisez `GODEBUG=http2debug=2 ./faass` pour [du _très_ verbeux](https://pkg.go.dev/net/http#pkg-overview)) ; - la deuxième avec `docker stats` pour suivre l'activité réelle des conteneurs ; - la troisième où vous tapez les commandes de requêtes. Dans cette dernière console, je vous conseille de tirer les images demandées dans la configuration, l'application ne prennant pas encore cette étape au démarrage : - `docker pull nginx` ; - `docker pull python:3`. Là, tout est prêt ! __Lancement d'une fonction (sans authentification par défaut)__ Dans la console n°3 : ```bash julien@julien-Vostro-7580:~/Developpement/faasd/test-go-faas$ curl -k -X GET 'https://127.0.0.1:9090/lambda/example-function' --data-raw 'coucou' coucou ``` Notez ici, que le fichier `example-function.py`, se comporte comme un serveur d'écho, en rajoutant simplement un entête HTTP supplémentaire. Dans la console n°2, vous aurez la sortie suivante : ``` INFO: 2022年09月04日 20:01:43 main_server.go:152: known real desired url : /lambda/example-function INFO: 2022年09月04日 20:01:43 main_server.go:168: known desired url : example-function WARNING: 2022年09月04日 20:01:43 main_server.go:95: run container for route 'exampleFunction' ``` Enfin dans la console n°1, vous aurez (si vous êtes rapide !), un aperçu du conteneur. Ce dernier a été entièrement "effacé" de votre système lorsque le processus Python s'est arrêté (comportement de l'arguemnt `--rm`). __Lancement d'un service (avec authentification par défaut)__ Dans la console n°3 : ```bash julien@julien-Vostro-7580:~/Developpement/faasd/test-go-faas$ curl -k -X GET -H "Authorization: Basic YWRtaW46YXplcnR5" https://127.0.0.1:9090/lambda/example-service Welcome to nginx!

Welcome to nginx!

If you see this page, the nginx web server is successfully installed and working. Further configuration is required.

For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.

Thank you for using nginx.

AltStyle によって変換されたページ (->オリジナル) /

``` Dans la console n°2, vous aurez la sortie suivante : ``` INFO: 2022年09月04日 20:03:08 main_server.go:152: known real desired url : /lambda/example-service INFO: 2022年09月04日 20:03:08 main_server.go:168: known desired url : example-service DEBUG: 2022年09月04日 20:03:09 main_server.go:202: running container for desired route : 172.17.0.2 (cId 152a81009abb8b0af39d0b81d2e3fab899327259d7405ddd331a73a45b9e3b9d ) DEBUG: 2022年09月04日 20:03:09 main_server.go:214: new url for desired route : http://172.17.0.2:80/ (cId 152a81009abb8b0af39d0b81d2e3fab899327259d7405ddd331a73a45b9e3b9d ) DEBUG: 2022年09月04日 20:03:09 main_server.go:244: result of desired route : 200 (cId 152a81009abb8b0af39d0b81d2e3fab899327259d7405ddd331a73a45b9e3b9d ) ``` Puis au bout d'une trentaine de secondes, le message : ``` INFO: 2022年09月04日 20:03:32 utils.go:112: Container exampleService (cId 152a81009abb8b0af39d0b81d2e3fab899327259d7405ddd331a73a45b9e3b9d ) stopped ``` La console n°1 n'aura alors aucun conteneur actif. Si vous relancez la même commande, vous constatez que c'est le même conteneur qui a été relancé (son ID est le même). __Usage de l'API (actif seulement s'il y a une authentification)__ Cas simple de récupération : ```bash julien@julien-Vostro-7580:~/Developpement/faasd/test-go-faas$ curl -k -X GET -H "Authorization: Basic YWRtaW46YXplcnR5" https://127.0.0.1:9090/api/functions/example-function {"name":"exampleFunction","service":false,"script":"/home/julien/Developpement/faasd/test-go-faas/tmp/example-function.py","cmd":["python3","/function"],"authorization":"Basic YWRtaW46YXplcnR5","env":{"faass-example":"true"},"image":"python:3","timeout":1000,"retry":0,"delay":0,"port":0,"LastRequest":"0001-01-01T00:00:00Z","Id":"","IpAdress":"","Mutex":{}} ``` Si vous tentez d'appeler un item avec le mauvais type de ressource, vous serez refoulé : ```bash julien@julien-Vostro-7580:~/Developpement/faasd/test-go-faas$ curl -k -X GET -H "Authorization: Basic YWRtaW46YXplcnR5" https://127.0.0.1:9090/api/functions/example-service {"message":"this route is a service, no a function"} ``` Enfin pour la récupération d'un service, notez que l'objet retourné est de la même construction : ```bash julien@julien-Vostro-7580:~/Developpement/faasd/test-go-faas$ curl -k -X GET -H "Authorization: Basic YWRtaW46YXplcnR5" https://127.0.0.1:9090/api/services/example-service {"name":"exampleService","service":true,"script":"","cmd":null,"authorization":"Basic YWRtaW46YXplcnR5","env":{"faass-example":"true"},"image":"nginx","timeout":250,"retry":3,"delay":8,"port":80,"LastRequest":"0001-01-01T00:00:00Z","Id":"","IpAdress":"","Mutex":{}} ``` Pour les autres actions possibles : - `Configuration` : - `GET` : obtenir la copie actuelle de la configuration en mémoire, - `PATCH` : modifier une des valeurs de la configuration globale (seul `delay` est implémenté), - l'export de la configuration en mémoire n'est pas encore implementé ; - `Functions` : - `POST` : soumettre un fichier (et un seul !) qui sera l'exécutable ou le script au sein du conteneur (écrase si existant), - `PATCH` : soumettre l'entité de configuration associée (liée à la configuration ; écrase si existant), - `GET` : obtenir la copie actuelle de la fonction définie dans la configuration, - `DELETE` : supprimer la fonction (ne supprime pas encore le fichier associé) ; - `Services` : - `POST` : soumettre l'entité de configuration (écrase si existant), - `GET` : obtenir la copie actuelle de la fonction définie dans la configuration, - `DELETE` : supprimer le service. Comme vous le constatez, il manque encore beaucoup de pièces pour avoir un ensemble digne d'agir en production... mais on a déjà un ensemble fonctionnel, largement suffisant pour des besoins personnels. ### Descendons encore un peu Nous avons donc un serveur _FaaS_, mais ce dernier reste en permanence en écoute, même s'il n'y a pas de clients depuis un certain temps. Ce processus-même est donc indésirable car il n'apporte rien. L'étape suivante va être de le stopper lui-aussi, grâce aux fonctionnalités intéressantes de _systemd_. L'idée est expliquée [dans un article de RedHat](https://www.redhat.com/en/blog/painless-services-implementing-serverless-rootless-podman-and-systemd) fort intéressant, où le principe du _serverless_ est d'ailleurs expliqué. Crééons trois fichiers, ici pour mon utilisateur courant : ```~/.config/systemd/user``` : - _faass-httpd-proxy.socket_, le fichier décrivant le port d'écoute entrant. Ici c'est mon IP de routeur, derrière ma box. Le port en écoute est le port 50000. Il va être dans notre scénario un déclencheur. ``` [Socket] ListenStream=192.168.2.110:50000 [Install] WantedBy=sockets.target ``` - _faass-httpd.service_, le fichier décrivant le service pour gérer notre programme FaaS, avec le contexte. Notez la commande _ExecStartPost_, __qui va retarder d'une seconde l'exécution du fichier suivant__ - ce n'est pas du tout anecdotique : "_ExecStartPost= commands are only run after the commands specified in ExecStart= have been invoked successfully (...) Note that the execution of ExecStartPost= is taken into account for the purpose of Before=/After= ordering constraints._" ([voir la documentation](https://www.freedesktop.org/software/systemd/man/systemd.service.html)). ``` [Unit] Description=Serverless Faass-httpd.service Wants=network.target After=network-online.target StopWhenUnneeded=yes [Service] Restart=on-failure ExecStartPre=/bin/echo "faass va démarrer" WorkingDirectory=/home/julien/Developpement/faasd/test-go-faas/ ExecStart=/home/julien/Developpement/faasd/test-go-faas/faass ExecStartPost=/bin/sleep 1 ExecStop=/bin/echo "faass va s'éteindre" ``` - _faass-httpd-proxy.service_ est le service, une unité, qui se chargera de "rassembler" les deux parties précédentes, en utilisant un utilitaire d'intermédiation entre le port entrant côté "OS" (_systemd_) et le port entrant côté programme `faass`. Cet intermédiaire se coupera ici de lui-même après 30 secondes d'inactivité, indépendamment de l'écoute réelle du port déclaré dans _faass-httpd-proxy.socket_. ``` [Unit] Requires=faass-httpd.service After=faass-httpd.service Requires=faass-httpd-proxy.socket After=faass-httpd-proxy.socket [Service] ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time=30s 127.0.0.1:9090 ``` Nous pouvons maintenant déclencher les hostilités et regarder ce qui se passe : ```bash systemctl --user daemon-reload \ && systemctl --user enable --now faass-httpd-proxy.socket \ && journalctl --user -u faass-httpd.service -f -n 0 ``` Lorsque je charge ma page côté navigateur (https://192.168.2.110:50000/lambda/toto?bidule), notre programme FaaS démarre puis, en l'absence d'activité, sera coupé par le système : ``` août 14 20:34:24 julien-Vostro-7580 systemd[2861]: Starting Serverless Faass-httpd.service... août 14 20:34:24 julien-Vostro-7580 echo[222328]: faass va démarrer août 14 20:34:24 julien-Vostro-7580 faass[222329]: WARNING: 2022年08月14日 20:34:24 faass.go:91: new value for delay cleaning containers : min 5 (seconds) août 14 20:34:24 julien-Vostro-7580 faass[222329]: INFO: 2022年08月14日 20:34:24 faass.go:948: UI path found : /home/julien/Developpement/faasd/test-go-faas/content août 14 20:34:24 julien-Vostro-7580 faass[222329]: INFO: 2022年08月14日 20:34:24 faass.go:955: Authorization secret found ; API active août 14 20:34:25 julien-Vostro-7580 systemd[2861]: Started Serverless Faass-httpd.service. août 14 20:34:25 julien-Vostro-7580 faass[222329]: INFO: 2022年08月14日 20:34:25 faass.go:780: known real desired url : /lambda/toto?bidule août 14 20:34:25 julien-Vostro-7580 faass[222329]: INFO: 2022年08月14日 20:34:25 faass.go:790: unknow desired url : toto ( unknow routes ) août 14 20:37:46 julien-Vostro-7580 systemd[2861]: Stopping Serverless Faass-httpd.service... août 14 20:37:46 julien-Vostro-7580 echo[222599]: faass va s'éteindre août 14 20:37:46 julien-Vostro-7580 systemd[2861]: Stopped Serverless Faass-httpd.service. ``` Vous pouvez vous rendre compte en visitant la page `https://127.0.0.1:9090/lambda/toto?bidule` à différent moment, soit qu'elle est accessible si une requête a été adressée sur _192.168.2.110:50000_, soit qu'elle est inaccessible. La visite par l'adresse _localhost_ ne produit dans tous les cas, aucun affichage côté _journalctl_ et c'est bien normal : il est squizzé... Enfin, un dernier moyen facile (au-delà du classique `ps`), c'est de regarder à plusieurs moment quels ports sont en écoute (notez la ligne avec `LISTEN 221837/faass` ) : ``` julien@julien-Vostro-7580:~/.config/systemd/user$ sudo netstat -ltpn Connexions Internet actives (seulement serveurs) Proto Recv-Q Send-Q Adresse locale Adresse distante Etat PID/Program name tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN 136133/cupsd tcp 0 0 192.168.2.110:50000 0.0.0.0:* LISTEN 2861/systemd tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 1407/systemd-resolv tcp 0 0 192.168.122.1:53 0.0.0.0:* LISTEN 1967/dnsmasq tcp6 0 0 :::9090 :::* LISTEN 221837/faass tcp6 0 0 :::1716 :::* LISTEN 3322/kdeconnectd tcp6 0 0 ::1:631 :::* LISTEN 136133/cupsd julien@julien-Vostro-7580:~/.config/systemd/user$ sudo netstat -ltpn Connexions Internet actives (seulement serveurs) Proto Recv-Q Send-Q Adresse locale Adresse distante Etat PID/Program name tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN 136133/cupsd tcp 0 0 192.168.2.110:50000 0.0.0.0:* LISTEN 2861/systemd tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 1407/systemd-resolv tcp 0 0 192.168.122.1:53 0.0.0.0:* LISTEN 1967/dnsmasq tcp6 0 0 :::1716 :::* LISTEN 3322/kdeconnectd tcp6 0 0 ::1:631 :::* LISTEN 136133/cupsd ``` Nous pouvons dire désormais que nous sommes vraiment dans un comportement _scale-to-zero_ ! ------------------------------------------------------------------------------ ## 3. "L'expérience Go" Je ne veux pas déchaîner ici un débat passionné sur ce langage, qui a des qualités (notamment `defer`) et des défauts ; si vous voulez faire votre hooligan, je vous en prie, mais n'attendez pas une réponse de ma part. Par contre je suis curieux d'avoir vos avis (surtout s'ils sont nuancés). Globalement je ressors de cette expérience d'apprentissage du Go, un côté plutôt pénible et peu satisfaisant. Pas un échec, mais pas une réussite non plus (j'imagine que le jugement définitif vient des connaisseurs qui auront le courage de lire mon code). J'ai réussi mon objectif de départ, mais sans l'effet "waaah". Contrairement à Rust, Haskell ou Python - et l'ultime : Common LISP ! -, je n'ai pas eu "l'étincelle". Pour un "développeur" qui a le choix, c'est un critère important car on tient là à la motivation de long terme. Je l'ai choisi pour cet exercice car il me semblait adapté (même si je n'ai pas repris les modules Docker en Go, car je trouve que c'était œuvrer d'un marteau pour écraser une mouche), et sans grande envie. Cela fait longtemps que j'en entends parler : mais après quelques recherches, on tombe vite sur quelques articles avec des arguments plutôt bétonnés sur ses défauts. La gestion catastrophique des modules (chemin, gestion des versions, etc.), une compilation qui ne garantie pas si bien que ça les défauts logiques (pointeur nul notamment), une grammaire sans enthousiasme... ça n'aide pas. Dans une vidéo qui m'avait découragé à l'époque sur Golang, il y aussi parfois dans le choix de ce langage et de le porter au nu (ou de le démolir), pas mal de _hype_. Et c'est pas moi qui le dit mais un ingénieur de chez Docker, Jean-Laurent de Morlhon (sa conférence est d'ailleurs très intéressante) : [フレーム] Je crois beaucoup à la comparaison entre Rust et Go. Les deux ont vocation au moment de leur création, à répondre au défi (1). de la croissance d'un projet en équipe nombreuse (interne à une organisation ou en _open-source_ à travers de nombreux contributeurs), mais plus encore (2). à remplacer le C (et le C++), dans un grand nombre de situations. Je crois que Rust gagne sur la plupart des tableaux face à Go, y compris sur la plan de l'apprentissage : Rust _ne vous permet pas_ de ne pas comprendre ce que vous faites. Le problème n'est pas sa grammaire, mais notre incapacité assez générale, à imaginer qu'on comprend ce que l'on fait tout le temps, sans restriction, dans toutes les conséquences. Rust nous oblige à formaliser _précisément_ notre pensée : ce n'est pas une courbe d'apprentissage, mais aussi la courbe de soumission à la réalité que notre code est souvent... un peu pourri (moi le premier). Go n'est pas différent sur le principe : il chercher à rationaliser la manière de faire, à rendre universel et collectif. C'est le même objectif à mon sens, mais avec deux approches strictement distinctes. Comme l'indique certains responsables de Google sur le "pourquoi" de ce langage, Go répond à cette problématique centrale d'aligner ces "manières de faire" pour gommer les différences entre les profils dans les équipes. Parfois en des termes un peu verts (cf les propos de [Rob Pike](https://www.scriptol.fr/programmation/go.php)) ! La volonté est louable, mais ça donne parfois l'impression de _pisser du code_. Certes j'ai été productif, et j'ai une version naïve d'un serveur _FaaS_ en un millier de lignes (bouclé en quelques semaines, depuis je fais du zèle !). Mais je ne suis pas raisonnablement sûr et raisonnablement satisfait de mon résultat. Plus largement Go souffre de défauts moins de jeunesse que de conception et de choix - dont trois me laissent vraiment perplexe : - _garbage collector_ à la place d'une désallocation statique... ?! - la gestion des erreurs dans les retours (non, non, non), - les `go routines` qui soient loin d'être aussi souples et faciles qu'affichées (si si si). Je comprends parfaitement ceux qui l'apprécient car il a des qualités - et il a un support de poids, qui lui garantie une pérennité quasi-absolue. Mais je ne veux pas être dupe sur les alternatives : tant mieux si Google y trouve son compte, mais je _ne suis pas_ Google, je _n'y travaille pas_ et mon activité ne hasarde pas sur leurs problématiques. Ce langage subit des failles selon moi dans sa construction, notamment des génériques "un peu à l'arrache", qui doivent être identiques pour tous les arguments d'une même fonction et cache la misère générale du système de type (_duck typing_ et c’est tout... ?). La mise en œuvre semble avoir été une réponse rapide à un besoin de la communauté. A mon sens on ne le remplit que partiellement. A défaut d'avoir les génériques immédiatement, on aurait pu naïvement imaginer que Go se dote de macros, y compris des macros à l'exécution, comme celles de LISP. Certes il existe [des approches "préprocesseur" en jouant un peu avec Makefile](https://deathbykeystroke.com/articles/20220106-real-macros-in-go.html) mais objectivement, ce n'est absolument pas une manière idiomatique d'écrire du code Go - simplement de _réécrire_ une partie d'un fichier en le remplaçant par un autre. Pourtant, combiner à un système de signature comme pour les génériques à partir de Go 1.18 et des macros à la construction, le côté répétitif de certaines fonctions aurait été considérablement allégées ou la gestion des retours et des erreurs... Non, le sujet est encore trop douloureux... Certes le langage Go est intéressant pour du développement de utilitaires ou d'applicatifs métier - sssi vous êtes une _grande_ entreprise ou un projet vaste, ou un truc rapide, à caser entre deux repas. Cependant à mon échelle, dans mes besoins immédiats (pro ou perso), il n'existera pas vraiment comme un choix plausible - trop de concurrence intéressante. Enfin il y a [l'émergence de Carbon](https://cpp.developpez.com/actu/335142/Le-projet-Carbon-un-successeur-experimental-du-Cplusplus-explore-une-direction-future-possible-pour-le-Cplusplus-etant-donne-les-difficultes-a-l-ameliorer-et-mise-sur-l-interoperabilite-comme-base-de-travail/), là aussi poussé par des équipes chez Google - difficile pour moi de ne pas y voir un concurrent aussi à Go, et vu les échanges sur les forums ça et là, je ne suis pas le seul à me poser la question... Bref Golang a gagné certaines de ses lettres de noblesses au travers de projet de qualité (et très, très visible, ou Kubernetes porté par... Google) comme Docker mais comme le montre la vidéo plus haut, c'était autant un choix commercial et de gestion, que véritablement un choix technique contraint où Go serait différenciant. Ce langage j'en voie bien l'intérêt mais j'en vois bien aussi, et c'est gênant lorsqu’on le débute, les limites. D'ici quelques semaines, mois ou années, peut-être j'y reviendrais avec un état d'esprit différent. L'âge fait gagner en maturité - qui est une façon élégante de dire qu'on est davantage patient et moins attentif à une forme de perfection. Alors, peut-être, il m'intéressera davantage. En attendant je suis content d'avoir une PdC d'un serveur_ FaaS_ - peu importe en quoi il est écrit. A la suite de ces lignes, je vais le lancer à pleine vitesse contre un platane, histoire de le tester. Bonne nuit.