<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Laís Sampara</title>
    <description>The latest articles on DEV Community by Laís Sampara (@las_sampara_d099b9bc9cfa).</description>
    <link>https://dev.to/las_sampara_d099b9bc9cfa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4018301%2Fb56f4893-2cc4-4a72-b356-9ebb3f8a3dfc.png</url>
      <title>DEV Community: Laís Sampara</title>
      <link>https://dev.to/las_sampara_d099b9bc9cfa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/las_sampara_d099b9bc9cfa"/>
    <language>en</language>
    <item>
      <title>Modelo ou código: quem decide a jogada num time de agentes de IA</title>
      <dc:creator>Laís Sampara</dc:creator>
      <pubDate>Thu, 05 Nov 2026 17:18:48 +0000</pubDate>
      <link>https://dev.to/las_sampara_d099b9bc9cfa/modelo-ou-codigo-quem-decide-a-jogada-num-time-de-agentes-de-ia-4cf7</link>
      <guid>https://dev.to/las_sampara_d099b9bc9cfa/modelo-ou-codigo-quem-decide-a-jogada-num-time-de-agentes-de-ia-4cf7</guid>
      <description>&lt;p&gt;No primeiro amistoso da Agentic Football Cup contra outro time da Dati, o Um Prompt Um Sonho, os nossos agentes de IA perderam de 4 a 0. Naquele jogo, quem decidia cada jogada era o modelo. O problema não era só o tempo que ele levava para responder.&lt;/p&gt;

&lt;p&gt;De 21 a 23 de outubro a Dati fez a Agentic Football Cup, com 14 times da empresa, em cima de um simulador de futebol da AWS. Cada time tinha cinco agentes de IA, um por jogador, rodando no Amazon Bedrock AgentCore, o serviço da AWS que hospeda agentes. A cada tick, uma rodada de 2 segundos, cada agente recebia o estado da partida e devolvia um comando, como correr, passar ou chutar.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsmavvxtb7phj6sr472fu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsmavvxtb7phj6sr472fu.png" alt="Tela inicial do portal da Agentic Football Cup, da AWS" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Eu era a técnica do Claudinho FC e, durante o jogo, só falava com o time por um chat de texto livre, com mensagens que a gente chamava de gritos. Eu não estava sozinha no time, mas toda a parte técnica ficou comigo, com algumas sessões do Claude Code me ajudando. Como nos outros times havia gente com mais experiência que eu, o meu plano tinha duas partes, e a primeira era passar da fase de grupos.&lt;/p&gt;

&lt;h2&gt;
  
  
  A primeira versão: o modelo decidia tudo
&lt;/h2&gt;

&lt;p&gt;Comecei pelo caminho que parecia natural, em que cada jogador tinha um prompt com a sua posição e as suas regras. A cada tick, o agente transformava o estado do jogo em texto, mandava para o Amazon Nova Lite e recebia de volta um comando em JSON. Contra os três times de treino que vinham prontos no simulador, isso até funcionava, mas no primeiro amistoso contra o Um Prompt Um Sonho levamos 4 a 0.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhu72fb9dhgs3v554mzm2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhu72fb9dhgs3v554mzm2.png" alt="Tela de carregamento de um treino do Claudinho FC contra o Total Attack United" width="799" height="181"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;O problema mais visível era o tempo, porque mesmo no melhor caso os nossos agentes levavam uns 600 milissegundos, enquanto o Um Prompt Um Sonho respondia em uns 100. Os números vinham do portal da copa, o site onde cada time cadastrava os agentes, mandava jogar e via o relatório de cada partida. Quase todo esse tempo era a chamada ao modelo. Testei outros modelos da família Nova, mas nenhum era mais rápido que o Lite. Os 600 cabiam no tick de 2 segundos, e o servidor só parava de esperar um agente depois de 5. Ainda assim eu quis chegar perto dos 100 do time que tinha ganhado da gente, sem ter como saber se era isso que decidia o jogo.&lt;/p&gt;

&lt;p&gt;Antes de chegar nesses 600, eu tropecei num problema que tem a ver com onde o agente roda. Cada jogador do Claudinho FC era um runtime no AgentCore, a unidade que recebe as chamadas e roda o código de um agente. Ali, o processo continua vivo de uma chamada para a outra. A biblioteca Strands, que montava a chamada ao modelo, guardava o histórico da conversa dentro desse processo. Como ele não morria entre os ticks, cada chamada acrescentava o estado novo ao histórico. No tick 60, o modelo já recebia 60 estados velhos. Zerar o histórico a cada tick foi um dos meus primeiros ajustes, depois que outro time mediu a resposta caindo de 1,5 para 0,7 segundo só com isso.&lt;/p&gt;

