Construindo um Bartender de IA Serverless - Parte 1: Pare de Ser o Cardápio

2026-01-22
This post cover image
#aws
#cloud
#serverless
#genai
#aurora-dsql

Este arquivo foi traduzido automaticamente por IA, erros podem ocorrer

Meus maiores interesses e hobbies estão relacionados a comida e bebidas, e adoro misturar coquetéis em casa.

Há algo estranhamente satisfatório em misturar aquele Negroni perfeito ou agitar um Whiskey Sour adequado. Mas quando recebo festas, enfrento o mesmo problema todas as vezes, eu me torno o cardápio humano. As pessoas nunca sabem o que querem, mesmo que eu venha equipado com um bar totalmente abastecido, então no final elas tendem a escolher os mesmos coquetéis de sempre.

Nesta véspera de Ano Novo estávamos indo para uma festa de alguns amigos, e basicamente como sempre, eles perguntaram se eu poderia misturar coquetéis durante a noite. Claro que eu poderia fazer isso, mas eu precisava de algum tipo de cardápio de bebidas que as pessoas pudessem escolher, eu não ia levar todo o meu setup comigo, apenas quase...

Então, meu plano inicial era apenas imprimi-lo em papel e as pessoas poderiam escolher a partir disso. Então, o engenheiro em mim acordou e E se eu criar um sistema de pedidos de bebidas serverless?

Este é o primeiro post de uma série de três partes onde construo um assistente de coquetéis de IA serverless. Deixe meus convidados navegarem pelo menu eles mesmos e pedirem o que quiserem diretamente em seus telefones. E no final, também obter recomendações de IA sobre o que pedir, em outras palavras, menos trabalho para mim.

O projeto será compartilhado no serverless-handbook após a publicação da parte 3.

Vamos começar!

O que estamos construindo

Uma aplicação web onde os convidados podem navegar pelo meu menu de bebidas, fazer pedidos e obter recomendações de IA. Pense nisso como um menu digital e assistente de bartender combinados.

Para realizar isso, eu preciso de uma base sólida que inclua um banco de dados para armazenar bebidas e pedidos, APIs REST que permitam que a aplicação leia e escreva dados, autenticação para manter o painel de administração seguro enquanto permite que os convidados naveguem livremente, e upload de imagens para que eu possa adicionar fotos a cada bebida. Tudo serverless, é claro—sem servidores para gerenciar, apenas serviços que escalam com a demanda.

Visão Geral da Arquitetura

Na primeira parte, vamos cobrir a base. Este é o terreno sobre o qual tudo se sustenta, sem uma base adequada será difícil estender com novos recursos e funcionalidades. Vamos criar o frontend de hospedagem, API hospedada pelo Amazon API Gateway, computação com AWS Lambda, Auth com Amazon Cognito, Amazon S3 para imagens, Amazon EventBridge para fluxos de trabalho orientados por eventos como geração de prompts de imagem de IA, e Amazon Aurora DSQL como nosso banco de dados principal.

Imagem da arquitetura de base

Aurora DSQL, o banco de dados relacional serverless

Para o banco de dados, escolhi o Aurora DSQL. É um banco de dados compatível com PostgreSQL serverless. Sem provisionamento, sem pools de conexão para gerenciar, e ele escala automaticamente.

Por que escolhi o DSQL? Quando comecei o projeto, eu estava entre usar DynamoDB ou DSQL. Um aspecto importante era que o banco de dados selecionado tinha que ser serverless e rodar sem VPC, o que elimina o Aurora Serverless. A razão pela qual escolhi o DSQL e um modelo relacional foi que eu precisaria de uma busca e filtragem adequadas, por exemplo, em ingredientes. DynamoDB é um ótimo armazenamento de chave-valor, mas nem todos os modelos de dados se encaixam nisso.

