Le Blog Amazon Web Services

Déployez vos applications web sur des fonctions serverless

Dans cet article, je vous montre une nouvelle option pour déployer vos applications web qui tournent dans des conteneurs. Une option qui ne nécessite pas de machine virtuelle, vous n’aurez donc rien à gérer ; une option qui scale automatiquement selon la charge réelle et qui est très abordable en termes de coûts. Cette solution s’appelle AWS Lambda. Il n’y pas besoin de modifier vos applications existantes, ni même de connaitre AWS Lambda pour y déployer vos applications.

Mon application web existante
En gros, il y a deux types d’applications web : les applications où le rendu HTML se fait dans le navigateur du client (développées avec React, Vue, ou Angular par exemple) et les applications où le rendu des vues (HTML) ou des données (JSON) se fait sur le serveur. Souvent, vos applications utilisent une combinaison des deux technologies : un rendu HTML dans le navigateur pour le front et une API en backend qui sert des données en JSON.

De nombreux frameworks facilitent le développement d’applications web ou d’API sur le backend.  Si vous développez en Java, vous utilisez probablement Spring, Play ou Vaadin, en Python, il y a Django, Flask ou Web2Py, en Node.js il y a Express.js, en C#, il y a ASP.Net. Tous les langages populaires ont un ou plusieurs frameworks web. Pardonnez-moi si j’ai oublié votre framework favori.

Ces frameworks fournissent, ou travaillent derrière un serveur HTTP. Ils routent les appels HTTP vers votre code. Ils interprètent et donnent accès aux paramètres de la requête, aux en-têtes HTTP, etc. En retour, votre code collecte les données, applique un template pour les intégrer dans de l’HTML ou sérialise vos structures de données en JSON et retourne le tout à l’appelant.

Un service géré, facturé selon l’utilisation réelle
Souvent, vous déployez ce code dans des containers ou sur des machines virtuelles que vous payez 24/7. Vous devez aussi maintenir ces machines, par exemple installer les correctifs et mises à jour de l’OS, du langage et des frameworks et librairies que vous utilisez. Vous êtes aussi responsables de la disponibilité de votre solution : il faut mettre en place des mécanismes de surveillance et de redémarrage automatique. Enfin, vous êtes responsable de la scalabilité de votre déploiement : quand votre application a du succès, c’est à vous de provisionner (et gérer) plus de machines virtuelles, de containers ou les deux, et de balancer le traffic sur cette flotte.  Pfew ! Quelle quantité de travail qui n’a rien à voir avec le développement de votre application.

D’un autre côté, vous avez entendu parler des fonctions serverless dans le cloud où on ne paye qu’à l’invocation et où le fournisseur de cloud gère la totalité des tâches opérationnelles décrites ci-avant. Mais vous pensez qu’il faut réécrire et repackager tout votre code pour que vous puissiez en tirer bénéfice. Rien n’est moins sûr !

AWS Lambda, c’est quoi ?
AWS Lambda est un service qui permet d’exécuter votre code dans le cloud, sans que vous n’ayez à gérer l’infrastructure sous-jacente. Quand vous développez une fonction Lambda, vous vous concentrez sur votre code et vous le déployez en un clic.  Le service déploie votre code, se charge de la disponibilité, de la scalabilité, de la maintenance de l’OS sous-jacent etc.  Vous n’avez aucune tâche opérationnelle à vous occuper. Qu’il y ait un appel à votre application ou API par heure ou des dizaines de milliers, le service mettra à disposition la quantité d’instances nécessaires pour répondre à la demande.

Vous ne payez que ce qui est utilisé réellement, calculé en millisecondes de temps CPU. S’il n’y a pas d’appels à votre application, vous ne payez rien.

