Sécurisation, monitoring et gestion des coûts sur AWS Lambda
Applications web, traitement de données, IoT, remédiation… l’utilisation de fonctions Serverless est devenu un standard dans les organisations, et sur de nombreux usages. Des fonctions Lambda sont aujourd’hui au coeur des systèmes déployés sur AWS. La facilité qu’offre le service à exécuter un code fait cependant qu’un bug potentiel sur une fonction Lambda critique peut facilement passer inaperçu.
De la même façon, on tend parfois à compter sur la sécurité et résilience d’AWS, en oubliant la responsabilité du client dans le modèle de responsabilité partagée du cloud : si AWS offre la sécurité et la résilience nécessaire du système qui exécute le code, cela n’empêche pas les risques introduits par le code lui-même ou la configuration du service.
Dans cet article nous verrons comment utiliser les services AWS pour faire des fonctions Serverless en prenant en compte :
- Le monitoring de l’exécution du code
- Les pratiques de sécurité
- La résilience du code
- Autres point d’attention
1. Monitoring du nombre d’erreurs d’exécution
Besoin
En déployant un code critique, il faut s’assurer qu’il soit exécuté correctement. S’il y a des erreurs au-delà du seuil toléré, il faut alerter les parties responsables.
Solution
On peut utiliser le service de monitoring CloudWatch pour monitorer les erreurs d’une fonction Lambda. Ici, nous allons créer une alarme CloudWatch qui déclenchera une alerte, envoyée par le service de notifications SNS, si le nombre d’erreurs dépasse un seuil. Voici les paramètres possibles d’une telle alerte CloudWatch :
- Name : <nom_alerte>
- metricName : Errors
- Namespace : AWS/Lambda
- Period : 600
- Statistic : Sum
- Threshold : <seuil_d_erreurs>
- ComparisonOperator : >=
- Dimensions : (FunctionName, <nom_de_fonction>)
- EvaluationPeriods : 1
- AlarmActions/InsufficientDataActions : Alerte SNS
- TreatMissingData : missing
Le seuil d’alerte et la période d’évaluation sont des paramètres qui varient d’une fonction à l’autre.
Par exemple:
- Lambda qui traite des logs : dans ce type de cas, on peut tolérer un nombre d’erreurs en dessous d’un seuil plus ou moins élevé.
Ce type de Lambda s’exécute des centaines, voire des milliers de fois toutes les heures, avec un retry automatique en cas d’erreur , surtout si la Lambda est intégrée nativement avec d’autres services AWS (logs dans Kinesis par exemple). On peut tolérer que la Lambda ne fonctionne pas pendant une heure (lors d’un incident par exemple), mais il reste quand même nécessaire de monitorer la reprise du bon fonctionnement. On peut aussi tolérer qu’il y ait des erreurs intermittentes dues à différentes raisons, comme une indisponibilité réseau, etc.
- Lambda qui remédie un événement de sécurité : dans ce genre de cas on ne peut pas tolérer de laisser échapper des événements, on placera donc le seuil à 1. L’alerte doit être actionnable : investigation, correction de l’erreur.
Exemple du texte de l’alerte:
2. Sécurité
Il existe plusieurs risques liés à l’utilisation du serverless, puisque Lambda offre la possibilité d’interagir avec tous les autres services AWS via les SDK. Une mauvaise configuration est le plus souvent à l’origine de ces risques, donnant la possibilité d’abus de privilèges à:
- Tout membre de l’organisation pouvant développer des Lambda: même si un développeur a des droits limités sur les compte AWS, il peut en gagner plus puisque son code sera exécuté sous le rôle Lambda, qui peut avoir plus de permissions.
- A un parti externe, avec la possibilité d’injecter du code après avoir gagné accès à un composant de la chaîne CI/CD par exemple.
Pour mitiger ces risques, il faut considérer les points suivants:
2.1 Les droits d’accès
Concernant le rôle IAM assigné à la Lambda, il faut :
- Appliquer le principe «Least Privilege » : autoriser la Lambda à faire seulement ce qu’elle a besoin de faire. Si elle lit des données dans S3, il ne faut pas lui donner des droits write/delete sur le bucket. Si la Lambda traite un sous-ensemble de ressources seulement, il faut limiter son rôle via des conditions sur les Tags.
- Une fonction Lambda est associée à un Rôle. Il faut éviter de partager un rôle entre plusieurs Lambda, sauf s’il y a une raison qui le justifie.
Pour appliquer le Least Privilege, il y a un minimum de droits de base qui assurent le bon fonctionnement de la fonction Lambda. Par rapport aux deux droits qui vont suivre, chaque droit supplémentaire doit être justifié par les actions que la Lambda va effectuer :
- Logging : La fonction doit avoir le droit d’écrire des logs CloudWatch, sinon on ne pourra pas trouver ses logs d’exécution. La policy « AWSLambdaBasicExecutionRole » peut être utilisée à cette fin.
- Gestion d’ENI (Elastic Network Interface) : Applicable seulement s’il y a une raison de déployer la Lambda dans un VPC (voir partie networking de ce document). Cela autorise la Lambda à créer/describe/delete des ENI. Dans ce cas, on peut utiliser la policy « AWSLambdaENIManagementAccess ».
2.2 Networking :
Il est possible de configurer la Lambda pour fonctionner à l’intérieur d’un VPC. Quand on spécifie un VPC, la Lambda instancie des ENI dans les subnets et route le traffic à travers ces ENIs.
Il faut éviter de choisir de faire tourner les Lambda dans les VPC quand ce n’est pas nécessaire (voir également l’article de Nicolas Pellegrin sur ce point, Etude de cas AWS Lambda avec XRay). C’est seulement nécessaire si la Lambda a besoin de communiquer avec les IP privées des ressources dans le VPC. Et dans ce cas, il faut considérer les Security Groups.
2.2.1 Les Security Groups
Comme pour les droits IAM, il faut aussi mitiger le risque de débordement d’accès réseau. Ici on pense firewall, dont l’équivalent est le “Security Group” dans AWS. Pour éviter, en cas de souci, que la Lambda soit capable de rejoindre les ressources critiques dans un VPC, il faut appliquer le même principe de « Least Privilege » sur les règles des “Security Group”, ce Sécurity Group étant appliqué aux ENI exploitées par la fonction Lambda:
- Pour le Security Group de la Lambda elle-même, il faut bien étudier les règles de sortie “outbound”: ne pas laisser la possibilité à un potentiel code malicieux (ou erroné) d’aller, par exemple, joindre les bases RDS de Prod. Il ne faut ouvrir que les ports nécessaires vers les destination nécessaires.
- En plus, si la Lambda a besoin de partager des Security Group avec d’autres ressources pour une raison ou une autre, et a besoin de flux autorisés supplémentaires, il faut penser à créer un Security Group propre à la Lambda, pour ne pas autoriser ce flux à toutes les autres ressources qui n’ont pas besoin.
2.2.2 : Elasticité et Adresses IP
Puisque la lambda crée des ENI dans le VPC, cela consomme les adresses IP. Puisque Lambda est élastique et permet l’exécution concurrente, le nombre d’ENI qui peuvent être créées par la fonction en même temps dépend de :
- Le nombre d’exécutions concurrentes
- La mémoire configurée (doc AWS)
Il faut donc bien étudier l’exécution concurrente (qui dépend aussi du trigger, voir la doc relative), considérer aussi de définir une limite de concurrence sur la fonction. Il faut éviter une élasticité infinie pour ne pas causer un DoS sur le VPC : impossibilité de créer d’autres ressources car les plages IP des subnets sont épuisées.
2.3: Gestion de secrets
Ne jamais mettre un mot-de-passe, clé, ou n’importe quelle donnée sensible dans le code. Les risques sont:
- Perte de contrôle sur les secrets
- Impossibilité de faire une rotation sans modifier tous les bouts de code qui l’utilisent.
On préférera une solution de gestion de secrets centralisée. Par exemple:
- Le service AWS Secret Manager: Pour créer (avec l’outil d’IaC en place) des containers de secrets et les chiffrer avec KMS. Au niveau du code, lancer un call API pour récupérer la valeur dynamiquement.
- Le Parameter Store du service AWS SSM: Gratuit et offrant une multitude de fonctionnalités, notamment le stockage et chiffrement de paramètres. Également,au niveau du code, lancer un call API pour récupérer la valeur du secret dynamiquement.
- Pour les besoins plus complexes, utiliser Vault de Hashicorp (voir ici nos articles sur Vault).
- Dans le pire des cas, et si on manque vraiment du temps pour retirer les mots de passe poussés en clair dans Git, on peut les remplacer par des variables d’environnements Lambda chiffrées. (doc AWS)
Bien-sûr, il faut bien penser à limiter l’accès aux secrets dans SecretManager ou ParameterStore, en implémentant les bonnes “Policies” IAM et Policies KMS.
3. Résilience de code Lambda
Même si le service Lambda facilite l’automatisation de différentes tâches et le déploiement de code, il faut absolument appliquer les best-practices de développement (voir aussi cet article sur les bonnes pratiques Lambda) :
- Gestion des exceptions : si une action critique du code retourne une erreur, penser à revenir vers un état stable avant de quitter. Ceci est valable pour les calls API, l’envoi de commandes sur le réseaux, etc. On doit toujours s’attendre à un échec, parce que ça va échouer un jour en production !
Exemple: Notre code arrête des instances pour en faire une image, puis les redémarre. Si l’appel API de création d’image échoue, par exemple parce qu’on a atteint la limite d’appels API, ou que l’on n’a pas les bons droits, la Lambda va quitter et on va se retrouver avec des instances arrêtées.
Solution: selon le langage de programmation et le SDK AWS utilisé, on essaye de gérer ces exceptions connues lors des tests, limites API, limites de règles de security group, etc. Si on rencontre une erreur inconnue, on essaye de retourner vers l’état initial en redémarrant l’instance arrêtée et en renvoyant un code d’erreur (le monitoring se chargera des alertes)
- Future-proofing : ne jamais se fier à une situation temporaire . Coder en dur une limite AWS, mettre une liste de comptes en dur dans le code. Si la valeur change, il faudra alors retourner changer le code partout où il est déployé. Le mieux est de les lire dynamiquement par des calls API, ou les passer en variable d’environnement.
- Gestion de secrets : voir ci-dessus dans la partie Sécurité.
- Variables d’environnement : à utiliser pour les valeurs susceptibles de changer (au niveau IaC). Afin de répondre à cette exigence, on peut appliquer un principe plus global de l’Infrastructure As Code. Les valeurs de configuration globales seront positionnées dans une configuration centrale dans un système VCS (git ou un équivalent), et passées dynamiquement comme variables d’environnement à la Lambda.
Prenons l’exemple d’une Lambda qui a besoin de la région et la période de rétention pour supprimer des backup. Pour ne pas avoir à modifier le code quand on veut inclure une nouvelle région ou diminuer la période de rétention, on peut passer ces valeurs comme variables d’environnement. Cela nous permet aussi d’avoir un code uniforme qu’on peut déployer sur plusieurs comptes avec des valeurs différentes par compte.
Si on a besoin de partager ces paramètres entre plusieurs Lambda, et avoir à les modifier une seule fois, on peut en faire des paramètres globaux stockés sur le service “Parameter Store”, et requêtés dynamiquement avec le SDK AWS.
- Éviter d’atteindre les limites des calls API EC2 : certains services ont une limite de calls API. Il est facile, par exemple en écrivant une Lambda qui supprime des snapshots, d’atteindre cette limite, surtout quand les calls sont nombreux et successifs (Doc). Dans ce cas, il faut insérer un temps d’attente entre les appels API. Mieux encore, on peut utiliser le service Step Function de AWS pour implémenter des retry face à des erreurs spécifiques. Face à une erreur de limite API EC2, on peut implémenter un retry avec un “back-off” exponentiel. C’est-à-dire que la Step Function relancera la Lambda après un temps d’attente, en multipliant ce temps à chaque fois, sans avoir à payer le temps d’exécution Lambda si on fait des simples “sleep”.
4. Optimiser les coûts
Comme avec les ressources compute, il faut être attentif aux coûts du code qu’on déploie.
La facture de Lambda est calculée en se basant sur la durée d’exécution de la fonction et la mémoire allouée, multipliés par le nombre d’exécutions. Mais il faut aussi faire attention aux coûts indirectement générés par le code :
- Logs CloudWatch
- Transfert de données (S3, dynamoDB, internet)
De plus, cela peut bloquer d’autres ressources en atteignant les limites :
- Limite du nombre d’invocations concurrentes de toutes les Lambdas dans un compte (Doc)
- Limite des Appels API EC2 lancés vers le service EC2, si la Lambda en fait (Doc)
4.1 Optimiser une fonction Lambda
Par souci de coûts et de limites API, il faut penser à optimiser :
- Le code : voir si on peut regrouper plusieurs calls API dans un seul, surtout le traitement de données qui peut devenir coûteux. Vérifier :
- Si on fait plusieurs appels read/write S3/DynamoDB non-nécessaires,
- Si on output des logs non-nécessaires,
- Si on peut simplifier le traitement pour consommer moins de temps
- Si on n’introduit pas une boucle infinie
- La mémoire : lors des tests, on peut facilement mesurer la mémoire consommée par les exécutions de test (dans les logs CloudWatch de la Lambda). En production, il faut définir une valeur de mémoire acceptable : pas trop basse (en général en production on traite un volume de données plus élevé), et pas beaucoup plus élevée que les tests pour diminuer la facture finale.
- Le Timeout : Comme la mémoire, le timeout doit être une valeur assez élevée pour pouvoir finir le traitement, mais pas trop élevée au point de laisser une boucle infinie, ou un timeout réseau augmenter la facture. La durée d’exécution est loggée à la fin de chaque exécution, ce qui peut donner une échelle du timeout à définir.
- Cold Start: Le “Cold Start” est le temps que prend la Lambda pour démarrer les composants nécessaires à l’exécution, et initialiser le code. Lambda garde les containers instanciés quelques minutes pour autoriser des invocations conséquentes de la même fonction à les réutiliser, et ainsi diminuer le temps du cold-start. (doc AWS)
Un autre avantage de cette période où le container est en “stand-by”, c’est qu’on peut réutiliser les parties du code déclarées en dehors du handler, car elles sont initialisées une seule fois quand le container est créé.
Cela nous permet, en utilisant des variables globales et des fonctions en dehors du handler, de récupérer des données de DynamoDB, S3, Secret Manager, etc une fois par container, puisque ces parties restent initialisées tant que le container est en vie.
Cela résulte, dans le cas d’invocations consécutives de la fonction Lambda, en l’optimisation:
- Du temps d’exécution, donc de la facture totale
- Du nombre d’appels faits par Lambda à d’autres services (données, paramètres, secrets), donc diminution du coût indirectement généré par le code
Dans un prochain article nous verrons en détail comment diminuer le temps de cold-start.
Il faut bien sûr adapter le code du cold start au besoin. Si on récupère des données qui changent tout le temps il sera mieux de ne pas les mettre en dehors du handler, contrairement au cas où on récupère une clé de chiffrement qui change tous les 3 mois.
4.2 Triggers de Lambda
Pour une Lambda basée sur un événement (CloudTrail par exemple), il faut bien étudier le trigger et le code. Il faut absolument éviter de configurer des triggers qui contiennent des événements faits par la Lambda elle-même, de sorte que l’exécution de Lambda re-génère le même event, qui re-déclenche la Lambda, etc. Cela peut devenir une boucle infinie, voir multiplier les exécutions concurrentes comme une « Fork Bomb ».
Dans ce cas, il faut :
- Raffiner la configuration du trigger, pour exclure les événements générés par la fonction Lambda elle-même
- Raffiner le code, pour ne pas répondre aux évents générés par la fonction Lambda elle-même
5. Mesures de remédiation utiles
Il est possible de renforcer les pratiques décrites dans cet article par des mesures de remédiations automatisées. Dans un prochain article nous détaillerons l’implémentation de telles mesures:
- Monitorer le nombre total d’exécutions concurrentes de Lambda du compte : avec CloudWatch, définir une alerte sur la somme de la métrique “ConcurrentExecutions” sur toutes les fonctions.
- Des règles de compliance, nécessitant un effort de développement supplémentaire (Utilisation de Lambda et AWS Config), listant les ressources comme non-compliant si plusieurs fonctions partagent le même rôle IAM, ou un Rôle Lambda contient des droits excessifs : (action: * et resource:*)
- Un module IaC unifié pour le monitoring des Lambda individuellement, pour les erreurs. Ce module peut-être appelé par toute stack IaC qui déploie une Lambda. Comme ça, toute Lambda déployée aura son alerte crée en même temps.
Conclusion
Tout au long de cet article, nous avons défini des pratiques à suivre pour monitorer les fonctions Lambda, optimiser leur coût d’exécution, et mitiger les risques de sécurité. Ce qui est proposé comme checklist avant de déployer un composant serverless:
- Monitorer l’exécution des fonctions critiques avec CloudWatch: Combien d’erreur tolère-je sur ma fonction?
- Assigner les bonnes permissions minimales dans le rôle IAM.
- Définir les flux nécessaire pour le Security Group, si la fonction sera dans un VPC.
- Où stocker les secrets s’il y en a?
- Combien d’exécutions concurrentes y aura-t-il? Suis-je bien préparé à cette échelle? (limites API EC2, limites Concurrence Lambda, limite d’ENI dans le VPC si c’est le cas)
- Comment optimiser le code pour une meilleure résilience: Gestion d’exceptions, variables d’environnement, diminuer les appels API,..
- Quelles valeurs de mémoire et timeout définir?
- Éviter les boucles infinies à tout prix. Bien étudier la relation entre le trigger et les actions que mon code exécute.
- Éviter de mettre les fonctions dans le VPC si ce n’est pas absolument nécessaire.