DSQL me dá várias vantagens que o tornam a escolha certa. É serverless, eu pago apenas pelo que uso e ele escala para zero quando ninguém está navegando no menu. Ser compatível com PostgreSQL significa que eu tenho SQL adequado com capacidades de busca e filtragem embutidas, essencial para consultas por ingredientes. Também não há gerenciamento de senha, pois a autenticação IAM mantém as coisas seguras sem rodar segredos. E importante, ele funciona naturalmente com o modelo de conexão efêmero do Lambda. Cada invocação do Lambda pode se autenticar independentemente sem gerenciar pools persistentes.

Configurando o DSQL

Criar um novo cluster DSQL é bastante simples. Eu uso CloudFormation para criar um cluster DSQL e duas funções IAM, uma para ler dados, outra para escrever. Por que duas funções diferentes? Principalmente para separação de tarefas. Neste sistema, 95% do tráfego serão operações de leitura, convidados navegando no menu. Apenas administradores realizam escritas ao gerenciar bebidas. Ao dividir as funções, eu garanto que as funções Lambda que lidam com requisições de convidados só podem ler. Elas fisicamente não podem modificar dados mesmo se eu cometer um erro na função. A função de escrita, por outro lado, é restrita às funções Lambda de administrador por trás do autorizador Cognito. É um pequeno esforço extra que cria uma fronteira significativa.

Funções de banco de dados e permissões

DSQL usa um modelo de permissão de duas camadas. Primeiro, você precisa de uma função IAM que possa se conectar ao cluster. Segundo, você precisa de uma função de banco de dados que controle o que você pode fazer uma vez conectado.

Em minha configuração, é assim que funciona.

  1. A função Lambda assume uma função IAM, como a DatabaseReaderRole de cima, via STS
  2. Usando essas credenciais, a função Lambda chama dsql.generate_db_connect_auth_token(), isso requer permissão dsql:DbConnect
  3. A função Lambda se conecta ao DSQL usando o token de autenticação como senha
  4. O DSQL mapeia a função IAM para uma função de banco de dados que controla o acesso à tabela

Aqui está o código Python que faz isso.

O setup da função de banco de dados parece assim.

A função de escrita segue o mesmo padrão, mas também concede permissões INSERT, UPDATE, DELETE.

Esquema de Banco de Dados

O esquema de banco de dados consiste em várias tabelas e índices, mas a tabela principal é a tabela de bebidas. Eu tive que fazer algumas escolhas de design deliberadas aqui. Mais importante, como eu armazeno ingredientes?

Eu uso JSONB para ingredientes porque cada bebida tem uma lista diferente de ingredientes. Uma Margarita precisa de três ingredientes como tequila, suco de limão e triple sec. Uma bebida Tiki clássica pode precisar de oito ou mais. Com um modelo relacional, eu precisaria de uma tabela separada de ingredientes com um relacionamento muitos-para-um (adicionando complexidade), ou criar um número fixo de colunas de ingredientes (desperdiçando espaço ou limitando a lista). JSONB me dá flexibilidade sem sacrificar a capacidade de busca—PostgreSQL pode consultar dentro de campos JSONB com índices adequados, então se eu quiser mais tarde buscar "todas as bebidas com tequila", ainda é eficiente.

Eu considerei armazenar ingredientes como um campo TEXT simples com valores separados por vírgula, mas isso não escala quando eu quero adicionar mais metadados mais tarde, quantidade, unidade de medida, substitutos. JSONB torna o esquema à prova de futuro sem exigir migrações.

O problema de permissões que me custou horas

Enquanto desenvolvia a solução, eu precisava atualizar tabelas e criar novas tabelas e aqui está algo que me pegou de surpresa com o DSQL. Eu tinha minhas funções de banco de dados configuradas, tudo funcionando perfeitamente. Mas depois que eu adicionei uma nova tabela para registro de usuário, implantei e imediatamente encontrei erros de permissão.

permission denied for table registration_codes

O problema? DSQL não suporta ALTER DEFAULT PRIVILEGES.

Em PostgreSQL regular, você pode executar algo assim.

-- Isso NÃO funciona em DSQL
ALTER DEFAULT PRIVILEGES IN SCHEMA cocktails
GRANT SELECT, INSERT, UPDATE ON TABLES TO lambda_drink_writer;

