Création d'un barman IA sans serveur - Partie 1 : Ne soyez plus le menu

Ce fichier a été traduit automatiquement par IA, des erreurs peuvent survenir
Mes plus grands intérêts et passe-temps tournent autour de la nourriture et des boissons, et j'adore mélanger des cocktails à la maison.
Il y a quelque chose d'étrangement satisfaisant à mélanger ce Negroni parfait ou à secouer un vrai Whiskey Sour. Mais quand j'organise des fêtes, je suis confronté au même problème à chaque fois, je deviens le menu humain. Les gens ne savent jamais ce qu'ils veulent, même si je suis équipé d'un bar bien garni, alors à la fin, ils ont tendance à choisir les mêmes vieux cocktails qu'ils prennent toujours.
Ce soir du Nouvel An, nous allions chez des amis pour une fête, et comme toujours, ils m'ont demandé si je pouvais préparer des cocktails pendant la soirée. Bien sûr que je pouvais le faire, mais j'avais besoin d'une sorte de menu de boissons parmi lesquelles les gens pourraient choisir, je n'allais pas apporter toute ma configuration avec moi, seulement presque...
Donc, mon plan initial était de l'imprimer sur papier et que les gens puissent choisir parmi cela. Puis, l'ingénieur en moi s'est réveillé et Et si je créais un système de commande de boissons sans serveur ?
Ceci est le premier article d'une série en trois parties où je construis un assistant de cocktail IA sans serveur. Laissez mes invités parcourir le menu eux-mêmes et commander ce qu'ils veulent directement sur leur téléphone. Et à la fin, obtenez également des recommandations IA sur ce qu'il faut commander, en d'autres termes, moins de travail pour moi.
Le projet sera partagé sur serverless-handbook après la publication de la partie 3.
Commençons !
Ce que nous construisons
Une application web où les invités peuvent parcourir mon menu de boissons, passer des commandes et obtenir des recommandations IA. Considérez-le comme un menu numérique et un assistant barman combinés.
Pour y parvenir, j'ai besoin d'une base solide qui comprend une base de données pour stocker les boissons et les commandes, des API REST qui permettent à l'application de lire et d'écrire des données, une authentification pour sécuriser le panneau d'administration tout en permettant aux invités de naviguer librement, et le téléchargement d'images pour que je puisse ajouter des photos à chaque boisson. Tout sans serveur, bien sûr - pas de serveurs à gérer, juste des services qui évoluent avec la demande.
Aperçu de l'architecture
Dans la première partie, nous couvrirons les bases. C'est le terrain sur lequel tout repose, sans une base appropriée, il sera difficile d'étendre avec de nouvelles fonctionnalités. Nous créerons l'hébergement du frontend, l'API hébergée par Amazon API Gateway, le calcul avec AWS Lambda, l'authentification avec Amazon Cognito, Amazon S3 pour les images, Amazon EventBridge pour les flux de travail pilotés par les événements comme la génération d'invites d'images IA, et Amazon Aurora DSQL comme base de données principale.