Souvent, vous ne déployez pas une fonction Lambda toute seule. Soit vous attachez une URL à votre fonction Lambda, soit un autre service est responsable de l’invocation de votre fonction. Dans le cas d’une API, il s’agira probablement d’une API Gateway (qui agit comme point de terminaison HTTPS, package les arguments de la requête et les en-têtes et appelle la fonction Lambda). Mais beaucoup de services dans le cloud AWS peuvent déclencher des fonctions Lambda, ce qui en fait une brique de base de vos architectures orientées évènements.

Un déploiement typique de Lambda pour une API HTTP

Le meilleur des deux mondes
Imaginez que vous puissiez déployer votre application telle quelle dans une fonction Lambda : plus de serveurs à gérer et une facturation selon l’usage réel. Cependant, il y a deux challenges pour déployer une application web dans une fonction Lambda.

Le premier est lié au modèle d’invocation de la fonction Lambda.  L’environnement d’exécution d’une fonction appelle le service AWS Lambda pour obtenir le prochain événement à traiter. Chaque événement est passé à la fonction comme un paramètre JSON.  C’est très différent d’une application web qui attend de recevoir des appels HTTP.

Modèle d’invocation traditionnel des fonctions Lambda

Le second challenge est le packaging. Comment déployer votre application et ses dépendances (l’environnement d’exécution du langage de programmation et le framework web) ?

Le second challenge est le plus facile à adresser, je commence par celui-ci. Il existe deux moyens de paqueter son code pour le déployer dans une fonction Lambda : les fichiers ZIP et les images conteneur, disponibles depuis décembre 2020. Donc, si votre application web est empaquetée dans un conteneur, vous pouvez la déployer dans une fonction Lambda. Et je parie que vos applications web sont déjà empaquetées dans un conteneur.

Mais comment appeler votre application web dans la fonction Lambda ?  La solution vient des extensions Lambda.  Une extension est un processus que vous déployez avec votre fonction Lambda. Il reçoit et peut agir sur les événements reçus et les réponses faites par votre code. L’usage typique des extensions Lambda est l’observabilité ou la télémétrie.

L’idée est de combiner API Gateway et les extensions Lambda. Il faut écrire une extension Lambda qui reçoit des événements HTTP envoyés par une API Gateway. Cette extension va recréer la requête HTTP initiale (le même verbe HTTP, les mêmes paramètres de la requête, les mêmes en-têtes HTTP, etc.) et appeler votre application web qui tourne dans son processus séparé.  A son tour, votre application traite la requête et retourne des en-têtes HTTP (des cookies par exemple) et un body (HTML ou JSON). L’extension récupère l’ensemble de la réponse et la retourne à l’appelant, selon le format des réponses des fonctions Lambda.

Votre application continue d’exposer son endpoint HTTP et l’extension Lambda l’appelle sur http://127.0.0.1:8080 (ou n’importe quel port vous avez choisi d’utiliser). Dans cette architecture, le moteur d’exécution de Lambda est ignoré, les événements sont interceptés par l’extension Lambda et traités par votre application qui n’y voit que du feu.

La séquence est illustrée par le schéma ci-après.

Le flux d’appels via une extension Lambda

La bonne nouvelle c’est que vous ne devez pas écrire vous-même l’extension Lambda, vous pouvez utiliser l’extension open-source écrite en Rust que nous fournissons.

L’autre bonne nouvelle, c’est que ce type de déploiement ne requiert aucun changement dans le code de votre application. Il faut juste ajouter une ligne à votre Dockerfile pour inclure l’extension Lambda dans votre conteneur.

En pratique
Pour vous montrer comment ca marche en pratique, je vais construire une API web et la déployer dans une fonction Lambda. Pour montrer que ça marche avec n’importe quel langage de programmation et framework web, je choisis un couple un peu moins populaire que ceux que j’ai cités dans mon introduction : Swift, le langage de programmation inventé et promu par Apple, et Vapor, le framework web open-source pour Swift.  Swift à l’avantage d’être fortement typé, concis et expressif, et facile à lire. Il y a tous les outils nécessaires et la communauté bienveillante et productive. C’est mon langage de choix pour coder sur le backend.