Isso concederia automaticamente permissões em qualquer tabela futura. Mas DSQL não suporta este comando. Quando você cria uma nova tabela, suas funções de banco de dados existentes têm zero acesso a ela até que você explicitamente conceda permissões.

A solução é simples, mas fácil de esquecer. Toda vez que você adiciona uma tabela, você precisa de uma declaração GRANT correspondente.

Eu resolvi isso mantendo um arquivo de migração dedicado que rastreia todas as permissões de tabela. Quando eu adiciono uma nova tabela, eu atualizo este arquivo com as concessões explícitas. É manual, mas evita as sessões de depuração por que meu Lambda não pode ler esta tabela.

Autenticação e Autorização

Antes de mergulhar na API, vamos olhar e configurar a autenticação. O sistema tem dois tipos de usuários com níveis de acesso muito diferentes. Convidados podem navegar pelas bebidas livremente, eles não precisam provar quem são até que queiram fazer um pedido. Administradores, por outro lado, precisam de autenticação completa para gerenciar o menu, ver todos os pedidos e atualizar o status do pedido.

Esta divisão significa que eu preciso tanto de autenticação, verificando quem você afirma ser, quanto de autorização, decidindo o que você tem permissão para fazer. Administradores passam por um fluxo Cognito completo com nome de usuário e senha. Convidados recebem um fluxo de registro leve baseado em convite que detalharei na Parte 2.

Então Administradores precisam fazer login para realizar tarefas de administrador, como criar novos ou atualizar bebidas existentes.

Imagem da arquitetura de administrador

Pool de Usuários Cognito para Administradores

O Amazon Cognito User Pool conterá e lidará com a autenticação de administradores. Ele fornece um pool de usuários hospedado, tokens JWT e gerenciamento de grupos sem que eu construa nada disso.

Login Hospedado Cognito

Em vez de construir meus próprios formulários de login, eu uso a UI hospedada do Cognito. Ela lida com todo o fluxo de login incluindo nome de usuário/senha, mensagens de erro e redefinição de senha, tudo com uma interface limpa e personalizável.

O ManagedLoginVersion: 2 te dá a experiência de login mais nova e mais personalizável. O recurso ManagedLoginBranding permite que você estilize a página de login. Estou usando os padrões do Cognito aqui, mas você pode personalizar cores, logotipos e CSS.

A URL da UI hospedada segue este padrão.

https://<prefixo-de-domínio>.auth.<região>.amazoncognito.com/login
  ?client_id=<client-id>
  &response_type=code
  &scope=email+openid+profile
  &redirect_uri=https://<seu-domínio>/admin/callback

O fluxo de login funciona assim.

  1. Administrador clica em "Login" no painel de administrador
  2. Frontend redireciona para a UI hospedada do Cognito
  3. Administrador insere credenciais
  4. Cognito valida e redireciona de volta com um código de autorização
  5. Frontend troca código por tokens JWT (acesso, ID, refresh)
  6. Token de acesso é armazenado e enviado com requisições de API

Quando um administrador faz login, o Cognito retorna um token de acesso JWT. Este token contém reivindicações incluindo cognito:groups que lista as associações de grupo do usuário. Isso será usado mais tarde ao fazer uma autorização do administrador.

Para um mergulho mais profundo em padrões de autenticação e autorização com Cognito, confira meu post PEP e PDP para Autorização Segura com Cognito.

API REST com API Gateway

Configurar e criar a API REST não é nada especial, vamos usar o Amazon API Gateway, e eu escolho a versão REST (v1) para o trabalho. Há algumas razões para isso, mas nesta parte vou cobrir uma delas. Eu preciso de suporte para API Keys e throttling e até agora a versão REST é a única que suporta isso. A API segue uma separação clara com endpoints públicos para convidados navegando pelas bebidas e fazendo pedidos, endpoints de administrador para gerenciar o menu. Cada um tem diferentes requisitos de autenticação e throttling.

