Lab 62 — Data lake em camadas: bronze, prata, ouro
O problema, e a empresa que o tem
A Cadência é um marketplace de material de construção com 40 lojistas parceiros. O ERP roda sobre RDS PostgreSQL, e desde o L61 uma replicação contínua extrai o banco operacional inteiro para o S3: carga completa mais captura de mudança, gravando um arquivo Parquet por execução. Isso resolveu o problema do L61 — o relatório de BI parou de travar o banco de produção.
O que ninguém previu foi o que acontece depois de oito meses disso. Três outros times — marketing, financeiro e suporte — perceberam que também podiam exportar seus próprios recortes para o mesmo bucket, e passaram a fazer isso quando precisavam, cada um com sua própria convenção de nome, ou sem nenhuma. Hoje o bucket tem 41.312 objetos, e a pergunta "qual arquivo tem os pedidos de julho" não tem resposta sem abrir arquivo por arquivo e olhar o conteúdo.
O sintoma que chegou à equipe de dados não foi "estamos sem espaço" — é armazenamento, e armazenamento é barato. Foi um analista de BI relatando que gasta em média 40 minutos por dia só localizando o arquivo certo antes de sequer começar a análise. O volume de arquivos não é o problema: é a ausência de um contrato que diga, para cada um, se é dado bruto, dado limpo ou dado pronto para consumo.
O que este laboratório NÃO é
Não é qualidade de dado automatizada, nem tabela transacional com UPDATE/DELETE. O contrato de qualidade — schema esperado, faixa de valor, quarentena por regra de negócio — é o L70. Corrigir um registro histórico sem reescrever a partição inteira é o L67, que troca Parquet simples por Iceberg. Este módulo entrega a estrutura sobre a qual os dois seguintes se apoiam.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um comando na seção de implantação, não com a sensação de ter entendido as três camadas.
- Explicar por que "mais armazenamento organizado em pastas" não é a mesma coisa que um contrato de camada.
- Nomear o que bronze, prata e ouro garantem cada uma, e o que cada uma explicitamente não garante.
- Escrever o contrato de uma camada — schema, formato, frequência, dono — e apontar onde ele vive no desenho.
- Configurar um job do Glue que atualiza o próprio schema no catálogo, sem crawler agendado sobre tudo.
- Escrever uma transformação idempotente: reexecutar o mesmo dia não duplica linha nenhuma.
- Conceder permissão no Lake Formation por tabela e por coluna, e provar a negativa com um teste real.
- Medir quantos bytes uma consulta varre em cada camada, e explicar a diferença de duas ordens de grandeza.
- Diagnosticar um caso em que a permissão de bucket contorna a permissão do Lake Formation.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Zonas do lake (raw/staging/curated) | MLA-C01, SAA-C03 | bronze/prata/ouro como as três zonas, com prefixo e contrato próprios | por que bronze nunca é sobrescrito, e o que cada zona seguinte resolve |
| Formato de arquivo | MLA-C01 | prata e ouro em Parquet; bronze recebe o formato que a fonte entrega | por que formato colunar reduz bytes varridos, e quando isso importa |
| Particionamento | MLA-C01, SAA-C03 | partição por data em todas as camadas, por loja também em ouro | poda de partição vs. varredura completa, e o efeito no custo por consulta |
| Glue Data Catalog e crawler | MLA-C01 | catálogo alimentado pelo job em prata/ouro; crawler só em bronze | diferença entre "descobrir schema" e "manter schema conhecido" |
| Job bookmarks e processamento incremental | MLA-C01 | cada execução processa só a partição nova, não o histórico inteiro | a diferença entre reprocessar tudo e reprocessar o que mudou |
| Lake Formation: permissão por tabela e coluna | SAP-C02, MLA-C01 | SELECT concedido por banco/tabela, e coluna excluída por nome | a diferença estrutural entre permissão de bucket e permissão de catálogo |
| Lake Formation: método nomeado vs. LF-TBAC | SAP-C02 | este módulo usa o método nomeado (tabela por tabela) | quando migrar para tag-based faz sentido: muitos times, muitas tabelas |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve um papel do IAM com `s3:GetObject` no bucket do lake E uma permissão do Lake Formation negando acesso a uma tabela — e pede o resultado. A resposta depende de como o recurso foi registrado: se o bucket está registrado no Lake Formation e o acesso passa pelo Data Catalog (Athena, Glue), o LF decide, e a negativa vale. Se alguém usa `s3 cp` direto no objeto, a permissão de bucket do IAM ainda se aplica, porque aquele caminho nunca passou pelo LF. É exatamente o antipadrão da seção de segurança adiante.
Requisitos, e como cada um muda o desenho
Requisito não funcional que não vira uma linha de contrato ou de permissão é intenção. A coluna da direita é onde cada um deixou marca no Terraform ou no job.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Dado bruto nunca é alterado | imutável por convenção de escrita | bronze é write-once por partição; correção é reprocessar, nunca editar |
| Analista de negócio só acessa dado agregado | obrigatório | Lake Formation concede SELECT ao papel do analista só no banco `cadencia_ouro` |
| Reprocessar um dia sem afetar os demais | obrigatório | partição por data em todas as camadas + `partitionOverwriteMode=dynamic` |
| Tempo entre chegada em bronze e disponível em ouro | abaixo de 2 h | jobs agendados por EventBridge em cadeia, não crawler indefinido nem processo manual |
| Custo de armazenamento sob controle | orçamento fixo, equipe de 2 pessoas | ciclo de vida por prefixo: bronze esfria em 30 dias, ouro permanece quente |
| Dado sensível não chega à camada de consumo amplo | CPF do cliente | a transformação bronze→prata já remove a coluna antes de gravar; ouro nunca a teve |
| Auditoria de quem acessou o quê | obrigatório | permissão via Lake Formation, que registra concessão e uso no CloudTrail |
| Times diferentes, acessos diferentes | 4 times, no mínimo 2 níveis de acesso | papéis distintos com permissão nomeada por banco/tabela, não por política de bucket |
Arquitetura mínima: o lake sem camada
Este é o desenho que a Cadência tem hoje. É implantável de verdade — é literalmente o resultado do L61 mais oito meses de uso real — e o defeito é visível assim que se tenta responder uma pergunta específica sobre o dado.
- → DMS: carga completa + captura contínua, um arquivo por execução
- → exportação ad hoc, sem prefixo nem formato combinado
- → varre o bucket inteiro numa passada só
- → tabela única, schema inferido de arquivos incompatíveis
- → "quero os pedidos de julho" — pergunta sem coluna que a responda
- → varredura completa: sem partição, sem prefixo por data
- Banco de dados
- Fora da AWS
- Armazenamento
- Analytics
Isto é o que a Cadência tem hoje, e não é preguiça — é o caminho mais curto para tirar dado do banco operacional. O defeito não é o volume de arquivos: é que nenhum deles diz se é bruto, limpo ou pronto. Percorra os passos e repare que a pergunta do analista não tem resposta porque o esquema não tem a resposta.
- Dois produtores, uma pasta. A replicação do banco operacional grava um arquivo por execução direto no bucket. Não há prefixo que diga "isto é carga de hoje" nem "isto veio do pedidos" — é só mais um objeto entre 41 mil.
- Uma convenção que ninguém combinou. Marketing, financeiro e suporte também escrevem no mesmo bucket quando precisam de um recorte. Cada time usa o nome de arquivo que faz sentido para ele — e nenhum dos quatro sabe o que os outros três estão fazendo.
- O crawler não separa bruto de recorte. Sem prefixo por camada, o crawler vê uma pasta só e tenta inferir UM schema para tudo dentro dela. O resultado é uma tabela que mistura coluna de um formato com coluna de outro — quando não falha silenciosamente em parte dos arquivos.
- Uma tabela, schemas incompatíveis. O Athena consulta a tabela que existe no Data Catalog, e essa tabela é o produto do passo anterior: uma tentativa de generalizar arquivos que nunca deveriam estar juntos.
- "Pedidos de julho" não é uma coluna. Sem partição por data e sem separação por domínio, a pergunta de negócio mais comum não tem tradução direta para SQL. O analista acaba abrindo arquivo por arquivo pelo nome, torcendo para reconhecer o conteúdo pelo timestamp da execução.
- Cada consulta paga o bucket inteiro. Mesmo quando alguém escreve a consulta certa, o Athena varre o bucket inteiro porque não existe partição para podar nem formato colunar para ler só a coluna pedida. O custo por consulta cresce junto com o volume, sem ninguém decidir isso.
Antes de estruturar qualquer coisa, meça o tamanho real do problema. Contar objetos por "tipo aparente" (pelo nome) é o primeiro número que torna o caos discutível.
# Quantos objetos existem, e quantos "tipos" de nome de arquivo aparecem?
aws s3api list-objects-v2 --bucket cadencia-lake \
--query "length(Contents)" --output text
# 41312
aws s3api list-objects-v2 --bucket cadencia-lake \
--query "Contents[].Key" --output text \
| tr "\t" "\n" | sed -E "s/[0-9]+/#/g" | sort -u | wc -l
# 187 padroes de nome distintos, nenhum deles documentadoA pergunta que ninguém consegue responder sem abrir arquivo
"Qual arquivo tem os pedidos de julho?" não é uma pergunta difícil — é uma pergunta sem informação suficiente para responder. Sem prefixo por domínio e por data, o nome do arquivo é o timestamp da execução que o gerou, não o conteúdo que ele carrega. Um analista que abre o arquivo errado perde tempo; um que confia no nome errado publica um número errado — e isso é mais caro do que a demora.
Arquitetura para produção: três camadas com contrato
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.
- → lê a partição do dia, sem filtrar nem limpar nada
- → grava limpo, deduplicado, tipado — ainda uma linha por pedido
- → registra o schema da tabela prata
- → lê prata da partição processada, nunca bronze de novo
- → grava total diário por loja
- → registra o schema da tabela ouro
- → aplica permissão por tabela e coluna sobre o catálogo
- → concede SELECT em bronze e prata
- → concede SELECT só em ouro
- → consulta de investigação, alcança prata
- → consulta de relatório, só ouro
- → poucos GB varridos: agregado e particionado
- → varredura maior: grão de evento, só quando autorizada
- Conceito de arquitetura
- Analytics
- Fora da AWS
A diferença estrutural não é "mais uma pasta": é que cada camada agora tem um dono, um formato e um job que a alimenta — e quem pode ler cada tabela é decisão do Lake Formation, não do bucket. Percorra os passos: cada peça nova rastreia a um requisito da seção anterior.
- Bronze não filtra nada. O primeiro job lê a partição do dia exatamente como a captura gravou — sem limpar, sem deduplicar. Essa fidelidade é o que torna bronze confiável como fonte de reprocessamento: se prata sair errada, o problema é do job, não do dado.
- Prata é a primeira vez que alguém decide o que é verdade. Deduplicação por chave e timestamp de captura, tipos corrigidos, registro inválido descartado. Prata ainda é granular — uma linha por pedido — porque nem toda pergunta futura é sobre agregado.
- Cada job registra a própria tabela. O job de bronze→prata atualiza só a entrada de prata no catálogo, com o schema daquela camada. Não existe um crawler genérico varrendo tudo: o schema de cada camada é responsabilidade de quem a escreve.
- Ouro é a única camada que resume. O segundo job lê prata — nunca bronze de novo — e agrega por loja e dia. É o trabalho pesado feito uma vez, para não ser repetido a cada consulta de BI.
- Lake Formation decide quem vê qual tabela, não o bucket. A permissão vive no catálogo, por tabela e coluna. O engenheiro de dados recebe SELECT em bronze e prata porque investiga incidente; o analista de negócio recebe SELECT só em ouro, porque prata exporia pedido individual sem necessidade.
- A pergunta de negócio quase nunca varre bronze. O analista consulta ouro e varre poucos gigabytes, porque a tabela já é o agregado particionado. O engenheiro de dados eventualmente consulta prata para investigar — e é uma exceção autorizada, não a rota padrão.
- A permissão fica registrada, e é auditável. Toda concessão do Lake Formation grava evento no CloudTrail. Não é preciso confiar de memória em quem tem acesso a quê — a pergunta "quem pode ler a coluna de CPF" tem resposta consultável, não uma lista mantida à parte.
A diferença estrutural em relação ao desenho mínimo não é "mais uma pasta": é que cada camada passa a ter um DONO — um job que a escreve e que é responsável pelo schema dela no catálogo — e um CONSUMIDOR autorizado por permissão explícita, não por conhecer o caminho do S3.
O ganho que mais se sente no dia a dia
Não é o armazenamento — é que a pergunta "onde está o dado de pedidos de julho, consolidado por loja" passa a ter uma resposta de uma linha: `SELECT * FROM cadencia_ouro.vendas_diarias_por_loja WHERE data_particao LIKE '2026-07%'`. Isso não existia na versão mínima; e o motivo não é o SQL, é que a tabela certa agora existe.
O caminho de um arquivo, ponta a ponta
O que separa bronze de prata de ouro não é só nome de pasta — é o que acontece com um arquivo entre a chegada e o momento em que alguém consulta o dado que ele carrega.
Por que a validação acontece ANTES do catálogo, não depois
Se o crawler ou o job visse o arquivo antes da validação, um Parquet corrompido ou fora do contrato entraria no catálogo e quebraria a consulta de quem já confia na tabela — silenciosamente, até alguém notar um número estranho. Validar na entrada custa uma função pequena; validar depois custa a confiança na tabela inteira.
O que a funcao registra quando move um arquivo para quarentena. E o payload que vale assinar em EventBridge para alertar o time produtor daquele dado.
{
"source": "cadencia.lake.validador",
"detail-type": "Arquivo movido para quarentena",
"detail": {
"bucket": "cadencia-lake",
"chaveOriginal": "bronze/pedidos/ano=2026/mes=08/arquivo-sem-particao-de-dia.parquet",
"chaveQuarentena": "quarentena/pedidos/ano=2026/mes=08/arquivo-sem-particao-de-dia.parquet",
"motivo": "caminho fora do contrato de bronze",
"detectadoEm": "2026-08-07T09:14:02.118Z"
}
}As decisões, e o que se perde em cada uma
📋 Uma equipe de dados de duas pessoas, quatro times produzindo dado para o mesmo lake, e a exigência de que dado sensível (CPF do cliente) não chegue à camada que o analista de negócio consulta.
O contrato por camada resolve o problema declarado — "qual arquivo serve" — sem exigir ferramenta nova: é convenção de prefixo, formato e job, e a equipe de duas pessoas consegue operar isso. Lake Formation resolve a exigência de dado sensível sem duplicar a lógica de permissão em cada consumidor: a coluna de CPF simplesmente não existe em ouro, e mesmo que existisse, LF poderia escondê-la por coluna sem mexer no S3.
Alt: Só convenção de nome, sem separação física de camada — Depende de disciplina humana sustentada por quatro times ao longo de anos — exatamente o que já falhou uma vez. Sem prefixo físico, o crawler continua sem como inferir schema por camada, e a permissão por tabela do LF não tem onde se apoiar, porque tabela e prefixo andam juntos.
Alt: Crawler agendado sobre o lake inteiro, em vez de o job atualizar o catálogo — Funciona, mas cobra por LIST e GET a cada execução mesmo quando nada mudou, e o schema fica desatualizado entre execuções. Vale para descoberta inicial de bronze; não para manter prata e ouro, que já sabem o próprio schema.
Alt: IAM em bucket em vez de Lake Formation — Restringe por prefixo, não por tabela nem coluna — não há como dar acesso a prata sem CPF e negar a coluna isoladamente. Também acopla a permissão à estrutura de pastas: mudar o layout do S3 quebra a política.
Alt: Um bucket por camada, em vez de um bucket com três prefixos — Isola mais, e é defensável em conta com múltiplos times donos de bucket. Para uma equipe de dois, triplica a superfície de política de bucket e de replicação sem resolver nenhum requisito que o prefixo não resolvesse.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Estrutura física da camada | três prefixos num bucket | três buckets separados; separação só por convenção de nome | menos superfície de política para uma equipe de duas pessoas | times com necessidade de bucket isolado por conta precisariam migrar depois |
| Quem alimenta o catálogo | o próprio job, em prata e ouro | crawler agendado sobre todas as camadas | schema sempre atual, sem custo de varredura sobre dado que não mudou | exige que o job seja escrito com o schema explícito, não "o que aparecer" |
| Modelo de permissão | Lake Formation, método nomeado por tabela | IAM em bucket; Lake Formation com LF-TBAC | granularidade de tabela/coluna sem a complexidade adicional de tags | com muitos times e tabelas, o método nomeado vira operação manual repetitiva — é quando migrar para LF-TBAC |
| Onde a coluna sensível é removida | na transformação bronze→prata | manter em prata e restringir só por permissão | quem tem SELECT em prata inteira nunca vê a coluna, mesmo por engano de permissão | se um dia a coluna for necessária a alguém com acesso a prata, exige reprocessar desde bronze |
| Formato de prata e ouro | Parquet, particionado | manter o formato de origem (JSON/CSV) | leitura colunar reduz bytes varridos em até duas ordens de grandeza | exige um job de conversão — que já existe, porque é o mesmo job que limpa e agrega |
| Reprocessamento | escrita com `partitionOverwriteMode=dynamic` | apagar e reprocessar a tabela inteira a cada execução | corrigir um dia não exige reler nem regravar os outros 240 dias de histórico | exige disciplina de sempre passar a partição certa como parâmetro do job |
A dívida que este desenho cria, e que este módulo não paga
Corrigir um registro histórico específico dentro de uma partição já gravada — sem reescrever a partição inteira — não é possível com Parquet simples. É exatamente o que o formato de tabela transacional resolve, com `UPDATE`/`DELETE` reais e histórico de versão. Enquanto você não tiver isso, uma correção pontual custa reprocessar o dia inteiro — aceitável aqui, e é o L67 que resolve de vez.
Construir: o bucket, e o ciclo de vida por camada
O contrato de cada camada não é só documentação — parte dele vira configuração de ciclo de vida, porque bronze, prata e ouro têm padrão de acesso diferente e não deveriam pagar o mesmo preço de armazenamento.
# lake.tf — um bucket, tres prefixos, cada um com o ciclo de vida da sua camada
resource "aws_s3_bucket" "lake" {
bucket = "${var.projeto}-lake"
}
# Bronze e imutavel por convencao de aplicacao (o job nunca sobrescreve uma
# partição existente), reforcada aqui por versionamento: se algo sobrescrever
# por engano, a versao anterior continua recuperavel. Nao e Object Lock —
# Object Lock impediria ate o dono apagar em caso de erro operacional legitimo,
# e a equipe de duas pessoas preferiu o botao de emergencia a rigidez total.
resource "aws_s3_bucket_versioning" "lake" {
bucket = aws_s3_bucket.lake.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "lake" {
bucket = aws_s3_bucket.lake.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.lake.arn
}
bucket_key_enabled = true
}
}
# Bloqueia acesso publico nas quatro dimensoes. Lake com dado de pedido de
# cliente nao tem cenario legitimo de leitura publica.
resource "aws_s3_bucket_public_access_block" "lake" {
bucket = aws_s3_bucket.lake.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# Cada camada tem um ciclo de vida DIFERENTE, porque cada uma serve a um uso
# diferente. E aqui que o contrato de camada vira configuracao, nao so convencao.
resource "aws_s3_bucket_lifecycle_configuration" "lake" {
bucket = aws_s3_bucket.lake.id
# BRONZE: existe para reprocessar, nao para consulta do dia a dia. Fica
# barato rapido, porque quase nunca e lido depois que prata foi gerada.
rule {
id = "bronze-esfria-rapido"
status = "Enabled"
filter { prefix = "bronze/" }
transition {
days = 30
storage_class = "STANDARD_IA"
}
transition {
days = 90
storage_class = "GLACIER"
}
}
# PRATA: consultada por investigacao, com menos frequencia que ouro mas mais
# que bronze. Esfria, mas mais devagar.
rule {
id = "prata-esfria-devagar"
status = "Enabled"
filter { prefix = "prata/" }
transition {
days = 90
storage_class = "STANDARD_IA"
}
}
# OURO: e o que o BI consulta todo dia. Fica em Standard indefinidamente —
# o volume e pequeno porque ja e agregado, entao o custo de nao esfriar e baixo.
rule {
id = "ouro-permanece-quente"
status = "Enabled"
filter { prefix = "ouro/" }
abort_incomplete_multipart_upload {
days_after_initiation = 7
}
}
# QUARENTENA: arquivo que quebrou o contrato. Ninguem investiga depois de
# 60 dias — se ninguem olhou, nao vai olhar, e expirar evita acumulo silencioso.
rule {
id = "quarentena-expira"
status = "Enabled"
filter { prefix = "quarentena/" }
expiration {
days = 60
}
}
}
resource "aws_kms_key" "lake" {
description = "Chave do data lake da Cadência"
enable_key_rotation = true
}
output "bucket_lake" {
value = aws_s3_bucket.lake.id
description = "Bucket unico com prefixos bronze/, prata/, ouro/ e quarentena/"
}
Por que versionamento, e não Object Lock
Object Lock impediria até o dono apagar um objeto por engano de verdade — e numa equipe de duas pessoas sem processo de exceção formal, isso vira bloqueio operacional em vez de proteção. Versionamento dá o mesmo resultado prático contra sobrescrita acidental, com o botão de emergência de restaurar a versão anterior sem precisar de um processo de destravamento.
Construir: catálogo e permissão por tabela e coluna
A decisão central desta seção: os papéis do engenheiro de dados e do analista de negócio NÃO têm `s3:GetObject` no bucket do lake. O acesso ao dado vem do Lake Formation, e é isso que torna a permissão auditável e granular por coluna.
# catalogo-e-permissao.tf — tres bancos no Data Catalog, permissao por tabela/coluna
resource "aws_glue_catalog_database" "bronze" {
name = "cadencia_bronze"
}
resource "aws_glue_catalog_database" "prata" {
name = "cadencia_prata"
}
resource "aws_glue_catalog_database" "ouro" {
name = "cadencia_ouro"
}
# Registra o bucket no Lake Formation. A partir daqui, acesso a qualquer
# recurso registrado passa pelo LF — mesmo que o papel IAM tenha permissao de
# S3, o LF e quem decide se a consulta do Athena enxerga a tabela.
resource "aws_lakeformation_resource" "lake" {
arn = aws_s3_bucket.lake.arn
}
# Papel do engenheiro de dados: pode investigar, entao alcanca bronze e prata.
resource "aws_iam_role" "engenheiro_dados" {
name = "${var.projeto}-engenheiro-dados"
assume_role_policy = data.aws_iam_policy_document.assume_por_identity_center.json
}
# Papel do analista de negocio: so precisa do agregado.
resource "aws_iam_role" "analista_negocio" {
name = "${var.projeto}-analista-negocio"
assume_role_policy = data.aws_iam_policy_document.assume_por_identity_center.json
}
# NAO ha "s3:GetObject" nestes papeis para o bucket do lake. Essa e a decisao
# central: o acesso ao DADO vem do Lake Formation (credential vending), nao de
# uma policy de S3. Um papel com s3:GetObject direto no bucket contornaria o
# LF por completo — e essa e a falha da secao "quebrar de proposito".
# Engenheiro de dados: SELECT em bronze inteiro e em prata inteiro.
resource "aws_lakeformation_permissions" "eng_bronze" {
principal = aws_iam_role.engenheiro_dados.arn
permissions = ["SELECT", "DESCRIBE"]
table {
database_name = aws_glue_catalog_database.bronze.name
wildcard = true
}
}
resource "aws_lakeformation_permissions" "eng_prata" {
principal = aws_iam_role.engenheiro_dados.arn
permissions = ["SELECT", "DESCRIBE"]
table {
database_name = aws_glue_catalog_database.prata.name
wildcard = true
}
}
# Analista de negocio: SELECT so em ouro. Sem acesso a prata nem bronze — nem
# por wildcard, nem por tabela nomeada.
resource "aws_lakeformation_permissions" "analista_ouro" {
principal = aws_iam_role.analista_negocio.arn
permissions = ["SELECT", "DESCRIBE"]
table {
database_name = aws_glue_catalog_database.ouro.name
wildcard = true
}
}
# Restricao por COLUNA: mesmo dentro de prata, o papel de auditoria externa
# (existe so para este exemplo) nao ve a coluna de CPF. E a prova de que a
# granularidade e de coluna, nao so de tabela.
resource "aws_lakeformation_permissions" "auditoria_prata_sem_cpf" {
principal = aws_iam_role.auditoria_externa.arn
permissions = ["SELECT"]
table_with_columns {
database_name = aws_glue_catalog_database.prata.name
name = "pedidos"
excluded_column_names = ["cpf_cliente"]
}
}
# Papel de execucao dos jobs Glue: PRECISA de s3:GetObject/PutObject no bucket,
# porque quem grava a partir do dado bruto e o job, nao uma consulta do Athena.
# Este e o unico lugar em que acesso direto ao S3 e correto.
data "aws_iam_policy_document" "execucao_job" {
statement {
effect = "Allow"
actions = ["s3:GetObject", "s3:PutObject", "s3:ListBucket"]
resources = [
aws_s3_bucket.lake.arn,
"${aws_s3_bucket.lake.arn}/*",
]
}
statement {
effect = "Allow"
actions = ["glue:GetTable", "glue:UpdateTable", "glue:GetDatabase", "glue:CreateTable"]
resources = ["*"]
# As quatro acoes de catalogo do Glue nao aceitam ARN de tabela individual
# neste conjunto — sao operacoes de metadado da conta/regiao. O acesso
# granular ao DADO continua sendo o do Lake Formation, acima.
}
}
O `*` que aparece na policy de execução do job, e por que ele se justifica
`glue:GetTable`, `glue:UpdateTable`, `glue:GetDatabase` e `glue:CreateTable` não aceitam ARN de tabela individual neste conjunto de ações — são operações de metadado da conta/região, e escrevê-las com ARN específico simplesmente não restringe nada. O acesso granular ao DADO em si continua sendo decidido pelo Lake Formation, na seção anterior. Sem esta frase, o `*` seria preguiça; com ela, é decisão registrada.
Construir: o job que limpa e o job que agrega
Dois jobs, duas perguntas diferentes. O primeiro decide o que é verdade — deduplica, tipa, descarta o inválido. O segundo só resume: nunca lê bronze, porque tudo que ele precisa já passou pela limpeza.
# bronze_para_prata.py — job do Glue (PySpark). Le uma particao, escreve outra.
#
# Roda com job bookmark ligado: cada execucao processa so os arquivos novos
# daquela particao desde a ultima vez. Reexecutar o MESMO dia nao duplica,
# porque a escrita usa partitionOverwriteMode=dynamic: substitui so a
# particao gravada, nunca o restante da tabela.
import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from awsglue.context import GlueContext
from awsglue.job import Job
from pyspark.context import SparkContext
from pyspark.sql import functions as F
from pyspark.sql.window import Window
args = getResolvedOptions(sys.argv, ['JOB_NAME', 'data_particao'])
sc = SparkContext()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args['JOB_NAME'], args)
# Necessario para o INSERT OVERWRITE atingir so a particao escrita.
spark.conf.set("spark.sql.sources.partitionOverwriteMode", "dynamic")
particao = args['data_particao'] # ex.: '2026-08-07'
bruto = glueContext.create_dynamic_frame.from_catalog(
database="cadencia_bronze",
table_name="pedidos",
push_down_predicate=f"data_particao = '{particao}'",
transformation_ctx="bruto_bookmark", # ancora do job bookmark
).toDF()
# Deduplicacao: a captura continua pode entregar o MESMO pedido mais de uma
# vez (reenvio apos falha de rede). Mantem a versao mais recente por chave.
janela = Window.partitionBy("pedido_id").orderBy(F.col("capturado_em").desc())
limpo = (
bruto
.withColumn("linha", F.row_number().over(janela))
.filter(F.col("linha") == 1)
.drop("linha")
# Tipagem explicita: bronze guarda como chegou (string), prata corrige.
.withColumn("valor_total", F.col("valor_total").cast("decimal(12,2)"))
.withColumn("criado_em", F.to_timestamp("criado_em"))
# Registro sem chave primaria nao entra em prata — vai para quarentena
# por um segundo job, fora do escopo deste laboratorio.
.filter(F.col("pedido_id").isNotNull())
.withColumn("data_particao", F.lit(particao))
)
saida = glueContext.create_dynamic_frame.from_dataframe(limpo, glueContext, "saida")
# updateBehavior=UPDATE_IN_DATABASE: o proprio job mantem o catalogo de prata
# atualizado. Nao existe crawler agendado sobre prata neste desenho.
glueContext.write_dynamic_frame.from_catalog(
frame=saida,
database="cadencia_prata",
table_name="pedidos",
additional_options={
"partitionKeys": ["data_particao"],
"updateBehavior": "UPDATE_IN_DATABASE",
},
)
job.commit()
# prata_para_ouro.py — job do Glue (PySpark). So agrega; nunca le bronze.
import sys
from awsglue.utils import getResolvedOptions
from awsglue.context import GlueContext
from awsglue.job import Job
from pyspark.context import SparkContext
from pyspark.sql import functions as F
args = getResolvedOptions(sys.argv, ['JOB_NAME', 'data_particao'])
sc = SparkContext()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args['JOB_NAME'], args)
spark.conf.set("spark.sql.sources.partitionOverwriteMode", "dynamic")
particao = args['data_particao']
prata = glueContext.create_dynamic_frame.from_catalog(
database="cadencia_prata",
table_name="pedidos",
push_down_predicate=f"data_particao = '{particao}'",
transformation_ctx="prata_bookmark",
).toDF()
# O UNICO ponto do pipeline em que a granularidade muda de evento para
# resumo. E por isso que ouro existe separado de prata: sao duas perguntas
# diferentes ("o que aconteceu" vs "quanto vendeu cada loja").
agregado = (
prata
.groupBy("loja_id", "data_particao")
.agg(
F.count("pedido_id").alias("qtd_pedidos"),
F.sum("valor_total").alias("valor_total_dia"),
F.avg("valor_total").alias("ticket_medio"),
)
)
saida = glueContext.create_dynamic_frame.from_dataframe(agregado, glueContext, "saida")
glueContext.write_dynamic_frame.from_catalog(
frame=saida,
database="cadencia_ouro",
table_name="vendas_diarias_por_loja",
additional_options={
"partitionKeys": ["data_particao"],
"updateBehavior": "UPDATE_IN_DATABASE",
},
)
job.commit()
A pegadinha do `partitionOverwriteMode`
O padrão do Spark é `static`: ao escrever com `INSERT OVERWRITE`, ele apaga a TABELA inteira antes de gravar, não só a partição do DataFrame. Sem trocar para `dynamic`, reexecutar o job para reprocessar um único dia apaga sozinho os outros 240 dias de histórico de prata. Este é o tipo de erro que só aparece na segunda execução — a primeira sempre "funciona".
Construir: o validador de contrato, em .NET 8
É a única peça de aplicação deste laboratório, e ela existe para um propósito estreito: decidir, antes de qualquer job olhar o arquivo, se ele obedece ao contrato de bronze. Contrato quebrado não é bloqueado silenciosamente — vai para quarentena, com o motivo anexado.
// ContratoBronzeValidator.cs — Lambda em .NET 8, acionada por evento de criacao
// de objeto no prefixo bronze/. E o unico ponto do pipeline que decide se um
// arquivo "conta" como dado de bronze ou vai para quarentena.
using Amazon.Lambda.Core;
using Amazon.Lambda.S3Events;
using Amazon.S3;
using Amazon.S3.Model;
using System.Text.RegularExpressions;
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace Cadência.Lake.Validador;
public class Function
{
// O contrato de bronze, em uma expressao: dominio conhecido, particionado
// por ano/mes/dia, e extensao que o job sabe ler. Qualquer coisa fora
// disto nao e "bronze invalido" — e simplesmente NAO bronze.
private static readonly Regex CaminhoValido = new(
@"^bronze/(pedidos|estoque|clientes)/ano=\d{4}/mes=\d{2}/dia=\d{2}/[\w-]+\.parquet$",
RegexOptions.Compiled);
private static readonly IAmazonS3 _s3 = new AmazonS3Client();
public async Task FunctionHandler(S3Event evento, ILambdaContext contexto)
{
foreach (var registro in evento.Records)
{
var bucket = registro.S3.Bucket.Name;
var chave = Uri.UnescapeDataString(registro.S3.Object.Key);
var motivo = await Validar(bucket, chave);
if (motivo is not null)
{
await MoverParaQuarentena(bucket, chave, motivo);
contexto.Logger.LogWarning(
$"quarentena: {chave} — {motivo}");
continue;
}
contexto.Logger.LogInformation($"contrato ok: {chave}");
}
}
private async Task<string?> Validar(string bucket, string chave)
{
// 1. Caminho e nome. Nao abre o arquivo se o caminho ja reprova —
// e a verificacao mais barata, entao vem primeiro.
if (!CaminhoValido.IsMatch(chave))
return $"caminho fora do contrato de bronze: '{chave}'";
// 2. Tamanho minimo. Arquivo de 0 byte e sintoma comum de escrita
// interrompida no meio (upload multipart que falhou parcialmente).
var cabecalho = await _s3.GetObjectMetadataAsync(bucket, chave);
if (cabecalho.ContentLength < 12) // menor Parquet valido tem magic bytes
return $"arquivo suspeito de escrita incompleta: {cabecalho.ContentLength} bytes";
// NAO verificado aqui: schema interno do Parquet. Abrir e ler o
// rodape do arquivo custa uma leitura completa do objeto na Lambda,
// e o job Glue ja reprova schema incompativel na proxima execucao.
// Declarar esse limite é mais honesto que fingir uma validação de
// schema que este laboratório não implementa.
return null;
}
private async Task MoverParaQuarentena(string bucket, string chave, string motivo)
{
var destino = chave.Replace("bronze/", "quarentena/", StringComparison.Ordinal);
await _s3.CopyObjectAsync(new CopyObjectRequest
{
SourceBucket = bucket,
SourceKey = chave,
DestinationBucket = bucket,
DestinationKey = destino,
MetadataDirective = S3MetadataDirective.REPLACE,
});
// O motivo fica no proprio objeto — quem for investigar a quarentena
// nao precisa cruzar com log do CloudWatch para saber por que caiu ali.
await _s3.PutObjectTaggingAsync(new PutObjectTaggingRequest
{
BucketName = bucket,
Key = destino,
Tagging = new Tagging
{
TagSet = [new Tag { Key = "motivo-quarentena", Value = motivo[..Math.Min(motivo.Length, 200)] }],
},
});
await _s3.DeleteObjectAsync(bucket, chave);
}
}
O que este validador NÃO verifica, por decisão explícita
Ele não abre o Parquet para conferir o schema interno — isso custaria uma leitura completa do objeto dentro da Lambda, para um arquivo que pode ter centenas de MB. A verificação de schema fica para o job do Glue, que já precisa ler o arquivo mesmo. O validador cobre o que é barato verificar sem abrir o conteúdo: caminho, extensão e tamanho mínimo plausível.
Implantar, e provar que o contrato se sustenta
#!/usr/bin/env bash
set -euo pipefail
terraform apply -auto-approve
# Publica os dois jobs do Glue a partir do script local.
aws s3 cp bronze_para_prata.py "s3://$(terraform output -raw bucket_lake)/scripts/"
aws s3 cp prata_para_ouro.py "s3://$(terraform output -raw bucket_lake)/scripts/"
# Publica a Lambda validadora (build previo com dotnet lambda package).
aws lambda update-function-code \
--function-name cadencia-contrato-bronze-validator \
--zip-file fileb://bin/Release/net8.0/ContratoBronzeValidator.zip
echo "lake no ar; proxima etapa: provas.sh"Cinco provas. Nenhuma aceita "parece que organizou" como resultado — cada uma tem um número ou um estado esperado.
BUCKET=$(terraform output -raw bucket_lake)
# ── Prova 1: a estrutura existe, e cada camada tem dono de schema ────────────
aws s3 ls "s3://$BUCKET/"
# Esperado: bronze/ prata/ ouro/ quarentena/ scripts/
aws glue get-tables --database-name cadencia_prata --query "TableList[].Name" --output text
# Esperado: pedidos — registrada PELO JOB, sem crawler agendado sobre prata
# ── Prova 2: bytes varridos, ouro contra bronze, mesma pergunta de negocio ───
aws athena start-query-execution \
--query-string "SELECT sum(valor_total_dia) FROM cadencia_ouro.vendas_diarias_por_loja WHERE data_particao LIKE '2026-07%'" \
--result-configuration OutputLocation=s3://$BUCKET/consultas/ \
--query-execution-context Database=cadencia_ouro
# aws athena get-query-execution --query-execution-id <id> --query "QueryExecution.Statistics.DataScannedInBytes"
# Esperado: poucas centenas de MB. A mesma pergunta contra bronze sem filtro de
# particao varre gigabytes — e a diferenca e a prova da secao de custo.
# ── Prova 3: a negativa do Lake Formation, testada de verdade ────────────────
aws sts assume-role --role-arn "$(terraform output -raw papel_analista_negocio)" \
--role-session-name teste-negativa --query "Credentials" --output json > /tmp/cred.json
# com essas credenciais, tente consultar cadencia_prata.pedidos no Athena
# Esperado: erro de permissao do Lake Formation, nao um resultado vazio
# ── Prova 4: reprocessar um dia nao duplica os outros ────────────────────────
aws athena start-query-execution --query-execution-context Database=cadencia_prata \
--query-string "SELECT count(*) FROM pedidos" --result-configuration OutputLocation=s3://$BUCKET/consultas/
# anote o numero, rode o job bronze_para_prata de novo para o MESMO dia, repita a
# contagem. Esperado: numero identico. Se cresceu, o overwrite nao esta dynamic.
# ── Prova 5: contrato invalido cai em quarentena, nao no catalogo ────────────
echo "lixo" | aws s3 cp - "s3://$BUCKET/bronze/sem-particao.txt"
sleep 5
aws s3 ls "s3://$BUCKET/quarentena/" | grep sem-particao
# Esperado: o arquivo aparece em quarentena, com a tag motivo-quarentena, e NUNCA
# aparece em bronze/ nem em nenhuma tabela do catalogo.| Prova | Comando | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|---|
| 1 · Estrutura e dono do schema | `s3 ls` + `glue get-tables` | quatro prefixos; tabela de prata existe sem crawler ter rodado sobre ela | se a tabela só aparece depois de crawler manual, o job não está atualizando o catálogo |
| 2 · Bytes varridos | `DataScannedInBytes` da consulta | ouro varre duas ordens de grandeza menos que bronze para a mesma pergunta | se a diferença é pequena, falta partição, ou a consulta não filtra por ela |
| 3 · Negativa do Lake Formation | assumir papel do analista e consultar prata | erro de permissão do LF, explícito | se retorna resultado vazio (não erro), a tabela pode não estar registrada no LF |
| 4 · Reprocessamento idempotente | contagem antes/depois de rerun do mesmo dia | contagem idêntica | contagem maior indica `partitionOverwriteMode` estático, sobrescrevendo tudo ou nada |
| 5 · Quarentena funciona | subir arquivo fora do contrato | aparece em quarentena com a tag de motivo, nunca em bronze nem no catálogo | se aparece no catálogo, a validação não está no caminho antes do job |
Quebrar de propósito: três falhas e o diagnóstico
As três falhas abaixo têm o mesmo sintoma superficial — "o número está errado" ou "o acesso está errado" — e são diagnosticadas em lugares completamente diferentes.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| Reprocessamento duplica linha | remova `partitionOverwriteMode=dynamic` do job e rode duas vezes para o mesmo dia | contagem de linhas em prata dobra a cada reexecução | contagem por partição antes/depois; comportamento do `INSERT OVERWRITE` | reativar `dynamic`, ou truncar explicitamente a partição antes de escrever |
| Permissão de bucket contorna o Lake Formation | adicione `s3:GetObject` no papel do analista, direto no bucket | Athena nega a consulta em prata, mas `aws s3 cp` do mesmo objeto funciona | policy IAM do papel, fora do que o Lake Formation administra | remover o acesso direto de S3; todo acesso ao dado passa pelo LF |
| Crawler substitui o job como dono do schema | agende um crawler genérico também sobre prata | a fatura do Glue sobe sem novo volume de dado, e o schema oscila entre execuções | histórico de execuções do crawler vs. do job na mesma tabela | um dono por tabela: o job atualiza; nenhum crawler agendado sobre camada já gerida por job |
A falha que a negativa do Lake Formation não cobre sozinha
O Lake Formation decide se uma consulta via Athena ou Glue enxerga a tabela. Ele não intercepta uma chamada direta de `s3:GetObject` feita com credencial que já tem permissão de bucket — porque esse caminho nunca passa pelo Data Catalog. É por isso que a Terraform desta seção deliberadamente NÃO dá `s3:GetObject` aos papéis de consumo: sem essa ausência, a permissão do LF seria decorativa.
Um analista de negócio tem SELECT concedido pelo Lake Formation apenas no banco `cadencia_ouro`. Ele consegue ler um objeto Parquet de `cadencia_prata` copiando-o diretamente com `aws s3 cp`. O que isso indica?
Segurança: o que muda quando o dado ganha camada
Estruturar o lake também estrutura o risco: ele deixa de ser "alguém acha um arquivo sensível por acaso" e passa a ser "a permissão da camada certa está configurada certo".
| Risco | Probabilidade | Impacto | Controle preventivo | Detecção | Resposta |
|---|---|---|---|---|---|
| Acesso direto ao S3 contorna o Lake Formation | média | alto | nenhum papel de consumo recebe `s3:GetObject` no bucket do lake | CloudTrail: `GetObject` por identidade que não é papel de execução de job | revogar a policy que concedeu o acesso direto; auditar o que foi lido |
| CPF do cliente chega a uma camada de consumo amplo | baixa | alto | coluna removida na transformação bronze→prata; ouro nunca a recebeu | Macie sobre prata e ouro, periodicamente | reprocessar a camada afetada sem a coluna; avaliar necessidade de notificação |
| Permissão do Lake Formation concedida ampla demais | média | médio | concessão por tabela nomeada, nunca banco inteiro por hábito | revisão periódica de `list-permissions` | revogar e reconceder no nível de tabela ou coluna que o caso exige |
| Bronze sobrescrito por engano | baixa | alto — perde a fonte de reprocessamento | versionamento no bucket; convenção de escrita write-once por partição | CloudTrail: `PutObject` sobre chave já existente em bronze | restaurar a versão anterior do objeto; se não houver, reextrair da fonte via L61 |
| Job com papel de execução amplo demais | média | médio | policy restrita a `s3:GetObject/PutObject/ListBucket` só no bucket do lake | IAM Access Analyzer sobre uso real do papel | derivar a política do uso medido, remover ação não exercida |
Por que remover a coluna em prata, e não só restringir por permissão
Restringir por Lake Formation protege quem consulta com a identidade certa. Remover a coluna na origem protege também contra erro de configuração de permissão — se algum dia um papel receber acesso mais amplo do que deveria, a coluna simplesmente não existe para ser exposta. É defesa em profundidade: duas camadas de proteção que falham de formas diferentes.
Observabilidade: as perguntas que o painel do lake responde
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| O job de bronze→prata está atrasado? | duração e horário do run do Glue job | captura acumulando sem ser processada | > 2 h desde a última execução bem-sucedida |
| Quanto cada consulta varre? | `DataScannedInBytes` por execução do Athena | consulta caiu numa camada errada ou perdeu a partição | consulta em ouro > 5 GB |
| Alguém consulta prata quando deveria usar ouro? | log de consulta do Athena por papel | padrão de acesso sugere que ouro não cobre a necessidade real | volume de consulta a prata crescendo sem novo caso de investigação |
| Quantos arquivos caem em quarentena por dia? | contagem de objetos no prefixo `quarentena/` | produtor está enviando fora do contrato de forma sistemática | > 1% do volume diário |
| A permissão do Lake Formation mudou? | CloudTrail em `GrantPermissions`/`RevokePermissions` | concessão fora do processo revisado | qualquer concessão fora de Terraform aplicado |
| O schema de uma tabela mudou sem aviso? | versão do schema no Data Catalog | coluna nova ou removida silenciosamente entre execuções | qualquer mudança de versão fora de deploy planejado |
A métrica que parece boa e esconde o problema
Custo de armazenamento estável não significa lake saudável — bronze pode estar crescendo enquanto prata e ouro ficam parados, porque o job está falhando silenciosamente e ninguém está olhando a duração do run. O painel certo mede o PIPELINE, não só o bucket.
Escala: 10, 10 mil, 1 milhão de objetos
| Volume | O que acontece | O que passa a doer | O que fazer |
|---|---|---|---|
| 10 arquivos/dia | jobs terminam em segundos; catálogo trivial de manter | nada; é abaixo do cenário deste laboratório | nada |
| 1.800 objetos/dia (Cadência hoje) | jobs em minutos; contrato de nome já necessário | exatamente o que este laboratório resolve | a estrutura de três camadas descrita aqui |
| 10 mil objetos/dia | o job ainda processa por partição, mas o número de arquivos por partição cresce | "small file problem": muitos arquivos pequenos custam mais em LIST e em abertura do que o volume de bytes justificaria | compactar dentro da partição — o job já escreve poucos arquivos grandes em vez de milhares de pequenos |
| 1 milhão de objetos | listagem do S3 vira parte relevante do tempo do job | crawler (se ainda existisse sobre essa camada) demoraria horas; catálogo desatualiza | S3 Inventory em vez de listagem via API; particionamento mais fino (por hora, não só por dia) |
| Pico de ingestão (ex.: Black Friday) | mais partições novas no mesmo dia | o job agendado por horário fixo pode não dar conta do volume extra | acionar por evento (chegada de dado) além de horário, com concorrência controlada |
| "Falha de AZ" | não se aplica da forma como se aplica a EC2/RDS | S3 é um serviço regional, replicado entre AZs por padrão — o ponto de atenção real é a disponibilidade regional do Glue e do próprio Athena, não uma AZ isolada | nenhuma ação de rede; a resiliência aqui vem de o dado bruto em bronze permitir reprocessar do zero em outra região, se necessário |
A pergunta de exame que este dimensionamento resolve
Um cenário de prova frequentemente pede para você escolher entre "aumentar o cluster" e "mudar a estratégia de arquivo" quando uma consulta está lenta em grande escala. Com lake bem particionado e em formato colunar, o gargalo raramente é poder computacional — é volume de bytes lido por arquivo pequeno demais. A resposta certa quase sempre é compactar, não escalar.
Custo: onde o dinheiro vai, camada por camada
Um lake em camadas não é mais caro que um bucket único — na maioria dos casos é mais barato, porque bronze esfria e ouro é pequeno. O risco de custo está nos hábitos que a estrutura, sozinha, não impede.
| Cenário | Volume | O que domina o custo | Tendência | Otimização |
|---|---|---|---|---|
| Protótipo | < 100 GB, poucas consultas por semana | armazenamento em Standard; quase tudo desprezível | baixa | nenhuma; otimizar aqui é gastar atenção onde não há dinheiro |
| Cadência hoje (produção pequena) | 2,3 TB, dezenas de consultas por dia | DPU-hora dos jobs Glue e bytes varridos por consulta de investigação em prata | crescente com o volume de captura | ciclo de vida por camada + formato colunar já reduzem bytes varridos |
| Alta escala | dezenas de TB, centenas de consultas por dia | bytes varridos passa a dominar se alguém consulta bronze/prata por hábito | pode crescer mais rápido que o armazenamento | CTAS para materializar agregados frequentes; revisar quem consulta o quê |
| Dimensão | Cobra por | Cuidado |
|---|---|---|
| Armazenamento S3, por camada | GB-mês, por classe de armazenamento | bronze sem regra de ciclo de vida nunca esfria, e é a maior parte do volume |
| Execução do Glue job | DPU-hora | job que lê partição inteira sem bookmark reprocessa histórico a cada execução |
| Consulta do Athena | TB varrido | consulta sem filtro de partição em prata ou bronze é a linha que mais surpreende |
| Requisições ao S3 | GET/LIST por milhar | muitos arquivos pequenos multiplicam requisições sem aumentar volume de dado útil |
| Lake Formation | sem custo direto de permissão | o custo indireto é operacional: manter concessão por tabela exige revisão periódica |
O custo oculto que a estrutura evita, mas não sozinha
Bronze sem regra de ciclo de vida é o erro mais comum: como "nunca se mexe nela", ninguém lembra de configurar a transição de classe de armazenamento, e ela cresce para sempre em Standard. A regra deste laboratório resolve isso — mas só porque foi escrita explicitamente. Copiar a estrutura de pasta sem copiar o Terraform do ciclo de vida reproduz exatamente o problema que a camada deveria evitar.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | contrato de camada explícito; jobs idempotentes; validação na entrada | ainda não há alerta automático de qualidade de dado por regra de negócio | Glue Data Quality com quarentena por regra (L70) | alta |
| Segurança | permissão por tabela/coluna no Lake Formation; coluna sensível removida na origem | concessão de permissão ainda é operação manual, sem revisão periódica automatizada | revisão programada de `list-permissions` + governança por coluna em escala (L69) | alta |
| Confiabilidade | bronze imutável garante reprocessamento; jobs reexecutáveis sem duplicar | correção pontual dentro de uma partição ainda exige reprocessar o dia inteiro | formato de tabela transacional com UPDATE/DELETE (L67) | média |
| Eficiência de performance | formato colunar, partição por data, agregação pronta em ouro | consulta ad hoc ainda pode escapar da partição por engano | projeção de partição e revisão de consultas mais custosas | média |
| Otimização de custos | ciclo de vida por camada; catálogo mantido pelo job, sem crawler redundante | sem alerta de orçamento específico para o lake | AWS Budgets com alarme por serviço (Glue, Athena) e por prefixo | média |
| Sustentabilidade | bronze esfria para classes de menor pegada; menos bytes lidos por consulta | histórico retido indefinidamente em Glacier ainda ocupa recurso físico | revisar horizonte real de reprocessamento e definir expiração de bronze muito antigo | baixa |
Evolução em níveis: o que muda, e o que passa a doer
A terceira arquitetura não é um desenho novo: é a resposta a QUANDO o lake em três camadas deixa de ser suficiente, e o que cada nível seguinte troca por um risco novo.
Um bucket, sem prefixo por camada, crawler genérico sobre tudo. É onde a Cadência estava, e continua legítimo para um experimento de poucos arquivos.Três camadas com contrato, jobs idempotentes atualizando o próprio catálogo, Lake Formation concedendo acesso por tabela e coluna.Glue Data Quality valida contrato de conteúdo — faixa de valor, schema esperado, taxa de nulo aceitável — e quarentena por regra de negócio, não só por caminho malformado (L70).Ingestão contínua via streaming em vez de lote, com compactação automática combatendo o problema do arquivo pequeno (L63, L64).Vários times donos de seus próprios dados, com Lake Formation tag-based cruzando contas, e governança por coluna em escala (L69).A camada ouro deixa de servir só o BI e passa a alimentar um MODELO: features para treino, ou uma base de conhecimento que um agente de IA consulta para responder pergunta de negócio sobre os DADOS agregados em linguagem natural.A ordem não é negociável, e o motivo é concreto
Um agente de IA respondendo sobre dado agregado (nível 6) depende de a agregação estar correta — que é o que a validação de conteúdo do nível 3 garante. Sem ela, o nível 6 produz respostas fluentes e erradas com a mesma confiança de respostas certas, porque nada na cadeia sinalizou o problema antes da IA.
Onde IA entra nesta arquitetura, e onde não entra
Neste módulo, IA não resolve o problema central, e forçá-la seria o antipadrão que a própria série critica. "Qual arquivo serve" tem resposta determinística: contrato de camada, formato e partição. Um modelo não organiza um lake — disciplina de engenharia organiza.
Há um lugar em que IA acrescentaria valor real, e ele é modesto: classificar um arquivo que chega fora do contrato de nome, quando ele é legítimo mas mal nomeado por um time novo. Hoje esse arquivo vai para quarentena e alguém corrige manualmente o nome. Um classificador sobre o conteúdo poderia sugerir o domínio correto — mas só depois que a regra simples (regex de caminho) mostrar seu limite real, não antes.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria? | sugerir o domínio correto de um arquivo mal nomeado, em vez de quarentena manual |
| Por que uma regra não bastaria? | a regra de caminho já resolve o caso comum; IA só se justificaria para o resíduo — arquivos legítimos mas fora do padrão de nome |
| De onde viriam os dados? | conteúdo do próprio arquivo (colunas, faixa de valor) comparado ao schema das tabelas já catalogadas |
| Qual o risco? | classificar errado e mover um arquivo sensível para uma camada com permissão mais ampla do que deveria |
| Por que não agora? | a Cadência tem 187 padrões de nome, a maioria corrigível por convenção simples primeiro |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para "olhar o bucket e organizar os arquivos" troca um contrato determinístico — caminho, formato, schema — por uma decisão probabilística sobre onde cada arquivo pertence. Um erro de classificação nessa tarefa não gera resposta chata: gera dado sensível na camada errada, com a permissão errada. Organização estrutural é exatamente o domínio onde regra determinística vence.
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 |
|---|---|---|---|---|---|
| Uma pasta, todo mundo escreve | é o caminho de menor resistência do DMS e das exportações ad hoc | nenhum arquivo diz se é bruto, limpo ou pronto; a pergunta de negócio não tem tradução | "qual arquivo serve" vira arqueologia; 41 mil objetos sem dono | prefixo por camada, com contrato de schema/formato/frequência documentado | nunca em produção; tolerável num rascunho de poucos arquivos |
| Corrigir dado direto na camada bronze | parece mais rápido do que reprocessar tudo | destrói a fonte de verdade para reprocessamento e quebra a auditoria de "o que a fonte enviou" | bronze não bate mais com o que a captura realmente entregou | bronze é imutável; correção entra via nova captura ou reprocessamento explícito | nunca |
| Permissão de bucket em vez de Lake Formation | IAM em S3 é o que todo time de infraestrutura já sabe fazer | contorna a governança por tabela/coluna; a negativa do LF vira decorativa | Athena nega, mas `s3 cp` do mesmo objeto funciona | acesso ao dado só via Lake Formation; papel de consumo sem `s3:GetObject` direto | nunca em dado com qualquer classificação de sensibilidade |
| Crawler agendado de hora em hora sobre o lake inteiro | parece garantir "sempre atualizado" | custo de LIST/GET cresce com o volume mesmo quando nada mudou | fatura do Glue crawler maior que a de armazenamento | crawler só para descoberta inicial de bronze; prata e ouro atualizados pelo próprio job | lake pequeno, poucas dezenas de milhares de objetos, sem job próprio ainda |
| Camada ouro sem contrato de atualização documentado | ninguém prioriza documentar frequência quando o pipeline "está funcionando" | decisão de negócio tomada sobre dado desatualizado sem ninguém saber que está desatualizado | número "errado" que na verdade está certo — só que de ontem | contrato explícito de frequência por tabela, e marca de "atualizado até" na própria tabela | nunca |
| Achar que o nome "ouro" garante qualidade sozinho | o nome da camada sugere confiabilidade | sem validação de conteúdo, um bug no job de agregação propaga silenciosamente até ouro | número redondo demais, e ninguém questiona porque "é a camada de confiança" | validação de contrato de conteúdo antes de publicar (L70) | protótipo em que o time ainda confere os números manualmente |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| `HIVE_PARTITION_SCHEMA_MISMATCH` no Athena | prata foi reescrita com schema diferente sem o catálogo saber | compare o schema do job com a versão registrada na tabela | histórico de versão da tabela no Data Catalog | garantir `updateBehavior=UPDATE_IN_DATABASE` no job, ou reconciliar manualmente |
| Contagem de linhas em prata dobra a cada execução | reprocessamento sem `partitionOverwriteMode=dynamic` | compare contagem por partição antes e depois de reexecutar o mesmo dia | configuração de escrita do job Spark | `partitionOverwriteMode=dynamic`, ou truncar a partição explicitamente antes de escrever |
| Analista recebe erro de permissão mesmo com SELECT concedido em ouro | a tabela ainda está em modo de controle só-IAM, não migrada para o Lake Formation | confira as configurações padrão do catálogo e da tabela específica | configurações do Data Catalog / configuração por tabela no console do LF | desativar o controle apenas-IAM legado nas tabelas que devem obedecer ao LF |
| Consulta varre o mesmo volume independente do filtro de data | a partição não está registrada no catálogo, ou o filtro não usa a coluna de partição | rode `EXPLAIN` na consulta e confira o plano de poda de partição | contagem de partições no catálogo vs. objetos existentes no S3 | garantir que o job registre a partição nova; filtrar sempre pela coluna de partição |
| Crawler classifica arquivo de bronze como `UNKNOWN` | arquivo corrompido, ou escrita interrompida no meio (upload parcial) | abra o arquivo suspeito isoladamente | log do crawler; o arquivo específico apontado | o validador de contrato deveria ter pego isso antes — confira se a Lambda está no caminho do evento |
| Job prata→ouro falha com falta de memória | agregação lendo prata inteira em vez de só a partição do dia | confira as métricas de DPU/memória da execução do job | métricas do run do Glue job | aumentar DPU como paliativo; corrigir o `push_down_predicate` como causa raiz |
| Arquivo legítimo cai em quarentena com frequência | time produtor mudou a convenção de nome sem avisar | leia o motivo anexado na tag do objeto em quarentena | tag `motivo-quarentena` no objeto | alinhar a convenção com o time produtor; ajustar a regex do validador se o contrato mudou de fato |
A pergunta que resolve metade destes casos
Antes de mexer em job ou permissão, pergunte: o problema é no DADO (schema, duplicata, valor) ou no ACESSO (quem pode ver o quê)? As duas famílias de sintoma se parecem — "está errado" — mas se investigam em lugares opostos: uma no job e no catálogo, a outra no Lake Formation e no IAM.
Limpeza: o que o destroy não leva
Este laboratório cria recurso que sobrevive ao terraform destroy de formas específicas — um bucket com objeto dentro, e permissões do Lake Formation que não pertencem ao ciclo de vida do bucket.
# 1. Revogue as permissoes do Lake Formation ANTES de tentar remover o catalogo —
# o Terraform normalmente resolve isso na ordem certa, mas se voce interrompeu
# o apply no meio, pode ficar permissao orfa apontando para tabela que sumiu.
aws lakeformation list-permissions --query "PrincipalResourcePermissions" --output table
# 2. Esvazie o bucket ANTES do destroy: bucket com objeto (e com versionamento,
# com VERSOES de objeto) faz o destroy falhar silenciosamente na aparencia — o
# erro aparece, mas e facil de rodar de novo sem investigar por que falhou.
aws s3api list-object-versions --bucket cadencia-lake \
--query "length(Versions)" --output text
aws s3 rm "s3://cadencia-lake/" --recursive
aws s3api list-object-versions --bucket cadencia-lake \
--query "Versions[].{Key:Key,VersionId:VersionId}" --output json \
| jq -c ".[]" | while read -r v; do
key=$(echo "$v" | jq -r .Key); id=$(echo "$v" | jq -r .VersionId)
aws s3api delete-object --bucket cadencia-lake --key "$key" --version-id "$id"
done
# 3. Agora o Terraform consegue remover bucket, catalogo, permissoes e a chave KMS.
terraform destroy -auto-approve
# 4. CHAVE KMS: entra em exclusao PENDENTE, nao some na hora — continua existindo
# (sem cobrar por uso, mas ocupando o limite de chaves da conta) pelo periodo
# de espera configurado.
aws kms list-keys --query "Keys[?KeyId=='$(terraform output -raw kms_key_id 2>/dev/null)']"
# 5. Prova final: nada com o nome do projeto de pe.
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=Projeto,Values=cadencia-lake \
--query "ResourceTagMappingList[].ResourceARN" --output table| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Bucket com objeto dentro | não, falha até esvaziar | sim, GB-mês | o destroy recusa remover bucket não vazio; com versionamento, "vazio" inclui apagar toda versão |
| Chave KMS | entra em exclusão pendente | não, mas ocupa cota | tem período de espera obrigatório antes de ser removida de fato |
| Permissões do Lake Formation | sim, se em Terraform | não | permissão criada fora do Terraform (à mão no console) não aparece no estado |
| Bancos do Data Catalog | sim | não | sem custo direto, mas tabela órfã confunde quem lista bancos depois |
| Versões antigas de objeto (S3 versionado) | não automaticamente | sim, GB-mês | apagar a versão "atual" não remove as anteriores; cada versão cobra separadamente |
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Não sei qual arquivo serve | três camadas com contrato explícito | prefixo + formato + frequência documentados tornam a pergunta respondível sem abrir arquivo |
| Schema desatualizado ou inconsistente | catálogo alimentado pelo próprio job | quem escreve a tabela é quem melhor sabe o schema dela — sem esperar crawler agendado |
| Reprocessar um dia afeta os outros | `partitionOverwriteMode=dynamic` | escreve só a partição processada, sem tocar no restante da tabela |
| Analista vê dado que não deveria | Lake Formation por tabela e coluna | permissão no catálogo, não no bucket — sobrevive mesmo se a estrutura de pasta mudar |
| CPF do cliente exposto em relatório | coluna removida na transformação bronze→prata | defesa na origem: a coluna nem existe para ser exposta por erro de permissão |
| Consulta cara e lenta | formato colunar + partição em prata e ouro | poda de partição e leitura de coluna reduzem bytes varridos em ordens de grandeza |
| Arquivo fora do contrato entra no catálogo | validador de contrato antes de bronze | barato de verificar sem abrir o conteúdo; quarentena com motivo anexado |
| Falha | O que a protege | O que ela NÃO protege |
|---|---|---|
| Reprocessamento que duplica dado | `partitionOverwriteMode=dynamic` | erro de lógica de agregação que produz número errado, mas sem duplicar linha |
| Acesso indevido via consulta | Lake Formation por tabela/coluna | acesso direto ao S3 com credencial que tem `s3:GetObject` no bucket |
| Arquivo malformado no catálogo | validador de contrato antes de bronze | schema interno inconsistente que passa no caminho e no tamanho — cabe ao L70 |
| Bronze sobrescrito | versionamento + convenção write-once | exclusão deliberada de todas as versões por quem tem permissão para isso |
| Custo de armazenamento crescendo sem limite | ciclo de vida por camada | crescimento do VOLUME de dado captado — ciclo de vida move classe, não reduz volume |
- A captura contínua grava um arquivo Parquet em bronze, dentro da partição do dia.
- O validador confere caminho, extensão e tamanho antes de qualquer job olhar o arquivo.
- Contrato quebrado vai para quarentena, com o motivo anexado como tag.
- O job bronze→prata roda com bookmark, processa só o que é novo naquela partição.
- Ele deduplica, tipa e remove a coluna sensível, gravando em prata com overwrite dinâmico.
- O próprio job atualiza o schema de prata no Data Catalog — nenhum crawler agendado.
- O job prata→ouro lê só prata, agrega por loja e dia, grava em ouro.
- O Lake Formation concede SELECT em ouro ao analista de negócio, e em bronze/prata ao engenheiro.
- Uma consulta em ouro varre poucos gigabytes; a mesma pergunta em bronze varria o histórico inteiro.
- A permissão concedida — e negada — fica registrada e auditável via CloudTrail.
Desafio — sem roteiro
O requisito
Adicione uma NOVA fonte de dado à camada bronze (um segundo tipo de evento, ou uma segunda tabela de origem) e leve-a até um agregado na camada ouro, provando a linhagem de ponta a ponta.
Critério de aceite — executável, não "verifique se funciona"
Um registro específico inserido na fonte nova é rastreável, por um identificador comum, desde o arquivo bruto na camada bronze até o número final que ele afeta na camada ouro — e alterar esse registro de origem muda o agregado ouro na PRÓXIMA execução do pipeline.
- Dica 1: A camada prata é onde a fonte nova precisa ganhar o MESMO formato/schema das fontes existentes (tipos, nomes de coluna, granularidade) — sem essa normalização, a camada ouro não consegue agregar as duas fontes juntas de forma correta.
- Dica 2: Inclua na prata uma coluna de origem (`fonte_dado`) — sem ela, quando o agregado ouro der um número errado, não dá pra saber se o problema veio da fonte antiga ou da nova.
- Dica 3: Prove a linhagem de verdade: pegue UM registro específico, anote seu identificador, e mostre ele (ou o efeito dele) presente nas três camadas — não baste rodar o pipeline e confiar que "não deu erro".
Lembrete de limpeza
Todo recurso que este desafio criar entra no mesmo `terraform destroy` do laboratório — nada fica para trás cobrando sozinho.
Perguntas frequentes
❓ Por que pastas bem nomeadas no S3 não bastam, sem Glue nem Lake Formation?
❓ Qual a diferença real entre a camada prata e a camada ouro, se as duas já são "limpas"?
❓ Preciso de um crawler do Glue rodando sobre prata e ouro?
❓ Lake Formation substitui a política de IAM no bucket do S3?
❓ Por que remover a coluna de CPF na limpeza, e não só restringir por permissão?
❓ O que acontece se eu rodar o job bronze→prata duas vezes para o mesmo dia?
❓ Bronze precisa ficar em Standard para sempre, caso eu precise reprocessar?
❓ Como decidir se um dado precisa das três camadas, ou só de bronze e prata?
Fixando
O job `bronze_para_prata.py` roda pela segunda vez para reprocessar o dia 2026-08-07, depois de uma correção no código. A tabela de prata tinha 240 dias de histórico. O que acontece se `partitionOverwriteMode` está no padrão do Spark (`static`) em vez de `dynamic`?
Uma tabela em `cadencia_ouro` mostra o mesmo total de vendas há três dias seguidos, mesmo com pedidos novos entrando em bronze todo dia. O disjuntor de qualidade do L70 ainda não foi implementado. Qual é a investigação mais direta?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L61 no ar (extração incremental do operacional para o S3), SQL básico, noções de Spark/PySpark e de particionamento |
| Conhecimentos adquiridos | o que cada camada garante e o que não garante; contrato de camada como schema + formato + frequência + dono; catálogo alimentado pelo job em vez de crawler agendado; permissão por tabela e coluna via Lake Formation, e por que ela não substitui revisar IAM de bucket; reprocessamento idempotente com overwrite dinâmico por partição |
| Limitação que fica | correção pontual dentro de uma partição já gravada exige reprocessar o dia inteiro — Parquet simples não tem UPDATE/DELETE real; e o conteúdo do dado ainda não é validado por regra de negócio, só o caminho e o tamanho do arquivo |
| Próximo exemplo recomendado | L65 — Catálogo e ETL: Glue Data Catalog, crawler e job idempotente em profundidade. Reutiliza os dois jobs deste módulo e detalha a evolução de schema |
| Também habilitado por este módulo | L67 (lakehouse com Iceberg, upsert e time travel) resolve a limitação de correção pontual; L69 (governança do lake por coluna, em escala) estende a permissão do Lake Formation com LF-TBAC; L70 (qualidade de dado e quarentena por regra) valida o CONTEÚDO que este módulo ainda não valida |
| Data da última validação técnica | 8 de agosto de 2026 |
Documentação oficial consultada: Lake Formation tag-based access control (LF-TBAC) — o modelo de permissão por tabela, coluna e tag, e a distinção entre método nomeado e tag-based; e Using crawlers to populate the Data Catalog / Data discovery and cataloging in AWS Glue — o papel do crawler na descoberta inicial e o suporte a crawling incremental via notificação de evento do S3, que é o que justifica não usar crawler agendado sobre camada já mantida por job. Os valores de preço não aparecem neste módulo por decisão: use o AWS Pricing Calculator, porque preço de armazenamento, DPU e byte varrido varia por região e muda com mais frequência do que este conteúdo.
O que não foi verificado, e você deve conferir na sua conta
O volume citado — 41.312 objetos, 2,3 TB — é o cenário de exemplo da Cadência, não uma referência de quando "vale a pena" estruturar em camadas; a resposta certa é "desde o primeiro dia de produção", porque o custo de retrofit cresce com o volume. Os prazos de transição de ciclo de vida (30 e 90 dias) também vêm do padrão de acesso hipotético deste exemplo — meça a frequência real de consulta a bronze e prata na sua conta antes de copiar esses números.
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…