Ce tutoriel fonctionne avec n’importe quel autre langage de programmation et framework web empaqueté dans un conteneur.

Apres avoir installé Swift et Vapor, je commence par créer un nouveau projet Vapor avec la commande vapor new <directory>. Je choisis de ne pas inclure Fluent, le framework ORM pour accéder à des bases de données, ni Leaf, le système de template pour générer de l’HTML, puisque je veux développer une API.

L’initialisation d’un nouveau projet Vapor

J’ouvre le projet dans VS Code et j’ajoute quelques lignes de code pour créer une API “echo”, c’est-à-dire que mon API va afficher en réponse l’ensemble des paramètres qui lui sont passés en entrée.

(Si vous développez en Swift dans VS Code, n’oubliez pas d’installer le plugin Swift pour VS Code)

une API minimale en Vapor qui répond avec les paramètres reçus

Je teste l’application avec la commande vapor run et j’utilise la commande pour vérifier si tout marche bien.

Démarrer l’application en local

J’appelle l’application avec curl et vérifie que les paramètres et les en-têtes sont retournées en réponse.

j’appelle l’application avec curl et vérifie que ètres et en-têtes sont retournées

Vapor me fournit un Dockerfile par défaut. Donc je peux aussi tester l’app dans Docker. J’utilise les commandes docker build et docker run comme sur l’illustration ci-après.

je teste l’application dans un conteneur en utilisant le fichier Dockerfile fournit par Vapor

Maintenant que sais que mon conteneur fonctionne, je modifie le Dockerfile pour y ajouter l’extension Lambda. J’ajoute à la ligne 59 la commande qui prend l’image de l’extension et la copie dans mon conteneur.

COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.6.1 /lambda-adapter /opt/extensions/lambda-adapter

Je fais aussi quelques changements pour ne pas créer d’utilisateur vapor et ne pas changer les permissions sur les fichiers. Ceci ne fonctionne pas dans une fonction Lambda.  Enfin, je change le port de l’application pour utiliser 9090 pour éviter un conflit lorsque je testerai l’application en local dans la section suivante.

Les lignes ainsi modifiées ont une marque bleue dans la figure ci-après. Une fois les modifications faites, j’assemble mon conteneur avec la commande docker build utilisée précédemment pour être certain que tout fonctionne.

docker build -t programmez:arm64 

les modifications faites au Dockerfile de Vapor

Si vous avez déjà utilisé le répertoire public d’images de AWS, ils se pourrait que vous receviez l’erreur 403 Forbidden en essayant de charger l’image de l’extension Lambda. C’est probablement parce que vous avez un token d’authentification expiré dans votre configuration docker. Dans ce cas, utilisez la commande suivante avant de faire le build :

docker logout public.ecr.aws 

L’étape suivante est de déployer ce code sur AWS Lambda.  En fait, il faut un peu plus que juste déployer le code, il faut aussi créer une API Gateway et lier les deux ensembles.  Tout cela pourrait se faire dans la console AWS, mais, en tant que développeur, je préfère utiliser du code pour décrire l’infrastructure que je souhaite et utiliser un outil qui va créer cette infrastructure pour moi. De cette façon, je peux répéter mes déploiements dans différents environnements (dev, starting, production) et je peux mettre la description de mon infrastructure dans mon outil de contrôle de version de source, avec le reste des sources de mon application.

Pour ce faire, j’utilise AWS SAM. Après avoir installé la ligne de commande SAM, je crée un fichier qui décrit mon déploiement. Je pars d’un fichier fourni par la documentation et je le modifie pour mon application. Regardons en détail ce fichier template.yaml.

un template de déploiement SAM