Endpoints públicos (API key apenas):
  GET  /drinks          - Listar todas as bebidas
  GET  /drinks/{id}     - Obter detalhes da bebida
  GET  /sections        - Listar seções do menu
  POST /orders          - Fazer um pedido
  GET  /orders/{id}     - Verificar status do pedido

Endpoints de administrador (API key + JWT):
  POST /admin/drinks       - Criar bebida
  PUT  /admin/drinks/{id}  - Atualizar bebida
  DELETE /admin/drinks/{id} - Deletar bebida
  GET  /admin/orders       - Ver todos os pedidos
  PUT  /admin/orders/{id}  - Atualizar status do pedido

Autorizador Lambda para AuthN + AuthZ

API Gateway usa um autorizador Lambda para validar requisições. É aqui que tanto a autenticação quanto a autorização acontecem.

O JWT valida as chaves públicas do Cognito (JWKS) e verifica.

  • Assinatura - Token não foi adulterado
  • Expiração - Token não expirou
  • Emissor - Token veio do meu pool de usuários Cognito

Se todas as verificações passarem, o autorizador olha para a reivindicação cognito:groups. Endpoints de administrador requerem que o usuário esteja no grupo admin, a verificação de autorização.

Por que um Autorizador Lambda?

API Gateway tem autorizadores Cognito embutidos, mas eles só fazem autenticação. Eles verificam se o token é válido, mas não podem verificar a associação de grupo. O autorizador Lambda me permite fazer ambos em um passo.

O autorizador também passa o contexto do usuário para funções Lambda downstream.

Este contexto está disponível em event["requestContext"]["authorizer"] então as funções Lambda sabem quem fez a requisição sem analisar o JWT novamente.

Por que API Keys para Endpoints Públicos?

Mesmo endpoints públicos requerem uma API key. Isso não é para autenticação, é para proteção e controle. API keys se linkam a planos de uso com limites de taxa, dando-me controle de throttling detalhado. Métricas do CloudWatch mostram uso por chave, fornecendo visibilidade de quem está chamando a API e com que frequência. E se algo der errado, eu posso desativar uma chave instantaneamente como um kill switch.

A API key está embutida no frontend, então não é secreta. Mas ela me dá controle sobre quem pode chamar a API e com que frequência.

Planos de Uso e Throttling

Diferentes endpoints têm diferentes limites de taxa baseados no uso esperado.

Browsing drinks can happen 100 times per second. Placing orders is limited to 50/second since it involves database writes and event publishing, so I want to be more conservative.

For a house party this is massive overkill. But it's a good habit, and if this ever became a real product, the foundation is there.

Caching do API Gateway

O menu de bebidas raramente muda, mas os convidados o navegam constantemente. Sem caching, cada carregamento de página atinge Lambda e DSQL. Com 20 convidados atualizando o menu a cada poucos segundos, isso é carga e custo desnecessários.

API Gateway tem caching embutido que é surpreendentemente eficaz para este cenário.

Agora GET /drinks retorna respostas em cache por 1 hora. O menu não muda com tanta frequência, então este é um trade-off razoável. Mas aqui está o problema, o que acontece quando eu adiciono uma nova bebida? Os convidados estariam navegando em um menu desatualizado até que o cache expire naturalmente.

A solução é invalidação agressiva. O Lambda createDrink esvazia todo o cache da API após salvar no banco de dados.

Este padrão, cache agressivo para leituras, invalidação imediata em escritas, te dá o melhor dos dois mundos: carregamentos rápidos de página para navegação, e atualizações instantâneas quando o menu muda. Note que esta abordagem funciona bem para o endpoint de menu porque as mudanças de dados são infrequentes e controladas. Se eu estivesse cacheando o status do pedido ou dados em tempo real, eu usaria um TTL mais curto ou pularia o caching inteiramente. A chave é combinar a estratégia de cache com a frequência com que seus dados realmente mudam.

Upload de Imagem com S3

Fotos de bebidas vão para o S3 usando URLs pré-assinadas. Isso mantém uploads grandes fora do Lambda, devido ao fato de que ele tem um limite de requisição de 10MB.