Aurora DSQL, la base de données relationnelle sans serveur
Pour la base de données, j'ai choisi Aurora DSQL. C'est une base de données compatible PostgreSQL sans serveur. Pas de provisionnement, pas de pools de connexions à gérer, et elle se développe automatiquement.
Pourquoi ai-je choisi DSQL ? Lorsque j'ai commencé le projet, j'hésitais entre utiliser DynamoDB ou DSQL. Un aspect important était que la base de données sélectionnée devait être sans serveur et fonctionner sans VPC, ce qui élimine Aurora Serverless. La raison pour laquelle j'ai choisi DSQL et un modèle relationnel était que j'aurais besoin d'une recherche et d'un filtrage appropriés, par exemple sur les ingrédients. DynamoDB est un excellent magasin clé-valeur, mais tous les modèles de données ne correspondent pas.
DSQL me donne plusieurs avantages qui en font le bon choix. C'est sans serveur, je ne paie que ce que j'utilise et il se réduit à zéro quand personne ne navigue dans le menu. Être compatible PostgreSQL signifie que j'obtiens du SQL propre avec des capacités de recherche et de filtrage intégrées, essentielles pour les requêtes par ingrédients. Il n'y a pas non plus de gestion des mots de passe, car l'authentification IAM garde les choses sécurisées sans rotation des secrets. Et surtout, cela fonctionne naturellement avec le modèle de connexion éphémère de Lambda. Chaque invocation de Lambda peut s'authentifier indépendamment sans gérer de pools persistants.
Configuration de DSQL
La création d'un nouveau cluster DSQL est assez simple. J'utilise CloudFormation pour créer un cluster DSQL et deux rôles IAM, un pour lire les données, un pour écrire. Pourquoi deux rôles différents ? Principalement pour la séparation des tâches. Dans ce système, 95 % du trafic sera des opérations de lecture, les invités naviguant dans le menu. Seuls les administrateurs effectuent des écritures lors de la gestion des boissons. En séparant les rôles, je m'assure que les fonctions Lambda traitant les demandes des invités ne peuvent que lire. Ils ne peuvent physiquement pas modifier les données même si je commets une erreur dans la fonction. Le rôle d'écriture, quant à lui, est limité aux fonctions Lambda d'administration derrière l'autorisateur Cognito. C'est un petit effort supplémentaire qui crée une frontière significative.
Rôles et autorisations de la base de données
DSQL utilise un modèle d'autorisation à deux niveaux. D'abord, vous avez besoin d'un rôle IAM qui peut se connecter au cluster. Ensuite, vous avez besoin d'un rôle de base de données qui contrôle ce que vous pouvez faire une fois connecté.
Dans ma configuration, cela fonctionne comme ceci.
- La fonction Lambda assume un rôle IAM, comme le
DatabaseReaderRoleci-dessus, via STS - En utilisant ces informations d'identification, la fonction Lambda appelle
dsql.generate_db_connect_auth_token(), cela nécessitera l'autorisationdsql:DbConnect - La fonction Lambda se connecte à DSQL en utilisant le jeton d'authentification comme mot de passe
- DSQL mappe le rôle IAM à un rôle de base de données qui contrôle l'accès aux tables
Voici le code Python qui fait cela.
Le rôle de base de données est configuré comme ceci.
La schéma de la base de données se compose de plusieurs tables et index, mais la table principale est la table des boissons. J'ai dû faire quelques choix de conception délibérés ici. Plus important encore, comment stocker les ingrédients ?
J'utilise JSONB pour les ingrédients car chaque boisson a une liste très différente. Une Margarita a besoin de trois ingrédients comme la tequila, le jus de citron vert et le triple sec. Une boisson Tiki classique peut en avoir besoin de huit ou plus. Avec un modèle relationnel, j'aurais besoin soit d'une table ingrédients séparée avec une relation un-à-plusieurs (ajoutant de la complexité), soit de créer un nombre fixe de colonnes d'ingrédients (perdant de l'espace ou limitant la liste). JSONB me donne de la flexibilité sans sacrifier la capacité de recherche - PostgreSQL peut interroger à l'intérieur des champs JSONB avec des index appropriés, donc si je veux plus tard rechercher "toutes les boissons avec de la tequila", c'est toujours efficace.
J'ai envisagé de stocker les ingrédients dans un simple champ TEXT avec des valeurs séparées par des virgules, mais cela ne s'adapte pas lorsque je veux ajouter plus de métadonnées plus tard, la quantité, l'unité de mesure, les substituts. JSONB prépare le schéma pour l'avenir sans nécessiter de migrations.
Le piège des autorisations qui m'a coûté des heures
Alors que je développais la solution, j'avais besoin de mettre à jour les tables et de créer de nouvelles tables et voici quelque chose qui m'a pris au dépourvu avec DSQL. J'avais configuré mes rôles de base de données, tout fonctionnait parfaitement. Mais après avoir ajouté une nouvelle table pour l'enregistrement des utilisateurs, l'avoir déployée et immédiatement rencontré des erreurs d'autorisation.
permission denied for table registration_codesLe problème ? DSQL ne prend pas en charge ALTER DEFAULT PRIVILEGES.
Dans PostgreSQL régulier, vous pouvez exécuter quelque chose comme ceci.
-- Cela ne fonctionne PAS dans DSQL
ALTER DEFAULT PRIVILEGES IN SCHEMA cocktails
GRANT SELECT, INSERT, UPDATE ON TABLES TO lambda_drink_writer;Cela accorderait automatiquement des autorisations sur toutes les futures tables. Mais DSQL ne prend pas en charge cette commande. Lorsque vous créez une nouvelle table, vos rôles de base de données existants n'ont aucun accès à celle-ci jusqu'à ce que vous accordiez explicitement des autorisations.
La solution est simple mais facile à oublier. Chaque fois que vous ajoutez une table, vous avez besoin d'une instruction GRANT correspondante.
J'ai résolu ce problème en conservant un fichier de migration dédié qui suit toutes les autorisations de table. Lorsque j'ajoute une nouvelle table, je mets à jour ce fichier avec les concessions explicites. C'est manuel, mais cela évite les sessions de débogage "pourquoi mon Lambda ne peut-il pas lire cette table".
Authentification et autorisation
Avant de plonger dans l'API, examinons et configurons l'authentification. Le système a deux types d'utilisateurs avec des niveaux d'accès très différents. Les invités peuvent parcourir les boissons librement, ils n'ont pas besoin de prouver qui ils sont jusqu'à ce qu'ils veuillent passer une commande. Les administrateurs, d'autre part, ont besoin d'une authentification complète pour gérer le menu, voir toutes les commandes et mettre à jour le statut des commandes.
Cette séparation signifie que j'ai besoin à la fois de l'authentification, vérifiant qui vous prétendez être, et de l'autorisation, décidant de ce que vous êtes autorisé à faire. Les administrateurs passent par un flux Cognito complet avec nom d'utilisateur et mot de passe. Les invités obtiennent un flux d'inscription léger basé sur invitation que je détaillerai dans la partie 2.
Donc, les administrateurs doivent se connecter pour effectuer des tâches d'administration, comme créer de nouvelles boissons ou mettre à jour les boissons existantes.

Pool d'utilisateurs Cognito pour les administrateurs
Le pool d'utilisateurs Amazon Cognito contiendra et gérera l'authentification des administrateurs. Il fournit un pool d'utilisateurs hébergé, des jetons JWT et une gestion des groupes sans que je construise quoi que ce soit.
Cognito Hosted Login
Au lieu de construire mes propres formulaires de connexion, j'utilise l'interface utilisateur hébergée de Cognito. Il gère l'ensemble du flux de connexion, y compris le nom d'utilisateur/mot de passe, les messages d'erreur et la réinitialisation du mot de passe, le tout avec une interface propre et personnalisable.
La ManagedLoginVersion: 2 vous donne l'expérience de connexion plus récente et plus personnalisable. La ressource ManagedLoginBranding vous permet de styliser la page de connexion. J'utilise les valeurs par défaut de Cognito ici, mais vous pouvez personnaliser les couleurs, les logos et le CSS.
L'URL de l'interface utilisateur hébergée suit ce modèle.
https://.auth..amazoncognito.com/login
?client_id=
&response_type=code
&scope=email+openid+profile
&redirect_uri=https:///admin/callback Le flux de connexion fonctionne comme ceci.
- L'administrateur clique sur "Connexion" dans le panneau d'administration
- Le frontend redirige vers l'interface utilisateur hébergée Cognito
- L'administrateur saisit ses informations d'identification
- Cognito valide et redirige en arrière avec un code d'autorisation
- Le frontend échange le code contre des jetons JWT (accès, ID, actualisation)
- Le jeton d'accès est stocké et envoyé avec les requêtes API
Lorsqu'un administrateur se connecte, Cognito renvoie un jeton d'accès JWT. Ce jeton contient des réclamations, y compris cognito:groups qui répertorie les appartenances aux groupes de l'utilisateur. Cela sera utilisé plus tard lors de l'autorisation de l'administrateur.
Pour une plongée plus approfondie dans les modèles d'authentification et d'autorisation avec Cognito, consultez mon article PEP et PDP pour une autorisation sécurisée avec Cognito.
API REST avec API Gateway
La configuration et la création de l'API REST ne sont rien de spécial, nous utiliserons Amazon API Gateway, et je choisis la version REST (v1) pour le travail. Il y a plusieurs raisons à cela, mais dans cette partie, j'en couvrirai une. J'ai besoin du support pour les clés API et le throttling et pour le moment, la version REST est la seule à prendre en charge cela. L'API suit une séparation claire avec des points de terminaison publics pour les invités parcourant les boissons et passant des commandes, des points de terminaison d'administration pour gérer le menu. Chacun a des exigences d'authentification et de throttling différentes.
Points de terminaison publics (clé API uniquement) :
GET /drinks - Liste toutes les boissons
GET /drinks/{id} - Obtenir les détails de la boisson
GET /sections - Liste les sections du menu
POST /orders - Passer une commande
GET /orders/{id} - Vérifier le statut de la commande
Points de terminaison d'administration (clé API + JWT) :
POST /admin/drinks - Créer une boisson
PUT /admin/drinks/{id} - Mettre à jour une boisson
DELETE /admin/drinks/{id} - Supprimer une boisson
GET /admin/orders - Voir toutes les commandes
PUT /admin/orders/{id} - Mettre à jour le statut de la commandeAutorisateur Lambda pour AuthN + AuthZ
API Gateway utilise un autorisateur Lambda pour valider les requêtes. C'est là que se produisent à la fois l'authentification et l'autorisation.
La validation JWT récupère les clés publiques de Cognito (JWKS) et vérifie.
- Signature - Le jeton n'a pas été altéré
- Expiration - Le jeton n'a pas expiré
- Émetteur - Le jeton provient de mon pool d'utilisateurs Cognito
Si toutes les vérifications passent, l'autorisateur examine la réclamation cognito:groups. Les points de terminaison d'administration nécessitent que l'utilisateur soit dans le groupe admin, la vérification d'autorisation.
Pourquoi un autorisateur Lambda ?
API Gateway a des autorisateurs Cognito intégrés, mais ils ne font que l'authentification. Ils vérifient que le jeton est valide mais ne peuvent pas vérifier l'appartenance au groupe. L'autorisateur Lambda me permet de faire les deux en une seule étape.
L'autorisateur transmet également le contexte de l'utilisateur aux fonctions Lambda en aval.
Ce contexte est disponible dans event["requestContext"]["authorizer"] afin que les fonctions Lambda sachent qui a fait la demande sans analyser à nouveau le JWT.
Pourquoi des clés API pour les points de terminaison publics ?
Même les points de terminaison publics nécessitent une clé API. Ce n'est pas pour l'authentification, c'est pour la protection et le contrôle. Les clés API sont liées à des plans d'utilisation avec des limites de taux, offrant un contrôle de throttling fin. Les métriques CloudWatch montrent l'utilisation par clé, fournissant une visibilité sur qui appelle l'API et à quelle fréquence. Et si quelque chose ne va pas, je peux désactiver une clé instantanément comme interrupteur d'arrêt.
La clé API est intégrée dans le frontend, donc ce n'est pas un secret. Mais cela me donne le contrôle sur qui peut appeler l'API et à quelle fréquence.
Plans d'utilisation et throttling
Différents points de terminaison ont des limites de taux différentes en fonction de l'utilisation attendue.
Le menu des boissons change rarement, mais les invités le parcourent constamment. Sans mise en cache, chaque chargement de page atteint Lambda et DSQL. Avec 20 invités actualisant le menu toutes les quelques secondes, c'est une charge et un coût inutiles.
API Gateway dispose d'une mise en cache intégrée qui est étonnamment efficace pour ce scénario.
Maintenant, GET /drinks renvoie des réponses mises en cache pendant 1 heure. Le menu ne change pas si souvent, donc c'est un compromis raisonnable. Mais voici le piège, que se passe-t-il lorsque j'ajoute une nouvelle boisson ? Les invités parcourraient un menu obsolète jusqu'à ce que la mise en cache expire naturellement.
La solution est une invalidation agressive. Le Lambda createDrink vide l'intégralité du cache de l'API après l'enregistrement dans la base de données.
Ce modèle, mise en cache agressive pour les lectures, invalidation immédiate lors des écritures, vous donne le meilleur des deux mondes : des chargements de page rapides pour la navigation et des mises à jour instantanées lorsque le menu change. Notez que cette approche fonctionne bien pour le point de terminaison du menu car les changements de données sont peu fréquents et contrôlés. Si je mettais en cache le statut de la commande ou des données en temps réel, j'utiliserais soit un TTL plus court, soit je sauterais complètement la mise en cache. La clé est d'adapter la stratégie de mise en cache à la fréquence réelle des changements de données.
Téléchargement d'images avec S3
Les photos de boissons vont sur S3 en utilisant des URL pré-signées. Cela garde les téléchargements volumineux hors de Lambda, en raison de sa limite de requête de 10 Mo.
Le flux est simple. Lorsqu'un administrateur veut télécharger une photo de boisson, il demande une URL de téléchargement à l'API. La fonction Lambda génère une URL PUT pré-signée qui est valide pendant 10 minutes, donnant à l'administrateur une autorisation temporaire et limitée pour télécharger directement sur S3. Le navigateur de l'administrateur télécharge ensuite l'image directement sur S3 sans toucher Lambda du tout. Une fois téléchargée, CloudFront sert l'image aux invités parcourant le menu.
Le CloudFormation du bucket S3.
Et le Lambda qui génère les URL pré-signées.
Génération d'invites d'images pilotée par les événements
C'est là que cela devient intéressant, et la partie et les solutions que je construis et sur lesquelles j'écris normalement. Lorsque j'ajoute une nouvelle boisson, je veux qu'une invite d'image générée par IA soit prête pour lorsque je créerai finalement la photo de la boisson. Au lieu d'écrire manuellement des invites, je laisse Amazon Bedrock les générer en fonction du nom et des ingrédients de la boisson.
La clé est le découplage à son meilleur. J'aurais pu appeler Bedrock directement depuis le Lambda createDrink, mais cela ajouterait une latence à la réponse de l'API car l'administrateur doit attendre la génération de l'IA. EventBridge résout cela avec élégance. L'API renvoie immédiatement après l'enregistrement dans la base de données, gardant les temps de réponse rapides. Le bus d'événements gère la génération d'invites de manière asynchrone, et si Bedrock est lent ou échoue temporairement, EventBridge réessaie automatiquement. Ce découplage lâche signifie également que je peux facilement étendre le système plus tard en ajoutant plus d'écouteurs d'événements pour les notifications ou les analyses sans toucher du tout le code de l'API.

Pourquoi un bus d'événements personnalisé ?
J'utilise un bus d'événements dédié au lieu du bus AWS par défaut.
Pourquoi ne pas utiliser simplement le bus par défaut ? Trois raisons.
- Isolation - Les événements de mon application ne se mélangent pas avec les événements des services AWS (CloudTrail, etc.)
- Autorisations - Je peux accorder à des rôles IAM spécifiques l'accès à ce bus uniquement
- Filtrage - Les règles ne voient que les événements de mon application, rendant les modèles plus simples
Pour un petit projet, cela peut sembler exagéré, mais c'est une bonne habitude. Lorsque vous avez plusieurs applications publiant des événements, un bus dédié par domaine garde les choses propres.
Le flux d'événements

La fonction Lambda createDrink publie un événement après l'enregistrement dans la base de données.
L'événement est du type tirer et oublier. S'il échoue, nous le consignons mais ne faisons pas échouer l'appel API. La boisson est déjà sauvegardée.
Génération de l'invite avec Bedrock
C'est là que l'ingénierie des invites devient critique. J'ai d'abord essayé une invite système générique comme "Créez une invite d'image pour une photographie de cocktail" et l'ai laissée libre. Les résultats étaient fonctionnels mais sans inspiration - des descriptions génériques de boissons dans des verres génériques sous un éclairage générique. Pas terrible, mais pas aligné avec ma vision du minimalisme nordique.
J'ai choisi Amazon Bedrock Nova Pro pour cette tâche car il équilibre le coût et la qualité. Nova Pro est moins cher que Claude tout en produisant des sorties cohérentes et détaillées. Le modèle n'hallucine pas de manière excessive, et pour les invites d'images, je n'ai pas besoin d'un raisonnement de pointe, j'ai besoin de sorties cohérentes et prévisibles qu'un modèle de génération d'images peut utiliser.
Le vrai travail est dans les invites. L'invite système établit la personnalité et la perspective du photographe. L'invite utilisateur fournit des contraintes spécifiques et des conseils esthétiques. Voici l'évolution :
Première tentative (trop générique) :
System: "Vous êtes un photographe. Générez une invite d'image pour un cocktail."
User: "Cocktail : Negroni. Ingrédients : Campari, gin, vermouth."
Résultat : "Un Negroni dans un verre en cristal, garni d'une twist d'orange, photographié dans un studio avec un éclairage professionnel."
Cela fonctionne, mais c'est générique - pourrait décrire n'importe quelle photo de cocktail.
Deuxième tentative (plus spécifique) :
System: "Vous êtes un photographe spécialisé dans la photographie de boissons. Créez une invite détaillée et photoréaliste pour une photographie de cocktail."
User: "Cocktail : Negroni. Générez une invite en mettant l'accent sur l'esthétique minimaliste nordique, un éclairage de studio professionnel."
Résultat : "Un Negroni dans un verre à glaçons, garni d'une twist d'orange, sur un fond gris doux. Éclairage de studio propre mettant en valeur la couleur rouge foncé et la clarté de la boisson. Composition minimaliste. Photographie professionnelle."
Mieux ! Plus spécifique, mais l'esthétique nordique n'est toujours pas assez claire.
Version finale (ce que j'utilise réellement) :
La température de 0,7 trouve un équilibre. Plus élevé (plus proche de 1,0) rendrait chaque invite plus aléatoire et créative, mais moins cohérente entre les boissons. Plus bas (plus proche de 0) serait répétitif. À 0,7, chaque invite est unique mais suffisamment prévisible pour qu'un modèle de génération d'images obtienne l'esthétique correcte. Le maxTokens: 300 est spécifiquement défini car Nova Canvas, le modèle de génération d'images que j'ai testé pendant le projet, a une limite de 300 jetons pour les invites. Cela force la génération de texte à rester concise tout en étant descriptive.
Cette expansion de l'invite système - expliquant que je veux spécifiquement le minimalisme nordique, décrivant ce que cela signifie (ombres douces, arrière-plans neutres, garniture minimale) - a été la percée clé. La première fois que j'ai généré des invites avec cette version, les résultats étaient enfin alignés avec ma vision, une photographie de cocktail propre, minimale et premium.
L'invite est sauvegardée sur S3 pour une utilisation ultérieure lorsque je génère réellement l'image (peut-être avec Nova Canvas dans une future version).
Extensibilité
Ce modèle se développe bien. Lorsque j'ajoute la génération d'images, c'est juste une autre règle écoutant le même événement. Aucun changement à l'API nécessaire.
Ce qui suit
La base est faite. Les invités peuvent parcourir le menu, les administrateurs peuvent gérer les boissons. Mais les invités ne peuvent pas encore passer de commandes, ils ont besoin d'un moyen de s'identifier sans créer de comptes complets.
Dans la partie 2, je construirai le flux de commande complet.
- Inscription des invités avec des codes d'invitation et des JWT auto-signés
- Passation des commandes avec un autorisateur Lambda personnalisé
- Notifications en temps réel utilisant AWS AppSync Events - lorsque je marque une boisson comme prête, les invités la voient instantanément
Restez à l'écoute et suivez-moi sur LinkedIn pour ne rien manquer !
Mots finaux
Cette base n'est peut-être pas la partie la plus éclatante du projet, mais c'est le travail de base qui rend tout le reste possible. Base de données, API, authentification, flux de travail pilotés par les événements. Tout est là, prêt à soutenir la vraie magie.
Parce que ce n'est que le début. L'assistant de cocktail IA est l'objectif, et c'est là que cela devient intéressant. Des recommandations IA basées sur les préférences gustatives, la génération d'images pour les photos de boissons, des mises à jour de commandes en temps réel, peut-être même des commandes conversationnelles. La base est faite. Vient maintenant la partie amusante.
C'est ce que j'aime dans les services sans serveur et gérés comme DSQL, Cognito, EventBridge et Bedrock. Ils gèrent le levage lourd indifférencié pour que vous puissiez avancer rapidement sur la base et passer votre temps sur ce qui rend réellement le projet unique. Les fonctionnalités IA. L'expérience utilisateur. Les choses qui font dire aux gens "c'est cool".
La partie 2 arrive bientôt. Inscription des invités, flux de commande et notifications en temps réel. Restez à l'écoute.
Consultez mes autres articles sur jimmydqv.com et suivez-moi sur X pour plus de contenu sans serveur.
Comme dit Werner, Maintenant, allez construire !
