Lab 65 — Catálogo e ETL: Glue Catalog, crawler, job
O problema, e a empresa que o tem
A Cadência é um marketplace de material de construção com 40 lojistas parceiros. O time de dados tem três pessoas e mantém um lake em camadas no S3 — bronze, prata e ouro, do L62 — onde pedidos chegam brutos em bronze/pedidos/dt= e saem tratados em prata/pedidos/dt=. Duda escreveu, seis meses atrás, o script que faz essa conversão.
Duda mudou de squad na semana passada. Theo, analista novo, precisa montar um relatório sobre os mesmos pedidos e não tem acesso ao código dela — só a um documento de arquitetura que ninguém atualiza há meses. Ele tenta consultar a tabela no Athena com uma definição copiada desse documento, e a consulta falha numa coluna que existe no arquivo mas não na definição: `canal_pedido`, adicionada há duas semanas pelo time de produto.
O problema não é a ausência de Duda — é que o schema nunca existiu em lugar nenhum além do código dela. Um catálogo central resolveria isso de um jeito que nenhuma wiki resolve: sendo alimentado automaticamente, e não por alguém lembrando de atualizar um documento.
O que este laboratório NÃO é
Não é sobre formato de arquivo ou tamanho de partição (isso é o L64, e este módulo assume que ele já foi resolvido). Não é sobre qualidade de dado com contrato e quarentena (L70) — aqui o job faz um cast explícito e para; validar o VALOR do dado é outro laboratório. E não é sobre quem pode ver qual coluna (L69) — o catálogo deste módulo é lido por qualquer pessoa com acesso ao banco, sem permissão fina nenhuma ainda.
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 o Athena não encontra dado que já está fisicamente no S3.
- Diferenciar o papel do crawler (inferir e popular) do papel do catálogo (armazenar e servir).
- Configurar a política de mudança de schema do crawler e justificar UPDATE_IN_DATABASE contra LOG, e DEPRECATE_IN_DATABASE contra DELETE_FROM_DATABASE.
- Encadear crawler e job por evento, dentro de um Glue Workflow, sem depender de dois agendamentos coincidirem.
- Escrever um job de ETL que roda duas vezes sobre o mesmo dia sem duplicar uma linha.
- Distinguir o que Job Bookmarks protegem do que a sobrescrita de partição protege.
- Atualizar a tabela do catálogo a partir do próprio job, sem uma segunda varredura completa.
- Provar idempotência com número — contagem de linha e de arquivo antes e depois de um rerun.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Papel do Glue Data Catalog | DEA-C01, SAA-C03 | metastore central, compatível com Hive Metastore | que Athena, EMR e Redshift Spectrum leem a MESMA tabela, sem cópia de metadado |
| Crawler: o que ele infere e o que decide | DEA-C01, DAS-C01 | varre o S3, infere schema, aplica SchemaChangePolicy | que a inferência pode errar, e que a política decide o que fazer quando erra |
| UpdateBehavior / DeleteBehavior | DEA-C01 | UPDATE_IN_DATABASE, DEPRECATE_IN_DATABASE, explícitos | a diferença entre aceitar mudança de tipo e apagar registro de partição sumida |
| Glue Workflows e gatilhos | DEA-C01 | gatilho agendado inicia o crawler; condicional inicia o job por estado | que a dependência crawler→job é do próprio serviço, não de coordenação de horário |
| Job Bookmarks vs idempotência de escrita | DEA-C01 | bookmark evita reler arquivo; overwrite de partição evita duplicar rerun | que são mecanismos diferentes, para riscos diferentes |
| Atualização de catálogo pelo próprio job | DEA-C01 | enableUpdateCatalog + updateBehavior no sink | que evita um segundo crawler só para o catálogo "perceber" uma partição nova |
| Particionamento e catálogo | DEA-C01, DAS-C01 | push_down_predicate lendo só a partição do dia | que a leitura filtrada depende de a partição já estar catalogada |
| Custo de crawler por frequência | DEA-C01 | agendado 1x/dia, não por evento de arquivo | schema muda raramente comparado à chegada de dado; crawlear por arquivo multiplica custo |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve um crawler que "apagou tabelas que existiam ontem" depois de uma mudança de configuração e pede a causa. A resposta é a política de exclusão: com `DELETE_FROM_DATABASE`, o crawler remove do catálogo tudo que não encontrou na varredura mais recente — mesmo que os arquivos continuem no S3, mesmo que a ausência seja só um erro temporário no caminho configurado.
Requisitos, e como cada um muda o desenho
Requisito que não muda uma linha de configuração é intenção, não requisito. A coluna da direita é onde cada um deixou marca.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Schema descoberto por consulta, não por pergunta | qualquer analista, sem depender de pessoa | obriga Glue Data Catalog central, alimentado por crawler — não DDL manual em wiki |
| Mais de um motor lendo a mesma tabela | Athena hoje; Redshift Spectrum é meta do L68 | catálogo compatível com Hive Metastore, um único registro, sem cópia de metadado por motor |
| Reprocessar um dia não pode duplicar pedido | zero duplicata sob rerun | job com apagar-e-reescrever a partição inteira, não escrita por acréscimo |
| Mudança de schema não trava o pipeline em silêncio | decisão explícita, não default implícito | SchemaChangePolicy declarada — UpdateBehavior e DeleteBehavior, cada um por escrito |
| Custo do crawler contido | crawler roda 1x por dia, não por arquivo | gatilho agendado do Workflow, não crawler disparado por evento de PutObject |
| Falha do pipeline gera alerta, não silêncio | antes de uma hora de atraso perceptível | regra do EventBridge em Glue Job State Change = FAILED, com SNS |
Arquitetura mínima: o schema que só existe numa cabeça
Este é o desenho que a Cadência tinha até a semana passada, e ele é legítimo como ponto de partida: converte dado de verdade, todos os dias, com poucas linhas. O laboratório começa reproduzindo o defeito, porque um erro reproduzível é mais convincente que "a Duda sabia".
- → escreveu e agenda o script; o schema vive nos comentários dele
- → lê o pedido bruto do dia
- → grava Parquet convertido, sem registrar em catálogo nenhum
- → copia um CREATE EXTERNAL TABLE de um documento antigo
- → consulta pelo caminho, com o schema que Theo escreveu à mão
- → nenhuma passagem de conhecimento formal acontece aqui
- Fora da AWS
- Compute
- Armazenamento
- Analytics
Este desenho publica dado de verdade, todo dia, com poucas linhas de Python. O defeito não é o script — é que nada além dele sabe o que ele decidiu sobre nome e tipo de coluna. Percorra os passos e repare que a consulta de Theo não falha por acaso: ela falha porque não existe onde perguntar.
- O schema mora só na cabeça de quem escreveu o script. Não existe documento vivo, não existe tabela: existe o código da Duda, e o que ele decidiu sobre nome e tipo de cada coluna nunca saiu dali. Enquanto ela está no time, isso funciona. No dia em que ela muda de squad, a decisão vai junto.
- O script lê e grava sem que nada além dele saiba a forma do dado. O arquivo Parquet resultante SABE o próprio schema — é um formato autodescritivo. O que não existe é um serviço que responda "quais colunas tem a tabela prata de pedidos" sem abrir um arquivo e olhar.
- Theo reconstrói de memória alheia, com um documento desatualizado. Sem catálogo, a única forma de o Athena enxergar uma tabela é alguém escrever a definição à mão. Theo faz o que qualquer pessoa razoável faria: procura a documentação mais recente que encontra — que já não é recente.
- A consulta expõe o defeito, e não o esconde. Uma coluna adicionada há duas semanas, `canal_pedido`, não existe na definição de Theo — a consulta falha com coluna não encontrada. O caso pior nem aparece aqui: um tipo levemente errado não falha, devolve valor incorreto sem aviso nenhum.
- Por que a equipe pequena convive com isso. Porque documentar um schema que já está "funcionando" parece trabalho sem retorno imediato — até a primeira pessoa nova. Com duas ou três pessoas que sempre conversam, o catálogo informal (perguntar) é mais rápido que escrever um. O defeito só aparece quando o time cresce ou alguém sai.
# DDL escrita a mao, com o schema de seis meses atras (5 colunas).
aws athena start-query-execution --query-string "
CREATE EXTERNAL TABLE pedidos (
pedido_id string, loja_id string, valor_centavos double,
status string, criado_em string
)
STORED AS PARQUET
LOCATION 's3://cadencia-lake/prata/pedidos/'" \
--result-configuration OutputLocation=s3://cadencia-lake/atenas-resultados/
aws athena start-query-execution \
--query-string "SELECT canal_pedido FROM pedidos LIMIT 1" \
--result-configuration OutputLocation=s3://cadencia-lake/atenas-resultados/
# Esperado: COLUMN_NOT_FOUND. A coluna existe no arquivo Parquet ha duas
# semanas — so nao existe na definicao que Theo escreveu.A falha visível não é a pior — a silenciosa é
Um `COLUMN_NOT_FOUND` pelo menos avisa. O caso que a Cadência não percebeu por três semanas foi outro: a DDL antiga declarava `valor_centavos` como `double`, mas o produtor mudou para inteiro em centavos. A consulta não falhou — devolveu um número plausível e errado, e um relatório de faturamento saiu com a casa decimal deslocada, sem nenhum erro no console.
Arquitetura para produção
Cada peça nova abaixo rastreia a uma linha da tabela de requisitos. A troca não é "acrescentar um crawler" ao desenho anterior — é trocar "alguém sabe" por "o catálogo sabe".
- → lista e amostra arquivos por partição
- → cria ou atualiza a tabela raw, conforme a SchemaChangePolicy
- → gatilho condicional do Workflow: crawl_state = SUCCEEDED inicia o job
- → lê a partição do dia via from_catalog
- → apaga e reescreve a partição do dia inteira
- → atualiza a tabela prata no mesmo passo (updateBehavior no sink)
- → Glue Job State Change = FAILED, via regra do EventBridge
- → consulta metadados: nome, tipo, localização
- → lê os arquivos Parquet indicados pelo catálogo
- Armazenamento
- Analytics
- Integração de apps
A troca não é "acrescentar um crawler" ao desenho anterior — é substituir "alguém sabe" por "o catálogo sabe", e "grava de novo" por "redesenha a partição inteira". Percorra os passos: cada peça nova resolve exatamente um requisito da seção anterior.
- O crawler não cataloga o arquivo; cataloga a tabela. Ele varre o prefixo bronze/pedidos/, infere schema a partir de uma amostra dos arquivos e registra (ou atualiza) UMA tabela no catálogo — uma entidade lógica que representa todos os arquivos daquele caminho, não um arquivo específico.
- A inferência pode errar, e a política decide o que fazer quando erra. Se um arquivo chega com `valor_centavos` como texto em vez de inteiro, o crawler encontra um conflito de schema entre arquivos. `UpdateBehavior` decide se ele aplica a mudança (`UPDATE_IN_DATABASE`) ou só registra em log; `DeleteBehavior` decide o que fazer com partição que sumiu da varredura — e são decisões distintas.
- O gatilho do Workflow substitui o intervalo fixo pela dependência real. Um gatilho condicional dentro do mesmo Glue Workflow observa o estado do crawler e só inicia o job quando ele termina com SUCEEDED. Não é "job às 3h, torcendo para o crawler das 2h já ter acabado": é "job depois que o catálogo, de fato, atualizou".
- O job lê pelo catálogo, não por um caminho decorado no script. `from_catalog` resolve localização, formato e schema a partir da tabela — se o crawler não rodou ou falhou, a tabela não existe ou está desatualizada, e essa falha é o sinal correto: nada para transformar sem catálogo confiável.
- Idempotência é apagar a partição inteira antes de escrever, não acrescentar. O escritor padrão do Glue ACRESCENTA arquivo ao prefixo existente. Sem apagar a partição do dia primeiro, rodar o job duas vezes duplicaria cada linha. A janela entre apagar e escrever não é atômica — é um risco aceito e documentado.
- O catálogo evolui com o dado, sem um segundo crawler. O próprio job atualiza a tabela prata no catálogo ao gravar, com `enableUpdateCatalog` e `updateBehavior`. Isso evita pagar uma segunda varredura completa só para o catálogo "perceber" que uma partição nova existe.
- Qualquer motor lê a mesma verdade, e a falha vira alarme, não silêncio. Athena consulta a tabela do catálogo sem DDL manual — o mesmo valeria para EMR ou Redshift Spectrum, porque o catálogo é compatível com Hive Metastore. E se o job falhar em qualquer etapa, uma regra do EventBridge notifica antes que alguém precise perceber olhando um painel.
A diferença estrutural em relação à arquitetura mínima não é a presença do crawler: é que nenhuma pessoa aparece no caminho crítico. Duda poderia sair do time de novo, e a tabela continuaria correta.
O ganho que mais economiza tempo de analista
Antes, toda pessoa nova gastava a primeira semana perguntando "o que tem essa tabela". Depois do catálogo, `DESCRIBE prata_pedidos` no Athena responde em segundos, sempre certo, porque quem o mantém atualizado é o job, não a memória de alguém.
O caminho do dado, ponta a ponta
Os nomes dos estados não são jargão: são a prova da ordem. O crawler termina com Succeeded, e só então o gatilho condicional do Workflow inicia o job — não um horário fixo depois. Isso é observável com get-workflow --include-graph, e a seção de provas faz exatamente isso.
O que o EventBridge entrega quando o crawler termina. Fonte, detail-type e o campo de estado estao documentados; os demais campos deste "detail" sao ILUSTRATIVOS — inspecione um evento real no seu console antes de decidir logica em cima de um campo que nao esta aqui confirmado.
{
"source": "aws.glue",
"detail-type": "Glue Crawler State Change",
"resources": ["arn:aws:glue:us-east-1:111122223333:crawler/cadencia-raw-pedidos"],
"detail": {
"crawlerName": "cadencia-raw-pedidos",
"state": "Succeeded",
"startTime": "2026-08-08T02:00:04Z"
}
}O que foi verificado nesse payload, e o que é ilustrativo
A AWS documenta `source: "aws.glue"` e `detail-type: "Glue Crawler State Change"` como o contrato do evento. Os demais campos do objeto `detail` acima — nome exato e formato — não foram confirmados nesta sessão; inspecione um evento real no seu console antes de escrever lógica que dependa de um campo que não seja `state`.
As decisões, e o que se perde em cada uma
📋 Um marketplace com 40 lojistas parceiros, time de dados de três pessoas, sem orçamento para operar um serviço a mais, e uma pergunta recorrente: "o que tem essa tabela mesmo?" — feita a cada analista novo que entra.
Resolve as duas dívidas separadamente, sem inventar peça nova na conta: o catálogo responde "quem sabe o schema" sem exigir que alguém pergunte a uma pessoa, e a sobrescrita de partição responde "o que acontece se eu rodar de novo" sem exigir um formato de tabela transacional novo. Nenhuma das duas depende do time crescer ou de disciplina de documentação — o catálogo é populado por um serviço gerenciado, e a idempotência é uma propriedade do código do job, não um acordo verbal.
Alt: Hive Metastore próprio em EC2 ou EMR — Funciona, e é o que o Glue Data Catalog imita por baixo (é compatível com o protocolo). Mas exige operar um banco relacional e um serviço sempre ligado — custo operacional que um time de três pessoas não tem, para ganhar um controle fino que este caso não precisa.
Alt: DDL manual documentado em wiki — É o antipadrão que a arquitetura mínima deste módulo mostra. Nunca acompanha a realidade, porque nada força a atualização quando o schema muda — a única garantia de sincronia é alguém lembrar.
Alt: AWS Lake Formation por cima do catálogo — Adiciona permissão fina por coluna e por linha, e é a resposta certa para "quem pode ver o quê" — um problema diferente de "o que existe". Presume que o catálogo técnico deste módulo já existe; é o L69, e depende deste.
Alt: Upsert transacional com Iceberg (S3 Tables) — Resolve idempotência de um jeito mais forte — merge em vez de reescrever a partição inteira — mas troca o formato de tabela e adiciona compaction e gestão de snapshot para operar. É o L67, e vale quando reescrever partição inteira começa a doer de verdade.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Metastore central | Glue Data Catalog gerenciado | Hive Metastore próprio em EC2/EMR; DDL manual em wiki | zero servidor para operar, e já é o que Athena/EMR/Redshift Spectrum esperam por padrão | menos controle fino sobre o protocolo Thrift — irrelevante para este caso |
| Política de schema | UPDATE_IN_DATABASE + DEPRECATE_IN_DATABASE | LOG (revisão manual) + DELETE_FROM_DATABASE | time de três pessoas não tem quem revise log toda manhã; apagar registro é risco maior que aceitar mudança automaticamente | mudança de schema por bug de upstream pode entrar sem revisão humana — mitigado pelos logs do crawler |
| Orquestração | gatilhos nativos do Glue Workflow | EventBridge customizado + Step Functions | zero peça nova na conta; a dependência crawler→job é garantida pelo próprio serviço | menos visibilidade num painel de orquestração genérico — fica dentro do console do Glue |
| Idempotência | apagar e reescrever a partição inteira | Job Bookmarks sozinho; upsert via Iceberg (L67) | resolve o caso de hoje sem adotar formato de tabela transacional novo | janela não atômica entre apagar e escrever — risco aceito e documentado |
| Frequência do crawler | 1x/dia, via gatilho agendado | por evento de cada arquivo novo (S3 PutObject) | schema muda raramente comparado à chegada de dado; crawlear por arquivo multiplicaria custo sem ganho | uma mudança de schema no meio do dia só é percebida no dia seguinte |
A dívida que este módulo cria, e que ele não paga
Um cast que falha silenciosamente — `"abc".cast("bigint")` vira `null`, não erro — passa batido pelo job inteiro. Ele grava a partição, atualiza o catálogo, e nenhum alarme dispara. Validar o VALOR do dado, não só o tipo declarado, é o L70, e este módulo não substitui aquele.
Construir: o catálogo, o crawler e a política de mudança de schema
A escolha mais consequente deste bloco não é o crawler em si — é a `schema_change_policy`. Sem ela explícita, você herda o padrão do provedor, que pode mudar entre versões sem aviso.
# catalogo.tf — o banco, o crawler, e a decisao explicita sobre schema mudando
resource "aws_glue_catalog_database" "lake" {
name = "${var.projeto}_lake"
}
data "aws_iam_policy_document" "crawler_assume" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["glue.amazonaws.com"]
}
}
}
resource "aws_iam_role" "crawler" {
name = "${var.projeto}-crawler"
assume_role_policy = data.aws_iam_policy_document.crawler_assume.json
}
# Politica gerenciada da AWS para o papel do Glue: cobre CloudWatch Logs e as
# chamadas de API do proprio servico. Nao cobre S3 — isso e sempre por conta
# de quem escreve o Terraform, e e a policy abaixo.
resource "aws_iam_role_policy_attachment" "crawler_gerenciada" {
role = aws_iam_role.crawler.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole"
}
data "aws_iam_policy_document" "crawler_leitura" {
statement {
effect = "Allow"
actions = ["s3:GetObject", "s3:ListBucket"]
resources = [
"arn:aws:s3:::${var.bucket_lake}",
"arn:aws:s3:::${var.bucket_lake}/bronze/pedidos/*",
]
}
}
resource "aws_iam_role_policy" "crawler_leitura" {
role = aws_iam_role.crawler.id
policy = data.aws_iam_policy_document.crawler_leitura.json
}
resource "aws_glue_crawler" "raw_pedidos" {
name = "${var.projeto}-raw-pedidos"
role = aws_iam_role.crawler.arn
database_name = aws_glue_catalog_database.lake.name
table_prefix = "raw_"
s3_target {
path = "s3://${var.bucket_lake}/bronze/pedidos/"
}
# Sem "schedule" aqui de proposito: quem dispara o crawler e o gatilho
# agendado do Workflow (workflow.tf), nao o crawler sozinho. Um agendamento
# dentro do crawler E outro no Workflow seriam dois relogios discordando.
schema_change_policy {
# UPDATE_IN_DATABASE: aplica a mudanca de tipo automaticamente. A escolha
# e "velocidade sobre revisao" — time pequeno, sem quem leia log toda
# manha. Times maiores, com coluna sensivel, preferem LOG + aprovacao.
update_behavior = "UPDATE_IN_DATABASE"
# NUNCA DELETE_FROM_DATABASE aqui: se o s3_target apontar, ainda que por
# engano temporario, para um prefixo vazio, essa opcao apagaria do
# catalogo particoes cujos arquivos continuam intactos no S3. Ver o
# callout de risco na secao de Construcao.
delete_behavior = "DEPRECATE_IN_DATABASE"
}
configuration = jsonencode({
Version = 1.0
Grouping = { TableGroupingPolicy = "CombineCompatibleSchemas" }
})
}
output "banco_do_lake" {
value = aws_glue_catalog_database.lake.name
description = "nome do banco no Glue Data Catalog"
}
DELETE_FROM_DATABASE apaga metadado — e não tem desfazer simples
O crawler decide o que "sumiu" pelo que ele VÊ na varredura, não pelo que realmente existe no bucket. Se o `s3_target` apontar, ainda que por um erro de configuração de um dia, para um subprefixo vazio, `DELETE_FROM_DATABASE` remove do catálogo partições cujos arquivos continuam intactos no S3 — e recuperar o registro exige rodar o crawler de novo apontando certo, não existe "restaurar" no catálogo.
Construir: o Workflow que troca agendamento fixo por dependência real
Dois gatilhos, dois papéis: um inicia o crawler por horário; o outro inicia o job por `crawl_state = SUCCEEDED`. A garantia não vem de calcular um atraso seguro entre os dois — vem do segundo gatilho literalmente esperar o primeiro terminar.
# workflow.tf — a dependencia crawler -> job vira garantia do proprio Glue
resource "aws_glue_workflow" "pedidos" {
name = "${var.projeto}-pedidos"
}
# Gatilho 1: agenda. So inicia o CRAWLER — nunca o job diretamente. O job so
# comeca depois que o catalogo, de fato, terminou de atualizar (gatilho 2).
resource "aws_glue_trigger" "agenda_diaria" {
name = "${var.projeto}-agenda-diaria"
type = "SCHEDULED"
schedule = "cron(0 2 * * ? *)" # 02:00 UTC
workflow_name = aws_glue_workflow.pedidos.name
actions {
crawler_name = aws_glue_crawler.raw_pedidos.name
}
}
# Gatilho 2: condicional. Observa o ESTADO do crawler dentro do mesmo
# Workflow — nao um horario fixo "3h, torcendo para o crawler ja ter
# acabado". Se o crawler demorar ou falhar, o job simplesmente nao comeca.
resource "aws_glue_trigger" "apos_catalogo_atualizado" {
name = "${var.projeto}-apos-catalogo"
type = "CONDITIONAL"
workflow_name = aws_glue_workflow.pedidos.name
predicate {
conditions {
crawler_name = aws_glue_crawler.raw_pedidos.name
crawl_state = "SUCCEEDED"
}
}
actions {
job_name = aws_glue_job.transformar_pedidos.name
# Sem "--dt" aqui: o job calcula "ontem" sozinho quando o argumento nao
# vem. Reprocessamento manual (reprocessar.tf/lambda) passa --dt explicito.
}
}
# job.tf — o job idempotente, e o alerta que substitui silencio por sinal
data "aws_iam_policy_document" "job_assume" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["glue.amazonaws.com"]
}
}
}
resource "aws_iam_role" "job" {
name = "${var.projeto}-job-transformar-pedidos"
assume_role_policy = data.aws_iam_policy_document.job_assume.json
}
resource "aws_iam_role_policy_attachment" "job_gerenciada" {
role = aws_iam_role.job.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole"
}
# O papel MAIS SENSIVEL deste laboratorio: ele tem s3:DeleteObject, porque a
# idempotencia exige apagar a particao antes de escrever. O escopo por
# PREFIXO e a unica coisa que impede um bug no calculo de "dt" (fuso horario,
# por exemplo) de apagar dado fora do que era pretendido. Ver o callout de
# risco na secao de Seguranca.
data "aws_iam_policy_document" "job_leitura_e_escrita" {
statement {
effect = "Allow"
actions = ["s3:GetObject", "s3:ListBucket"]
resources = [
"arn:aws:s3:::${var.bucket_lake}",
"arn:aws:s3:::${var.bucket_lake}/bronze/pedidos/*",
]
}
statement {
effect = "Allow"
actions = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket"]
resources = [
"arn:aws:s3:::${var.bucket_lake}",
"arn:aws:s3:::${var.bucket_lake}/prata/pedidos/*",
]
}
}
resource "aws_iam_role_policy" "job_leitura_e_escrita" {
role = aws_iam_role.job.id
policy = data.aws_iam_policy_document.job_leitura_e_escrita.json
}
resource "aws_glue_job" "transformar_pedidos" {
name = "${var.projeto}-transformar-pedidos"
role_arn = aws_iam_role.job.arn
glue_version = "4.0"
worker_type = "G.1X"
number_of_workers = 4
command {
script_location = "s3://${var.bucket_scripts}/jobs/transformar_pedidos.py"
python_version = "3"
}
default_arguments = {
# Evita RELER arquivo ja processado entre execucoes agendadas. Isto NAO
# e a defesa contra duplicar dado num rerun manual do mesmo dia — essa
# defesa e a sobrescrita de particao, dentro do script.
"--job-bookmark-option" = "job-bookmark-enable"
"--enable-continuous-cloudwatch-log" = "true"
"--enable-metrics" = "true"
"--TempDir" = "s3://${var.bucket_scripts}/tmp/"
}
# Sem retry automatico: reexecutar sozinho sobre uma falha nao investigada
# so multiplicaria a mesma janela de risco (particao vazia). Rerun manual,
# depois de olhar o log, e mais seguro — e e seguro porque o job e idempotente.
max_retries = 0
timeout = 30
}
resource "aws_sns_topic" "alertas" {
name = "${var.projeto}-alertas-pipeline"
}
resource "aws_cloudwatch_event_rule" "job_falhou" {
name = "${var.projeto}-job-pedidos-falhou"
description = "Glue Job State Change = FAILED ou TIMEOUT para o job de pedidos"
event_pattern = jsonencode({
source = ["aws.glue"]
detail-type = ["Glue Job State Change"]
detail = {
jobName = [aws_glue_job.transformar_pedidos.name]
state = ["FAILED", "TIMEOUT", "ERROR"]
}
})
}
resource "aws_cloudwatch_event_target" "avisa_time" {
rule = aws_cloudwatch_event_rule.job_falhou.name
arn = aws_sns_topic.alertas.arn
}
Por que não um Step Functions aqui
Step Functions orquestra bem quando os passos cruzam serviços heterogêneos — Lambda, ECS, aprovação humana. Aqui os dois passos são do próprio Glue, e o Glue já expõe a dependência nativamente como gatilho. Adicionar Step Functions por cima seria uma segunda ferramenta fazendo o que a primeira já faz.
Construir: o job idempotente
O script é PySpark porque o Glue ETL não roda outra linguagem para este tipo de job — é uma diferença deliberada em relação ao restante da série, que usa C#/.NET 8 para a aplicação. Aqui a "aplicação" é o próprio job Spark, e ele só existe em Python ou Scala.
# transformar_pedidos.py — le pelo catalogo, escreve com overwrite de particao
import sys
from datetime import datetime, timedelta, timezone
import boto3
from awsglue.context import GlueContext
from awsglue.dynamicframe import DynamicFrame
from awsglue.job import Job
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from pyspark.sql import functions as F
BUCKET = "cadencia-lake"
DATABASE = "cadencia_lake"
RAW_TABLE = "raw_pedidos"
PARTICAO = "dt"
args = getResolvedOptions(sys.argv, ["JOB_NAME"])
# --dt e OPCIONAL: o gatilho agendado do Workflow nao passa nada, e o job
# calcula "ontem" sozinho. Reprocessamento manual (ver secao de construcao
# do utilitario em C#) passa --dt explicito para escolher outro dia.
data_ref = None
for i, arg in enumerate(sys.argv):
if arg == "--dt" and i + 1 < len(sys.argv):
data_ref = sys.argv[i + 1]
if data_ref is None:
data_ref = (datetime.now(timezone.utc).date() - timedelta(days=1)).isoformat()
sc = SparkContext()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args["JOB_NAME"], args)
# 1. Le a particao do dia PELO CATALOGO — nao por um caminho decorado no
# script. push_down_predicate filtra por PARTICAO JA CATALOGADA: se o
# crawler nao rodou hoje, a particao nao existe no catalogo e a leitura
# volta vazia, mesmo que o arquivo ja esteja fisicamente no S3.
bruto = glueContext.create_dynamic_frame.from_catalog(
database=DATABASE,
table_name=RAW_TABLE,
push_down_predicate=f"{PARTICAO} = '{data_ref}'",
transformation_ctx="fonte_raw_pedidos", # chave do job bookmark
)
if bruto.count() == 0:
print(f"nenhum arquivo novo para {PARTICAO}={data_ref}; nada a fazer")
job.commit()
sys.exit(0)
# 2. Transformacao com tipo EXPLICITO. A inferencia ja aconteceu no crawler;
# aqui so convertemos para o tipo que a tabela prata deve ter.
df = bruto.toDF()
prata = (
df.withColumn("valor_centavos", F.col("valor_centavos").cast("bigint"))
.withColumn("criado_em", F.to_timestamp("criado_em"))
.withColumn(PARTICAO, F.lit(data_ref))
# dropDuplicates aqui protege contra o MESMO arquivo de origem reenviado
# com o mesmo pedido_id — NAO e a defesa contra rodar o job duas vezes.
# Essa defesa e o passo 3, abaixo.
.dropDuplicates(["pedido_id"])
)
# 3. IDEMPOTENCIA REAL: apaga a particao de destino inteira antes de escrever.
# O escritor do Glue ACRESCENTA arquivo por padrao — sem este passo, rodar
# o job duas vezes sobre o mesmo dia duplicaria cada linha da particao.
#
# ATENCAO: a janela entre apagar e escrever NAO e atomica. Se o job morrer
# aqui, a particao fica vazia ate a proxima execucao bem-sucedida. E um
# risco aceito e documentado — ver o callout na secao de construcao.
s3 = boto3.client("s3")
prefixo_destino = f"prata/pedidos/{PARTICAO}={data_ref}/"
paginador = s3.get_paginator("list_objects_v2")
chaves = [
obj["Key"]
for pagina in paginador.paginate(Bucket=BUCKET, Prefix=prefixo_destino)
for obj in pagina.get("Contents", [])
]
if chaves:
s3.delete_objects(Bucket=BUCKET, Delete={"Keys": [{"Key": k} for k in chaves]})
saida = DynamicFrame.fromDF(prata, glueContext, "saida")
# 4. Grava E atualiza o catalogo no MESMO passo, sem precisar de um segundo
# crawler. useGlueParquetWriter=True e OBRIGATORIO para o catalogo aceitar
# atualizacao de schema em Parquet — sem ele, a chamada grava o arquivo e
# ignora silenciosamente a parte de atualizar o catalogo.
sink = glueContext.getSink(
connection_type="s3",
path=f"s3://{BUCKET}/prata/pedidos/",
enableUpdateCatalog=True,
updateBehavior="UPDATE_IN_DATABASE",
partitionKeys=[PARTICAO],
)
sink.setFormat("parquet", useGlueParquetWriter=True)
sink.setCatalogInfo(catalogDatabase=DATABASE, catalogTableName="prata_pedidos")
sink.writeFrame(saida)
job.commit()
useGlueParquetWriter é fácil de esquecer, e o efeito é silencioso
Sem `setFormat("parquet", useGlueParquetWriter=True)`, a chamada grava o arquivo Parquet normalmente e simplesmente NÃO atualiza o catálogo quando o schema muda — sem erro, sem aviso. O sintoma aparece dias depois, como "a tabela não sabe da coluna nova", exatamente o problema que este módulo existe para resolver.
Construir: reprocessar um dia sob demanda, sem medo
Todo pipeline de dados eventualmente precisa de um botão "roda de novo". A pergunta que decide se esse botão é seguro ou assustador é exatamente a deste laboratório: o que acontece se alguém apertar duas vezes?
// Reprocessar.cs — reprocessar um dia e seguro PORQUE o job e idempotente
using Amazon.Glue;
using Amazon.Glue.Model;
using Amazon.Lambda.Core;
namespace Cadencia.Operacao;
public record ReprocessarPedido(string Dia); // "2026-08-07", validado antes de chegar aqui
public class ReprocessarHandler
{
// Um so cliente por invocacao fria; reutilizado entre chamadas quentes.
private static readonly AmazonGlueClient _glue = new();
// Exposta via API Gateway com autorizacao restrita ao time de dados —
// este endpoint chama StartJobRun, e por isso nao pode ser publico.
public async Task<string> ReprocessarAsync(ReprocessarPedido pedido, ILambdaContext contexto)
{
// Nao ha protecao extra de "clique duplo" aqui, e e proposital: a
// protecao E o job. Chamar isto duas vezes para o MESMO dia
// sobrescreve a MESMA particao duas vezes — o resultado final e
// identico, e essa e a garantia que a secao de provas mediu.
var resposta = await _glue.StartJobRunAsync(new StartJobRunRequest
{
JobName = "cadencia-transformar-pedidos",
Arguments = new Dictionary<string, string> { ["--dt"] = pedido.Dia },
});
contexto.Logger.LogInformation(
$"reprocessamento iniciado para dt={pedido.Dia}: JobRunId={resposta.JobRunId}");
return resposta.JobRunId;
}
}
Por que este utilitário não precisa de trava contra clique duplo
Numa arquitetura que apenas acrescenta dado, chamar isto duas vezes seria um incidente, e precisaria de um lock ou de um botão desabilitado após o clique. Aqui, chamar duas vezes para o mesmo dia produz o mesmo resultado da primeira vez — a idempotência do job é a trava, e ela vale para qualquer chamador, não só para este botão.
Implantar, e provar que reprocessar não duplica nada
Cinco provas. Nenhuma aceita "o painel mostrou verde" como resultado — cada uma compara um número antes e depois, e a segunda é a que mais gente pula.
# provas.sh — cinco medicoes; nenhuma conclusao vem de "parece que atualizou"
PROJETO=cadencia; BANCO=cadencia_lake; DIA=2026-08-07
# ── Prova 1: o Workflow roda ponta a ponta e a particao fica com N linhas ────
aws glue start-workflow-run --name "${PROJETO}-pedidos"
aws glue get-workflow --name "${PROJETO}-pedidos" --include-graph \
--query 'Workflow.LastRun.Statistics'
# Esperado: TotalActions == SucceededActions, e nenhuma FailedAction.
N1=$(aws athena start-query-execution \
--query-string "SELECT COUNT(*) FROM ${BANCO}.prata_pedidos WHERE dt = '${DIA}'" \
--result-configuration OutputLocation=s3://cadencia-lake/atenas-resultados/ \
--query 'QueryExecutionId' --output text)
echo "guarde N1 (contagem apos a primeira execucao)"
# ── Prova 2: rodar o JOB de novo para o MESMO dia nao duplica nada ───────────
aws glue start-job-run --job-name "${PROJETO}-transformar-pedidos" \
--arguments "{\"--dt\":\"${DIA}\"}"
aws glue wait job-run-complete --job-name "${PROJETO}-transformar-pedidos" \
--run-id "$(aws glue get-job-runs --job-name ${PROJETO}-transformar-pedidos \
--query 'JobRuns[0].Id' --output text)"
N2=$(aws athena start-query-execution \
--query-string "SELECT COUNT(*) FROM ${BANCO}.prata_pedidos WHERE dt = '${DIA}'" \
--result-configuration OutputLocation=s3://cadencia-lake/atenas-resultados/ \
--query 'QueryExecutionId' --output text)
# Esperado: N2 == N1. Se N2 == 2 * N1, o passo de apagar a particao nao rodou.
# ── Prova 3: objetos no S3 foram SUBSTITUIDOS, nao acumulados ────────────────
aws s3api list-objects-v2 --bucket cadencia-lake \
--prefix "prata/pedidos/dt=${DIA}/" --query 'length(Contents)'
# Esperado: contagem de arquivo estavel entre a primeira e a segunda execucao,
# nao crescente. Contagem crescente indica escrita por acrescimo, nao overwrite.
# ── Prova 4: o catalogo aprendeu a coluna nova SEM um segundo crawler ────────
aws glue get-table --database-name "${BANCO}" --name prata_pedidos \
--query 'Table.StorageDescriptor.Columns[].Name'
# Esperado: 7 colunas, incluindo "canal_pedido". Se faltar, o
# enableUpdateCatalog do job nao aplicou — confira useGlueParquetWriter.
# ── Prova 5: falha real dispara alerta, nao fica em silencio ─────────────────
# Revogue temporariamente s3:DeleteObject do papel do job, rode de novo, e
# confirme a notificacao:
aws sns list-subscriptions-by-topic --topic-arn "$(terraform output -raw topico_alertas)" \
--query 'Subscriptions[].[Endpoint,SubscriptionArn]'
aws glue get-job-runs --job-name "${PROJETO}-transformar-pedidos" \
--query 'JobRuns[0].[JobRunState,ErrorMessage]'
# Esperado: JobRunState = FAILED, e a mensagem cita AccessDenied em DeleteObject.
| Prova | Comando | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|---|
| 1 · O Workflow termina sem falha | `get-workflow --include-graph` | TotalActions == SucceededActions | qualquer FailedAction indica que o job nunca chegou a rodar, ou que o crawler falhou antes dele |
| 2 · Rerun não duplica | rodar o job de novo para o mesmo `dt`, comparar contagem | N2 igual a N1 | N2 igual a 2×N1 significa que o passo de apagar a partição não executou |
| 3 · Arquivos foram substituídos | contagem de objetos no prefixo, antes e depois | contagem estável entre execuções | contagem crescente indica escrita por acréscimo, não overwrite |
| 4 · Catálogo aprendeu a coluna nova | `get-table` → `Columns[].Name` | 7 colunas, incluindo `canal_pedido` | coluna faltando indica que `useGlueParquetWriter` não foi configurado |
| 5 · Falha real dispara alerta | revogar `s3:DeleteObject`, rodar, checar SNS | notificação chega, e `JobRunState = FAILED` com o erro de permissão | silêncio na notificação é o defeito mais caro deste desenho: ninguém percebe a partição vazia |
Quebrar de propósito: três falhas e o diagnóstico
As três se parecem no sintoma superficial — "a tabela está errada". O que separa uma da outra é ONDE o defeito mora: no destino, no catálogo, ou no valor.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| Janela não atômica de apagar-e-escrever | negue `s3:DeleteObject` ao papel do job DEPOIS de ele já ter apagado a partição (interrompa entre os dois passos) | a partição do dia fica vazia; `COUNT(*)` retorna zero para um dia que tinha pedido | Glue Job State Change = FAILED, e o horário bate com o instante do delete | reprocessar é seguro — é idempotente — mas o alerta precisa disparar ANTES que um painel de negócio leia a partição vazia como "zero pedidos hoje" |
| DeleteBehavior apagando partição válida | aponte o `s3_target` do crawler para um subprefixo vazio por uma execução | partições de dias anteriores somem do catálogo, mesmo com os arquivos intactos no S3 | histórico de execução do crawler → contagem de "partitions deleted" | DEPRECATE_IN_DATABASE em vez de DELETE_FROM_DATABASE; recriar exige rodar o crawler certo de novo |
| Cast silencioso mascarando dado ruim | envie um arquivo com `valor_centavos` como texto não numérico ("N/D") | o job termina com sucesso; a soma de faturamento cai sem nenhum erro registrado | contagem de `null` em `valor_centavos` na partição, comparada ao total de linhas | validar tipo antes do cast e desviar para quarentena em vez de aceitar — é o L70 |
A pergunta que resolve metade destes casos
Antes de mexer em parâmetro, pergunte: o defeito está no DADO (valor errado, tipo mal convertido) ou no METADADO (catálogo desatualizado, partição sumida)? Falha de dado o job reporta sucesso e mente; falha de metadado o crawler ou o job falham abertamente. São dois tipos de investigação diferentes.
Theo cria manualmente uma tabela no Athena com CREATE EXTERNAL TABLE apontando para prata/pedidos/, usando um schema de seis meses atrás. Duas semanas atrás, o produtor adicionou a coluna canal_pedido. O que acontece quando Theo roda SELECT canal_pedido FROM pedidos?
Segurança: o preço de um papel que pode apagar
A idempotência exige `s3:DeleteObject` no papel do job — é a peça que mais aumenta a superfície de risco deste laboratório, porque um bug no cálculo de `dt` (fuso horário, por exemplo) tem o mesmo poder de destruição que um ataque.
| Risco | Probabilidade | Impacto | Controle preventivo | Detecção | Resposta |
|---|---|---|---|---|---|
| Bug de fuso horário apaga a partição errada | baixa | alto | política de IAM restrita ao prefixo `prata/pedidos/*`, nunca ao bucket inteiro | CloudTrail em `DeleteObject` fora do prefixo esperado | restaurar de versão anterior, se o bucket tiver versionamento ligado — senão, reprocessar do bronze |
| Crawler catalogando prefixo além do pretendido | baixa | médio | um `s3_target` por crawler, revisão de PR obrigatória em mudança de caminho | contagem de tabelas novas inesperadas após uma execução | ajustar o `s3_target` e rodar o crawler de novo |
| Coluna de dado pessoal exposta a quem não deveria | média | alto | este laboratório NÃO resolve — é o L69, com Lake Formation | consulta de auditoria do Lake Formation | aplicar permissão por coluna antes de liberar acesso amplo ao catálogo |
| Papel de execução compartilhado entre vários jobs | média | médio | um papel por job, política derivada do uso real, não copiada de outro job | IAM Access Analyzer sobre uso real | separar papéis — é o L41 |
| DeleteBehavior removendo metadado por engano | baixa | alto | `DEPRECATE_IN_DATABASE`, nunca `DELETE_FROM_DATABASE` | métrica de "partitions deleted" na execução do crawler | recriar o registro rodando o crawler com o caminho correto |
| Segredo de conexão futura hardcoded no script | baixa hoje (sem JDBC ainda) | alto | Secrets Manager referenciado em tempo de execução, nunca no código | busca por padrão de credencial no script e nos logs | rotacionar e mover para o Secrets Manager |
O `s3:DeleteObject` do papel do job é o maior risco deste laboratório
Ele existe porque a idempotência exige — não há como reescrever uma partição sem antes poder apagá-la. Um bug de fuso horário que calcule `dt` errado, ou uma política de IAM escrita contra o bucket inteiro em vez do prefixo `prata/pedidos/*`, transforma o mecanismo que protege contra duplicata no mecanismo que apaga dado de verdade, fora do alcance de qualquer rerun.
Observabilidade: as perguntas que o painel tem de responder
Um painel deste pipeline tem uma função estreita: dizer se o catálogo de hoje é confiável. Métrica que não ajuda nessa pergunta pertence a outro painel.
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| O catálogo foi atualizado hoje? | última execução do crawler = SUCCEEDED, com timestamp | crawler falhou ou não rodou — nada abaixo dele é confiável | ausência de execução em 26 h |
| O job processou o dia certo? | log "nenhum arquivo novo" vs. volume normal | `dt` calculado errado, geralmente por fuso horário | qualquer dia com zero linhas inesperado |
| A partição ficou vazia depois de rodar? | `COUNT(*)` da partição do dia via Athena | falha entre apagar e escrever — a janela não atômica | 0 linhas num dia com pedido conhecido |
| O schema mudou sem ninguém decidir? | CloudWatch Logs do crawler, linha de mudança de schema | UpdateBehavior aplicou uma mudança sem revisão humana | qualquer ocorrência, revisar manualmente |
| Quantas partições o crawler tocou? | métricas de execução: criadas, atualizadas, removidas | "removidas" maior que zero de forma inesperada é o sinal do DeleteBehavior errado | > 0 removidas |
| O job está reprocessando sem necessidade? | contagem de StartJobRun por dia, via CloudTrail | gatilho condicional disparando mais de uma vez por dia | > 1 execução/dia sem reprocessamento manual |
| O alerta de falha realmente chega? | teste periódico da assinatura do tópico SNS | assinatura expirada ou e-mail não confirmado | teste mensal |
A métrica que engana neste pipeline
Um job que termina com `SUCCEEDED` não significa "dado correto" — significa "nenhuma exceção não tratada". Um cast que vira `null` silenciosamente, ou uma partição gravada com zero linhas porque o arquivo de origem veio vazio, passam pelo status de sucesso sem nenhuma bandeira.
Escala: 10, 10 mil, 1 milhão, e o que muda sem AZ nenhuma
| Volume | O que acontece | O que passa a doer | O que fazer |
|---|---|---|---|
| 10 pedidos/dia (piloto) | crawler e job terminam em minutos, custo desprezível | nada | nada |
| 18 mil pedidos/dia (Cadência hoje) | é o cenário deste laboratório | nada | nada |
| 1 milhão de pedidos/dia | arquivos bronze crescem em número; o crawler amostra mais arquivo por execução | tempo (e custo) de cada varredura do crawler cresce, mesmo sem mudança de schema | crawlear apenas prefixos novos, se a versão do Glue em uso suportar — confirme a disponibilidade dessa opção antes de assumi-la |
| Pico de chegada de arquivo | mais arquivos por partição, mesmo volume total de linha | o job lê mais arquivos pequenos, e mais workers processam menos dado cada | é o problema que o L64 (formato e tamanho de arquivo) resolve — não este módulo |
| Falha de AZ | não há AZ para este desenho planejar do seu lado | Glue e S3 já são serviços regionais geridos pela AWS; a única dependência real é o bucket | garantir versionamento do bucket S3 — é a proteção contra apagar errado, não contra falha de AZ |
| Schema mudando com frequência crescente | o crawler aplica mudança automaticamente a cada execução | UPDATE_IN_DATABASE deixa de ser seguro quando a mudança passa a ser regra, não exceção | revisar para LOG + aprovação quando a frequência de mudança justificar o custo de revisão |
O gargalo que só aparece com muito arquivo pequeno
O crawler paga por tempo de execução, e amostrar milhares de arquivos pequenos custa mais que amostrar dezenas de arquivos grandes com o mesmo volume total de dado. É o L64 cobrando a dívida de particionamento que ele mandou evitar.
Custo: o que este laboratório acrescenta à fatura
O crawler e o job são os itens visíveis; o que raramente aparece na conta mental é a varredura de S3 que o passo de apagar-e-escrever faz a cada execução, mesmo sem nenhuma linha nova.
| Cenário | Volume | O que acrescenta | Tendência | Otimização |
|---|---|---|---|---|
| Piloto | 1 execução/dia, poucos MB | minutos de DPU do crawler e do job | desprezível | nenhuma; otimizar aqui é gastar atenção onde não há dinheiro |
| Produção pequena | Cadência hoje: 18 mil pedidos/dia, ~48 arquivos | DPU-hora do crawler (varredura curta) e do job (poucos minutos) | baixa e previsível | crawler com `table_prefix` e `s3_target` restrito evita varrer prefixo maior que o necessário |
| Alta escala | 1 milhão de pedidos/dia, milhares de arquivos | DPU-hora do crawler cresce com o número de arquivo, não só com o volume de dado | passa a ser linha visível na fatura de Glue | crawler incremental (quando disponível) e consolidação de arquivo pequeno (L64) |
| Dimensão | Cobra por | Cuidado |
|---|---|---|
| DPU do crawler | DPU-hora, faturado por segundo com mínimo de 10 minutos | uma varredura de poucos segundos ainda cobra o mínimo — execução por evento de arquivo multiplicaria isso |
| DPU do job | DPU-hora, proporcional a workers × duração | a listagem S3 do passo de apagar soma tempo, mesmo sem escrever nada novo |
| Requisições S3 de listagem e delete | por milhar de requisições | partição com muitos arquivos pequenos multiplica chamadas de `list_objects_v2` e `delete_objects` |
| Armazenamento do catálogo | gratuito até 1 milhão de objetos, depois cobrado por objeto | irrelevante neste volume; relevante em lakes com dezenas de milhares de tabelas |
| CloudWatch Logs do job | GB ingerido e retido | log contínuo habilitado (`--enable-continuous-cloudwatch-log`) grava bastante em job com muito worker |
O custo que não aparece em nenhuma linha da AWS
Antes deste módulo, cada analista novo gastava dias reconstruindo o schema de memória alheia. Esse custo nunca teve linha na fatura, mas era o maior dos dois — e ele desaparece com o catálogo, não com nenhuma otimização de DPU.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | catálogo sempre atual, sem intervenção manual; falha vira alerta | janela não atômica entre apagar e escrever a partição | escrever em prefixo temporário e mover (rename lógico) ao final — L67 resolve com upsert | média |
| Segurança | IAM restrito por prefixo, sem `DELETE_FROM_DATABASE` | papel do job tem `s3:DeleteObject`, o poder de destruição mais alto do módulo | revisão de política por par antes de qualquer mudança de escopo de recurso | alta |
| Confiabilidade | dependência crawler→job garantida pelo Workflow, não por horário coincidente | cast silencioso mascarando dado ruim sem falhar o job | contrato de dado com quarentena (L70) | alta |
| Eficiência de performance | leitura filtrada por partição via `push_down_predicate` | crawler varre todo o prefixo a cada execução, mesmo sem mudança | crawler incremental, quando disponível na versão em uso | média |
| Otimização de custos | crawler agendado 1x/dia, não por evento de arquivo | muitos arquivos pequenos multiplicam DPU do crawler sem ganho de dado | consolidação de arquivo pequeno (L64) | média |
| Sustentabilidade | job lê só a partição do dia, não a tabela inteira | partição reescrita inteira mesmo quando só uma fração do dado mudou | avaliar upsert granular (L67) quando o volume por dia crescer muito | 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 — inclusive o próprio risco de idempotência que este módulo resolveu de um jeito, e que um nível acima resolve de outro.
Script solto, sem crawler, sem catálogo — DDL manual quando alguém precisa consultar. É onde a Cadência estava, e continua legítimo em experimento de curta duração.Glue Data Catalog alimentado por crawler com SchemaChangePolicy explícita, job idempotente encadeado por Glue Workflow.Glue Data Quality valida VALOR, não só tipo — registro que viola o contrato vai para quarentena em vez de virar `null` silencioso (L70).Formato Iceberg com upsert e time travel — corrige uma linha sem reescrever a partição inteira (L67).Lake Formation por cima do catálogo: permissão por coluna e por linha, não só por tabela (L69).O catálogo confiável vira a camada de DADOS que um MODELO de linguagem consulta: pergunta em português vira SQL sobre a tabela do catálogo, e a resposta cita a consulta que a gerou.A ordem não é negociável, e o motivo é concreto
Texto-para-SQL do nível 6 depende de um catálogo em que o nome das colunas já significa algo — que é o nível 2. Pedir a um modelo para gerar SQL sobre um schema que ninguém revisou monta uma resposta convincente em cima de metadado que pode estar simplesmente errado. É 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 nenhum dos dois problemas centrais, e forçá-la seria o antipadrão que a própria série critica. "Quem sabe o schema" tem resposta determinística: um catálogo. "O que acontece ao rodar duas vezes" também: apagar e reescrever a partição.
Há um lugar onde IA acrescentaria valor real, e ele aparece no nível 6 da evolução: traduzir pergunta em português para consulta sobre a tabela do catálogo. É modesto, e depende de o catálogo já ser confiável — um modelo gerando SQL sobre metadado errado só embala o erro com mais confiança.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria? | traduzir pergunta em português para SQL sobre a tabela do catálogo |
| Por que uma regra não bastaria? | um conjunto fixo de 10 a 15 relatórios recorrentes cobre a maior parte do pedido real; IA só se justifica depois que esse conjunto fixo mostrar seu limite |
| De onde viriam os dados? | o próprio schema do catálogo, mais o histórico de consultas do Athena, como exemplo de pergunta-para-SQL |
| Qual o risco? | SQL gerado que agrega errado — soma sem filtrar duplicata, ou ignora a partição certa — e é apresentado com confiança, sem sinalizar incerteza |
| Por que não agora? | a Cadência tem duas tabelas e três analistas; um dicionário fixo de perguntas frequentes resolve isso mais barato e sem risco de alucinação |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para INFERIR o schema em vez do crawler — "leia esses arquivos e me diga as colunas" — troca um mecanismo determinístico e auditável, compatível com Hive Metastore, por uma suposição que nenhum outro motor de consulta compartilha. O crawler já resolve isso de um jeito verificável; IA aqui remove a garantia, não acrescenta nenhuma.
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 |
|---|---|---|---|---|---|
| DDL manual mantida em wiki | é mais rápido de escrever uma vez do que configurar um crawler | nunca acompanha a realidade — nada força atualização quando o schema muda | `COLUMN_NOT_FOUND`, ou pior, tipo errado devolvendo valor plausível e falso | crawler agendado alimentando o Glue Data Catalog automaticamente | protótipo de um dia, descartado antes de qualquer pessoa nova consultar |
| Escrever sem apagar a partição antes | é menos código, e "funciona" na primeira execução | todo rerun duplica cada linha da partição — sem erro nenhum, o job "termina com sucesso" | contagem de pedido no relatório maior que a contagem de pedido real | apagar objetos do prefixo de destino antes de escrever | tabela append-only de fato, onde duplicata é impossível por design (ex.: log de evento imutável) |
| DELETE_FROM_DATABASE por padrão | parece "limpeza automática", e o nome sugere manutenção saudável | apaga registro de partição cujo arquivo continua existindo, só porque a varredura não o viu desta vez | consulta a dia antigo passa a retornar tabela/partição inexistente, do nada | DEPRECATE_IN_DATABASE, e investigar antes de qualquer exclusão real | ambiente descartável, recriado do zero a cada execução, sem histórico a preservar |
| Confiar só no status SUCCEEDED do job | é o sinal mais visível no console, e parece suficiente | um cast que vira `null` silenciosamente passa por ele sem nenhuma bandeira | soma de faturamento cai, sem nenhum erro registrado em lugar nenhum | contagem de `null` pós-cast como métrica própria, mais contrato de dado (L70) | nunca; é sempre insuficiente sozinho, mesmo em protótipo |
| Job Bookmarks como defesa de idempotência de escrita | o nome "bookmark" soa como "não processa de novo", e parece cobrir o caso | bookmark evita reler ARQUIVO já processado; não evita duplicar ao reescrever de propósito | reprocessamento manual dobra a contagem de linha da partição | apagar-e-reescrever a partição, independente do estado do bookmark | pipeline que nunca reprocessa de propósito e nunca falha no meio — na prática, quase nunca |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| Athena diz que a tabela não existe | crawler nunca rodou, ou rodou apontando para caminho errado | confira a última execução do crawler e o `s3_target` configurado | histórico de execução do crawler no console do Glue | rodar o crawler manualmente uma vez e conferir `get-table` |
| Consulta falha com COLUMN_NOT_FOUND numa coluna que existe no arquivo | DDL manual desatualizada, ou UpdateBehavior em LOG sem revisão feita | compare `get-table` → `Columns` com o schema real de um arquivo de amostra | diferença entre colunas do catálogo e colunas do Parquet | trocar DDL manual pelo catálogo alimentado por crawler; ou aplicar a mudança pendente |
| Partição do dia com o dobro de linhas esperado | o job rodou duas vezes sem apagar a partição antes de escrever | compare contagem de arquivo no prefixo com o número de execuções do job hoje | `list_objects_v2` no prefixo da partição | incluir o passo de apagar antes de escrever; nunca confiar só em Job Bookmarks para isso |
| Partição do dia com zero linhas, mas havia pedido | falha do job entre apagar e escrever — a janela não atômica | cheque `JobRunState` e o horário do evento de falha contra o horário do delete | `get-job-runs` → `ErrorMessage`, e CloudWatch Logs do job | corrigir a causa da falha e reprocessar — é seguro, porque o job é idempotente |
| Partições antigas somem do catálogo sem aviso | DELETE_FROM_DATABASE combinado a uma varredura que não viu os arquivos | histórico de execução do crawler, campo de partições removidas | execução do crawler mais recente | trocar para DEPRECATE_IN_DATABASE; recriar rodando o crawler com o caminho certo |
| Faturamento reportado menor que o esperado, sem erro em lugar nenhum | cast silencioso: valor não numérico virou `null` em vez de falhar | conte `null` em `valor_centavos` na partição do dia contra o total de linhas | saída do job comparada ao arquivo de origem | validar tipo antes do cast, desviar linha ruim para quarentena — é o L70 |
| Job nunca inicia, mesmo com arquivo novo em bronze | o gatilho condicional está esperando um crawler que falhou | veja o estado do crawler dentro do Workflow, não só o do job | `get-workflow --include-graph` → nó do crawler | corrigir a causa da falha do crawler; o job propaga a dependência, não pula ela |
A pergunta que resolve metade destes casos
Antes de mexer em parâmetro, pergunte: o Workflow terminou com sucesso, ou parou no crawler? Um job que "nunca roda" quase sempre não é um problema do job — é um crawler que falhou silenciosamente e o gatilho condicional, corretamente, nunca disparou.
Limpeza: o que o destroy não leva
Este laboratório cria pouco recurso que sobrevive por conta própria, mas dois deles pedem atenção: o dado em si, que não é deste módulo para apagar, e resultado de consulta do Athena, que não tem ciclo de vida por padrão.
#!/usr/bin/env bash
# limpar.sh — o que o destroy nao leva, e o que continua cobrando
set -euo pipefail
PROJETO="${PROJETO:?defina PROJETO}"
# 1. Derrube o que o Terraform administra (crawler, job, workflow, gatilhos,
# regra do EventBridge, topico SNS, papeis).
terraform destroy -auto-approve
# 2. O QUE O DESTROY NAO TOCA: o BANCO do catalogo pode ficar com tabelas
# orfas se voce criou alguma manualmente por fora do Terraform durante o
# laboratorio (por exemplo, testando o antipadrao da arquitetura minima).
aws glue get-tables --database-name "${PROJETO}_lake" \
--query 'TableList[].Name' --output table
# 3. Os DADOS em si: bronze e prata continuam no S3 do L62. Este laboratorio
# nao os apaga porque nao foi ele quem os criou — apague-os apenas se voce
# tambem estiver encerrando o L62.
aws s3 ls "s3://cadencia-lake/prata/pedidos/" --recursive --summarize \
| tail -2
# 4. RESULTADOS DE CONSULTA DO ATHENA: cada execucao grava um arquivo no
# bucket de resultados, e ele NAO tem ciclo de vida por padrao.
aws s3 rm "s3://cadencia-lake/atenas-resultados/" --recursive
# 5. Prova final: nada com o nome do projeto de pe fora do que era esperado.
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=Projeto,Values="${PROJETO}" \
--query "ResourceTagMappingList[].ResourceARN" --output table
| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Crawler, job, Workflow, gatilhos | sim | não | são definições — sem execução, não geram custo |
| Banco e tabelas do catálogo | sim | não diretamente | gratuito até 1 milhão de objetos de catálogo; irrelevante neste volume |
| Dado em bronze/ e prata/ no S3 | não — não pertence a este módulo | sim, GB-mês | veio do L62; apagar aqui apagaria o lake inteiro, não só este laboratório |
| Resultados de consulta do Athena | não | sim, GB-mês | cada execução grava um arquivo; sem ciclo de vida configurado, acumula para sempre |
| Tópico SNS e regra do EventBridge | sim, se em Terraform | centavos | regra ou assinatura criada à mão no console não aparece no estado |
| Logs do CloudWatch do job | depende de `skip_destroy` | sim, retenção | tem ciclo próprio; sobrevive à definição do job que o alimentava |
Apagar o banco do catálogo não apaga o dado
É tentador achar que remover a tabela do Glue Data Catalog "limpa" o laboratório. Ela só remove o REGISTRO — os arquivos Parquet em `prata/pedidos/` continuam no S3, cobrando armazenamento, e um novo crawler os catalogaria de novo a qualquer momento.
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Schema só na cabeça de quem escreveu o script | Glue Data Catalog alimentado por crawler | metastore único, consultável por qualquer motor, sem depender de pessoa |
| DDL manual desatualizada | crawler com SchemaChangePolicy explícita | decide o que fazer com mudança de schema, em vez de improviso |
| Job pode rodar antes do catálogo atualizar | gatilho condicional do Glue Workflow (crawl_state=SUCCEEDED) | substitui agendamento fixo coincidente por dependência real, do próprio serviço |
| Reprocessar duplica linha | apagar e reescrever a partição inteira antes de gravar | garante que rerun produz o mesmo resultado, nunca o dobro |
| Catálogo desatualizado depois do job | enableUpdateCatalog + updateBehavior no sink do job | evita um segundo crawler só para o catálogo "perceber" a partição nova |
| Falha vira silêncio | regra de EventBridge em Glue Job State Change = FAILED, com SNS | o alerta chega antes de alguém precisar perceber pelo painel |
| Partição pode ficar vazia entre apagar e escrever | não resolvido neste módulo | risco residual aceito e documentado; mitigado por alerta rápido e rerun seguro |
| Coluna com tipo errado vira null silencioso | cast explícito + observação de contagem de null | primeira linha de defesa; contrato de dado de verdade é o L70 |
| Falha | O que a protege | O que ela NÃO protege |
|---|---|---|
| Analista sem saber o schema | catálogo central alimentado por crawler | schema desatualizado NO catálogo por UpdateBehavior mal escolhido |
| Reprocessamento duplicando linha | apagar e reescrever a partição inteira | falha no meio da própria troca — a janela não atômica |
| Mudança de schema silenciosa | UpdateBehavior explícito + log revisável | mudança de tipo que ainda "casa" sem erro aparente, como int virando bigint |
| Perda de registro de partição no catálogo | DeleteBehavior = DEPRECATE_IN_DATABASE | apontar o crawler para o prefixo errado desde o início |
| Job falhando sem ninguém notar | alerta via EventBridge + SNS | assinatura de e-mail expirada ou não confirmada |
| Dado pessoal exposto a quem não deveria | nada neste módulo | é o L69 inteiro, com Lake Formation |
- Arquivos do dia pousam em bronze/pedidos/dt= (resolvido pelo L62).
- Às 02:00 UTC, o gatilho agendado do Glue Workflow inicia o crawler.
- O crawler varre o prefixo, infere schema e atualiza a tabela raw, conforme a SchemaChangePolicy.
- O crawler termina com SUCCEEDED, e o gatilho condicional inicia o job — não um horário fixo.
- O job lê a partição do dia pela tabela do catálogo, não por um caminho decorado no script.
- Antes de escrever, o job apaga todos os objetos da partição de destino do dia.
- O job grava o Parquet transformado e atualiza a tabela prata no catálogo no mesmo passo.
- Qualquer motor — hoje o Athena — consulta a tabela prata sem DDL manual.
- Se o job falhar em qualquer etapa, uma regra do EventBridge dispara um alerta no SNS.
- Rodar o job de novo para o mesmo dia é seguro: a partição é redesenhada inteira, não somada.
Perguntas frequentes
❓ Por que minha consulta no Athena diz que a tabela não existe, mesmo com arquivos no S3?
❓ O crawler precisa rodar toda vez que chega um arquivo novo?
❓ Qual a diferença entre Job Bookmarks e a sobrescrita de partição deste módulo?
❓ Por que usar UPDATE_IN_DATABASE em vez de LOG na política do crawler?
❓ Rodar o job duas vezes seguidas para o mesmo dia é seguro?
❓ Por que o schema do Parquet não basta, já que o formato é autodescritivo?
❓ O que acontece se o job falhar depois de apagar a partição, mas antes de escrever a nova?
❓ Preciso do Lake Formation para ter esse catálogo funcionando?
Fixando
O job de transformação da Cadência tem Job Bookmarks habilitado, mas grava o Parquet sem apagar a partição de destino antes. Um operador reprocessa manualmente o dia de ontem, depois de corrigir um bug no cast de valor_centavos, passando --dt explícito. O que acontece na partição dt=2026-08-07?
O crawler da Cadência está configurado com delete_behavior = DELETE_FROM_DATABASE. Por um erro de configuração temporário, o s3_target passou a apontar para um subprefixo vazio por uma execução. O que acontece com as partições que já existiam no catálogo?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L62 (data lake em camadas) — bronze/pedidos/dt= já populado; Terraform e S3 básicos |
| Conhecimentos adquiridos | papel do catálogo vs. papel do crawler; SchemaChangePolicy (UpdateBehavior/DeleteBehavior); gatilhos nativos do Glue Workflow; diferença entre Job Bookmarks e idempotência de escrita; atualização de catálogo pelo próprio job |
| Limitação que fica | a janela entre apagar e escrever a partição não é atômica; um cast que falha vira null silencioso, sem validação de contrato |
| Próximo exemplo recomendado | L66 — consultar o lake e pagar pouco (Athena, custo por byte varrido). Reutiliza a tabela prata deste módulo, e é onde o catálogo alimentado aqui passa a ser consultado de verdade |
| Também habilitado por este módulo | L69 (governança do lake, permissão por coluna) e L70 (qualidade de dado, contrato e quarentena) dependem de um catálogo confiável, que é o entregável deste laboratório |
| Data da última validação técnica | 8 de agosto de 2026 |
Documentação oficial consultada: AWS Glue SchemaChangePolicy — os valores de UpdateBehavior e DeleteBehavior; AWS Glue events on Amazon EventBridge — fonte e detail-type dos eventos de crawler e job; AWS Glue Data Catalog support for Spark SQL jobs — a compatibilidade com Hive Metastore; Overview of workflows in AWS Glue — gatilhos agendados e condicionais dentro de um Workflow; Updating the schema, and adding new partitions in the Data Catalog using AWS Glue ETL jobs — enableUpdateCatalog, updateBehavior e a exigência do useGlueParquetWriter; e Tracking processed data using job bookmarks — o que Job Bookmarks realmente protegem. Nenhum valor de preço aparece 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 campos do payload de evento do crawler além de `source`, `detail-type` e `state` são ilustrativos — inspecione um evento real antes de decidir lógica em cima deles. A disponibilidade de crawler incremental (varrer só prefixo novo) depende da versão do Glue em uso e não foi confirmada nesta sessão. Os volumes da Cadência — 18 mil pedidos/dia, 48 arquivos — são os do cenário de exemplo; meça os seus antes de copiar um limiar de alarme.
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…