O fluxo é simples. Quando um administrador quer fazer upload de uma foto de bebida, eles requisitam uma URL de upload da API. A função Lambda gera uma URL PUT pré-assinada que é válida por 10 minutos, dando ao administrador uma permissão temporária e limitada para fazer upload diretamente para o S3. O navegador do administrador então faz upload da imagem diretamente para o S3 sem tocar no Lambda de todo. Uma vez feito o upload, o CloudFront serve a imagem para os convidados navegando no menu.

O CloudFormation do bucket S3.

E a Lambda que gera URLs pré-assinadas.

Geração de Prompt de Imagem Orientada por Eventos

Aqui é onde fica interessante, e a parte e as soluções que eu normalmente construo e escrevo sobre. Quando eu adiciono uma nova bebida, eu quero um prompt de imagem gerado por IA pronto para quando eu eventualmente criar a foto da bebida. Em vez de escrever prompts manualmente, eu deixo o Amazon Bedrock gerá-los baseado no nome e ingredientes da bebida.

A chave é o desacoplamento no seu melhor. Eu poderia ter chamado o Bedrock diretamente da Lambda createDrink, mas isso adicionaria latência à resposta da API já que o administrador teria que esperar pela geração da IA. EventBridge resolve isso elegantemente. A API retorna imediatamente após o salvamento no banco de dados, mantendo os tempos de resposta rápidos. O barramento de eventos lida com a geração do prompt assincronamente, e se o Bedrock for lento ou falhar temporariamente, o EventBridge automaticamente retentar. Este acoplamento solto também significa que eu posso facilmente estender o sistema mais tarde adicionando mais ouvintes de eventos para notificações ou análises sem tocar no código da API de todo.

Imagem da arquitetura de administrador

Por que um Barramento de Eventos Personalizado?

Eu uso um barramento de eventos dedicado em vez do barramento AWS padrão.

Por que não usar apenas o barramento padrão? Três razões.

1. **Isolamento** - Meus eventos de aplicação não se misturam com eventos de serviço AWS (CloudTrail, etc.)
2. **Permissões** - Eu posso conceder a funções IAM específicas acesso apenas a este barramento
3. **Filtragem** - Regras só veem eventos da minha aplicação, tornando padrões mais simples

Para um projeto pequeno isso pode parecer exagero, mas é um bom hábito. Quando você tem múltiplas aplicações publicando eventos, um barramento dedicado por domínio mantém as coisas limpas.

O Fluxo de Eventos

Imagem da arquitetura de administrador

A função Lambda createDrink publica um evento após salvar no banco de dados.

O evento é fire-and-forget. Se falhar, nós o registramos mas não falhamos a chamada da API. A bebida já está salva.

Gerando o Prompt com Bedrock

Aqui é onde a engenharia de prompts se torna crítica. Eu inicialmente tentei um sistema de prompt genérico como "Crie um prompt de imagem para uma fotografia de coquetel" e o deixei solto. Os resultados foram funcionais mas sem inspiração—descrições genéricas de bebidas em copos genéricos sob iluminação genérica. Não terrível, mas não alinhado com minha visão de minimalismo nórdico.

Eu escolhi o Amazon Bedrock Nova Pro para esta tarefa porque ele equilibra custo e qualidade. Nova Pro é mais barato que Claude enquanto ainda produz saídas coerentes e detalhadas. O modelo não alucina loucamente, e para prompts de imagem, eu não preciso de raciocínio de ponta, eu preciso de saídas consistentes e previsíveis que um modelo de geração de imagem possa trabalhar.

O trabalho real está nos prompts. O prompt do sistema estabelece a persona e perspectiva do fotógrafo. O prompt do usuário fornece restrições específicas e orientação estética. Aqui está a evolução:

Primeira tentativa (muito genérica):

System: "Você é um fotógrafo. Gere um prompt de imagem para um coquetel."
User: "Coquetel: Negroni. Ingredientes: Campari, gin, vermouth."