&lt;p&gt;O outro problema era que o modelo não obedecia. Nos testes, o Nova Lite ignorou quatro regras do prompt. Chutou de longe demais, correu atrás da bola de um companheiro, mandou o zagueiro cruzar o meio-campo e pressionou fora da faixa do campo que cada jogador cobria. A resposta foi uma camada de sanidade, um código que confere cada comando do modelo antes de ele ir para o jogo. Se o modelo mandava o zagueiro passar do meio-campo, por exemplo, a sanidade trocava esse comando por outro calculado dentro da faixa dele.&lt;/p&gt;

&lt;p&gt;Com a camada de sanidade, o código já decidia depois do modelo. Faltava fazer o agente responder sem esperar o modelo, e sem tirar o modelo do time.&lt;/p&gt;

&lt;h2&gt;
  
  
  A versão híbrida: o código joga, o modelo ouve
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqvf5bqb5x6e6thvjzc11.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqvf5bqb5x6e6thvjzc11.png" alt="Diagrama do agente na versão híbrida: o código decide a jogada em menos de 2 ms, e o modelo lê o chat a cada tick, em paralelo" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Antes de mexer na arquitetura, tentei deixar o modelo mais rápido. Tirei a biblioteca que montava a chamada e fixei o modelo numa região só, e a chamada caiu de 877 para 641 milissegundos no meu computador. Encurtar o prompt não mudou nada. No portal, o tempo continuava em uns 600 milissegundos, porque a chamada ao modelo dentro da AWS eu não conseguia cortar.&lt;/p&gt;

&lt;p&gt;Então, na versão híbrida, o agente parou de esperar o modelo e quem decide a jogada passou a ser o código. Um arquivo de regras tem os cinco prompts traduzidos regra por regra, com os mesmos números. Se o prompt do meio-campo mandava chutar só de perto do gol, a regra em código usa a mesma distância. Como o código responde em menos de 2 milissegundos, o tempo no portal caiu para uns 100. Daí não dava para baixar, porque o resto era a rede e o próprio AgentCore. A desobediência também acabou, porque uma regra escrita em código é cumprida em todo tick.&lt;/p&gt;

&lt;p&gt;O modelo continuou no time, com a única coisa que o código não sabe fazer, que é ler o que eu escrevo no chat. A cada tick, o agente chama o modelo em paralelo, com o estado do jogo e os gritos, e devolve na hora o comando do código, sem esperar. A resposta do modelo só é usada quando a chamada foi feita com um grito novo no chat. Nesse caso, ela assume o comando no tick seguinte, por dois ticks, uns 4 segundos. Depois o código volta, e a sanidade confere tudo, inclusive a resposta do modelo.&lt;/p&gt;

&lt;p&gt;O chat ficou com o modelo porque é o único lugar onde o time recebe texto livre. Uma regra em código entende uma posição no campo, mas não entende uma frase que eu escrevo na hora. O custo é que o grito só muda o comando um tick depois, uns 2 segundos. Ele serve para uma situação que dura alguns ticks, como mudar a postura do time, mas para um lance chega tarde.&lt;/p&gt;

&lt;p&gt;Esse jeito de chamar o modelo só funciona por causa de onde o agente roda. A chamada era feita num tick, e a resposta só era usada no tick seguinte, 2 segundos depois. Nesse meio-tempo, a chamada precisava continuar viva em algum lugar.&lt;/p&gt;

&lt;p&gt;Cada tick chegava ao runtime como uma chamada separada, e numa função serverless comum, como o Lambda, nada garante que a chamada seguinte caia no mesmo processo. O processo também fica congelado entre uma chamada e outra, então a chamada ao modelo podia ficar parada ou se perder no caminho.&lt;/p&gt;

&lt;p&gt;No AgentCore, quem resolve isso é a sessão, que ganha uma microVM própria, uma máquina virtual pequena e isolada. As chamadas com o mesmo id de sessão vão para a mesma microVM, e o processo Python continua vivo de um tick para o outro. Então a chamada ao modelo disparada num tick ainda está lá, rodando, quando o tick seguinte chega. É a mesma característica que tinha atrapalhado a primeira versão com o histórico do Strands, mas agora era ela que segurava a chamada ao modelo.&lt;/p&gt;