Les lignes 1 et 2 sont standard pour tous les templates SAM. A partir de la ligne 4, je définis les ressources que je veux créer dans le cloud. La première ressource est une fonction Lambda qui s’appelle “Programmez” et dont la description commence sur la ligne 5 et va jusqu’à la ligne 23. Sur la ligne 6, le type indique à SAM que je veux créer une fonction Lambda.

La ligne 8 indique que la fonction Lambda doit tourner sur une architecture Arm64. Lambda permet aussi de tourner sur une architecture x64. Le choix de l’architecture doit correspondre à celle la machine que j’utilise pour la commande docker build. Dans mon cas, je build sur un Macbook Pro M1, je déploie donc sur Arm64. Attention si vous utilisez des chaînes de CI/CD, assurez-vous que votre infrastructure de build corresponde à l’environnement d’exécution.

La ligne 9 indique que le package est une image de conteneur (et non un fichier ZIP). La ligne 10 indique la quantité de mémoire que je souhaite allouer à l’environnement d’exécution Lambda. Notez que la puissance CPU va de pair avec la quantité de mémoire. Au plus vous allouez de mémoire, au plus de cores CPU sont disponibles. La documentation de AWS Lambda vous renseignera sur les détails. Attention, ce paramètre a un impact sur le prix. A nouveau, la page de tarifs de AWS Lambda vous donnera les détails.

Les lignes 13-15 définissent une variable d’environnement qui indique à l’extension Lambda sur quel port se trouve mon application. Le port par default est TCP 8080. Comme indiqué précédemment, je choisis d’utiliser le port TCP 9090 pour pouvoir faire des tests localement dans l’étape suivante.

Les lignes 16-18 indiquent que je souhaite créer une API gateway pour déclencher ma fonction Lambda. Comme je n’indique ni chemin ni commande HTTP, l’API Gateway agira en passe-plat : tout ce qui est reçu sera envoyé à la fonction Lambda.

Les lignes 20-23 sont spécifiques à mon conteneur : où se trouve le Dockerfile, comment s’appelle-t-il et quels tags utiliser?

Enfin, les lignes 25 et suivantes définissent les valeurs à afficher en sortie de la commande SAM. Dans ce cas-ci, j’affiche l’URL de l’API Gateway. De cette façon je connaitrai l’URL à utiliser pour tester mon application.

Je peux maintenant construire mon conteneur pour le préparer au déploiement.  J’utilise la commande sam build.

Après quelques minutes, vous devriez voir le message Build Succeeded.

SAM build est réussi

Tester en local
SAM me permet de tester mon conteneur en local avec une émulation de API Gateway et Lambda.  Pour ce faire, je lance le conteneur avec la commande sam local start-api.

démarrer le serveur de test local SAM

SAM démarre un serveur en local sur le port TCP 3000. Je peux le tester avec une commande curl similaire à celle utilisée précédemment.

je teste mon packaging SAM en local

Déployer et Tester dans le cloud
Maintenant que suis sûr que mon application fonctionne, je peux la déployer dans le cloud avec la commande sam deploy —guided. Notez qu’il est nécessaire d’avoir un compte AWS pour passer cette étape. L’option —guided n’est nécessaire qu’au tout premier déploiement. Cette option indique à SAM de collecter quelques paramètres du déploiement qui seront sauvegardés et réappliqués automatiquement lors des prochains déploiements de ce projet. Pour la plupart des questions, j’accepte la réponse proposée par défaut.

le tout premier déploiement avec SAM

C’est un bon moment pour se dégourdir les jambes et prendre une boisson.  Le déploiement crée l’infrastructure dans le cloud : la fonction Lambda, l’API Gateway mais aussi quelques rôles et permissions pour que tout fonctionne ensemble.

Après une ou deux minutes, vous devriez voir un message de succès et l’URL de l’API Gateway juste créée.

le déploiement SAM est fini

Comme l’ai fait précédemment, je teste le déploiement avec la commande curl. J’utilise l’URL de l’API Gateway retournée par SAM.