Resultado: "Um coquetel Negroni em um copo de cristal, guarnecido com uma torção de laranja, fotografado em um estúdio com iluminação profissional."

Isso funciona, mas é genérico—poderia descrever qualquer foto de coquetel.

Segunda tentativa (mais específica):

System: "Você é um fotógrafo especializado em fotografia de bebidas. Crie um prompt detalhado e fotorrealista para uma fotografia de coquetel."
User: "Coquetel: Negroni. Gere um prompt enfatizando estética minimalista nórdica, iluminação profissional de estúdio."

Resultado: "Um Negroni em um copo de pedras, minimamente guarnecido com uma torção de laranja, contra um fundo cinza suave. Iluminação de estúdio limpa enfatizando a cor vermelha profunda e clareza da bebida. Composição minimalista. Fotografia profissional."

Melhor! Mais específico, mas a estética nórdica ainda não está clara o suficiente.

Versão final (o que eu realmente uso):

A temperatura de 0.7 atinge um equilíbrio. Mais alta (mais próxima de 1.0) tornaria cada prompt mais aleatório e criativo, mas menos consistente entre bebidas. Mais baixa (mais próxima de 0) seria repetitiva. Em 0.7, cada prompt é único mas previsível o suficiente que um modelo de geração de imagem entende a estética correta. O maxTokens: 300 é definido especificamente porque o Nova Canvas, o modelo de geração de imagem que eu testei durante o projeto, tem um limite de 300 tokens para prompts. Isso força a geração de texto a permanecer concisa enquanto ainda é descritiva.

Esta expansão do prompt do sistema—explicando que eu quero especificamente minimalismo nórdico, descrevendo o que isso significa (sombras suaves, fundos neutros, guarnição mínima), foi o avanço chave. A primeira vez que eu gerei prompts com esta versão, os resultados finalmente estavam alinhados com minha visão, fotografia de coquetel limpa, minimalista e premium.

O prompt é salvo no S3 para uso posterior quando eu realmente gero a imagem (talvez com Nova Canvas em uma versão futura).

Extensibilidade

Este padrão escala bem. Quando eu adiciono geração de imagem, é apenas outra regra ouvindo o mesmo evento. Nenhuma mudança na API necessária.

O que Vem a Seguir

A base está pronta. Convidados podem navegar pelo menu, administradores podem gerenciar bebidas. Mas os convidados ainda não podem realmente fazer pedidos, eles precisam de uma maneira de se identificar sem criar contas completas.

Na Parte 2, vou construir o fluxo completo de pedidos.

  • Registro de convidado com códigos de convite e JWTs auto-assinados
  • Colocação de pedido com um autorizador Lambda personalizado
  • Notificações em tempo real usando AWS AppSync Events - quando eu marco uma bebida como pronta, os convidados veem instantaneamente

Fique atento e siga-me no LinkedIn para não perder!

Palavras Finais

Esta base pode não ser a parte mais chamativa do projeto, mas é o trabalho de base que torna tudo o resto possível. Banco de dados, APIs, autenticação, fluxos de trabalho orientados por eventos. Está tudo aqui, pronto para suportar a verdadeira magia.

Porque isso é apenas o começo. O assistente de coquetéis de IA é o objetivo, e é aí que fica interessante. Recomendações de IA baseadas em preferências de gosto, geração de imagem para fotos de bebidas, atualizações de pedidos em tempo real, talvez até pedidos conversacionais. A base está pronta. Agora vem a parte divertida.

Isso é o que eu amo sobre serverless e serviços gerenciados como DSQL, Cognito, EventBridge e Bedrock. Eles lidam com o trabalho pesado não diferenciado para que você possa se mover rápido na base e gastar seu tempo no que realmente torna o projeto único. Os recursos de IA. A experiência do usuário. As coisas que fazem as pessoas dizerem "isso é legal".

A Parte 2 está chegando. Registro de convidado, fluxo de pedidos e notificações em tempo real. Fique atento.

Confira meus outros posts em jimmydqv.com e siga-me no X para mais conteúdo serverless.

Como Werner diz, Agora Vá Construir!