&lt;p&gt;Eu não vi como o servidor da copa escolhia o id de sessão, mas o log mostra que ele reaproveitava o mesmo id. Num dos jogos, as sessões do Claude Code acharam no log a resposta do modelo a um grito sendo usada. Essa resposta só existe se a chamada disparada num tick foi encontrada no tick seguinte.&lt;/p&gt;

&lt;h2&gt;
  
  
  O código também lê errado
&lt;/h2&gt;

&lt;p&gt;O código decide em cima do que lê no estado do jogo, e o pior caso em que ele leu errado foi o tamanho do campo. O código de exemplo da AWS supunha um campo de -55 a 55, mas nas posições da bola gravadas no log ela nunca passou de 6,5 para cada lado. Todo alvo que o time mandava estava fora do campo, e o motor do jogo empurrava o jogador até a grade, onde ele ficava se forçando. Eu vi o erro nos primeiros jogos e corrigi na primeira noite, com uma escala na entrada e na saída de cada agente. O 4 a 0 do primeiro amistoso também tinha sido com o campo errado, então aquele placar sozinho não mostra que o problema era o modelo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fag1b60hkubnunsj2l29o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fag1b60hkubnunsj2l29o.png" alt="Diagrama: o campo que o código de exemplo supunha, de -55 a 55, contra o campo real, de cerca de -6,5 a 6,5" width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Conferindo o log com as sessões do Claude Code, achei mais quatro erros de leitura, como o goleiro que corria para onde a bola estava 2 segundos antes. Cada correção virava um deploy no AgentCore, que levava alguns minutos para subir os cinco runtimes, e isso limitava o que dava para mudar entre dois jogos. A última correção subiu cerca de uma hora antes do primeiro jogo da fase de grupos.&lt;/p&gt;

&lt;p&gt;Para o nosso time, quem decide a jogada é o código, e o modelo fica com o que o código não entende, que é o chat. Eu passei a decisão para o código por causa do tempo, mas não sei quanto o tempo pesou no placar. O que eu sei é que cada erro achado no log virava uma correção que valia em todo tick. Com o modelo, a correção era uma frase a mais no prompt, que o modelo podia ignorar.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fase de grupos
&lt;/h2&gt;

&lt;p&gt;A fase de grupos foi na quinta, dia 22, à tarde, e o time jogou com a versão híbrida e as cinco correções. Como o nosso grupo tinha três times, foram só dois jogos. Com uma derrota de 2 a 1 e uma vitória de 1 a 0, deu para passar.&lt;/p&gt;

&lt;p&gt;Nas quartas de final, o adversário era o Um Prompt Um Sonho, o time de melhor campanha da copa. Ele tinha ganhado os três jogos da fase de grupos, liderava a tabela dos amistosos e já tinha ganhado da gente de 4 a 0 e de 5 a 1. Foi contra eles que a segunda parte do plano, que eu vinha preparando desde o primeiro dia, ia ser testada, e essa é a história do próximo texto.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>agents</category>
      <category>llm</category>
    </item>
    <item>
      <title>O difícil começa quando o tutorial acaba: Rekognition, segurança e sparse GSIs em produção</title>
      <dc:creator>Laís Sampara</dc:creator>
      <pubDate>Mon, 27 Jul 2026 20:13:30 +0000</pubDate>
      <link>https://dev.to/las_sampara_d099b9bc9cfa/o-dificil-comeca-quando-o-tutorial-acaba-rekognition-seguranca-e-sparse-gsis-em-producao-1hkc</link>
      <guid>https://dev.to/las_sampara_d099b9bc9cfa/o-dificil-comeca-quando-o-tutorial-acaba-rekognition-seguranca-e-sparse-gsis-em-producao-1hkc</guid>
      <description>&lt;p&gt;Qualquer tutorial de Rekognition te ensina a indexar faces em 5 minutos. O que ninguém mostra é o que vem depois, quando tem gente de verdade usando o sistema: apagar uma foto sem quebrar o resto, segurar o custo de um endpoint aberto, fazer uma exclusão em massa não travar na metade. Esse é o relato do meu primeiro sistema AWS em produção, com os bugs e as decisões de arquitetura que vieram junto.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Introdução&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Um mês de casa e me passaram um sistema serverless em produção pra manter e melhorar. Não era projeto de estudo: já estava publicado, evento marcado, e se eu errasse qualquer decisão quebrava o que já estava rodando.&lt;/p&gt;