je teste le déploiement dans le cloud

Le premier appel peut prendre quelques secondes, le temps de charger l’image du conteneur depuis le répertoire d’images et démarrer le container et l’extension Lambda. C’est le phénomène de démarrage à froid de Lambda.

Combien ça coûte ?
Nous avons fait tout ça pour deux raisons. La première est de ne plus devoir gérer d’infrastructure, ne plus s’occuper de la haute disponibilité, de la scalabilité, etc.

La seconde raison est de ne plus payer pour des serveurs 24/7 mais juste pour ce qui est utilisé. Faisons donc une petite estimation de coûts.

Imaginons que cette API soit appelée 10.000 fois par heure pendant les heures de bureau (10h / jour / 5 jours par semaine) et 100 fois / heure le reste du temps. Chaque invocation prend 50 ms de temps d’exécution.

  • Je déploie sur un conteneur avec 512 MB de mémoire sur la région de Paris.
    10.000 (invocations) x 10 (heures) x 5 (jours) x 4 (semaines dans un mois) = 2.000.000 invocations / mois.
    (100 (invocations) x 14 (heures) x 5 (jours)) + (100 (invocations) x 24 (heures) x 2 (jours de week-end)) x 4 (semaines dans un mois) = 47.200 invocations / mois.
    Soit un total de (2.000.000 + 47.200 invocations) * 50 ms * 0,001 (ms vers secondes) = 102.360 secondes de temps CPU par mois.
  • 102.360 secondes * 0,5 GB (mémoire) = 51.180 GB / secondes de temps CPU par mois, soit en dessous du tier de fonctionnement gratuit qui est de 400.000 GB / Sec par mois.
    Total : $0.0 / mois
  • Les invocations de la fonction Lambda sont facturées $0.20 par million, avec le premier million gratuit tous les mois.
    Soit (2.047.200 – 1.000.000 (gratuit par mois) / 1.000.000) * $0.20 = $0.21
    Selon les hypothèses énoncées ci-dessus, cette fonction me coûtera donc $0.0 + $0.21 = $0.21 / mois
  • L’API Gateway quant à elle est facturée $1.17 par million d’appels.
    (2.047.200 / 1.000.000) * $1.17 = $2.40 / mois

Le coût total de mon API sera donc de $2.61 / mois. Ce calcul ne tient pas compte du tier de fonctionnement gratuit valable les douze premiers mois de la vie d’un nouveau compte AWS. Ce calcul ne tient pas compte non plus des options de pré-payement ou d’engagement sur la durée qui permettent de réduire les coûts.

Vous pouvez consulter le détail du calcul et changer mes hypothèses en utilisant la calculatrice des tarifs AWS.

Essayez dès aujourd’hui
Et voilà! En modifiant seulement quelques lignes de code dans votre fichier Dockerfile et en utilisant AWS SAM, vous avez empaqueté votre application web ou API existante dans une fonction AWS Lambda et vous pouvez la déployer sur une infrastructure serverless. Vous n’avez pas dû modifier le code de l’application elle-même.

Pour cet exemple, j’ai utilisé un combo moins populaire que les frameworks classiques que sont Spring, Django, Express ou ASP.Net mais vous pouvez utiliser l’extension Lambda web-adapter avec n’importe quelle application web empaquetée dans un conteneur.

SAM permet beaucoup d’autre possibilités comme la gestion d’environnements multiples (dev, test, staging, prod par exemple) et le déploiement et l’intégration continue. Je vous laisse explorer ces possibilités en lisant la doc de SAM.

L’étape suivante est de protéger votre API par une des méthode d’authentification supportées : OAuth2, AWS IAM, ou une authentification via une autre fonction Lambda. Ne déployez pas d’application ou API sans authentification.

Pour démarrer dès aujourd’hui, rendez-vous sur la page de l’extension web de Lambda et sur la page de SAM.