Lab 24 — EventBridge como espinha dorsal
O problema, e a empresa que o tem
A Cadência cresceu. O que começou com o fanout de um evento em três assinantes fixos — o L23 — hoje são seis times publicando e consumindo eventos entre si: Pedidos, Faturamento, Estoque, Prevenção a Fraude, Notificação e, desde o mês passado, Contabilidade tentando entrar.
Ninguém desenhou essa topologia de propósito. Cada integração nasceu do menor jeito de resolver o problema do dia: Faturamento chama Fraude direto porque precisava de uma checagem antes de confirmar o pagamento; Fraude chama Notificação porque um risco alto precisa de alerta. Hoje existem 14 chamadas diretas documentadas entre esses times — e provavelmente mais que não estão.
O incidente que trouxe o problema à mesa: o Faturamento renomeou um campo do evento que manda para Fraude, avisou no canal do time, e ninguém em Contabilidade — que também dependia daquele campo, sem que Faturamento soubesse — leu o aviso. A checagem de risco ficou quebrada por três dias, silenciosamente, até uma auditoria de rotina notar pedidos de alto valor sem avaliação nenhuma.
O que este laboratório NÃO é
Não é um laboratório de orquestração de fluxo com estado — isso é o Step Functions do L25, para quando existe uma SEQUÊNCIA de passos com retry e compensação. Aqui o problema é roteamento: quem deve saber de um evento, não em que ordem executar um processo. Misturar as duas coisas é o antipadrão que a seção 24 nomeia: regra de EventBridge decidindo lógica de negócio.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um comando na seção de implantação.
- Explicar por que SNS/SQS (L23) e EventBridge resolvem problemas diferentes, sem tratar um como substituto do outro.
- Criar um bus customizado isolado do bus default, com política de recurso que impede origem forjada.
- Registrar um contrato de evento versionado e evoluí-lo de forma aditiva sem quebrar consumidor existente.
- Escrever uma regra que roteia por padrão de conteúdo entre produtores que não se conhecem.
- Configurar retentativa e DLQ por alvo, e explicar por que a granularidade é do alvo, não da regra.
- Adicionar um consumidor novo sem alterar nenhum recurso do produtor.
- Provar, com replay de um archive, a recuperação de um consumidor que ficou fora do ar.
- Nomear o antipadrão de regra como orquestrador disfarçado, e por que ele é tentador.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Padrão de evento (content-based) | SAA-C03, DOP-C02 | regra que casa `source` e uma condição numérica em `detail.valor` | operadores do padrão: prefixo, numérico, `anything-but`, combinação com "OR" |
| Bus custom vs default | SAA-C03 | bus de domínio de negócio isolado do bus que recebe evento nativo da conta | quando isolar, e como uma política de recurso restringe quem publica |
| Schema registry e contrato | DOP-C02 | contrato versionado do evento PedidoPago, com campo aditivo vs quebra | discovery por inferência vs registro manual; o que é mudança aditiva |
| Archive e replay | DOP-C02, SAA-C03 | recuperação do consumidor Fraude após 40 min fora do ar | replay não reordena por chegada; até 10 replays simultâneos por conta/região |
| DLQ em alvo de regra | SAA-C03, DVA-C02 | fila morta configurada no alvo Fraude, não na regra | granularidade por ALVO; só SQS é destino válido de DLQ do EventBridge |
| Regra entre contas | SAP-C02, DOP-C02 | citado como próximo passo, não implementado neste laboratório | política de recurso do bus autorizando uma conta parceira a publicar ou assinar |
| Entrega pelo menos uma vez | DVA-C02, SAA-C03 | consumidor idempotente via Id do evento, a mesma prática do L22 | por que "at-least-once" exige dedupe no consumidor, nunca supor exactly-once |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve um cenário de "um produtor, vários assinantes fixos" e espera EventBridge como resposta — quando SNS resolveria com menos peça. O sinal que decide é outro: existem MÚLTIPLOS produtores que não se conhecem, e o roteamento depende do CONTEÚDO do evento, não de qual tópico alguém assinou.
Requisitos, e como cada um muda o desenho
Requisito não funcional 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 |
|---|---|---|
| Consumidor novo não exige PR no produtor | zero linhas alteradas em quem publica | roteamento por REGRA de padrão no bus, não por assinatura de tópico do produtor |
| Mudança de campo não quebra consumidor de outro domínio sem aviso | contrato versionado, explícito | schema registry com versão no envelope; consumidor referencia a versão que entende |
| Recuperar consumidor fora do ar sem perder o que passou | replay do intervalo exato | archive contínuo com retenção definida + replay por janela de tempo |
| Eventos de negócio isolados dos eventos nativos da conta AWS | nenhum evento de serviço AWS no mesmo fluxo de decisão de negócio | bus CUSTOM, não o default |
| Só quem tem a claim certa publica em nome de um domínio | produtor não pode forjar `source` | política de recurso do bus com condição sobre o papel IAM que assina o PutEvents |
| Falha de entrega não pode desaparecer | todo alvo tem para onde ir quando esgota a tentativa | DeadLetterConfig por alvo, com fila própria |
SNS/SQS (L23) resolve outro problema — e continua certo resolver
É tentador ler este laboratório como "a versão melhor do L23". Não é. As duas peças resolvem topologias diferentes, e usar a errada para o problema errado produz sistemas piores nos dois sentidos: EventBridge para um fanout simples é complexidade sem retorno; SNS/SQS para roteamento entre domínios desconhecidos é o consumidor reinventando, no próprio código, o que uma regra faria melhor.
| Dimensão | SNS/SQS (L23) | EventBridge (aqui) |
|---|---|---|
| Quem publica | um produtor conhecido, dono do tópico | qualquer produtor autorizado no bus, sem tópico fixo por consumidor |
| Base da entrega | assinatura no tópico — você assina O QUE aquele produtor publica | padrão de CONTEÚDO — você assina O QUE COMBINA, de qualquer origem autorizada |
| O que o consumidor precisa saber | o ARN do tópico do produtor | o formato do evento, não quem o publicou |
| Contrato | implícito, versionado por convenção do time | schema registry, com versão explícita no envelope |
| Recuperar evento perdido | reprocessar da fonte, ou construir replay próprio | archive nativo com API de replay por janela de tempo |
| Caso ideal | Pedido criado → Estoque, Notificação e Analytics assinam o mesmo tópico (fanout simples, um produtor) | Fraude precisa de PedidoPago (Faturamento) E PedidoCancelado (Pedidos) — dois domínios que não se conhecem |
| Custo de adotar sem necessidade | baixo — é a peça mais simples do menu | schema registry e bus custom são overhead real quando só existe um produtor e assinantes fixos |
SNS não "perde" aqui — é a ferramenta certa para outro desenho
Se o seu caso é um produtor com assinantes fixos que só querem o que aquele produtor publica, SNS/SQS continua sendo a resposta mais simples, mais barata e com menos peça para operar. O barramento se justifica quando o roteamento depende do CONTEÚDO do evento e da combinação de múltiplas origens que não se conhecem — que é exatamente o requisito da Contabilidade neste laboratório.
Arquitetura mínima: a teia que já dói
Este é o desenho que a Cadência tem hoje, e ele é legítimo como ponto de partida: cada chamada, isoladamente, foi a menor linha que resolveu o problema do dia. O laboratório começa por tornar o custo visível, contando quantos arquivos uma mudança de consumidor toca hoje.
- → invocação síncrona: payload de PedidoCriado, campo a campo combinado por e-mail entre os dois times
- → invocação síncrona: dados de pagamento para a checagem de risco
- → invocação síncrona: pedido de envio do e-mail de confirmação
- → invocação síncrona: nível de estoque, num formato que só o time de Estoque documentou
- → invocação síncrona: alerta de risco alto, motivo redigido em texto livre
- → não existe ainda: exigiria abrir o código do Faturamento para nascer
- Compute
Cada seta aqui foi escrita dentro do código de quem publica, não numa configuração central — é por isso que este desenho não existe em nenhum lugar único: existe espalhado em cinco repositórios. Percorra os passos: o defeito não é falta de fila entre dois serviços, é que a decisão de quem recebe o quê está decidida no código de quem produz.
- Quem publica decide quem escuta. A chamada de Pedidos para Faturamento está no código de Pedidos: é uma linha de infraestrutura escondida dentro de lógica de negócio. Trocar de destinatário, ou adicionar um segundo, é alterar e reimplantar o PRODUTOR — nunca uma configuração isolada.
- Um consumidor, dois produtores que não se falam. Fraude depende de Faturamento e de Estoque ao mesmo tempo, e os dois não combinam formato nem ordem de chegada entre si. Se Estoque muda um campo, ninguém em Faturamento fica sabendo — e não deveria, porque os dois nem se conhecem como equipe.
- A cadeia se prolonga sem ninguém desenhar o total. Fraude chama Notificação depois de decidir o risco. Some as cinco setas deste diagrama e pergunte a qualquer uma das cinco pessoas envolvidas qual é o desenho inteiro — nenhuma sabe, porque nenhuma precisou saber para escrever a sua parte.
- Adicionar quem escuta é abrir o código de quem publica. A Contabilidade só precisa saber quando um pedido é pago — uma pergunta de leitura. Para respondê-la hoje, alguém do Faturamento precisa aceitar um PR no repositório dele, revisar, testar e reimplantar um serviço que não tem nenhum interesse na Contabilidade.
- Um formato por produtor, e cada consumidor decodifica sozinho. Não existe envelope comum. Fraude mantém dois parsers diferentes — um para o que Faturamento manda, outro para o que Estoque manda — e quando um dos dois muda um nome de campo, o parser quebra sem aviso, silenciosamente, até alguém notar o efeito três domínios adiante.
- Por que a teia cresce assim, e ninguém decide crescê-la. Cada seta, isoladamente, foi a menor mudança que resolveu o problema do dia — uma chamada de função é mais rápida de escrever que uma integração nova. O custo não aparece no PR que a criou; aparece três meses depois, quando alguém tenta desenhar o sistema inteiro numa reunião.
# Rode ANTES de mudar nada. Conte quantos repositorios um consumidor novo
# precisaria tocar no desenho atual.
grep -rl "InvokeFunctionAsync\|HttpClient.*faturamento\|HttpClient.*pedidos" \
--include="*.cs" ./servicos/ | sort -u
# Na Cadencia: 5 arquivos, em 4 repositorios diferentes, cada um com um dono
# de time distinto — e a Contabilidade ainda nem entrou no grafo.A checagem de risco que parou por três dias, e ninguém viu
Quando Faturamento renomeou um campo sem versionar o contrato, o parser de Fraude passou a falhar silenciosamente — sem exceção que subisse alarme, porque o código tratava campo ausente como "sem risco" em vez de rejeitar. Pedidos de alto valor passaram sem checagem por três dias, e a descoberta foi por auditoria manual, não por alerta. Ausência de contrato explícito não é só desorganização: é dinheiro exposto sem ninguém saber que está exposto.
Arquitetura para produção: o barramento como espinha dorsal
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.
- → PutEvents: PedidoCriado, contrato v2
- → PutEvents: PedidoPago, contrato v1
- → PutEvents: EstoqueBaixo, contrato v1
- → evento novo comparado ao contrato registrado
- → regra risco-pagamento-alto: casa origem e detail.valor
- → regra notificar-cliente: casa detail-type em duas listas
- → regra fechamento-contabil: casa detail-type=PedidoPago
- → evento não entregue após esgotar as tentativas do alvo
- → cópia contínua do que casa o padrão de arquivamento
- → replay reenvia o intervalo perdido, um minuto de cada vez
- → condição de origem: só quem tem a claim certa publica com aquele source
- → invocação de regra, falha e descarte por tentativa esgotada
- Compute
- Integração de apps
- Conceito de arquitetura
- Segurança e identidade
- Gestão e governança
A decisão de quem recebe o quê saiu do código de cada produtor e virou uma regra nomeada, lida por qualquer pessoa do time de plataforma. Percorra os passos: a diferença central em relação ao desenho anterior não é o barramento em si — é que um consumidor novo nasce como configuração, e um evento perdido tem um caminho de volta.
- O produtor só conhece o barramento. Pedidos, Faturamento e Estoque chamam `PutEvents` contra o mesmo bus custom e nada mais. Nenhum dos três sabe, nem precisa saber, quantos consumidores existem hoje ou vão existir amanhã.
- O contrato é conferido antes de qualquer regra decidir. O schema registry guarda a forma esperada de cada tipo de evento, com versão explícita. Não é validação bloqueante na entrada por padrão — é referência para o consumidor, e para quem revisa uma mudança de campo antes de publicá-la.
- A regra roteia por conteúdo, não por remetente fixo. A regra de Fraude não assina "o tópico do Faturamento": ela casa `source: ["faturamento"]` combinado com uma condição numérica em `detail.valor`. Se amanhã outro domínio publicar um evento com o mesmo formato, a mesma regra o alcança sem mudar uma linha.
- O mesmo evento alimenta outro consumidor sem o primeiro saber. A regra de Notificação casa PedidoCriado e PedidoPago vindos de dois produtores diferentes. Ela e a regra de Fraude avaliam o MESMO evento de forma independente — uma não sabe da existência da outra, e não precisa.
- Um consumidor novo é uma regra, não um pedido de mudança. A Contabilidade nasce com uma regra nova apontando para o mesmo padrão de PedidoPago que Fraude já lê. Nenhum arquivo do Faturamento foi tocado — a diferença central em relação ao desenho anterior.
- Alvo fora do ar não apaga o evento, só o atrasa. Quando Fraude está fora do ar, o EventBridge tenta de novo segundo a política do ALVO e, esgotadas as tentativas, entrega na DLQ dele. O evento não desaparece — fica visível como pendência a resolver.
- O arquivo é o que torna o passado recuperável. O archive grava, em paralelo, uma cópia de todo evento que casa o padrão configurado. Quando Fraude volta ao ar, um replay pelo intervalo exato da indisponibilidade reconstrói o que ela perdeu — sem pedir a nenhum produtor que publique de novo.
A diferença estrutural em relação ao desenho anterior não é "trocar chamada direta por fila": é que a decisão de roteamento saiu do código de cada produtor e virou um artefato nomeado, versionado e revisável — a regra — que qualquer pessoa do time de plataforma lê sem abrir cinco repositórios.
O teste real de que a teia acabou
Peça para alguém que nunca viu o sistema listar quem recebe um PedidoPago. No desenho mínimo, a resposta exige ler o código de Faturamento inteiro. Aqui, a resposta é `aws events list-rules` filtrado por `PedidoPago` no padrão — um comando, sem abrir nenhum repositório.
O caminho de um evento, ponta a ponta
Um evento não tem um único destino, como teria numa fila: ele é avaliado contra CADA regra ativa do bus, de forma independente. Duas regras podem casar o mesmo evento sem que uma saiba da outra — é assim que Fraude e Notificação recebem o mesmo PedidoPago sem estarem acopladas entre si.
A garantia que o EventBridge dá, e a que ele NÃO dá
Entrega é pelo menos uma vez: um evento pode chegar duplicado ao alvo depois de uma falha de confirmação. O EventBridge não promete ordem de entrega entre eventos nem exactly-once. Quem precisa das duas coisas dentro de um único domínio conhecido continua bem servido pelo SQS FIFO do L22 — a decisão não é "qual é melhor", é "qual problema você tem".
O envelope que o produtor publica. schemaVersion e canalVenda sao o que torna a evolucao do contrato observavel: v1.0 nao tinha canalVenda, v1.1 adicionou como campo OPCIONAL — e por isso nao quebrou quem ainda le a v1.0.
{
"version": "0",
"id": "9a1b2c3d-...",
"detail-type": "PedidoPago",
"source": "faturamento",
"account": "111122223333",
"time": "2026-08-07T14:22:31Z",
"resources": ["pedido/p-001"],
"detail": {
"pedidoId": "p-001",
"valor": 8200.00,
"moeda": "BRL",
"clienteHash": "a1b2...",
"schemaVersion": "1.1",
"canalVenda": "app"
}
}As decisões, e o que se perde em cada uma
📋 Cadência com seis times publicando e consumindo eventos entre domínios diferentes (Pedidos, Faturamento, Estoque, Fraude, Notificação, e agora Contabilidade pedindo entrada), sem orçamento para operar um cluster gerenciado dedicado e com um time de plataforma de três pessoas cobrindo os seis domínios.
O barramento não pede operação de cluster nem gestão de partição: é cobrado por evento publicado e por regra invocada, e a política de recurso decide quem publica sem exigir coordenação humana a cada consumidor novo. Ele resolve exatamente o problema declarado — roteamento por conteúdo entre produtores e consumidores que não se conhecem — com uma equipe pequena. A honestidade que a decisão exige: o EventBridge não é uma fila com ordenação garantida nem promete exactly-once; quem precisa das duas coisas dentro de um único domínio continua bem servido pelo SQS do L22 ou pelo SNS/SQS do L23, e as duas ferramentas convivem no mesmo sistema.
Alt: Continuar crescendo com mais tópicos SNS por domínio — resolve fanout dentro de um domínio, não roteamento por conteúdo entre domínios que não se conhecem; um consumidor entre domínios continua assinando vários tópicos e reconciliando formatos sozinho
Alt: Kafka autogerenciado ou Amazon MSK — entrega ordenação forte por partição e replay mais rico, mas cobra operação de cluster, dimensionamento de partição e curva de aprendizado que um time de três pessoas cobrindo seis domínios não tem capacidade de sustentar agora
Alt: Step Functions como orquestrador central — centraliza LÓGICA com estado, não roteamento de notificação; é a ferramenta certa quando existe uma sequência de passos com retry e compensação — não quando a pergunta é "quem mais precisa saber disto"
Alt: Manter chamada direta e só documentar melhor o grafo — documentação não impede o próximo PR de esquecer um consumidor; o acoplamento continua compilado no binário de quem publica, só o papel muda
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Ferramenta de roteamento | EventBridge com bus custom | SNS/SQS por domínio (L23); Kafka/MSK; chamada direta | roteia por conteúdo entre produtores que não se conhecem, sem cluster para operar | sem ordenação garantida entre eventos e sem exactly-once nativo |
| Isolamento do bus | bus custom, separado do default | publicar no bus default da conta | evita misturar evento de negócio com evento nativo de serviço AWS na mesma cota | mais um recurso para nomear e versionar no Terraform |
| Contrato de evento | schema registry com versão explícita no envelope | sem contrato formal; documentação em wiki | campo obrigatório ausente é detectável em código, não descoberto por auditoria | disciplina extra: quem muda o produtor precisa lembrar de versionar |
| Granularidade de DLQ | por alvo, com fila própria para Fraude | DLQ única compartilhada entre todos os alvos | permite política de retentativa diferente por criticidade do consumidor | mais filas para monitorar; sem consolidação automática |
| Recuperação de falha | archive + replay por janela de tempo | reprocessamento manual a partir dos sistemas de origem | não depende de os produtores guardarem histórico nem de reconstruir manual | replay não garante ordem exata; agrupa por minuto do evento original |
| Onde a lógica de negócio mora | no código do consumidor (Lambda Fraude) | regras de EventBridge encadeadas decidindo o risco | lógica versionada, testável e revisável como código | nada aqui; é a decisão que evita o antipadrão da seção 24 |
A dívida que este desenho cria, e que o módulo não paga
Um consumidor que precisa de ordem estrita entre dois eventos do mesmo agregado — por exemplo, PedidoCriado antes de PedidoPago do mesmo pedido — não tem essa garantia aqui. O EventBridge entrega por regra, não por fila ordenada. Quando ordem importa dentro de um domínio, SQS FIFO (L22) continua sendo a peça certa, e as duas convivem no mesmo sistema.
Construir: o barramento e a política de quem publica
O bus é o recurso central, mas a peça que faz a diferença de segurança é a política de recurso: sem ela, qualquer papel IAM com `events:PutEvents` no ARN certo pode publicar um evento fingindo ser qualquer domínio.
# bus.tf — o barramento CUSTOM, isolado do default, e quem pode publicar nele
# O bus default de toda conta ja recebe, sozinho, os eventos nativos dos
# servicos AWS daquela conta (GuardDuty, Config, e por ai). Publicar evento de
# NEGOCIO nele mistura duas fontes com dono e finalidade diferentes na mesma
# cota e nas mesmas regras. Um bus custom isola as duas coisas.
resource "aws_cloudwatch_event_bus" "pedidos" {
name = "${var.projeto}-pedidos-eventbus"
tags = { Projeto = var.projeto, Dominio = "pedidos" }
}
# So publica quem tem a claim certa. A condicao amarra o PAPEL IAM que assina
# o PutEvents ao "source" que ele declara — Pedidos nao pode publicar um
# evento fingindo ser Faturamento, porque o papel dele nao tem a tag exigida.
data "aws_iam_policy_document" "resource_policy_bus" {
statement {
sid = "SoQuemTemClaimPublica"
effect = "Allow"
actions = ["events:PutEvents"]
resources = [aws_cloudwatch_event_bus.pedidos.arn]
principals {
type = "AWS"
identifiers = ["*"]
}
condition {
test = "StringEquals"
variable = "aws:PrincipalTag/dominio-eventbridge"
values = ["pedidos", "faturamento", "estoque"]
}
}
}
resource "aws_cloudwatch_event_bus_policy" "pedidos" {
event_bus_name = aws_cloudwatch_event_bus.pedidos.name
policy = data.aws_iam_policy_document.resource_policy_bus.json
}
# O archive grava, em paralelo a entrega, uma copia filtrada. Retencao em
# dias; por padrao o EventBridge guarda indefinidamente se voce omitir o
# campo — aqui e explicito porque armazenamento indefinido e divida.
resource "aws_cloudwatch_event_archive" "pedidos" {
name = "${var.projeto}-archive"
event_source_arn = aws_cloudwatch_event_bus.pedidos.arn
retention_days = 90
event_pattern = jsonencode({
source = ["pedidos", "faturamento", "estoque"]
})
}
output "bus_arn" {
value = aws_cloudwatch_event_bus.pedidos.arn
description = "ARN do bus custom; PutEvents e as regras apontam para aqui, nunca para o default"
}
Por que o archive é criado junto com o bus, não depois
Um archive só captura eventos a partir do momento em que existe — ele não retroage. Criar o archive no dia 1, mesmo sem nenhum plano de replay imediato, é o que garante que, quando a necessidade aparecer, o histórico já estará lá.
Construir: o contrato do evento no schema registry
O contrato é o que teria evitado os três dias de checagem de risco quebrada. Um discoverer infere o que já circula; um schema registrado manualmente declara o que DEVE circular — são coisas diferentes, e o laboratório usa as duas.
# schema.tf — o contrato, versionado, e o registro automatico de inferencia
resource "aws_schemas_registry" "pedidos" {
name = "${var.projeto}-contratos"
description = "Contratos de evento de negocio entre os dominios da Cadencia"
}
# O discoverer INFERE schema a partir do trafego real do bus. Util para
# descobrir o que ja esta em producao e nao foi documentado — nao e o
# contrato que os produtores devem seguir, e sim um raio-x do que ja existe.
resource "aws_schemas_discoverer" "pedidos" {
source_arn = aws_cloudwatch_event_bus.pedidos.arn
description = "Infere o formato dos eventos que circulam no bus, para auditoria"
}
# O contrato DECLARADO, que os produtores devem seguir. JSONSchema Draft4 e
# OpenAPI 3 sao os dois formatos aceitos pelo registro; aqui, Draft4.
# A versao 1 e a que Fraude e Contabilidade leem hoje. Adicionar campo
# opcional gera versao 2 sem quebrar quem ainda le a 1; RENOMEAR ou remover
# um campo exige uma versao nova e um periodo de transicao com os dois nomes
# publicados — nunca sobrescrever a versao que consumidor em producao usa.
resource "aws_schemas_schema" "pedido_pago_v1" {
name = "faturamento@PedidoPago"
registry_name = aws_schemas_registry.pedidos.name
type = "JSONSchemaDraft4"
content = jsonencode({
"$schema" = "http://json-schema.org/draft-04/schema#"
title = "PedidoPago"
type = "object"
required = ["pedidoId", "valor", "moeda", "clienteHash", "schemaVersion"]
properties = {
pedidoId = { type = "string" }
valor = { type = "number" }
moeda = { type = "string" }
clienteHash = { type = "string" }
schemaVersion = { type = "string" }
# canalVenda e ADITIVO: opcional, nao entra em "required". E assim que
# um campo novo nao quebra quem ainda le a versao anterior do contrato.
canalVenda = { type = "string" }
}
})
}
Renomear campo sem versionar é o incidente que já aconteceu aqui
O comentário no `canalVenda` não é decoração: é a diferença entre uma mudança que não quebra ninguém (campo novo, opcional) e uma que quebra em silêncio (renomear ou remover um campo que um consumidor já lê). A segunda categoria exige publicar os dois nomes por um período de transição — nunca sobrescrever o contrato que produção usa.
Construir: as regras, os alvos e a fila de morto
Cada regra é um artefato nomeado e legível — o oposto do "abrir o código de Faturamento para descobrir quem escuta" do desenho anterior.
# rules.tf — regras nomeadas, alvo com propria politica de retentativa e DLQ
# A fila morta e do ALVO, nao da regra nem do bus. Cada regra pode ter varios
# alvos, e cada um com sua propria DLQ e politica de repeticao.
resource "aws_sqs_queue" "dlq_fraude" {
name = "${var.projeto}-fraude-dlq"
message_retention_seconds = 1209600 # 14 dias — o teto do SQS, para dar tempo de investigar
}
resource "aws_cloudwatch_event_rule" "risco_pagamento_alto" {
name = "${var.projeto}-risco-pagamento-alto"
event_bus_name = aws_cloudwatch_event_bus.pedidos.name
description = "Roteia para Fraude todo PedidoPago com valor acima do limite de risco"
# O padrao casa por CONTEUDO, nao por quem publicou. "faturamento" aqui e o
# valor do campo source, nao uma assinatura de topico — Estoque ou qualquer
# outro dominio poderia publicar no mesmo formato e a regra alcancaria.
event_pattern = jsonencode({
source = ["faturamento"]
detail-type = ["PedidoPago"]
detail = {
valor = [{ numeric = [">", 5000] }]
}
})
}
resource "aws_cloudwatch_event_target" "fraude" {
rule = aws_cloudwatch_event_rule.risco_pagamento_alto.name
event_bus_name = aws_cloudwatch_event_bus.pedidos.name
arn = aws_lambda_function.fraude.arn
role_arn = aws_iam_role.invocar_fraude.arn
retry_policy {
maximum_event_age_in_seconds = 3600 # teto de 24h; 1h cobre o SLA de risco da Fraude
maximum_retry_attempts = 2
}
dead_letter_config {
arn = aws_sqs_queue.dlq_fraude.arn
}
}
# A regra de Notificacao casa por TIPO de evento, de QUALQUER origem — e o
# que mostra que uma regra nao e uma assinatura de topico de um produtor so.
resource "aws_cloudwatch_event_rule" "notificar_cliente" {
name = "${var.projeto}-notificar-cliente"
event_bus_name = aws_cloudwatch_event_bus.pedidos.name
event_pattern = jsonencode({
detail-type = ["PedidoCriado", "PedidoPago"]
})
}
resource "aws_cloudwatch_event_target" "notificacao" {
rule = aws_cloudwatch_event_rule.notificar_cliente.name
event_bus_name = aws_cloudwatch_event_bus.pedidos.name
arn = aws_lambda_function.notificacao.arn
role_arn = aws_iam_role.invocar_notificacao.arn
}
# A Contabilidade nasce AQUI, como regra nova. Nenhum recurso de Faturamento
# foi tocado nesta mudanca — e a prova concreta do requisito de zero PR
# cruzado.
resource "aws_cloudwatch_event_rule" "fechamento_contabil" {
name = "${var.projeto}-fechamento-contabil"
event_bus_name = aws_cloudwatch_event_bus.pedidos.name
event_pattern = jsonencode({
source = ["faturamento"]
detail-type = ["PedidoPago"]
})
}
resource "aws_cloudwatch_event_target" "contabilidade" {
rule = aws_cloudwatch_event_rule.fechamento_contabil.name
event_bus_name = aws_cloudwatch_event_bus.pedidos.name
arn = aws_lambda_function.contabilidade.arn
role_arn = aws_iam_role.invocar_contabilidade.arn
}
A pegadinha da granularidade da DLQ
A fila de mensagens mortas é do ALVO, não da regra e não do bus. Se uma regra tem dois alvos, cada um precisa da própria configuração — copiar a política de retentativa de um alvo esquecendo o outro deixa metade dos eventos sem rede de segurança, sem nenhum erro que avise disso.
| Onde | Parâmetro | Padrão | Aqui | Por quê |
|---|---|---|---|---|
| Alvo Fraude | `MaximumRetryAttempts` | 2 (padrão) | 2 | o padrão já cobre falha transitória; mais tentativas atrasam a chegada na DLQ |
| Alvo Fraude | `MaximumEventAgeInSeconds` | 3.600 s (padrão) | 3.600 s | a checagem de risco perde valor depois de uma hora; manter no padrão é decisão, não esquecimento |
| Archive | `retention_days` | indefinido, se omitido | 90 | sem prazo, o archive cresce para sempre e cobra armazenamento sem teto |
| Bus | política de recurso | nenhuma, por padrão | condição por tag de domínio | sem ela, qualquer papel com a ação liberada publica em nome de qualquer domínio |
Construir: quem publica, em C#
O produtor só conhece o bus — nenhuma referência a Fraude, Notificação ou Contabilidade aparece neste arquivo, e essa ausência é o ponto central do laboratório.
// PublicadorEventos.cs — o produtor so conhece o bus, nunca o consumidor
public sealed class PublicadorEventos
{
private readonly IAmazonEventBridge _eventBridge;
private readonly string _busName;
public PublicadorEventos(IAmazonEventBridge eventBridge, IConfiguration config)
{
_eventBridge = eventBridge;
// Sempre o bus CUSTOM, nunca "default" — publicar la misturaria
// evento de negocio com evento nativo de servico AWS da conta.
_busName = config["EventBridge:BusName"]!;
}
public async Task PublicarPedidoPagoAsync(Pedido pedido, CancellationToken ct)
{
// O envelope carrega a versao do contrato explicitamente. E o que
// permite ao consumidor decidir, sem adivinhar, se sabe ler este
// formato ou se precisa tratar como versao desconhecida.
var detalhe = JsonSerializer.Serialize(new
{
pedidoId = pedido.Id,
valor = pedido.ValorLiquido,
moeda = "BRL",
clienteHash = HashCliente(pedido.ClienteId),
schemaVersion = "1.1",
canalVenda = pedido.Canal, // campo aditivo da v1.1; ausente na v1.0 e opcional
});
var pedidoPutEvents = new PutEventsRequestEntry
{
Source = "faturamento",
DetailType = "PedidoPago",
Detail = detalhe,
EventBusName = _busName,
// O ID do proprio dominio (pedidoId) vira a chave de dedupe do
// consumidor — a entrega e pelo menos uma vez, nunca exatamente uma.
Resources = { $"pedido/{pedido.Id}" },
};
var resposta = await _eventBridge.PutEventsAsync(
new PutEventsRequest { Entries = { pedidoPutEvents } }, ct);
// PutEvents pode aceitar parte do lote e rejeitar outra parte — o
// sucesso da chamada NAO garante que todo evento foi aceito.
if (resposta.FailedEntryCount > 0)
{
throw new EventBridgePublishException(
$"{resposta.FailedEntryCount} evento(s) rejeitado(s) pelo PutEvents");
}
}
private static string HashCliente(string clienteId) =>
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(clienteId)));
}
Por que `Resources` carrega o ID do pedido
Não é obrigatório, mas custa uma linha e habilita busca e filtro por recurso específico em ferramentas de auditoria e no console — "todos os eventos deste pedido", sem precisar abrir o `Detail` de cada um.
Construir: quem consome, e a versão que ele entende
O consumidor trata duas coisas que o produtor não controla: duplicata de entrega e contrato desconhecido. As duas são responsabilidade de quem lê, não do EventBridge.
// FuncaoFraude.cs — o consumidor confere a versao, e trata duplicata como normal
public sealed class FuncaoFraude
{
private readonly IIdempotenciaStore _idempotencia; // mesma pratica do L22
private readonly IRiscoService _risco;
public async Task<APIGatewayProxyResponse> ManipularAsync(
CloudWatchEvent<PedidoPagoDetalhe> evento, ILambdaContext contexto)
{
// Entrega e PELO MENOS UMA VEZ — tanto por retry do alvo quanto por
// replay do archive. O Id do envelope da EventBridge, nao o pedidoId
// do dominio, e a chave de dedupe: dois PutEvents do mesmo pedido tem
// Ids diferentes, mas o MESMO evento reentregue mantem o mesmo Id.
if (await _idempotencia.JaProcessadoAsync(evento.Id))
{
contexto.Logger.LogInformation("evento {Id} ja processado — ignorando", evento.Id);
return Sucesso();
}
var detalhe = evento.Detail;
// Compatibilidade ADITIVA: campo novo e opcional (schemaVersion 1.1
// trouxe canalVenda) e simplesmente ignorado se ausente. Nao falha.
// Campo OBRIGATORIO ausente, ou de tipo trocado, e outra historia:
// rejeita e registra — processar com dado errado e pior que rejeitar.
if (string.IsNullOrEmpty(detalhe.SchemaVersion))
{
contexto.Logger.LogWarning(
"evento {Id} sem schemaVersion — versao de contrato desconhecida, rejeitando",
evento.Id);
throw new ContratoDesconhecidoException(evento.Id);
}
var resultado = await _risco.AvaliarAsync(new AvaliacaoRisco(
detalhe.PedidoId, detalhe.Valor, detalhe.ClienteHash));
await _idempotencia.MarcarProcessadoAsync(evento.Id, ttl: TimeSpan.FromDays(7));
if (resultado.RiscoAlto)
{
contexto.Logger.LogWarning(
"pedido {PedidoId} marcado de risco alto: {Motivo}",
detalhe.PedidoId, resultado.Motivo);
}
return Sucesso();
}
}
Campo ausente tratado como "sem risco" é o bug real do incidente
A versão anterior deste consumidor, na Cadência real, tratava a ausência de um campo esperado como valor padrão em vez de rejeitar o evento — e foi assim que a checagem de risco continuou "funcionando" sem checar nada. Rejeitar explicitamente contrato desconhecido troca uma falha silenciosa por uma visível, e visível é sempre mais barato de corrigir.
Implantar, e provar que o roteamento e o replay funcionam
#!/usr/bin/env bash
# implantar.sh — sobe o barramento, o contrato, as regras e os consumidores
set -euo pipefail
PROJETO="${PROJETO:?defina PROJETO}"
REGIAO="${REGIAO:-us-east-1}"
terraform init -upgrade
terraform apply -auto-approve
BUS_ARN="$(terraform output -raw bus_arn)"
echo "barramento no ar: ${BUS_ARN}"
# Confirma que as tres regras foram criadas e estao ENABLED — regra
# desabilitada nao aparece como erro em lugar nenhum, so nao roteia.
aws events list-rules --event-bus-name "${PROJETO}-pedidos-eventbus" \
--query "Rules[].{nome:Name,estado:State}" --output table
Cinco provas. Nenhuma aceita "a regra parece certa" como resultado — cada uma tem um número ou um estado esperado, e a quarta é a que mais demonstra o valor deste laboratório sobre o L23.
# provas.sh — cinco medicoes; nenhuma conclusao vem de "a regra parece certa"
PROJETO=ffv-lab
BUS="${PROJETO}-pedidos-eventbus"
# ── Prova 1: o evento casa a regra certa, e SO ela ────────────────────────
# Publica um PedidoPago acima do limite de risco. Espera Fraude E
# Contabilidade dispararem; Notificacao tambem, porque a regra dela casa
# PedidoPago de qualquer origem.
aws events put-events --entries "$(cat <<'JSON'
[{
"Source": "faturamento", "DetailType": "PedidoPago",
"EventBusName": "ffv-lab-pedidos-eventbus",
"Detail": "{\"pedidoId\":\"p-001\",\"valor\":8200,\"moeda\":\"BRL\",\"clienteHash\":\"abc\",\"schemaVersion\":\"1.1\"}"
}]
JSON
)"
sleep 5
aws cloudwatch get-metric-statistics --namespace AWS/Events \
--metric-name Invocations --dimensions Name=RuleName,Value=${PROJETO}-risco-pagamento-alto \
--start-time "$(date -u -v-5M +%FT%TZ)" --end-time "$(date -u +%FT%TZ)" \
--period 60 --statistics Sum --query "Datapoints[].Sum" --output text
# Esperado: 1. Zero significa que o padrao da regra nao casou — confira
# "source", "detail-type" e o operador numeric em "detail.valor".
# ── Prova 2: alvo fora do ar entrega na DLQ, nao perde o evento ───────────
aws lambda put-function-concurrency --function-name ffv-lab-fraude --reserved-concurrent-executions 0
aws events put-events --entries file://evento-risco-alto.json
sleep 300 # tentativas + MaximumEventAgeInSeconds do alvo
aws sqs get-queue-attributes --queue-url "$(terraform output -raw dlq_fraude_url)" \
--attribute-names ApproximateNumberOfMessages --query "Attributes.ApproximateNumberOfMessages" --output text
aws lambda put-function-concurrency --function-name ffv-lab-fraude --reserved-concurrent-executions 5
# Esperado: pelo menos 1 mensagem na DLQ. Zero significa que o
# DeadLetterConfig nao esta no ALVO, so declarado e nao aplicado.
# ── Prova 3: contrato aditivo nao quebra, contrato quebrado rejeita ───────
# 3a. Publica SEM schemaVersion — simula produtor antigo ou evento malformado.
aws events put-events --entries '[{
"Source":"faturamento","DetailType":"PedidoPago",
"EventBusName":"ffv-lab-pedidos-eventbus",
"Detail":"{\"pedidoId\":\"p-002\",\"valor\":9000,\"moeda\":\"BRL\",\"clienteHash\":\"xyz\"}"
}]'
aws logs filter-log-events --log-group-name /aws/lambda/ffv-lab-fraude \
--filter-pattern "contrato desconhecido" --query "events[].message" --output text
# Esperado: uma linha de rejeicao explicita — nao um erro generico de parse.
# ── Prova 4: replay reconstroi o intervalo perdido ─────────────────────────
# Espere pelo menos 10 min apos os eventos de teste (atraso de gravacao do
# archive) antes de disparar o replay, ou parte deles ficará de fora.
aws events start-replay --replay-name recuperar-fraude-teste \
--event-source-arn "$(terraform output -raw archive_arn)" \
--event-start-time "$(date -u -v-15M +%FT%TZ)" \
--event-end-time "$(date -u +%FT%TZ)" \
--destination '{"Arn":"'"$(terraform output -raw bus_arn)"'","FilterArns":["'"$(terraform output -raw regra_fraude_arn)"'"]}'
aws events describe-replay --replay-name recuperar-fraude-teste \
--query "{estado:State,ultimoReplayed:EventLastReplayedTime}" --output table
# Esperado: State eventualmente COMPLETED, e o consumidor processa de novo
# os eventos do intervalo — confira pelo Id do evento com "replay-name" no
# metadado, distinguindo do PutEvents original.
# ── Prova 5: adicionar consumidor nao gera diff em Pedidos nem Faturamento ─
git log --oneline -- terraform/faturamento/ | head -1 # SHA antes de adicionar Contabilidade
terraform apply -target=aws_cloudwatch_event_rule.fechamento_contabil -auto-approve
git log --oneline -- terraform/faturamento/ | head -1 # mesmo SHA de antes
# Esperado: os dois comandos devolvem o MESMO commit — prova de que nenhuma
# linha do dominio de Faturamento mudou para a Contabilidade nascer.
| Prova | Comando | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|---|
| 1 · A regra certa casa o evento | `put-events` + métrica `Invocations` | contagem 1 na regra de risco | zero indica padrão que não casou — confira `source`, `detail-type` e o operador `numeric` |
| 2 · Alvo fora do ar entrega na DLQ | zera concorrência do Lambda, espera esgotar tentativa | ao menos 1 mensagem na fila morta | zero indica `DeadLetterConfig` declarado e não aplicado ao alvo certo |
| 3 · Contrato desconhecido é rejeitado | evento sem `schemaVersion` + log do consumidor | linha explícita de rejeição no log | ausência da linha indica que o consumidor trata campo ausente como padrão, não como erro |
| 4 · Replay reconstrói o intervalo | `start-replay` + `describe-replay` | estado `COMPLETED` e evento reprocessado, marcado com `replay-name` | estado que não avança após dez minutos indica que o padrão do archive não cobre a origem do evento de teste |
| 5 · Consumidor novo não toca o produtor | diff do repositório de Faturamento antes/depois | mesmo commit nos dois momentos | diff não vazio indica que a regra nova ainda dependeu de mudança no produtor — o requisito central não foi cumprido |
O time de Contabilidade precisa saber de PedidoPago (publicado pelo Faturamento) e também de PedidoCancelado (publicado por Pedidos), decidindo por conteúdo se o valor passa de um limite — sem que Faturamento ou Pedidos precisem alterar código algum. Qual arquitetura resolve isso com menor acoplamento?
Quebrar de propósito: três falhas e o diagnóstico
As três acontecem de verdade, e as três parecem "a regra não está funcionando" à primeira vista. O que as separa é onde se olha.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| Padrão que não casa por diferença de tipo | publique `"valor": "8200"` (string) quando a regra espera número | a regra nunca invoca o alvo, sem erro visível em lugar nenhum | métrica `Invocations` da regra permanece em zero mesmo com `PutEvents` bem-sucedido | o produtor precisa respeitar o TIPO declarado no contrato, não só o nome do campo |
| DLQ sem permissão de escrita | crie a fila sem a política de recurso que autoriza o EventBridge a escrever nela | evento simplesmente desaparece após esgotar tentativas — nem chega ao alvo nem à DLQ | CloudTrail em `SendMessage` recusado na fila; métrica `DeadLetterInvocations` não sobe | política de recurso da fila precisa permitir `events.amazonaws.com` explicitamente |
| Archive sem cobrir a origem do evento | configure o `event_pattern` do archive só para `source: ["pedidos"]` e teste replay de um evento de `faturamento` | replay conclui `COMPLETED` sem reprocessar nada | `describe-archive` mostra `EventCount` zero para o intervalo testado | o padrão do archive precisa incluir toda origem que algum dia vai precisar de replay |
A falha irreversível: apagar o archive achando que é só histórico
Um archive apagado não é recuperável, e junto com ele some a única forma nativa de reconstruir um consumidor que ficar fora do ar por horas. Diferente de uma tabela do banco, não existe snapshot automático de archive do EventBridge — a proteção é operacional: exigir aprovação explícita para `terraform destroy` neste recurso específico, e nunca incluí-lo num destroy de ambiente inteiro sem confirmação separada.
Segurança: quem pode publicar em nome de quem
Um barramento compartilhado por seis domínios move o risco: a pergunta deixa de ser "este serviço tem acesso ao banco de outro?" e passa a ser "este serviço pode PUBLICAR um evento fingindo ser outro domínio?" — e a resposta, sem a política de recurso, é sim.
| Risco | Probabilidade | Impacto | Controle preventivo | Detecção | Resposta |
|---|---|---|---|---|---|
| Produtor forja o `source` de outro domínio | baixa | alto | condição na política de recurso amarrando `PrincipalTag` ao `source` declarado | CloudTrail em `PutEvents` com origem e tag divergentes | revogar a permissão do papel; auditar todo evento publicado com aquele source |
| Regra dá poder de negócio a quem só devia rotear | média | alto | lógica de decisão vive no código do consumidor, nunca no padrão da regra | revisão de PR do Terraform tratando regra complexa como sinal de alerta | mover a lógica identificada para o Lambda; simplificar a regra |
| Dado sensível no `detail` do evento | média | alto | hipótese declarada exclui PCI; hash em vez de identificador direto do cliente | busca por padrão de dado sensível no archive e nos logs de DLQ | expurgar do archive quando aplicável; corrigir o produtor |
| Fila de DLQ acumulando payload sensível sem retenção definida | média | médio | retenção explícita na fila; a mesma disciplina de dado sensível vale para a DLQ | auditoria de idade média das mensagens na fila | reduzir retenção; processar pendências antes de expirar |
| Papel de execução do Lambda com escopo maior que o alvo precisa | média | médio | papel de invocação (`role_arn` do alvo) restrito à ação de invocar, sem escopo de negócio | IAM Access Analyzer sobre uso real | derivar a política do uso medido — é o L41 |
Por que a condição usa `PrincipalTag`, e não uma lista de ARNs
Uma lista de ARNs de papéis autorizados cresce a cada novo produtor e vira mais um lugar a lembrar de atualizar. A tag no papel IAM (`dominio-eventbridge`) é atribuída uma vez, na criação do papel de cada domínio, e a política de recurso do bus nunca muda quando um domínio novo entra — desde que a tag certa esteja lá.
Observabilidade: as perguntas que o painel de regras responde
Um painel de barramento de eventos tem uma função estreita: dizer se cada regra está entregando, e onde o evento parou quando não entrega.
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| A regra está sendo invocada? | `Invocations` por `RuleName` | zero com `PutEvents` bem-sucedido indica padrão que não casa | qualquer publicação esperada sem invocação correspondente |
| Quantas invocações falharam? | `FailedInvocations` por `RuleName` | alvo rejeitando, permissão faltando ou payload inválido para o alvo | > 0 sustentado por mais de 2 períodos |
| Alguma regra está sendo limitada? | `ThrottledRules` | volume de eventos acima da capacidade configurada da regra ou do alvo | qualquer valor > 0 |
| Quantos eventos foram para a fila morta? | `DeadLetterInvocations` por alvo | tentativas esgotadas — o evento não desapareceu, mas também não foi processado | > 0 já é sinal de investigar, mesmo sem limiar de alarme |
| O evento demora para chegar ao alvo? | `IngestionToInvocationStartLatency` | atraso entre a entrada no bus e a primeira tentativa de entrega | p99 acima do que o consumidor mais sensível a atraso tolera |
| Quem publicou fora do papel esperado? | CloudTrail em `PutEvents` cruzado com `PrincipalTag` | origem forjada ou papel com permissão além do esperado | qualquer identidade fora da lista de domínios conhecidos |
A métrica que não existe, e por isso não adianta procurar
Não existe uma métrica nativa de "evento perdido sem chegar a nenhuma regra". Um evento publicado que não casa padrão nenhum simplesmente não gera `Invocations` em lugar algum — é um não-evento do ponto de vista de observabilidade. A forma de pegar isso é contar `PutEvents` bem-sucedidos no produtor e comparar com a soma de invocações esperadas, não procurar um alarme que a AWS não oferece pronto.
Escala: 10, 10 mil, 1 milhão de eventos por dia
| Volume | O que acontece | O que passa a doer | O que fazer |
|---|---|---|---|
| 10 eventos/dia | cada regra invoca o alvo sem contenção nenhuma | nada; é o cenário deste laboratório | nada |
| 10 mil eventos/dia (~0,1/s) | padrão de conteúdo continua barato de avaliar | nada de infraestrutura; o risco passa a ser contrato não versionado com mais times publicando | schema registry deixa de ser opcional a essa escala |
| 1 milhão de eventos/dia (~12/s) | aproxima-se de cotas de conta para `PutEvents` e de invocação por regra | throttling de regra (`ThrottledRules`) e concorrência do alvo Lambda viram gargalo real | confira as cotas atuais em Service Quotas antes de escalar; solicite aumento com folga, não no dia do pico |
| Pico repentino (campanha, Black Friday) | EventBridge absorve e tenta entregar; o alvo é quem sofre primeiro | concorrência reservada do Lambda esgota; DLQ enche rápido se o alvo não acompanha | concorrência provisionada no alvo crítico; `MaximumEventAgeInSeconds` calibrado ao SLA real do consumidor |
| Falha de região (não de AZ) | EventBridge já é multi-AZ, gerenciado pela AWS, dentro de uma região | o failure mode real aqui é REGIÃO, não AZ — diferente do L02 | bus replicado em outra região é decisão de nível 5 da evolução; não é padrão deste laboratório |
O gargalo que aparece primeiro não é o barramento
EventBridge escala como serviço gerenciado sem que você provisione capacidade. O gargalo real, na prática, é sempre o ALVO: concorrência do Lambda, capacidade de escrita do banco que ele atualiza, ou a cota de outro serviço que ele chama. Dimensionar o barramento é o problema errado a resolver primeiro.
Custo: o que este laboratório acrescenta à fatura
A cobrança acompanha o uso, não a existência do barramento — não há um cluster ocioso cobrando enquanto ninguém publica.
| Cenário | Volume | O que acrescenta | Tendência | Otimização |
|---|---|---|---|---|
| Protótipo | ~40 mil eventos/dia, 3 regras | PutEvents publicado, invocação de regra e alguns MB de archive | baixa e previsível | nenhuma; otimizar aqui é gastar atenção onde não há dinheiro |
| Produção pequena | 6 domínios, ~200 mil eventos/dia | volume de PutEvents cresce linear com o número de produtores ativos | linear com o número de domínios | consolidar eventos correlatos num único PutEvents em lote quando possível |
| Alta escala | 1 milhão+ eventos/dia, dezenas de regras | o custo de invocação de regra multiplica pelo número de regras que casam cada evento | pode crescer mais rápido que o volume de eventos, se regras se sobrepõem demais | revisar padrões sobrepostos; um evento que casa 8 regras paga 8 invocações |
| Dimensão | Cobra por | Cuidado |
|---|---|---|
| PutEvents no bus custom | evento publicado, acima da camada gratuita | cresce linear com produtores; eventos entre serviços AWS no bus default não entram nesta conta |
| Invocação de alvo por regra | por regra que casa, não por evento | um evento que casa 3 regras é cobrado como 3 invocações, não como 1 |
| Armazenamento do archive | GB-mês retido | sem `retention_days`, cresce indefinidamente |
| Replay | como novos `PutEvents` | um replay de um intervalo longo pode gerar volume comparável ao tráfego original daquele período |
| Schema registry | armazenamento de schema, geralmente dentro de camada gratuita generosa | não é o custo que domina a fatura deste laboratório |
| Alvo (Lambda, SQS) | segue o preço do próprio serviço, não do EventBridge | a invocação do EventBridge é barata; o que ela dispara pode não ser |
Onde ficar de olho, sem número fixo neste texto
Preço e cota mudam mais rápido que este módulo é revisado. Use o AWS Pricing Calculator para o cenário real, e o console de Service Quotas para conferir limites de `PutEvents` por segundo e regras por bus antes de dimensionar para produção.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | roteamento visível em `list-rules`, sem precisar ler código de cada produtor | nenhum alarme automático para "evento não casou regra nenhuma" | métrica derivada comparando `PutEvents` do produtor com soma de invocações esperadas | média |
| Segurança | política de recurso condiciona publicação à tag de domínio do papel IAM | schema registry pode conter exemplo de payload com dado sensível se mal configurado | revisão de conteúdo de schema antes de registrar; mascarar exemplo | alta |
| Confiabilidade | DLQ por alvo evita perda silenciosa; archive permite replay | sem ordenação garantida entre eventos do mesmo agregado | SQS FIFO (L22) para o subconjunto que precisa de ordem estrita | média |
| Eficiência de performance | roteamento por padrão é avaliado pelo serviço gerenciado, sem código seu no caminho | regras com padrão muito amplo avaliam e descartam volume desnecessário | refinar o padrão para reduzir invocação que nunca deveria ter disparado | baixa |
| Otimização de custos | cobrança proporcional ao uso, sem capacidade ociosa provisionada | evento que casa muitas regras multiplica custo de invocação | auditar sobreposição de padrões entre regras periodicamente | baixa |
| Sustentabilidade | nenhum recurso fixo ligado 24h além do bus e do archive, que são leves | archive sem prazo de retenção cresce indefinidamente | `retention_days` já configurado; revisar o número 90 com base em uso real | baixa |
Evolução em níveis: do ponto a ponto ao dado e à IA
A terceira arquitetura não é um desenho: é a resposta a QUANDO trocar de desenho. Cada nível resolve um risco e compra outro.
Chamada direta entre serviços, como o desenho mínimo deste laboratório. Legítimo com dois ou três domínios.Bus custom, regras nomeadas por padrão de conteúdo, schema registry versionado, DLQ por alvo, archive com replay.Regras entre contas, com o bus de um domínio sensível numa conta separada e política de recurso autorizando parceiros específicos.EventBridge Pipes com enriquecimento antes do alvo, idempotência padronizada numa biblioteca compartilhada, DLQ com reprocessamento automatizado.Bus replicado entre regiões para o subconjunto de eventos que precisa sobreviver a falha regional.O histórico de eventos, já no archive, alimenta um lago de dados para detectar padrão de fraude entre domínios que uma regra de conteúdo simples não capturaria — é o terreno do L96.A ordem não é negociável, e o motivo é concreto
Correlação entre domínios (nível 6) depende de contrato estável entre eles (nível 2) — sem schema versionado, o modelo aprenderia sobre um formato que muda sem aviso, e cada mudança de campo silenciosamente corromperia o treinamento. É a mesma dívida que quebrou a checagem de risco, um nível acima.
Onde IA entra nesta arquitetura, e onde não entra
Neste módulo, IA não resolve o problema central. Roteamento por padrão de conteúdo é comparação determinística — `source`, prefixo, faixa numérica — e um modelo não decide isso melhor do que uma condição explícita e testável.
Há um lugar onde IA acrescentaria valor real, adiante deste laboratório: detectar fraude que só aparece na CORRELAÇÃO entre eventos de domínios diferentes — um padrão de compra em Pedidos combinado com histórico de estorno em Faturamento e geolocalização inconsistente em Estoque, nenhum dos quais isoladamente dispara uma regra de conteúdo simples.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria? | correlacionar sinal fraco entre múltiplos domínios que, isolado, nenhuma regra de padrão simples capturaria |
| Por que uma regra de conteúdo não bastaria? | porque o sinal de risco não está num campo do evento — está na COMBINAÇÃO de eventos ao longo do tempo, algo que padrão de conteúdo do EventBridge não expressa |
| De onde viriam os dados? | o archive já existente deste barramento, correlacionado por `clienteHash` entre os tipos de evento dos vários domínios |
| Qual o risco? | falso positivo bloqueando cliente legítimo, e falso negativo liberando fraude — exige avaliação com dado retido, limiar calibrado e revisão humana para decisão de alto valor |
| Por que não agora? | a Cadência tem seis domínios recém-conectados e contrato recém-versionado; o dado histórico correlacionável ainda é pequeno e o próprio contrato ainda está estabilizando |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para "decidir que regra criar" ou "sugerir o padrão de evento certo" troca uma decisão de design — que precisa de dono, revisão e rastreabilidade — por uma sugestão probabilística sobre a TOPOLOGIA do sistema. Regra de roteamento é arquitetura; arquitetura decidida por modelo sem revisão humana é o tipo de decisão que ninguém consegue explicar depois.
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 |
|---|---|---|---|---|---|
| Regra do EventBridge decidindo lógica de negócio | o padrão de conteúdo aceita condição complexa, e parece "configuração" em vez de "código" — sem PR de revisão de código, sem teste automatizado | lógica de negócio sem versionamento de código, sem teste fácil, espalhada em várias regras que ninguém lê como um todo | comportamento de negócio muda quando alguém edita uma regra no console, sem passar por revisão | regra decide QUEM recebe; o código do consumidor decide O QUE fazer | nunca; filtro simples de roteamento não é a mesma coisa que decisão de negócio |
| Publicar eventos de negócio no bus default | é o bus que já existe, sem precisar criar recurso novo | mistura evento de negócio com evento nativo de serviço AWS na mesma cota e nas mesmas regras | regra de negócio precisa de filtro extra só para excluir tráfego que a AWS gera sozinha | bus CUSTOM dedicado ao domínio de negócio | protótipo descartável, nunca em ambiente que vira produção |
| Mudança de campo sem versionar o contrato | parece só uma refatoração de nome, sem efeito colateral óbvo no próprio serviço | consumidor de outro domínio quebra silenciosamente, sem erro visível no produtor que mudou | painel de risco "funcionando" sem processar nada de fato, como no incidente deste módulo | schema versionado, com período de transição publicando os dois formatos | nunca em campo que já tem consumidor conhecido; tudo bem em campo interno nunca exposto no contrato |
| Alvo sem DLQ configurada | o caminho feliz funciona no teste, e DLQ parece só burocracia extra | evento que esgota tentativas desaparece sem rastro, sem alerta | "sumiu um pedido" investigado por horas sem log nenhum que explique | DeadLetterConfig em todo alvo que processa algo que importa perder | alvo genuinamente descartável, como métrica de debug que não afeta negócio |
| Assumir que replay preserva ordem exata | o nome "replay" sugere reprodução fiel do que aconteceu | consumidor que depende de ordem processa fora de sequência e produz estado inconsistente | dado reconciliado errado depois de um replay, difícil de depurar porque parece "os dados certos, na ordem errada" | projetar o consumidor para ser idempotente e tolerante a ordem, ou usar SQS FIFO para o que exige ordem estrita | quando o consumidor já é comutativo — a ordem genuinamente não importa para o resultado final |
| Uma regra por consumidor com padrão idêntico copiado e colado | copiar uma regra existente é mais rápido que entender o padrão dela | mudar o contrato exige lembrar de atualizar N regras quase iguais, e uma ficar desatualizada sem ninguém notar | dois consumidores do "mesmo" evento reagindo de forma diferente porque um padrão ficou para trás | revisar se os consumidores realmente precisam de padrões distintos, ou se um único padrão com múltiplos alvos resolve | quando os consumidores genuinamente precisam de subconjuntos diferentes do mesmo tipo de evento |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| Regra nunca invoca o alvo | padrão de conteúdo não casa o evento real | compare o JSON efetivamente publicado com o `event_pattern` da regra, campo a campo, inclusive tipo | `Invocations` da regra em zero com `PutEvents` bem-sucedido | ajuste o padrão; tipo (string vs número) é o erro mais comum |
| Evento chega duplicado no consumidor | comportamento normal de entrega pelo menos uma vez, não uma falha | confira se o `Id` do evento se repete entre as duas entregas | log do consumidor com o campo `id` do envelope | idempotência por `Id` do evento, não decisão de "isso não deveria acontecer" |
| Evento não aparece no replay | archive não cobre a origem, ou replay disparado antes do atraso de gravação | `describe-archive` para o `EventCount` do intervalo; confirme os dez minutos de espera recomendados | padrão do archive e o timestamp de início do replay | ampliar o padrão do archive; esperar a janela de gravação antes de disparar |
| `PutEvents` recusado com erro de acesso | papel IAM sem a tag exigida pela política de recurso do bus | compare a tag do papel que chamou `PutEvents` com a condição da política | CloudTrail no evento `PutEvents` recusado | atribuir a tag `dominio-eventbridge` correta ao papel do produtor |
| Fila DLQ crescendo sem investigação | alvo falhando de forma persistente, não transitória | leia o payload de uma mensagem da DLQ e reproduza a falha isolada no alvo | log de erro do alvo no momento das tentativas esgotadas | corrigir o alvo antes de reprocessar; reprocessar sem corrigir só reabastece a mesma fila |
| Replay "funcionou" mas dado ficou inconsistente | consumidor não idempotente, ou depende de ordem que o replay não garante | compare o resultado esperado com o real para eventos do mesmo agregado | lógica de aplicação do consumidor, não o EventBridge | tornar o consumidor idempotente e tolerante a ordem antes de confiar em replay |
| Consumidor novo não recebe nada mesmo com regra criada | permissão de invocação do alvo não foi concedida ao EventBridge | confira a resource policy do Lambda/SQS alvo, não só a regra | `iam:GetPolicy` do alvo; erro de permissão no `FailedInvocations` | a regra precisa de permissão para invocar o alvo, além de existir |
A pergunta que resolve metade destes casos
Antes de mexer em regra, pergunte: o evento chegou ao BUS? Confirme com `PutEvents` bem-sucedido e, se preciso, um archive temporário sem filtro. Só depois de confirmar a chegada vale investigar por que uma regra específica não casou ou por que um alvo específico não respondeu — são camadas diferentes do mesmo caminho.
Limpeza: o que o destroy não leva
O archive é o recurso deste laboratório que mais precisa de atenção na limpeza: ele guarda dado, cobra por armazenamento e, uma vez apagado, não volta.
# 1. Confirme que ninguem precisa mais do archive ANTES de tocar nele —
# apagar e IRREVERSIVEL, diferente da maioria dos recursos deste modulo.
aws events describe-archive --archive-name ffv-lab-archive \
--query "{eventos:EventCount,tamanho:SizeBytes}" --output table
# 2. Cancele replay em andamento antes do destroy, ou ele fica orfao.
aws events list-replays --query "Replays[?State=='RUNNING'].ReplayName" \
--output text | xargs -n1 -I{} aws events cancel-replay --replay-name {}
# 3. Derrube o que o Terraform administra — bus, regras, archive, schema.
terraform destroy -auto-approve
# 4. FILAS DLQ: o destroy remove a fila, mas NAO garante que as mensagens
# dentro dela foram tratadas. Drene antes, ou perca o que estava pendente
# de investigacao.
aws sqs receive-message --queue-url "$(terraform output -raw dlq_fraude_url)" \
--max-number-of-messages 10 --query "Messages[].Body" --output json
# 5. Confirme que nao ha regra ORFA apontando para um bus ja apagado, se
# voce interrompeu o destroy no meio.
aws events list-rules --query "Rules[].{nome:Name,bus:EventBusName}" --output table
# 6. Prova final: nada com o 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 |
|---|---|---|---|
| Bus customizado e regras | sim | não | sem uso, não há evento nem invocação a cobrar |
| Archive | sim, mas o CONTEÚDO é irrecuperável depois | sim, GB-mês | diferente de um snapshot de banco, não existe undelete para archive do EventBridge |
| Filas DLQ com mensagem dentro | sim, e a mensagem some junto | sim, enquanto existir | destruir sem drenar perde qualquer pendência de investigação que estava ali |
| Schema registrado | sim | praticamente nada | custo de armazenamento de schema é marginal, mas o CONTRATO documentado some junto |
| Replay concluído (metadado) | não é afetado pelo destroy do bus | não | o próprio EventBridge apaga o metadado de replay depois de 90 dias, por conta |
| Papéis IAM de invocação | sim, se em Terraform | não | papel criado à mão fora do estado do Terraform não é removido junto |
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Consumidor novo exige mudar o produtor | regra de padrão de conteúdo no bus custom | a decisão de roteamento sai do código e vira configuração nomeada e revisável |
| Campo renomeado quebra consumidor sem aviso | schema registry com versão explícita | contrato declarado é o que torna mudança incompatível uma decisão, não um acidente |
| Consumidor fora do ar perde eventos | archive + replay por janela de tempo | recupera o intervalo exato sem pedir aos produtores que publiquem de novo |
| Alvo instável perde evento silenciosamente | DLQ por alvo com política própria | granularidade fina permite tratar cada consumidor pela criticidade real dele |
| Origem forjada em nome de outro domínio | política de recurso do bus com condição de tag | amarra o papel IAM ao `source` que ele pode declarar, sem lista de ARNs para manter |
| Lógica de negócio escondida em regra | decisão sempre no código do consumidor | regra roteia; código decide — a linha que evita o antipadrão da seção 24 |
O fluxo em até 10 passos
Produtor chama PutEvents no bus custom → EventBridge confere a forma do envelope → cada regra ativa avalia o padrão de forma independente → cópia filtrada segue para o archive em paralelo → alvo é invocado com sua própria política de repetição → esgotada a tentativa, o evento vai para a DLQ daquele alvo → consumidor confere a versão do contrato antes de processar → consumidor deduplica pelo Id do evento → se necessário, replay reconstrói o intervalo perdido, agrupado por minuto → consumidor idempotente processa de novo sem efeito colateral duplicado.
Perguntas frequentes
❓ SNS já faz fanout de um evento para vários assinantes. Por que preciso do EventBridge?
❓ O que muda no bus custom em relação ao bus default do EventBridge?
❓ Preciso registrar o schema manualmente, ou o EventBridge descobre sozinho?
❓ Adicionar um campo novo a um evento quebra os consumidores existentes?
❓ O replay do archive entrega os eventos na mesma ordem em que aconteceram?
❓ Quantos replays simultâneos o EventBridge permite?
❓ O que acontece quando o alvo de uma regra esgota as tentativas de entrega?
❓ Uma regra do EventBridge pode decidir lógica de negócio, tipo aprovar um pedido?
Fixando
O time de Faturamento planeja renomear o campo `valorTotal` para `valorLiquido` no evento PedidoPago. Fraude e Contabilidade leem esse campo hoje. Qual caminho evita quebrar os dois consumidores sem exigir coordenação simultânea entre os três times?
Depois de um replay do archive para recuperar 40 minutos em que o consumidor de Fraude ficou fora do ar, o time nota que alguns pedidos foram avaliados duas vezes — uma durante o replay e outra que já tinha sido processada por um retry anterior à queda. O que isso revela sobre como o consumidor deveria ter sido escrito desde o início?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L21 (Lambda), L22 (fila com DLQ e idempotência) e L23 (fanout com SNS/SQS) concluídos |
| Conhecimentos adquiridos | a diferença real entre fanout (SNS/SQS) e roteamento por conteúdo (EventBridge); contrato de evento versionado e evolução aditiva vs quebra; archive e replay como recuperação de consumidor; DLQ por alvo; o antipadrão de regra decidindo lógica de negócio |
| Limitação que fica | sem ordenação garantida entre eventos do mesmo agregado; para isso, o domínio continua usando SQS FIFO do L22 ao lado do barramento |
| Próximo exemplo recomendado | L25 — Step Functions ou código, para quando o problema deixa de ser "quem precisa saber" e passa a ser "em que ordem executar, com retry e compensação" |
| Também habilitado por este módulo | L96 (agente de operação que diagnostica incidente) depende do histórico de eventos e do padrão de roteamento construídos aqui |
| Data da última validação técnica | 7 de agosto de 2026 |
Documentação oficial consultada: Archiving and replaying events in Amazon EventBridge — retenção do archive, atraso de gravação, até 10 replays simultâneos por conta/região e o agrupamento em lotes de 1 minuto por horário do evento; Dead-letter queues — configuração por alvo, valores padrão de `MaximumRetryAttempts` e `MaximumEventAgeInSeconds`, e a restrição a SQS como destino; Schema registries in Amazon EventBridge — discoverer por inferência versus registro manual, e os dois formatos de schema suportados. Os valores de preço não aparecem neste módulo por decisão: use o AWS Pricing Calculator, porque preço varia por região e envelhece mais rápido que o conteúdo.
O que não foi verificado, e você deve conferir na sua conta
Os nomes de métrica do CloudWatch citados na seção de observabilidade (`Invocations`, `FailedInvocations`, `ThrottledRules`, `DeadLetterInvocations`, `IngestionToInvocationStartLatency`) refletem a documentação consultada na data acima; confirme no console CloudWatch da sua conta, porque métrica nova é adicionada ao serviço sem aviso prévio neste tipo de módulo. Da mesma forma, a disponibilidade de código de binding gerado a partir do schema registry para C#/.NET não foi confirmada nesta revisão — a documentação de referência historicamente cobre Java, Python e TypeScript; trate o schema como fonte de verdade e gere o DTO manualmente até confirmar suporte a .NET no console.
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…