&lt;p&gt;O sistema é uma plataforma de busca de fotos por reconhecimento facial. O admin cria um evento e sobe as fotos, o participante abre a página do evento, tira uma selfie, e o sistema devolve todas as fotos onde aquele rosto aparece. Por baixo, cada imagem vira três coisas: arquivo no S3, registro no DynamoDB, face indexada no Rekognition. O problema é manter esses três sincronizados. Quando um lado sai de sincronia, e sai, você tem exclusão que apaga metade, consulta que não bate, dado que fica órfão. A maioria dos problemas que vou contar aqui nasce disso.&lt;/p&gt;

&lt;p&gt;Quando peguei o projeto a arquitetura já fazia o básico: criar evento, subir imagem, buscar face. Ele tinha rodado num evento em outubro de 2025: funcionou, deu visibilidade, e a visibilidade virou um pedido de  transformar aquilo num produto de verdade. Foi quando me apresentaram ao projeto. O próximo teste real ia ser o aniversário de 15 anos da empresa, em maio, já com as coisas que eu tinha mexido. É aí que começam os problemas que nenhum tutorial prepara.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Como o sistema funciona&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Da esquerda pra direita no diagrama: admin e participantes acessam por dois frontends no Amplify. Requests passam pelo API Gateway, batem na &lt;br&gt;
Lambda principal e disparam o EventBridge. Ali se divide em dois pipelines independentes: o de indexação (SQS → Lambda Face Indexer → Rekognition, &lt;br&gt;
gravando no DynamoDB Metadata) e o de thumbnails (SQS → Lambda Thumb Generator). CloudWatch monitora tudo, e tabelas separadas no DynamoDB guardam configurações e jobs de exclusão.&lt;/p&gt;

&lt;p&gt;São duas páginas na prática: a do admin, onde cria evento, configura e faz upload das fotos, e a do participante, a página pública do evento, onde a pessoa tira selfie ou sobe uma foto, e o Rekognition busca contra as faces já indexadas.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6owgzx4sm66rvumo9hc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6owgzx4sm66rvumo9hc.png" alt="Diagrama de arquitetura" width="512" height="249"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A primeira ilusão: excluir uma imagem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Todas as tarefas foram montadas para que o sistema saísse de uma aplicação usada uma única vez e se tornasse um sistema viável de produção para algum cliente. O primeiro ponto que eu fiquei responsável nesse desenvolvimento foi deixar a parte do administrador de eventos mais completa. A aplicação já fazia uploads de imagens mas não havia como o usuário ver as imagens que havia subido e nem como apagar as imagens, caso ocorresse algum problema nesse processo, o evento inteiro precisava ser excluído.&lt;/p&gt;

&lt;p&gt;Quando se olha a lógica da tarefa de forma geral aparenta ser algo simples, "é só criar uma função para deletar imagens", mas dando uma olhada mais profunda começam a surgir variáveis que interferem na lógica que o sistema tem que seguir para que não aconteça uma falha silenciosa. Sem o entendimento completo da funcionalidade pode se construir um sistema que acumula dados órfãos, e esse acúmulo gera custos desnecessários.&lt;/p&gt;

&lt;p&gt;Considerando a arquitetura do projeto, quando uma imagem é excluída por completo, ela não é só simplesmente apagada no S3. Dentro do bucket existem três pastas com diferentes versões da mesma imagem. No sistema nem sempre é necessário ter a melhor qualidade de imagem, por isso foram criadas as pastas para diminuir esses custos.&lt;/p&gt;

&lt;p&gt;As versões da mesma imagem são divididas em otimizada (usada quando um usuário clica em uma foto na galeria e ela amplia), miniaturas (usada quando um usuário pesquisa seu rosto e aparece a galeria de fotos) e originais (somente na parte de upload e download da imagem, no caso a que o usuário baixa).&lt;/p&gt;

&lt;p&gt;Cada face dentro de uma imagem é considerada uma entidade, que é registrada e guardada dentro de um banco de dados (DynamoDB) para que depois consiga ser identificada na coleção do Rekognition. Se o fluxo não for implementado na ordem correta, ao pesquisar uma face o usuário pode receber uma imagem que não deveria estar na galeria e, ao tentar abrir essa imagem, quebrar o sistema porque a imagem não existe mais.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9junu1979o3b1ph7xk96.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9junu1979o3b1ph7xk96.jpg" alt="Fluxograma do processo de exclusão" width="722" height="1384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;E o processo de exclusão ao ser aplicado em uma escala maior? Excluir um evento já com imagens subidas, por exemplo. Como já vimos, nem tudo que parece ser simples realmente é, e esse foi outro impasse no projeto: o sistema falhava, travava, às vezes dizia que a exclusão havia sido completada mas ao olhar na galeria grande parte das fotos continuavam lá.&lt;/p&gt;

