Lab 28 — Processar o arquivo que acabou de chegar
O problema, e a empresa que o tem
A Cadência — a mesma equipe do L01, do L17 e do L22 — abriu um canal para que os doze fornecedores das trinta lojas mandem planilhas de preço e estoque. O upload já é seguro: usa exatamente o fluxo do L17, com URL pré-assinada e teto de tamanho. O problema começa depois que o arquivo chega.
Hoje, nada acontece quando a planilha cai no bucket. Um analista precisa lembrar de abrir o console do S3, ou esperar um e-mail avulso do fornecedor avisando, para então baixar o arquivo e rodar um script local que atualiza o catálogo linha a linha. Numa semana medida de exemplo, das 34 planilhas recebidas, 6 esperaram mais de 4 horas até alguém notar, e uma passou o fim de semana inteiro parada.
O conserto óbvio — lembrete no calendário, checagem mais frequente — ataca o sintoma. O defeito real é estrutural: não existe gatilho nenhum entre "o arquivo chegou" e "alguém processa o arquivo". O S3 sabe exatamente quando o objeto foi criado; a Cadência nunca perguntou a ele.
O que este laboratório NÃO é
Não é o pipeline que interpreta documento não estruturado — foto, PDF escaneado, layout livre. Aqui a planilha já chega com schema fixo, e é exatamente por isso que regra determinística resolve sem IA nenhuma. O caso do documento não estruturado, com extração e revisão humana, é o L92, e ele evolui deste laboratório.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um comando na seção de implantação, não com a sensação de ter entendido.
- Explicar por que "alguém lembrar de rodar o script" é sintoma de ausência de gatilho, não de falta de disciplina.
- Habilitar a entrega de eventos do S3 ao EventBridge e escrever uma regra filtrada por prefixo.
- Nomear o limite da notificação direta de bucket — um destino por tipo de evento — e explicar por que ele trava evolução.
- Implementar um Lambda validador que decide aceitar ou rejeitar um arquivo antes de processar qualquer linha.
- Tratar a entrega "ao menos uma vez" com escrita condicional no DynamoDB, evitando processar o mesmo arquivo duas vezes.
- Configurar fila e DLQ para o processamento pesado, reaproveitando o padrão do L22.
- Provar, com medição, que um arquivo malformado termina em rejeitado/ com o motivo registrado, não desaparece.
- Diagnosticar um arquivo "preso" distinguindo entre falha de roteamento, falha de validação e falha de processamento.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| S3 Event Notifications → EventBridge | DVA-C02, SAA-C03 | entrega habilitada no bucket, todas as regras vivem no EventBridge | diferença entre habilitar a entrega e configurar destino direto no bucket |
| Padrão de evento (event pattern) filtrado por prefixo | DVA-C02 | a regra decide o que importa sem tocar o bucket de novo | como acrescentar um segundo consumidor sem reconfigurar a origem |
| Limite de destino da notificação direta | DVA-C02, SAA-C03 | um único destino por tipo de evento é o motivo de usar EventBridge | quando a notificação direta ainda é aceitável, e quando ela trava |
| Entrega "ao menos uma vez" e duplicidade | DVA-C02 | escrita condicional no DynamoDB antes de qualquer efeito | que duplicata é o contrato documentado, não uma falha rara |
| Dead-letter queue e redrive policy | DVA-C02, SAA-C03 | reaproveitada do L22, agora para arquivo em vez de pedido | que DLQ sem alarme é tão inútil quanto não ter DLQ |
| Partial batch response (ReportBatchItemFailures) | DVA-C02 | evita reprocessar arquivo que já teve sucesso dentro de um lote | o comportamento padrão sem essa configuração: o lote inteiro volta |
| Risco de loop de execução Lambda ↔ S3 | DVA-C02 | evitado com filtro de prefixo na regra do EventBridge | por que saída do pipeline no mesmo bucket, sem filtro, dispara o próprio gatilho |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve um bucket com notificação configurada para um Lambda e pergunta como adicionar um segundo consumidor sem alterar o produtor. A resposta não é "crie outra notificação no mesmo bucket" — a notificação direta aceita um único destino por tipo de evento. A resposta é EventBridge: habilitar a entrega uma vez e acrescentar regras depois, sem tocar o bucket outra vez.
Requisitos, e como cada um muda o desenho
Requisito que não aparece numa linha de configuração é intenção. A coluna da direita é onde cada um deixou marca.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Zero arquivo esquecido ou processado com atraso de horas | obrigatório | EventBridge habilitado no bucket; nada de checagem manual nem notificação direta |
| Rastreabilidade do estado de cada arquivo | obrigatório | tabela DynamoDB como fonte de verdade — não inferência pelo prefixo do S3 |
| Arquivo malformado não trava nem desaparece | obrigatório | validação estrutural síncrona antes de enfileirar; prefixo rejeitado/{motivo} |
| Não processar o mesmo arquivo duas vezes | obrigatório | escrita condicional por ArquivoId antes de qualquer efeito, tratando o at-least-once |
| Acrescentar consumidor futuro sem reconfigurar o bucket | desejado pela equipe | EventBridge em vez de notificação direta, que só permite um destino por tipo de evento |
| Validação rápida não pode esperar o processamento pesado | obrigatório | dois Lambdas separados por fila, não um handler monolítico |
| Falha persistente vista por humano | requisito operacional | DLQ com alarme de mensagem visível, igual ao L22 |
| Formato aceito hoje | CSV em UTF-8, cabeçalho fixo (sku, preco, estoque) | autoriza validação estrutural simples; XLSX ou layout livre fica fora de escopo |
A hipótese do formato fixo tem prazo de validade
Enquanto os 12 fornecedores mandarem CSV com o mesmo cabeçalho, uma checagem de três colunas resolve a validação estrutural inteira. No dia em que um fornecedor mandar XLSX, ou mudar a ordem das colunas sem avisar, essa hipótese deixa de valer — e o desenho certo nesse momento é versionar o schema por fornecedor, não reescrever a validação a cada exceção nova.
Arquitetura mínima: o processo real de hoje
Este é o processo que a Cadência tem hoje, e ele é legítimo como ponto de partida: processa a planilha de verdade, sem nenhum recurso novo. O laboratório começa por medir o defeito — porque um número torna o atraso discutível, e "às vezes demora" não.
- → POST assinado, mesmo fluxo do L17
- → nenhum evento emitido; o olho humano é o único gatilho
- → checagem manual, sem horário fixo — às vezes só no fim do dia
- → UPDATE linha a linha, rodado à mão a partir do laptop
- → serve o preço antigo até a atualização rodar
- Fora da AWS
- Armazenamento
- Banco de dados
Este é o processo real da Cadência hoje, e ele funciona — a planilha chega, e mais cedo ou mais tarde alguém a processa. O atraso não vem de nenhuma falha técnica: vem da ausência total de um gatilho. Percorra os passos e repare que o defeito não é "o script é ruim" — é que nada dispara o script.
- O upload termina, e nada reage. O fluxo do L17 grava o objeto no bucket com toda a segurança que aquele laboratório construiu — chave escolhida pelo servidor, teto de tamanho, validação de tipo. Nenhuma dessas peças dispara mais nada depois do PUT: o bucket recebeu o arquivo e ficou em silêncio.
- O bucket não fala com ninguém. Sem notificação configurada — nem para o EventBridge, nem direta para um Lambda —, o evento de criação do objeto acontece e morre ali. O S3 sabe exatamente quando o arquivo chegou; simplesmente ninguém perguntou a ele.
- O gatilho é um humano, e humano tem agenda. A única forma de o arquivo ser notado é alguém abrir o console do S3 por iniciativa própria, ou o fornecedor escrever um e-mail avisando — o que nem sempre acontece. Numa semana medida de exemplo, das 34 planilhas recebidas, 6 esperaram mais de 4 horas até alguém notar, e uma chegou numa sexta às 18h e só foi processada na segunda de manhã.
- Quando alguém lembra, o script roda à mão. O analista baixa o arquivo com um comando de linha, roda um script local que lê a planilha e emite `UPDATE` linha a linha no catálogo. Funciona — mas é um processo sem trilha: se o script travar na linha 340, não existe registro de que parou ali, nem de quais linhas já foram aplicadas.
- O preço errado fica no ar enquanto ninguém roda o script. Entre o upload e a execução manual, a loja continua consultando o preço antigo. Não é um erro de sistema — é exatamente o comportamento esperado de um catálogo que não foi atualizado ainda. O custo do atraso aparece como venda com preço errado, não como exceção em log nenhum.
- Por que a Cadência opera assim. Porque funcionou desde o primeiro fornecedor, quando havia uma planilha por semana e dava para checar o e-mail de manhã. O desenho não é ingênuo: é a extrapolação de um caso em que estava certo. Com 12 fornecedores e picos de início de mês, a mesma checagem manual deixou de dar conta.
Os dois defeitos não são o mesmo, e um esconde o outro
O primeiro é atraso: o arquivo espera horas até alguém notar. O segundo, mais caro, é silencioso — um arquivo pode não ser notado NUNCA, se o e-mail avulso do fornecedor se perder ou o analista estiver de férias. Não existe alarme para "arquivo esquecido", porque não existe nenhum sistema observando o bucket.
Arquitetura para produção: o evento dirige o pipeline
Cada peça nova abaixo rastreia a uma linha da tabela de requisitos. Se você não conseguir apontar o requisito, a peça é adorno — e este desenho não tem nenhuma.
- → POST assinado, mesmo fluxo do L17
- → evento Object Created, entregue ao menos uma vez
- → invocação filtrada por prefixo entrada/ e tipo do evento
- → PutItem condicional: attribute_not_exists(ArquivoId)
- → move o objeto para rejeitado/{motivo} quando a estrutura falha
- → SendMessage só se a condição foi nova e a estrutura passou
- → poll em lote, até 10 mensagens
- → UPSERT por SKU, linha a linha
- → move o objeto de entrada/ para processado/ ao concluir
- → grava o veredito final e o total de linhas aplicadas
- → após 5 tentativas, redrive policy
- → ApproximateNumberOfMessagesVisible > 0
- Fora da AWS
- Armazenamento
- Integração de apps
- Compute
- Banco de dados
- Gestão e governança
A mudança estrutural não é "plugar um Lambda no bucket": é trocar quem escuta o evento (o EventBridge, não o bucket) e separar decidir-rápido de processar-pesado em duas funções ligadas por fila. A tabela do DynamoDB é quem sabe o estado real de cada arquivo — o prefixo do S3 é só conveniência visual. Percorra os passos: cada peça nova rastreia a um requisito da seção anterior.
- O bucket fala com o EventBridge, não direto com o Lambda. A notificação direta do S3 aceita um único destino por tipo de evento: se o validador já ocupa o `ObjectCreated`, um segundo consumidor — auditoria, métrica — exige desmontar a configuração do bucket para caber. É a mesma dívida que o fanout do L23/L24 resolveu para SNS/SQS, agora aplicada a S3: o EventBridge aceita quantas regras forem necessárias sem tocar o bucket de novo.
- A regra filtra sem tocar o bucket. O padrão de evento compara `detail.bucket.name` e o prefixo da chave contra `entrada/`. Adicionar um segundo consumidor no futuro é uma regra nova apontando para o mesmo barramento — o bucket nunca é reconfigurado outra vez.
- A entrega repetida é tratada antes de qualquer efeito. S3 e EventBridge entregam "ao menos uma vez", nunca "exatamente uma vez" — e a AWS documenta explicitamente que retries ocasionais produzem evento duplicado para o mesmo objeto. O `PutItem` com `ConditionExpression: attribute_not_exists(ArquivoId)` só tem sucesso na PRIMEIRA vez que aquele arquivo é visto — é a mesma técnica do L22, agora com a chave sendo o arquivo, não o pedido.
- A validação estrutural decide sem processar linha nenhuma. O validador confere cabeçalho, codificação e a presença das colunas esperadas — não abre a planilha linha a linha. Se a estrutura falha, o objeto é movido para `rejeitado/{motivo}` e a tabela grava a razão: o arquivo não desaparece, ele fica visível com um motivo legível.
- O trabalho pesado sai do caminho síncrono. Só depois de a condição ter sido nova E a estrutura ter passado é que a mensagem vai para a fila. Isso mantém o validador rápido — ele nunca fica preso processando milhares de linhas — e é o que evita que um arquivo grande atrase a validação do próximo.
- O processador aplica linha a linha, e sabe quando desistir. O Lambda Processador consome em lote, aplica o `UPSERT` por SKU e, se falhar, devolve só o item com problema (`ReportBatchItemFailures`, o mesmo mecanismo do L22). Depois de 5 tentativas sem sucesso, a mensagem vai para a DLQ, e o alarme avisa — sem isso, um arquivo com erro fica reentregando silenciosamente.
- O estado final mora na tabela; o arquivo movido é conveniência visual. O `UPSERT` no catálogo e a gravação do veredito na tabela acontecem antes de o objeto ser movido a `processado/`. Se alguém for procurar "o que aconteceu com o arquivo X", a resposta certa vem da consulta na tabela — o mesmo princípio que o L17 já tinha estabelecido para os laudos.
O que muda para o fornecedor: nada
A forma de enviar a planilha é idêntica à do L17 — o mesmo POST assinado, a mesma URL. Toda a mudança acontece depois do upload, sem tocar o contrato que o fornecedor já usa.
O caminho de um arquivo, ponta a ponta
Os oito passos abaixo separam duas fases que o processo manual misturava numa só: "perceber que o arquivo chegou" e "processar o arquivo". Nenhuma pessoa participa de nenhuma das duas.
O que o EventBridge entrega ao Validador. Repare que o corpo da planilha nao esta aqui — so metadado. A funcao ainda vai buscar o conteudo no S3 se precisar.
{
"version": "0",
"source": "aws.s3",
"detail-type": "Object Created",
"account": "111122223333",
"region": "sa-east-1",
"time": "2026-08-07T13:04:12Z",
"detail": {
"bucket": { "name": "ffv-lab-planilhas" },
"object": {
"key": "entrada/fornecedor-08/9c2f-precos-agosto.csv",
"size": 184320,
"etag": "d41d8cd98f00b204e9800998ecf8427e",
"sequencer": "0062E99A88DC407460"
},
"request-id": "8N2J5M3R7K1QZP4W",
"requester": "111122223333",
"reason": "PutObject"
}
}Por que o ArquivoId usa o ETag, e não só bucket e chave
Duas ENTREGAS do mesmo evento — retry do S3 ou do EventBridge — trazem o mesmo ETag, e a condição no DynamoDB as reconhece como o mesmo arquivo. Um fornecedor que reenvia um arquivo com o mesmo nome, mas conteúdo diferente, produz um ETag diferente — e é tratado como arquivo novo, que é o comportamento correto: conteúdo novo merece processamento novo.
As decisões, e o que se perde em cada uma
📋 Doze fornecedores da Cadência enviam de 3 a 8 planilhas de preço e estoque por dia, com pico no início do mês, e a equipe de duas pessoas não pode depender de alguém lembrar de checar o bucket para que o catálogo fique atualizado.
O EventBridge resolve o problema declarado — ninguém mais precisa notar o arquivo — sem prender a validação rápida ao tempo de processar milhares de linhas, porque as duas coisas vivem em funções separadas ligadas por fila. A escrita condicional no DynamoDB trata a entrega "ao menos uma vez" como o contrato documentado que ela é, não como falha rara a ignorar. E como o EventBridge aceita múltiplas regras sem reconfigurar o bucket, acrescentar um segundo consumidor — por exemplo, auditoria — no futuro não exige desmontar nada que já funciona.
Alt: Notificação direta do bucket para o Lambda — Funciona igual no dia 1, mas só permite um destino por tipo de evento. No dia em que a Cadência quiser mandar cópia do evento para auditoria, vai precisar desmontar a configuração do bucket e reconciliar com quem já usa aquele destino — a mesma dívida que o L23 resolveu para SNS/SQS, agora em S3.
Alt: Polling periódico do bucket (listar objetos a cada N minutos) — Troca "lembrar" por "agendar", mas não elimina o atraso: com intervalo de 15 minutos, o atraso médio é metade disso e o pior caso é o intervalo inteiro. Em escala, listar o bucket repetidamente também tem custo de requisição que cresce com o número de objetos, mesmo quando nada mudou.
Alt: O fornecedor chama uma API avisando que terminou o envio — Confia que o cliente vai terminar a chamada. Se o navegador fechar ou a requisição de aviso falhar depois do upload já ter sido confirmado pelo S3, o arquivo fica exatamente tão órfão quanto hoje — o mesmo defeito, só transferido para outro ponto de falha que a Cadência não controla.
Alt: Um único Lambda fazendo validação e processamento pesado no mesmo handler — Menos peça para configurar, mas acopla o tempo de resposta ao evento ao tempo de processar milhares de linhas. Um arquivo grande de um fornecedor atrasa a validação do próximo arquivo, de outro fornecedor, que não tem nada a ver com o primeiro.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Roteamento do evento | EventBridge | notificação direta do bucket para o Lambda | múltiplos destinos e filtro por padrão, sem reconfigurar o bucket a cada mudança | mais uma peça (a regra) para entender e depurar |
| Separação leve/pesado | dois Lambdas ligados por fila | um handler único fazendo tudo | validação rápida não fica presa ao tempo de processar milhares de linhas | mais latência de ponta a ponta no caminho feliz — poucos segundos a mais |
| Dedupe de entrega repetida | PutItem condicional no DynamoDB por ArquivoId | confiar que o evento não duplica | at-least-once é o contrato documentado do S3 e do EventBridge, não exceção rara | mais uma tabela e uma chave (bucket + chave + ETag) para manter |
| Onde o malformado vai parar | prefixo rejeitado/{motivo} + registro na tabela | descartar silenciosamente ou deixar em entrada/ | dá ao analista onde olhar sem vasculhar log, e evita reprocessamento em loop | ocupa espaço até uma regra de ciclo de vida limpar |
| Tipo de fila | SQS standard | SQS FIFO | arquivos de fornecedores diferentes são independentes; ordem entre eles não importa | nenhuma garantia de ordem — irrelevante para este requisito |
| Fonte de verdade do estado | DynamoDB | inferir pelo prefixo do objeto no S3 | um move que falha no meio deixa o prefixo mentindo; a tabela não tem esse risco | mais uma escrita por transição de estado |
Construir: o bucket fala com o EventBridge, a regra filtra
A habilitação da entrega ao EventBridge é uma linha. O que exige atenção é a regra: sem o filtro de prefixo, o objeto movido para `processado/` pelo próprio pipeline dispara um `Object Created` novo, e o validador seria invocado sobre a própria saída.
# eventbridge.tf — o bucket fala com o EventBridge, a regra filtra
# Habilita a entrega de TODOS os eventos do bucket ao EventBridge. Diferente da
# notificacao direta, aqui nao se escolhe tipo de evento nem destino — isso fica
# a cargo das regras abaixo, e e por isso que multiplos consumidores cabem sem
# reconfigurar este bloco de novo.
resource "aws_s3_bucket_notification" "planilhas" {
bucket = aws_s3_bucket.planilhas.id
eventbridge = true
}
# A regra e quem decide o que importa: bucket certo, evento de criacao, e
# SOMENTE o prefixo de entrada. Sem o filtro de prefixo, um objeto movido para
# processado/ ou rejeitado/ pelo proprio pipeline gera um NOVO ObjectCreated —
# e o validador dispararia a si mesmo. Este e o loop de execucao que a
# documentacao da AWS avisa para Lambda-com-S3, e a causa e sempre a mesma:
# saida do pipeline caindo dentro do alcance do proprio gatilho.
resource "aws_cloudwatch_event_rule" "planilha_recebida" {
name = "${var.projeto}-planilha-recebida"
event_pattern = jsonencode({
source = ["aws.s3"]
detail-type = ["Object Created"]
detail = {
bucket = { name = [aws_s3_bucket.planilhas.id] }
object = { key = [{ prefix = "entrada/" }] }
}
})
}
resource "aws_cloudwatch_event_target" "invoca_validador" {
rule = aws_cloudwatch_event_rule.planilha_recebida.name
target_id = "validador"
arn = aws_lambda_function.validador.arn
}
resource "aws_lambda_permission" "eventbridge_invoca_validador" {
statement_id = "AllowEventBridgeInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.validador.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.planilha_recebida.arn
}
# A tabela e a fonte de verdade do estado de cada arquivo. O prefixo do S3
# (entrada/processado/rejeitado) e so conveniencia para quem navega pelo
# console — se um move falhar no meio, e esta tabela que continua confiavel.
resource "aws_dynamodb_table" "arquivos" {
name = "${var.projeto}-arquivos"
billing_mode = "PAY_PER_REQUEST"
hash_key = "ArquivoId"
attribute {
name = "ArquivoId"
type = "S"
}
point_in_time_recovery {
enabled = true
}
}
output "tabela_arquivos" {
value = aws_dynamodb_table.arquivos.name
description = "fonte de verdade do estado de cada arquivo recebido"
}
O loop de execução é documentado pela própria AWS, e é real
A documentação de Lambda com S3 avisa explicitamente: se a notificação escreve de volta no MESMO bucket que a dispara, isso pode causar um loop de execução — o Lambda indiretamente dispara a si mesmo. Aqui o risco existe porque o Processador move o objeto de `entrada/` para `processado/` dentro do mesmo bucket; sem o filtro de prefixo na regra, cada arquivo processado geraria uma nova invocação do Validador, em looping — e cada invocação é custo real, não hipotético.
Um único recurso de notificação por bucket, não um por evento
`aws_s3_bucket_notification` com `eventbridge = true` é a única declaração de notificação para o bucket inteiro — diferente da notificação direta, que exige um bloco por combinação de evento e destino. Escrever uma segunda regra do EventBridge depois não toca este recurso de novo: a decisão de quem escuta o quê fica inteira do lado do EventBridge.
Construir: o Validador decide sem processar linha nenhuma
O Validador tem uma única responsabilidade: registrar a intenção de forma idempotente e decidir, pela estrutura, se o arquivo segue para processamento pesado ou para rejeitado. Ele nunca abre a planilha linha a linha.
// Validador/Function.cs — decide rapido, sem processar linha nenhuma
public class Function
{
private readonly IAmazonS3 _s3;
private readonly IAmazonDynamoDB _db;
private readonly IAmazonSQS _sqs;
private const string TabelaArquivos = "ffv-lab-arquivos";
private const string FilaProcessamento = "https://sqs.sa-east-1.amazonaws.com/.../ffv-lab-planilhas-processamento";
public async Task FunctionHandler(EventBridgeEvent<S3ObjectCreatedDetail> evento, ILambdaContext ctx)
{
var bucket = evento.Detail.Bucket.Name;
var chave = evento.Detail.Object.Key;
// O ETag do proprio evento e parte da identidade: duas ENTREGAS do
// mesmo objeto (retry do S3/EventBridge) tem o mesmo ETag; um objeto
// REENVIADO pelo fornecedor com o mesmo nome, nao — e por isso ele
// conta como arquivo novo, o que e o comportamento correto.
var arquivoId = $"{bucket}/{chave}#{evento.Detail.Object.ETag}";
// Escrita condicional: so tem sucesso na PRIMEIRA vez que este
// ArquivoId e visto. E a mesma tecnica do L22 (PutItem condicional
// por PedidoId), aplicada aqui por ARQUIVO. Entrega "ao menos uma
// vez" e o contrato documentado do S3 e do EventBridge — nao uma
// falha rara a ignorar.
try
{
await _db.PutItemAsync(new PutItemRequest
{
TableName = TabelaArquivos,
Item = new Dictionary<string, AttributeValue>
{
["ArquivoId"] = new() { S = arquivoId },
["Bucket"] = new() { S = bucket },
["Chave"] = new() { S = chave },
["Estado"] = new() { S = "RECEBIDO" },
["RecebidoEm"] = new() { S = DateTimeOffset.UtcNow.ToString("O") },
},
ConditionExpression = "attribute_not_exists(ArquivoId)",
});
}
catch (ConditionalCheckFailedException)
{
// Entrega repetida do MESMO evento. Nao e erro: e o contrato "ao
// menos uma vez" se manifestando. Registrar e sair e a resposta
// correta — reprocessar aqui duplicaria o efeito la na frente.
ctx.Logger.LogInformation($"entrega repetida ignorada: {arquivoId}");
return;
}
var cabecalho = await LerPrimeirasLinhasAsync(bucket, chave);
var (valido, motivo) = ValidarEstrutura(cabecalho);
if (!valido)
{
var chaveRejeitada = chave.Replace("entrada/", $"rejeitado/{Sanear(motivo)}/");
await _s3.CopyObjectAsync(bucket, chave, bucket, chaveRejeitada);
await _s3.DeleteObjectAsync(bucket, chave);
await AtualizarEstadoAsync(arquivoId, "REJEITADO", motivo);
return;
}
await AtualizarEstadoAsync(arquivoId, "VALIDADO", motivo: null);
// So enfileira o trabalho pesado depois de a condicao ter sido nova
// E a estrutura ter passado. O validador nunca fica preso lendo
// milhares de linhas — isso e trabalho do Processador, em outra funcao.
await _sqs.SendMessageAsync(new SendMessageRequest
{
QueueUrl = FilaProcessamento,
MessageBody = JsonSerializer.Serialize(new { arquivoId, bucket, chave }),
});
}
private static (bool valido, string motivo) ValidarEstrutura(string primeirasLinhas)
{
// Confere SO estrutura: cabecalho esperado e codificacao. Nenhuma
// linha de dado e interpretada aqui — isso e responsabilidade do
// Processador, deliberadamente separado.
var colunasEsperadas = new[] { "sku", "preco", "estoque" };
var primeiraLinha = primeirasLinhas.Split('\n').FirstOrDefault() ?? "";
var colunas = primeiraLinha.Trim().ToLowerInvariant().Split(',');
if (!colunasEsperadas.All(colunas.Contains))
return (false, "cabecalho-inesperado");
// Protege quem eventualmente abrir o arquivo rejeitado no Excel: uma
// celula comecando com =, +, - ou @ e interpretada como formula pelo
// Excel, e pode executar comando. Nao processamos o arquivo, mas
// marcamos o risco no motivo para quem for investigar manualmente.
if (primeirasLinhas.Contains(",=") || primeirasLinhas.Contains(",+") || primeirasLinhas.Contains(",@"))
return (false, "possivel-injecao-de-formula");
return (true, motivo: null);
}
}
Por que a validação de injeção de fórmula está aqui, e não é o foco do módulo
Uma célula CSV começando com `=`, `+`, `-` ou `@` é interpretada como fórmula pelo Excel — inclusive fórmulas que executam comando. O pipeline nunca abre o arquivo em planilha eletrônica nenhuma, mas o analista que for investigar um arquivo rejeitado manualmente pode abrir. Marcar esse padrão no motivo de rejeição é barato e evita que o próximo passo humano vire o ponto fraco que o pipeline automatizado evitou.
Construir: a fila, a DLQ e a tabela de estado
A fila e a DLQ seguem exatamente o padrão do L22 — a diferença é o que elas carregam: não é mais um pedido, é a referência a um arquivo inteiro no S3.
# fila.tf — fila e DLQ do processamento pesado, o mesmo padrao do L22
resource "aws_sqs_queue" "planilhas_dlq" {
name = "${var.projeto}-planilhas-dlq"
message_retention_seconds = 1209600 # 14 dias — teto do servico, para dar tempo de investigar
sqs_managed_sse_enabled = true
}
resource "aws_sqs_queue" "planilhas_processamento" {
name = "${var.projeto}-planilhas-processamento"
# 6x o timeout da funcao processadora (60 s) — cobre reentrega por
# throttling sem multiplicar demais o atraso caso a mensagem precise
# mesmo ser reentregue.
visibility_timeout_seconds = 360
message_retention_seconds = 345600 # 4 dias cobrem qualquer pico de inicio de mes
sqs_managed_sse_enabled = true
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.planilhas_dlq.arn
# 5, como no L22: da margem a falha transitoria (ex.: pool do Postgres
# momentaneamente cheio) sem deixar mensagem ruim reentregando para sempre.
maxReceiveCount = 5
})
}
resource "aws_sqs_queue_redrive_allow_policy" "dlq_allow" {
queue_url = aws_sqs_queue.planilhas_dlq.id
redrive_allow_policy = jsonencode({
redrivePermission = "byQueue"
sourceQueueArns = [aws_sqs_queue.planilhas_processamento.arn]
})
}
resource "aws_cloudwatch_metric_alarm" "dlq_com_mensagem" {
alarm_name = "${var.projeto}-planilhas-dlq-nao-vazia"
namespace = "AWS/SQS"
metric_name = "ApproximateNumberOfMessagesVisible"
statistic = "Maximum"
period = 300
evaluation_periods = 1
threshold = 0
comparison_operator = "GreaterThanThreshold"
treat_missing_data = "notBreaching"
dimensions = {
QueueName = aws_sqs_queue.planilhas_dlq.name
}
alarm_actions = [aws_sns_topic.alertas.arn]
}
resource "aws_lambda_event_source_mapping" "processador_consome_fila" {
event_source_arn = aws_sqs_queue.planilhas_processamento.arn
function_name = aws_lambda_function.processador.arn
batch_size = 10
# Sem isto, UM item com erro no lote de 10 devolve os 10 para a fila —
# inclusive os 9 que ja tiveram efeito aplicado. O mesmo mecanismo do L22.
function_response_types = ["ReportBatchItemFailures"]
}
# O validador so pode gravar mensagem NESTA fila — nao em qualquer fila da
# conta. GetAuthorizationToken-like: sqs:SendMessage aceita recurso
# especifico, entao aqui o ARN e restrito, sem "*" nenhum.
data "aws_iam_policy_document" "validador_envia_fila" {
statement {
effect = "Allow"
actions = ["sqs:SendMessage"]
resources = [aws_sqs_queue.planilhas_processamento.arn]
}
}
| Onde | Parâmetro | Valor aqui | Por quê |
|---|---|---|---|
| Fila principal | `visibility_timeout_seconds` | 360 s (6× o timeout da função) | cobre reentrega por throttling sem multiplicar demais o atraso de uma reentrega real |
| Fila principal | `maxReceiveCount` | 5 | falha transitória (pool do Postgres momentaneamente cheio) não deve virar alarme na primeira tentativa |
| Mapeamento de evento | `batch_size` | 10 | processa até 10 arquivos por invocação, sem exigir capacidade provisionada com antecedência |
| Mapeamento de evento | `function_response_types` | `ReportBatchItemFailures` | evita reprocessar arquivo que já teve sucesso dentro do mesmo lote |
Construir: o Processador aplica linha a linha
O Processador só é acionado depois de o Validador ter aprovado a estrutura. Seu trabalho é aplicar cada linha no catálogo, mover o objeto e gravar o veredito final.
// Processador/Function.cs — aplica linha a linha, e sabe quando desistir
public class Function
{
private readonly NpgsqlDataSource _db;
private readonly IAmazonS3 _s3;
private readonly IAmazonDynamoDB _dynamo;
public async Task<SQSBatchResponse> FunctionHandler(SQSEvent evento, ILambdaContext ctx)
{
var falhas = new List<SQSBatchResponse.BatchItemFailure>();
foreach (var msg in evento.Records)
{
try
{
var tarefa = JsonSerializer.Deserialize<TarefaProcessamento>(msg.Body)!;
var linhas = await LerPlanilhaAsync(tarefa.Bucket, tarefa.Chave);
await using var conexao = await _db.OpenConnectionAsync();
await using var transacao = await conexao.BeginTransactionAsync();
// UPSERT por SKU com o valor ABSOLUTO da planilha — nao um
// incremento. E o que torna reaplicar a mesma linha duas
// vezes inofensivo: o resultado final e o mesmo preco e o
// mesmo estoque, nao um estoque somado duas vezes.
var comando = new NpgsqlCommand(
"INSERT INTO catalogo (sku, preco_centavos, estoque, atualizado_em) " +
"VALUES (@sku, @preco, @estoque, now()) " +
"ON CONFLICT (sku) DO UPDATE SET " +
" preco_centavos = EXCLUDED.preco_centavos, " +
" estoque = EXCLUDED.estoque, " +
" atualizado_em = now()",
conexao, transacao);
int aplicadas = 0;
foreach (var linha in linhas)
{
comando.Parameters.Clear();
comando.Parameters.AddWithValue("sku", linha.Sku);
comando.Parameters.AddWithValue("preco", linha.PrecoCentavos);
comando.Parameters.AddWithValue("estoque", linha.Estoque);
await comando.ExecuteNonQueryAsync();
aplicadas++;
}
await transacao.CommitAsync();
var chaveFinal = tarefa.Chave.Replace("entrada/", "processado/");
await _s3.CopyObjectAsync(tarefa.Bucket, tarefa.Chave, tarefa.Bucket, chaveFinal);
await _s3.DeleteObjectAsync(tarefa.Bucket, tarefa.Chave);
await AtualizarEstadoAsync(tarefa.ArquivoId, "PROCESSADO", linhasAplicadas: aplicadas);
}
catch (Exception ex)
{
ctx.Logger.LogError($"falha ao processar {msg.MessageId}: {ex.Message}");
// Devolve SO este item — os demais mensagens do lote, se
// ja tiverem tido sucesso, nao voltam para a fila.
falhas.Add(new SQSBatchResponse.BatchItemFailure { ItemIdentifier = msg.MessageId });
}
}
return new SQSBatchResponse { BatchItemFailures = falhas };
}
}
O UPSERT usa o valor absoluto da planilha, nunca um incremento
Se o processamento fosse `estoque = estoque + linha.Quantidade`, reaplicar a mesma linha duas vezes — por exemplo, numa reentrega da fila — dobraria o estoque. Como o UPSERT grava o valor absoluto que veio na planilha, reaplicar a mesma linha é inofensivo: o resultado final é o mesmo, não a soma de duas aplicações. É uma segunda camada de proteção, além da escrita condicional do Validador — nem toda duplicata possível é pega no mesmo lugar.
Implantar, e provar que o arquivo não depende de ninguém lembrar
Cinco provas. Nenhuma delas aceita "o catálogo parece atualizado" como resultado — cada uma tem um número ou um estado esperado.
# ── Prova 1: do upload ao veredito, sem toque humano ────────────────────────
aws s3 cp planilha-valida.csv \
"s3://ffv-lab-planilhas/entrada/fornecedor-08/$(uuidgen)-precos.csv"
INICIO=$(date +%s)
watch -n2 "aws dynamodb scan --table-name ffv-lab-arquivos \
--query 'Items[?Estado.S==\`PROCESSADO\`]' --output json | jq length"
# Esperado: o estado chega a PROCESSADO sem nenhum comando manual entre o cp e o scan.
# Medicao de exemplo, arquivo de 800 linhas: 8 a 14 s do upload ao veredito final,
# dominados pelo tempo de leitura e UPSERT das linhas, nao pelo roteamento do evento.
# ── Prova 2: arquivo malformado nao desaparece — vai para rejeitado/ com motivo ──
echo "coluna1,coluna2" > planilha-invalida.csv
aws s3 cp planilha-invalida.csv "s3://ffv-lab-planilhas/entrada/fornecedor-08/teste-invalido.csv"
sleep 10
aws s3 ls s3://ffv-lab-planilhas/rejeitado/ --recursive | grep teste-invalido
aws dynamodb get-item --table-name ffv-lab-arquivos \
--key '{"ArquivoId":{"S":"ffv-lab-planilhas/entrada/fornecedor-08/teste-invalido.csv#..."}}' \
--query "Item.Motivo.S"
# Esperado: "cabecalho-inesperado", e ZERO linhas tocadas no catalogo.
# ── Prova 3: entrega duplicada nao processa duas vezes ──────────────────────
# Publica o MESMO evento de amostra duas vezes, simulando o retry documentado.
for i in 1 2; do
aws events put-events --entries file://evento-de-amostra.json
done
aws logs filter-log-events --log-group-name /aws/lambda/ffv-lab-validador \
--filter-pattern "entrega repetida ignorada" --query "length(events)"
# Esperado: 1 — a segunda entrega foi reconhecida e ignorada, nao reprocessada.
# ── Prova 4: falha persistente vai para a DLQ, com alarme ───────────────────
# Force um erro no Processador (ex.: aponte para uma tabela Postgres inexistente)
# e envie um arquivo valido. Depois de 5 tentativas:
aws sqs get-queue-attributes \
--queue-url "$(terraform output -raw fila_dlq_url)" \
--attribute-names ApproximateNumberOfMessagesVisible
# Esperado: 1 mensagem visivel na DLQ, e o alarme ffv-lab-planilhas-dlq-nao-vazia em ALARM.
# ── Prova 5: a notificacao direta NAO aceita dois destinos sobrepostos ──────
# Prova negativa: mostra por que este modulo nao usou notificacao direta.
aws s3api put-bucket-notification-configuration \
--bucket ffv-lab-planilhas \
--notification-configuration file://duas-notificacoes-mesmo-prefixo.json
# Esperado: erro da API — configuracao ambigua para o mesmo tipo de evento e prefixo
# sobreposto. E o limite que a decisao desta secao evita, na pratica, nao so na teoria.| Prova | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|
| 1 · Upload até veredito, sem toque humano | estado chega a PROCESSADO sozinho | se fica em RECEBIDO, a regra do EventBridge não invocou o Validador |
| 2 · Malformado não desaparece | objeto em rejeitado/ com motivo na tabela | se sumiu, a cópia falhou antes do delete — confira a ordem das duas chamadas |
| 3 · Duplicata não reprocessa | log de "entrega repetida" exatamente 1 vez | se processou duas vezes, a condição do PutItem está ausente ou a chave está errada |
| 4 · Falha persistente alcança a DLQ | mensagem na DLQ e alarme em ALARM | se fica reentregando para sempre, confira `maxReceiveCount` e a redrive policy |
| 5 · Notificação direta recusa sobreposição | API devolve erro de configuração ambígua | se aceitou, os prefixos não se sobrepõem de verdade — ajuste o teste |
O que a prova 5 prova, de fato
O erro devolvido pela API é sobre o BUCKET não conseguir decidir, sozinho, para qual dos dois destinos mandar o mesmo evento — não sobre limite de conta nem de permissão. É a prova de que a decisão de usar EventBridge nesta seção não é preferência de estilo: é a resposta a uma restrição que a API do S3 aplica e recusa contornar.
Quebrar de propósito: três falhas e o diagnóstico
As três acontecem de verdade, e nenhuma delas produz uma exceção clara na hora — o sintoma aparece minutos ou horas depois, longe da causa.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| Escrita condicional removida do Validador | tire o `ConditionExpression` do `PutItem` | entrega duplicada do EventBridge processa o mesmo arquivo duas vezes | duas linhas PROCESSADO para o mesmo ArquivoId na tabela | devolver a condição `attribute_not_exists(ArquivoId)` |
| Regra sem filtro de prefixo | remova `detail.object.key` do padrão de evento | o mesmo arquivo entra em RECEBIDO repetidamente, sem fim | contagem de invocações do Validador muito acima do número de uploads | restringir a regra ao prefixo entrada/, como no Terraform desta seção |
| `maxReceiveCount` alto demais (ex.: 50) | suba o valor na redrive policy e envie um arquivo com erro real de parsing | a mensagem reentrega dezenas de vezes antes de qualquer alarme disparar | invocações do Processador muito acima do número de arquivos recebidos | valor entre 3 e 5 — dá margem à falha transitória sem esconder a permanente |
O loop de reprocessamento é o mesmo risco documentado pela AWS para Lambda-com-S3
Sem o filtro de prefixo, mover o arquivo processado para dentro do mesmo bucket que dispara o evento cria exatamente o loop de execução que a documentação da AWS avisa: o Processador termina, move o objeto, o move gera um `Object Created` novo, e o Validador é invocado de novo sobre um arquivo que já está pronto. Cada volta do laço é uma invocação real de duas funções Lambda, cobrando enquanto ninguém percebe.
Uma equipe configura a notificação do bucket S3 diretamente para um Lambda de validação, e meses depois precisa acrescentar um segundo consumidor — um Lambda de auditoria — para o mesmo evento de criação de objeto. Qual é o obstáculo?
Segurança: o que muda quando o pipeline processa sozinho
Automatizar o gatilho também automatiza o efeito de um arquivo malicioso ou malformado — antes, um humano olhava a planilha antes de rodar o script; agora, o primeiro olhar é o do Validador.
| Risco | Probabilidade | Impacto | Controle preventivo | Detecção | Resposta |
|---|---|---|---|---|---|
| Planilha com fórmula que executa comando ao ser aberta no Excel | baixa | médio | pipeline nunca abre em planilha eletrônica; marca o padrão como motivo de rejeição | contagem de arquivos rejeitados por "possivel-injecao-de-formula" | orientar quem for investigar manualmente a nunca abrir o arquivo bruto |
| Fornecedor consegue sobrescrever a planilha de outro | baixa | alto | chave escolhida pelo servidor com o fornecedorId, herdado do L17 | CloudTrail em PutObject fora do padrão de chave esperado | revogar credencial e investigar; a chave por fornecedor já limita o alcance |
| Papel do Processador com permissão ampla no catálogo | média | alto | IAM restrito a INSERT/UPDATE numa tabela específica, sem DROP nem DELETE | IAM Access Analyzer sobre o uso real do papel | derivar a política do uso medido — é o L41 |
| Evento duplicado processado duas vezes | média | médio | escrita condicional no DynamoDB e UPSERT com valor absoluto, não incremento | alarme comparando total de PROCESSADO com total de uploads | investigar a causa da falta de condição; reconciliar o catálogo se necessário |
| Loop de execução consumindo invocação sem parar | baixa se a regra estiver correta | médio | filtro de prefixo entrada/ na regra do EventBridge | gráfico de invocações do Validador muito acima do volume de upload | pausar a regra do EventBridge; corrigir o filtro; reprocessar arquivos represados |
Observabilidade: as perguntas que o painel tem de responder
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| Algum arquivo está parado em RECEBIDO? | consulta na tabela por Estado + tempo desde RecebidoEm | travou entre validação e enfileiramento | > 15 min em RECEBIDO |
| A fila está crescendo mais rápido do que esvazia? | `ApproximateNumberOfMessagesVisible` da fila principal | o Processador não acompanha o volume | tendência de alta sustentada por 10 min |
| Quantos arquivos foram rejeitados hoje, e por quê? | consulta agregada por Motivo na tabela | fornecedor mudou formato sem avisar | qualquer aumento súbito frente à média |
| A regra do EventBridge está invocando o Validador de fato? | Invocations do Validador vs uploads no bucket | regra mal filtrada ou entrega desabilitada | qualquer discrepância |
| A DLQ está vazia? | `ApproximateNumberOfMessagesVisible` da DLQ | falha persistente sem verificação humana | > 0 dispara o alarme |
| Quantas invocações o Validador teve hoje? | Invocations, comparado ao número de uploads | valor muito maior que uploads indica loop de reprocessamento | > 2× o número de uploads do dia |
A consulta que substitui "olhar o bucket"
Com a tabela como fonte de verdade, "quantos arquivos estão pendentes agora" vira uma consulta por `Estado`, não uma listagem do S3. É a diferença entre um painel que responde em milissegundos e um que pagina por milhares de objetos para inferir estado que nunca foi gravado como estado.
Escala: 10, 10 mil, 1 milhão de linhas
| Volume | O que acontece | O que passa a doer | O que fazer |
|---|---|---|---|
| 10 arquivos/dia, ~500 linhas cada | processamento em segundos por arquivo | nada; é o cenário deste laboratório | nada |
| 1 arquivo de 10 mil linhas | o Processador se aproxima do teto de 15 min do Lambda | timeout no meio do processamento, sem transação completa | dividir o arquivo em lotes menores por mensagem, ou mover o processamento pesado para Fargate/Step Functions |
| 1 milhão de linhas/dia, muitos fornecedores simultâneos | muitas invocações concorrentes do Processador | contenção de conexão no Postgres — cada invocação abre a sua | pool de conexão compartilhado (RDS Proxy) ou UPSERT em lote maior por transação |
| Falha de AZ | Lambda, SQS e EventBridge são multi-AZ nativamente | o que pode faltar é o Postgres do L01, se não estiver em Multi-AZ | revisar a configuração de disponibilidade do RDS — é o L01/L02 cobrando de novo |
O teto de 15 minutos do Lambda não é um detalhe distante
Uma planilha de 10 mil linhas com UPSERT linha a linha pode levar minutos, não segundos. O laboratório assume arquivos de até 5 mil linhas porque é o volume medido hoje; se o volume típico crescer, o UPSERT em lote (`unnest` de arrays no Postgres, em vez de um comando por linha) é o próximo ajuste antes de trocar de serviço.
Custo: o que este laboratório acrescenta à fatura
Todo o pipeline novo é cobrado por uso, sem hora ligada parada — o custo cresce com o volume de arquivos e linhas, não com o tempo relógio.
| Cenário | Volume | O que acrescenta | Tendência |
|---|---|---|---|
| Protótipo | 3 arquivos/dia, 500 linhas cada | invocações de Lambda e requisições de SQS/DynamoDB na casa de dezenas por dia | desprezível |
| Produção pequena | 8 arquivos/dia, 2 mil linhas em média | centenas de invocações e UPSERTs por dia; ainda dominado pelo Postgres do L01 | baixa e previsível |
| Alta escala | 50 arquivos/dia, 10 mil linhas em média | invocações concorrentes do Processador e volume de escrita no Postgres crescem juntos | passa a ser linha visível — calcule no AWS Pricing Calculator com o volume real |
| Dimensão | Cobra por | Cuidado |
|---|---|---|
| EventBridge | evento publicado por regra correspondente | regra sem filtro de prefixo multiplica eventos correspondentes por loop, não só por volume real |
| Lambda (Validador e Processador) | invocações e duração × memória | o Processador paga pelo tempo de leitura da planilha inteira, não só pelo UPSERT |
| SQS | requisições de API (SendMessage, ReceiveMessage) | long polling do SDK do Lambda já reduz `ReceiveMessage` vazio |
| DynamoDB (modo sob demanda) | leitura e escrita por requisição | uma escrita por transição de estado; quatro estados por arquivo é o padrão deste desenho |
| S3 | armazenamento e requisições PUT/COPY | o `CopyObject` do move para processado/rejeitado é uma segunda cobrança de armazenamento até o delete completar |
O que sai da fatura em relação ao processo manual
Nada do que este pipeline acrescenta cobra parado. O script manual de hoje já "custava" em forma de tempo de analista — só que esse custo não aparecia em nenhuma fatura da AWS. Trocar horas de atenção humana por invocações medidas em milissegundos é o verdadeiro ganho de custo aqui, mesmo sem nenhuma linha nova relevante na conta.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | pipeline dirigido por evento, com estado consultável e alarme de DLQ | formato de arquivo ainda é fixo (CSV, cabeçalho único) | validação de schema versionada por fornecedor | média |
| Segurança | chave de objeto controlada pelo servidor, IAM restrito por função | arquivo rejeitado ainda pode ser aberto manualmente por um analista | documentar e treinar o time sobre injeção de fórmula em planilha | média |
| Confiabilidade | DLQ com alarme, escrita condicional contra duplicata, UPSERT idempotente | arquivo maior que o teto prático do Lambda ainda não tem caminho definido | dividir em lotes ou mover para Fargate quando o volume típico crescer | média |
| Eficiência de performance | validação rápida separada do processamento pesado por fila | UPSERT linha a linha não escala para dezenas de milhares de linhas | UPSERT em lote no Postgres | baixa hoje |
| Otimização de custos | tudo cobrado por uso, sem recurso ocioso | regra do EventBridge mal filtrada pode gerar loop e cobrança inesperada | alarme de invocações do Validador acima do volume esperado | alta |
| Sustentabilidade | nenhum recurso fica ligado esperando arquivo | objetos rejeitados e processados acumulam sem ciclo de vida | regra de expiração por prefixo, revisada com dado de uso real | baixa |
Evolução em níveis: o que muda, e o que passa a doer
A terceira arquitetura não é um desenho: é a resposta a QUANDO trocar de desenho. Cada nível resolve um risco e compra outro.
Arquivo cai no S3 (herdado do L17) e um analista roda um script à mão quando lembra. É onde a Cadência estava.EventBridge roteia o evento, um Validador decide rápido, uma fila com DLQ absorve o processamento pesado, DynamoDB rastreia o estado.Parser plugável por fornecedor, versionamento de schema, suporte a XLSX além de CSV.Arquivos grandes divididos em lotes menores por mensagem, ou processamento em Fargate/Step Functions para o que passa do teto do Lambda.Cota e SLA por fornecedor, painel próprio por fornecedor, contrato de formato versionado e comunicado.Extração antes de interpretação para documento não estruturado — foto, PDF escaneado, layout livre — com revisão humana no desenho.A ordem não é negociável, e o motivo é concreto
O nível 4 (particionar arquivo grande) depende de o estado por arquivo já ser consultável — que é o nível 2, construído neste laboratório. Quem tenta particionar processamento sem uma tabela de estado por trás monta a mecânica de dividir o trabalho sem conseguir dizer qual lote pertence a qual arquivo. É a decoração que o nível anterior evita.
Onde IA entra nesta arquitetura, e onde não entra
Neste módulo, IA não resolve o problema central, e forçá-la seria pagar tokens para reinventar um `if`. A planilha chega como CSV com cabeçalho fixo — dado já estruturado — e uma regra determinística (colunas esperadas presentes, codificação válida) resolve com menos código, menos latência e sem chance de "alucinar" um preço.
Há um lugar onde IA acrescentaria valor real, e ele é o oposto deste cenário: quando o arquivo NÃO chega estruturado — foto de uma tabela impressa, PDF escaneado, planilha com layout livre que muda por fornecedor. Aí a ordem importa: extração determinística primeiro (Textract), modelo só para o campo que exige interpretação, e revisão humana quando a confiança cai. É o desenho do L92, que evolui deste laboratório.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria aqui? | nenhum — o schema é fixo e a validação é determinística |
| Por que não usar um LLM para "entender" a planilha mesmo assim? | trocaria um sinal exato (cabeçalho presente ou ausente) por um probabilístico, sem ganho nenhum |
| Quando IA passa a valer a pena neste fluxo? | quando o arquivo deixa de ter schema fixo — documento não estruturado é o L92 |
| O que muda no desenho quando isso acontece? | extração determinística entra ANTES do modelo, e revisão humana vira parte do fluxo, não exceção |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para "ler a planilha e decidir se está certa" substitui uma checagem de cabeçalho — determinística, rápida e sem custo de token — por uma chamada probabilística que pode aprovar um cabeçalho quase certo. Onde existe validação exata possível, IA só acrescenta latência e a chance de errar com confiança.
Anti-padrões deste laboratório
| Anti-padrão | Por que alguém faz | Por que é problema | Sintoma em produção | Forma correta | Quando é aceitável |
|---|---|---|---|---|---|
| Notificação direta do bucket para o Lambda | é a primeira opção que o console mostra, e parece mais simples | aceita um único destino por tipo de evento — trava quando surge um segundo consumidor | precisa desmontar a configuração do bucket para acrescentar auditoria | entrega ao EventBridge, com regras filtradas | quando é comprovadamente certo que nunca haverá segundo consumidor |
| Um Lambda só fazendo validação e processamento pesado | é menos peça para configurar e implantar | acopla o tempo de resposta ao evento ao tempo de processar milhares de linhas | arquivo grande de um fornecedor atrasa a validação do próximo, de outro fornecedor | dois Lambdas ligados por fila, como neste módulo | arquivos sempre pequenos e função com folga de tempo confortável |
| Confiar que o evento nunca duplica | na prática, raramente duplica, e parece exagero tratar | at-least-once é o contrato documentado do S3 e do EventBridge, não bug raro | processamento duplicado silencioso, sem erro nenhum para investigar | escrita condicional por ArquivoId antes de qualquer efeito | nunca — o custo de tratar é uma condição a mais no PutItem |
| Regra do EventBridge sem filtro de prefixo | parece mais simples capturar todo Object Created do bucket | objeto movido para processado/ pelo próprio pipeline dispara o mesmo evento de novo | loop de reprocessamento — o mesmo arquivo entra em RECEBIDO repetidamente | filtrar por prefixo entrada/ na regra | nunca, no mesmo bucket que recebe a saída do próprio pipeline |
| Inferir o estado do arquivo pelo prefixo do S3 | "o console já mostra isso, por que duplicar numa tabela" | um move que falha no meio deixa o prefixo mentindo sobre o estado real | painel mostra sucesso baseado numa suposição que nunca foi verificada | DynamoDB como fonte de verdade, prefixo como conveniência visual | só para inspeção visual humana, nunca para decisão automatizada |
| `maxReceiveCount` em 1 | "manda pra DLQ no primeiro erro, resolve rápido" | falha transitória — Postgres momentaneamente sem conexão livre — vira alarme falso | DLQ recebendo arquivo que teria sido processado com sucesso na segunda tentativa | `maxReceiveCount` entre 3 e 5 | quando toda falha possível for, por natureza, permanente — raro |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| Arquivo nunca sai do estado RECEBIDO | a regra do EventBridge não invocou o Validador | compare invocações do Validador com uploads no bucket | CloudWatch Invocations do Validador | confira `eventbridge = true` na notificação do bucket e o padrão da regra |
| Arquivo válido nunca chega a PROCESSADO | mensagem presa na fila sem consumidor saudável | compare mensagens visíveis com mensagens em voo | métricas da fila de processamento | veja o log de erro do Processador para a mensagem |
| Mesmo arquivo processado duas vezes | escrita condicional ausente ou chave (ArquivoId) errada | procure duas linhas PROCESSADO para o mesmo ArquivoId | tabela de arquivos no DynamoDB | devolver `ConditionExpression: attribute_not_exists(ArquivoId)` |
| O mesmo arquivo entra em RECEBIDO repetidamente | regra sem filtro de prefixo, capturando o próprio destino do move | conte quantos Object Created a mesma chave gerou | CloudTrail data events no bucket | restringir a regra ao prefixo entrada/ |
| DLQ crescendo | mensagem malformada real, ou dependência (Postgres) fora do ar | veja se todas as mensagens da DLQ têm o mesmo motivo de erro | corpo da mensagem e log do Processador | corrigir o parser, ou aguardar e reprocessar quando a dependência voltar |
| Arquivo some do prefixo entrada/ sem estado final na tabela | move feito no S3, mas a escrita final no DynamoDB falhou no meio | compare a listagem do bucket com a consulta na tabela | log do Processador no instante da falha | reprocessar a partir do ArquivoId gravado, não a partir do prefixo do S3 |
| A regra do EventBridge não casa com nada | padrão de evento com nome de campo errado | teste o padrão na própria console do EventBridge com um evento de amostra | "Test pattern" do EventBridge | corrigir o `event_pattern`, comparando com o payload real recebido |
A pergunta que resolve metade destes casos
Antes de mexer em qualquer parâmetro, pergunte: o evento chegou até o Validador? Se as invocações do Validador batem com o número de uploads, o problema está depois — validação ou fila. Se não batem, o problema está no roteamento: entrega do bucket ao EventBridge, ou o padrão da regra.
Limpeza: o que o destroy não leva
Este laboratório acrescenta pouco recurso fixo, mas três coisas sobrevivem ao terraform destroy e merecem checagem explícita.
# 1. Derrube o que o Terraform administra.
terraform destroy -auto-approve
# 2. OBJETOS NO BUCKET: o destroy nao apaga o CONTEUDO de um bucket que ja
# existia (herdado do L17). Planilhas em entrada/, processado/ e rejeitado/
# continuam la, cobrando armazenamento.
aws s3 ls s3://ffv-lab-planilhas --recursive --summarize | tail -2
aws s3 rm s3://ffv-lab-planilhas/entrada/ --recursive
aws s3 rm s3://ffv-lab-planilhas/processado/ --recursive
aws s3 rm s3://ffv-lab-planilhas/rejeitado/ --recursive
# 3. TABELA DYNAMODB: guarda o historico de estado de cada arquivo. Apagar a
# tabela apaga a TRILHA inteira, nao so o "estado atual" — confirme que
# ninguem precisa dela para auditoria antes de seguir.
aws dynamodb delete-table --table-name ffv-lab-arquivos
# 4. FILA COM MENSAGEM: se voce interrompeu no meio de um teste de falha, a
# DLQ pode ainda ter mensagem — o destroy de uma fila SQS nao falha por
# causa disso, mas confirme que nao ha nada relevante antes de seguir.
aws sqs get-queue-attributes \
--queue-url "$(terraform output -raw fila_dlq_url 2>/dev/null || echo "")" \
--attribute-names ApproximateNumberOfMessagesVisible 2>/dev/null || true
# 5. Prova final: nada com nome do projeto de pe.
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=Projeto,Values=ffv-lab \
--query "ResourceTagMappingList[].ResourceARN" --output table| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Regra do EventBridge e alvo | sim | não | gerenciados pelo Terraform, sem estado próprio |
| Fila e DLQ | sim | não | SQS não cobra por fila vazia parada |
| Tabela DynamoDB | sim, se não tiver `prevent_destroy` | sim, enquanto existir | apaga o histórico inteiro de estado — confirme antes |
| Objetos no bucket (entrada/processado/rejeitado) | não | sim, GB-mês | o bucket é herdado do L17; destroy de infraestrutura não apaga conteúdo de bucket existente |
| Grupos de log dos dois Lambdas | depende de `skip_destroy` | sim, retenção | têm ciclo próprio; sobrevivem à função que os alimentava |
| Alarme e tópico SNS | sim se em Terraform | centavos | alarme criado à mão no console não aparece no estado |
Apagar a tabela apaga a trilha, não só o "estado atual"
Diferente de um cache que se reconstrói sozinho, a tabela de arquivos é o único registro de quando cada planilha chegou, quem a enviou e por que foi aceita ou rejeitada. Um `terraform destroy` que inclui essa tabela remove esse histórico por completo — exporte para o S3 antes, se auditoria futura importar.
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Ninguém é avisado quando o arquivo chega | entrega de eventos do S3 ao EventBridge | o S3 já emite o evento; faltava alguém ouvir |
| Notificação direta travaria a evolução | regra do EventBridge filtrada por prefixo | aceita quantas regras forem necessárias, sem reconfigurar o bucket |
| Validação lenta atrasaria o próximo arquivo | dois Lambdas ligados por fila | separa decidir-rápido de processar-pesado |
| Entrega "ao menos uma vez" pode duplicar efeito | PutItem condicional por ArquivoId | só a primeira entrega do mesmo arquivo tem efeito |
| Arquivo malformado sumindo sem explicação | prefixo rejeitado/{motivo} + registro na tabela | visível para quem investiga, com a razão registrada |
| Falha persistente sem ninguém notar | DLQ com alarme de mensagem visível | reaproveita o padrão do L22 — DLQ sem alarme é tão inútil quanto não ter DLQ |
| Estado inferido por onde o objeto está | DynamoDB como fonte de verdade | um move que falha no meio não engana quem consulta a tabela |
| Objeto processado disparando o próprio gatilho | filtro de prefixo entrada/ na regra | é o risco de loop que a documentação da AWS avisa para Lambda-com-S3 |
- O fornecedor envia a planilha pelo mesmo POST assinado do L17.
- O S3 anuncia o objeto criado ao EventBridge, sem reconfiguração do bucket.
- A regra filtra por prefixo entrada/ e invoca o Validador.
- O Validador registra a intenção com escrita condicional por ArquivoId.
- Entrega repetida do mesmo evento é reconhecida e ignorada, sem efeito duplo.
- A estrutura é conferida — cabeçalho, colunas, codificação — sem abrir linha nenhuma.
- Malformado vai para rejeitado/{motivo}; válido entra na fila de processamento.
- O Processador aplica UPSERT linha a linha e move o objeto para processado/.
- O veredito final é gravado na tabela, que é a fonte de verdade do estado.
- Falha persistente sai para a DLQ, e o alarme avisa um humano.
Perguntas frequentes
❓ Por que meu pipeline processa a mesma planilha duas vezes?
❓ Posso simplesmente configurar a notificação do S3 direto para um Lambda?
❓ O que acontece se a planilha enviada tiver o cabeçalho errado?
❓ Por que separar a validação do processamento pesado em duas funções Lambda?
❓ Por que um arquivo processado pode disparar o pipeline de novo?
❓ Como sei se um arquivo específico foi processado, sem vasculhar o bucket?
❓ Preciso de FIFO na fila de processamento, já que os arquivos têm ordem de chegada?
❓ Este pipeline funciona para PDF ou foto de planilha impressa?
Fixando
O Processador move cada arquivo processado de entrada/ para processado/, dentro do mesmo bucket que dispara os eventos ao EventBridge. Sem nenhum filtro de prefixo na regra, o que acontece?
Depois de processar 3.000 linhas de uma planilha com sucesso, a conexão do Processador com o Postgres cai na linha 3.001, e a função Lambda relança exceção. A mensagem volta para a fila e é reentregue. O que evita que as 3.000 primeiras linhas sejam alteradas de novo, incorretamente, na reentrega?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L17 no ar (upload assinado ao S3), L22 no ar (fila, DLQ, idempotência), catálogo do L01 acessível |
| Conhecimentos adquiridos | entrega de eventos do S3 ao EventBridge; limite de destino da notificação direta; escrita condicional aplicada por arquivo; separação de validação rápida e processamento pesado; risco documentado de loop de execução Lambda-com-S3 |
| Limitação que fica | formato aceito é CSV com cabeçalho fixo; arquivo maior que o teto prático do Lambda ainda não tem caminho de particionamento definido |
| Próximo exemplo recomendado | L92 — IDP: documento não estruturado, extração determinística antes do modelo, e revisão humana no desenho. Reaproveita a trilha de estado deste laboratório |
| Data da última validação técnica | 7 de agosto de 2026 |
Documentação oficial consultada: Amazon S3 → EventBridge — os eventos que o S3 envia e a diferença frente à notificação direta; Event notification types and destinations — o limite de um destino por tipo de evento na notificação direta, o aviso de loop de execução para Lambda acionado por objeto do mesmo bucket, e a garantia de entrega "ao menos uma vez" com possibilidade de duplicata; e DynamoDB conditional writes — o comportamento de `ConditionExpression` e `ConditionalCheckFailedException` usado para deduplicar por arquivo.
O que não foi verificado, e você deve conferir na sua conta
O tempo de "8 a 14 s do upload ao veredito" e os números da semana de exemplo (34 planilhas, 6 com mais de 4 horas de atraso) são medições ilustrativas do cenário deste laboratório, não uma referência da AWS. Meça na sua conta, com o volume real dos seus arquivos: o tempo do Processador cresce com o número de linhas, e o ponto em que ele se aproxima do teto do Lambda depende do tamanho típico da SUA planilha, não da do exemplo.
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…