Développer sur AWS Lambda : les bonnes pratiques
Cet article est librement inspiré de la session AWS Lambda en 2019, faciliter le développement présentée par Alexandre Pinhel au AWS Summit Paris. Vous pouvez retrouver l’enregistrement vidéo à cette adresse.
Maîtriser AWS Lambda est nécessaire pour tirer pleinement parti des services d’AWS, notamment lorsque l’on est dans une optique serverless ou DevOps. En effet, ce service de Function as a Service (FaaS) est la « colle » qui permet de relier les différents éléments d’une architecture AWS entre eux.
Voici quelques exemples d’utilisation de fonctions Lambda :
- Créer des vignettes automatiquement lorsqu’une image est enregistrée dans S3 ;
- Vérifier quotidiennement qu’aucun utilisateur IAM n’a d’autorisation qui lui soit liée directement ;
- Traiter les appels à une API Gateway.
Cet article s’adresse aux personnes qui ne maîtrisent pas encore AWS Lambda et qui souhaitent acquérir les bons réflexes pour éviter les obstacles les plus communs et profiter au maximum des possibilités offertes par ce service.
Développer et déployer son code : comment s’y prendre ?
Développer
Si vous avez déjà créé une fonction Lambda, c’est sans doute en utilisant l’éditeur de code intégré à la console AWS. Cet outil bien pratique permet d’éditer son code directement depuis un navigateur web puis, en un clic, d’enregistrer son travail et déployer la nouvelle version de la fonction. Pour expérimenter et se faire une première impression du service, c’est idéal !
Pour une utilisation plus soutenue, l’éditeur montrera cependant rapidement ses limites : fonctionnalités basiques, impossibilité de versionner son code sous Git, manque d’intégration à la chaîne de CI/CD, limitation de la taille du paquetage de la fonction à 3 Mo, etc. Il est donc préférable d’utiliser un IDE pour le développement et de déployer les fonctions à l’aide d’un framework.
Le choix de l’IDE peut se faire selon les préférences des développeurs, le langage de programmation et les contraintes organisationnelles. On notera toutefois qu’AWS propose des plugins pour Visual Studio Code, PyCharm et IntelliJ qui facilitent notamment l’exécution en local des lambdas et des tests.
Déployer
Pour le déploiement, Alexandre Pinhel préconise l’utilisation d’un framework. Il y en a beaucoup et son choix pourrait faire l’objet d’un article à lui tout seul. Citons en tout de même deux qui sont développés par AWS :
- SAM (Serverless Application Model), qui a une approche orientée microservices ;
- Amplify, qui vise les applications web et mobile.
Parmi les frameworks tiers, Serverless dispose de la plus large communauté et répond à la plupart des besoins grâce à un écosystème de plugins très fourni.
Que fait donc un framework pour être si indispensable ? Il gère la création du paquetage contenant le code et ses dépendances ainsi que son déploiement sur AWS. Souvent, il peut aussi générer un squelette d’application. Le développeur se contente d’écrire un fichier de configuration, souvent en YAML, pour décrire la fonction et les ressources dont elle a besoin.
Les outils d’Infrastructure as Code, CloudFormation et Terraform, peuvent aussi créer des fonctions Lambda, mais ils ne gèrent pas nativement le paquetage du code. Si vous choisissez de les utiliser, il vous faudra donc les intégrer avec un framework ou écrire vos propres scripts de paquetage.
Optimiser les performances
Comprendre le cycle de vie d’une Lambda
Lorsqu’une fonction Lambda est invoquée après une période d’inactivité, on parle d’un démarrage à froid (cold start en anglais). AWS doit télécharger votre code, créer un nouveau container et y configurer le runtime.
Réduire la durée du cold start
Il est possible d’agir à deux niveaux pour réduire le temps de démarrage. Premièrement, le choix du langage de programmation n’est pas neutre, car leurs runtimes n’ont pas tous la même taille. Les bons élèves sont Python, Go et Node.js. Sans surprise, C# et Java sont nettement plus lents.
La réduction de la taille de la fonction et de ses dépendances est le deuxième point d’optimisation. Alexandre Pinhel donne le chiffre de 1 seconde par 100 Mo. C’est le moment de faire la chasse aux dépendances inutilisées et de réévaluer si vous avez vraiment besoin d’une bibliothèque de calcul scientifique pour faire une addition… Cela incite aussi à éviter les fonctions monolithiques et à adopter le découpage en microservices et le principe de responsabilité unique. Enfin, certains langages permettent des optimisations supplémentaires comme Minify pour Node.js.
Réutilisation du container et du contexte d’exécution
Après l’exécution d’une fonction Lambda, le container va rester disponible pendant un temps variable (entre 5 et 15 minutes) et il pourra servir à de nouvelles exécutions si nécessaire. On parlera alors de démarrage à chaud (warm start). En évitant la phase de lancement du container, on gagne déjà beaucoup de temps, mais on peut en plus réutiliser les variables globales. Par exemple, en Python, on ne créera qu’un seul client boto3 qui sera réemployé d’une exécution à l’autre.
Attention à ne pas non plus initialiser toutes les variables globales au lancement de la fonction ! Il est préférable de faire du lazy loading et de les initialiser uniquement lorsque cela est nécessaire. Attention également aux effets de bords quand vous réutilisez vos variables: les fonctions lambda doivent garder un comportement stateless.
VPC ou pas VPC ?
Les fonctions Lamba peuvent être lancées à l’intérieur d’un réseau virtuel privé, ou VPC, mais cela a un fort impact sur le temps de démarrage, car les containers VPC ne sont pas identiques aux containers classiques et ne bénéficient pas des mêmes optimisations à l’heure actuelle. Demandez-vous donc si la Lambda a réellement besoin d’accéder à un réseau privé.
Il y a plusieurs justifications valables, comme la connexion à une base de données non accessible depuis Internet. Dans ce cas, il est conseillé de créer un sous-réseau (subnet) pour votre fonction avec une plage d’adressage CIDR dédiée et suffisamment large. En effet, chaque container a une interface réseau et sa propre adresse IP. En cas de forte charge, une déplétion du pool d’IP disponibles rendrait impossible le lancement de nouvelles fonctions ou instances EC2.
Choix de la quantité de RAM
Un autre facteur de performance à étudier est la quantité de RAM attribuée à la fonction, sachant que la capacité CPU et la bande passante allouées par AWS sont proportionnelles à celle-ci. Il y a deux types de charges de travail qui auront des besoins différents :
- Les tâches orientées I/O passent beaucoup de temps à attendre la réponse à leurs appels vers des services externes : services managés AWS ou APIs publiques. Elles ne nécessitent en général pas beaucoup de ressources de calcul et se contentent de la quantité minimale de mémoire.
- Les tâches de calcul et de traitement de données pourront bénéficier d’une allocation de ressource plus généreuse. Pour déterminer si un gain de performances et/ou une réduction du coût sont possibles, Amazon propose un outil à base de Step Functions qui testera pour vous plusieurs valeurs différentes de RAM.
Comportement en cas d’erreur lors de l’exécution
Le dernier point à surveiller est la politique de rejeu, c’est-à-dire le comportement d’AWS Lambda en cas d’échec d’une fonction. Pour les appels asynchrones, jusqu’à trois tentatives seront faites tandis que les streams boucleront à l’infini si rien n’est fait pour les en empêcher ! Si l’on ajoute à cela la gestion des erreurs interne aux SDK Amazon, qui vont faire plusieurs tentatives en cas d’échec de certains appels d’API, cela peut rapidement mener à une facture salée comme illustrée ci-dessous.
Pour éviter une telle situation, la fonction doit pouvoir gérer les erreurs qu’elle rencontre et, si nécessaire, placer les événements problématiques sur une Dead Letter Queue qui peut être une queue SQS ou un sujet SNS.
Il est également recommandé de surveiller le bon fonctionnement de sa fonction en se basant sur les logs applicatifs et les métriques remontées dans CloudWatch. Dans le cadre des architectures en microservices, les problématiques de monitoring sont radicalement différentes de ce que l’on rencontrait avec les applications monolithiques. Il faut donc revoir son outillage et ses pratiques afin d’assurer l’observabilité de l’ensemble.
Utiliser AWS Lambda en toute sécurité
Privilèges d’exécution
Le principe de moindre privilège est aussi valable sur le cloud qu’ailleurs. Avec AWS Lambda, cela se concrétise par les autorisations attribuées au rôle IAM affecté à une fonction. Amazon fournit des stratégies (policies) par défaut qui peuvent être utilisées et auxquelles on peut suppléer avec des stratégies ad hoc. Pour celles-ci, il vaut mieux restreindre les autorisations à ce qui est strictement nécessaire à la fonction et éviter de donner accès à toutes les actions et toutes les ressources d’un service.
Gestion des secrets
La seconde problématique de sécurité rencontrée sur Lambda est la gestion des secrets : mot de passe d’une base de données, clé d’API… Il faut éviter d’écrire ces secrets directement dans le code, car d’une part cela complique la réutilisation du même code dans plusieurs environnements, mais surtout il ne faut jamais versionner de telles informations dans Git. À la place, AWS Lambda permet de définir des variables d’environnement, lesquelles peuvent être chiffrées avec KMS.
Si l’on souhaite partager un même secret entre plusieurs fonctions ou services, on préfèrera utiliser le Parameter Store ou Secrets Manager. Le premier permet de stocker tout type de données, tandis que le second est plus spécialisé sur les identifiants de base de données. Epsagon a écrit un excellent article comparant ces deux solutions à Hashicorp Vault. Vous pourrez le lire sur leur blog.
Et D2SI dans tout ça ?
Chez D2SI, nous pensons que l’apprentissage doit inclure une part d’acquisition des fondamentaux théoriques, une part de partage avec ses pairs et, surtout, 70 % d’expérimentation. Cela tombe bien, l’offre gratuite d’AWS comprend un million d’exécutions et 400K Go-secondes de calcul par mois. C’est largement suffisant pour découvrir Lambda ou même créer une petite application !