&lt;p&gt;A quantidade de requisições que eram feitas para apagar todas as imagens fazia com que o sistema se sobrecarregasse gerando uma falha interna, chamada de throttling. Cada requisição é equivalente a uma única foto dentro do S3, e considerando que cada imagem era versionada em 3, isso triplica o número de requisições. Outro pilar que também falhava era o Rekognition, que tem como limite 4096 faces por chamada. Considerando todas essas informações, a melhor maneira de resolver esses problemas foi dividir em pedaços as requisições de exclusão, chamado de chunking.&lt;/p&gt;

&lt;p&gt;Uma vez que os limites da aplicação foram descobertos, depois foi questão de testes para saber se o sistema suportava o limite de cada serviço sem falhar, considerando a velocidade do processamento mas também a qualidade. Não adiantava de nada ele ser rápido se metade das fotos ficava falsamente deletada. Usando o limite de cada serviço o sistema teve um desempenho ágil e eficaz, e decidi usar os valores de capacidade máxima.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quando segurança deixa de ser teoria&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Depois que a tarefa da implantação do excluir ficou pronta, a empresa em que eu trabalho usou um bot para fazer uma auditoria do sistema, que revelou alguns pontos de falha na segurança, gerando algumas correções por minha parte.&lt;/p&gt;

&lt;p&gt;A primeira falha era proxy aberto: o endpoint usado nos downloads aceitava qualquer URL de destino, então um atacante poderia redirecionar a requisição para onde quisesse. Esse tipo de falha ficou famoso no EC2, onde a requisição forjada alcança o serviço de metadata e volta com as credenciais IAM da máquina. No Lambda esse caminho não existe, as credenciais não ficam num endpoint que a requisição consiga alcançar. Só que o risco aqui era outro: a função virava um proxy do atacante, que usa o servidor do sistema pra escanear e atacar redes de terceiros escondido atrás do IP da AWS, e cada requisição dessas ainda roda por nossa conta.&lt;/p&gt;

&lt;p&gt;Considerando esse problema havia algumas alternativas para fazer essa correção. Bloquear IPs privados, mas era uma opção vulnerável e difícil de manter 100% atualizada; remover o proxy, mas gera quebra de downloads em massa; e por último a alternativa escolhida, allowlist de prefixo do S3. Além de ser uma alternativa "fácil", com apenas uma linha de código, não possui dependências externas e tem zero ambiguidade por conta do S3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;O ataque que não queria roubar dados&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;O produto tem como pretensão ser público, por conta disso o endpoint ficou aberto. Com a estrutura atual não fazia sentido ter uma parte de login para os participantes. Mas aí que mora o problema: participantes de um evento acessam o site e buscam suas fotos, esse é o objetivo do sistema, mas o Rekognition cobra por chamada, que custa aproximadamente US$ 0,001. Pode parecer um número insignificante, mas o sistema estava totalmente aberto e desprotegido contra qualquer automação. Um atacante poderia facilmente escrever um loop simples com um bot e ficar chamando o endpoint sem parar. O risco não era técnico e sim financeiro.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm52hsz7i7l0uopjth2bo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm52hsz7i7l0uopjth2bo.jpg" alt="Fluxograma do processo de segurança" width="800" height="1174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As opções de solução eram tokens no Redis (créditos temporários utilizados no algoritmo de Token Bucket para controlar o limite de requisições e proteger a API contra sobrecargas), adicionando o Redis como dependência, mas se houvesse algum problema iria bloquear a aplicação para todo mundo; outra opção era o captcha, que elimina totalmente os bots mas, se fosse fazer um antes de cada busca, mata a experiência para o usuário; além disso adicionar uma parte de login também ajudaria, mas isso ia contra o modelo de negócio do produto.&lt;/p&gt;

&lt;p&gt;A solução implementada foi uma camada dupla sem dependência externa: um rate limiter in-memory no backend, com limite por IP, e o throttle do API Gateway segurando o ataque distribuído. Aqui tem um detalhe que eu só entendi mexendo com Lambda. Cada invocação concorrente roda num ambiente de execução isolado, então o contador em memória não é compartilhado entre elas. Na prática o in-memory é o filtro mais barato, pega o atacante que fica batendo em sequência, mas quem aguenta o volume total é o throttle do API Gateway. Quando o limite bate, a API joga um 429 com Retry-After, funcionando como um freio temporário e não como um block definitivo. &lt;/p&gt;

&lt;p&gt;A aplicação tinha o CORS aberto, permitindo que qualquer site pudesse fazer requisições à API a partir do browser do participante. Uma pessoa mal intencionada poderia criar um site malicioso fazendo request para a API em nome do usuário administrador.&lt;/p&gt;

&lt;p&gt;O caminho mais fácil seria deixar o asterisco e confiar só na API key, mas API key vaza: commit errado, log, dev tools. Se o CORS tá aberto, qualquer site consegue usar essa key. Para resolver, configurei o CORS com whitelist usando wildcard como curinga para o subdomínio, validando a origin direto no Lambda antes de devolver os headers, porque o API Gateway não suporta wildcard parcial tipo *.dominio.com nativamente. Com whitelist, um site malicioso não consegue mais usar o browser do participante contra a API, porque o browser barra request de domínio que não estão na lista. Mas CORS é proteção de browser: se a key vazar e alguém chamar a API direto por script, o CORS nem entra na jogada, esse cenário fica por conta do rate limit de antes. Wildcard em vez de lista fixa porque assim novos subdomínios funcionam sem precisar redeployar nada, e só entra quem a gente autoriza.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;O problema que virou um GSI&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Depois de ser usado em produção, recebemos alguns feedbacks em relação a fotos onde não havia faces indexadas, por exemplo fotos do espaço do evento ou de algum prato. Esse tipo de imagem não aparecia para os usuários mesmo com o upload feito pelo administrador, por conta das imagens não terem faces.&lt;/p&gt;

&lt;p&gt;Analisando o sistema, vi duas opções para implantar essa feature. Opção 1: criar uma tabela separada no Dynamo só para as fotos sem face. Mas aí o registro podia se perder da imagem já indexada no Rekognition, e qualquer delete ou reupload precisava acertar duas tabelas na mão, sem transação atômica entre elas.&lt;/p&gt;

&lt;p&gt;A segunda opção, que foi a escolhida: manter tudo na mesma tabela e criar um Sparse GSI. O índice usa event_no_face como partition key e processed_at como sort key. O pulo do gato é que o Lambda só escreve esse event_no_face quando o Rekognition volta com zero faces. O DynamoDB não indexa item que não tem o atributo da chave, então o índice já nasce só com foto sem face. Consulto direto, sem filter expression queimando leitura à toa, sem duplicar nada. Uma tabela, uma verdade só, a manutenção cai pela metade.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Furwrf3bsfolk5eocv26e.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Furwrf3bsfolk5eocv26e.jpg" alt="Fluxograma imagens sem face indexada" width="748" height="1335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;O que eu aprendi&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Se eu tivesse que resumir tudo em uma palavra seria "evolução", pelo impacto duplo. A arquitetura do sistema evoluiu até ficar pronta pra ser lançada como produto de uma empresa. E eu evolui junto: saio com mais conhecimento técnico, mas principalmente com um senso crítico que eu não tinha quando comecei.&lt;/p&gt;

&lt;p&gt;Cada tarefa trouxe desafios muito maiores que só desenvolver. Manter sistema em produção, defender decisões perante o modelo de negócio, recusar feature que não faz sentido pro produto (como o captcha e o login) mesmo sabendo implementar. E prever quebras: a falha silenciosa da exclusão me ensinou que código que "funciona" não é código seguro, é código que ainda não falhou do jeito errado. Esse tipo de coisa tutorial nenhum ensina, só sistema real rodando com dado real.&lt;/p&gt;

&lt;p&gt;Entendi que tirar dúvida nunca é problema, mas saber quais dúvidas te fazem avançar e quais te distraem do foco: isso é objetividade. Usei as ferramentas ao meu redor, mas sempre sendo direcionada. Nenhuma decisão foi aleatória, todas têm um propósito de servir algo maior: transformar esse sistema num produto comercializável.&lt;/p&gt;

&lt;p&gt;O difícil começa quando o tutorial acaba. E o que o difícil me ensinou é que o valor não tá no código que você escreve. Tá na decisão que você toma antes de escrever.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>softwareengineering</category>
      <category>cloud</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
