Configuração de um Data Lake na AWS usando S3 como storage central, com Medallion Architecture (Bronze / Silver / Gold), particionamento, lifecycle policies e controle de acesso via IAM.
flowchart TD
subgraph SOURCES["📥 Sources"]
A1[Banco Relacional]
A2[APIs / SaaS]
A3[Arquivos / FTP]
A4[Streaming Events]
end
subgraph INGESTION["⚙️ Ingestion Layer"]
B1[Python ETL]
B2[AWS Glue]
B3[Kinesis Firehose]
end
subgraph LAKE["🗄️ S3 Data Lake"]
direction TB
C1["🔴 Bronze\n(raw / as-is)\ns3://bucket/bronze/"]
C2["🟡 Silver\n(cleaned / validated)\ns3://bucket/silver/"]
C3["🟢 Gold\n(aggregated / business)\ns3://bucket/gold/"]
C1 --> C2 --> C3
end
subgraph CATALOG["🗂️ Metadata"]
D1[AWS Glue Data Catalog]
D2[AWS Athena]
end
subgraph SERVING["📊 Serving"]
E1[Amazon Redshift]
E2[Amazon QuickSight]
E3[BI Tools]
end
SOURCES --> INGESTION
INGESTION --> C1
LAKE --> D1
D1 --> D2
C3 --> E1
E1 --> E2 & E3
style C1 fill:#8B0000,color:#fff
style C2 fill:#B8860B,color:#fff
style C3 fill:#006400,color:#fff
O S3 é o padrão de fato para Data Lakes na AWS porque combina três propriedades difíceis de encontrar juntas: durabilidade de 99.999999999%, custo de armazenamento baixíssimo e desacoplamento entre storage e compute. Esse desacoplamento é a chave — você pode usar Spark, Athena, Glue, Redshift Spectrum ou qualquer engine no mesmo dado sem movê-lo.
A separação em camadas resolve um problema real: o dado bruto precisa ser preservado. Em produção, pipelines falham, regras de negócio mudam e reprocessamento é inevitável. Se você só guarda o dado transformado, perdeu a fonte da verdade.
| Camada | Prefixo S3 | Formato | Conteúdo |
|---|---|---|---|
| Bronze | s3://bucket/bronze/ |
JSON, CSV, Avro (original) | Dado bruto, sem modificação |
| Silver | s3://bucket/silver/ |
Parquet + Snappy | Limpo, tipado, validado |
| Gold | s3://bucket/gold/ |
Parquet + Snappy | Agregado, pronto para consumo |
Particionar dados no S3 é como criar índices em banco relacional — muda drasticamente o custo e velocidade de query. A convenção Hive é suportada nativamente por Athena, Glue e Spark:
s3://bucket/silver/orders/
year=2024/
month=01/
day=15/
part-00000.parquet
A regra prática: particione pela coluna mais usada nos filtros das queries. Para dados transacionais, geralmente é a data. Para dados de eventos, pode ser event_type + data.
Dados acessados com frequência decrescente ao longo do tempo devem automaticamente mudar de Storage Class para reduzir custo:
0-30 dias → S3 Standard (acesso frequente)
30-90 dias → S3 Standard-IA (acesso infrequente)
90-365 dias → S3 Glacier Instant (arquivamento rápido)
365+ dias → S3 Glacier Deep (arquivamento frio)
s3-data-lake-setup/
├── README.md
├── terraform/
│ ├── main.tf # Recursos principais
│ ├── variables.tf # Variáveis configuráveis
│ ├── outputs.tf # Outputs (ARNs, URLs)
│ ├── iam.tf # Roles e policies
│ └── lifecycle.tf # Regras de lifecycle
├── python/
│ ├── setup_lake.py # Script de setup inicial
│ ├── upload_bronze.py # Ingestão para Bronze
│ └── promote_silver.py # Bronze → Silver
└── athena/
└── create_tables.sql # DDL das tabelas externas
# ─────────────────────────────────────────────
# Bucket principal do Data Lake
# ─────────────────────────────────────────────
resource "aws_s3_bucket" "data_lake" {
bucket = var.bucket_name
# Nunca use force_destroy = true em produção
force_destroy = false
tags = {
Environment = var.environment
ManagedBy = "Terraform"
Project = "DataLake"
}
}
# Versionamento: essencial para auditoria e recuperação
resource "aws_s3_bucket_versioning" "data_lake" {
bucket = aws_s3_bucket.data_lake.id
versioning_configuration {
status = "Enabled"
}
}
# Criptografia em repouso com chave gerenciada pela AWS
resource "aws_s3_bucket_server_side_encryption_configuration" "data_lake" {
bucket = aws_s3_bucket.data_lake.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
# Bloqueia acesso público — Data Lakes não são sites
resource "aws_s3_bucket_public_access_block" "data_lake" {
bucket = aws_s3_bucket.data_lake.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}resource "aws_s3_bucket_lifecycle_configuration" "data_lake" {
bucket = aws_s3_bucket.data_lake.id
# Bronze: dado bruto migra para armazenamento frio rapidamente
rule {
id = "bronze-lifecycle"
status = "Enabled"
filter {
prefix = "bronze/"
}
transition {
days = 30
storage_class = "STANDARD_IA"
}
transition {
days = 90
storage_class = "GLACIER_INSTANT_RETRIEVAL"
}
# Bronze nunca é deletado — é a fonte da verdade
}
# Silver: acesso moderado, migra mais devagar
rule {
id = "silver-lifecycle"
status = "Enabled"
filter {
prefix = "silver/"
}
transition {
days = 60
storage_class = "STANDARD_IA"
}
transition {
days = 180
storage_class = "GLACIER_INSTANT_RETRIEVAL"
}
}
# Gold: dado quente, fica em Standard por mais tempo
rule {
id = "gold-lifecycle"
status = "Enabled"
filter {
prefix = "gold/"
}
transition {
days = 90
storage_class = "STANDARD_IA"
}
# Deleta versões antigas após 365 dias para controlar custo
noncurrent_version_expiration {
noncurrent_days = 365
}
}
}# ─────────────────────────────────────────────
# Policy de leitura geral (analistas e BI tools)
# ─────────────────────────────────────────────
resource "aws_iam_policy" "lake_reader" {
name = "${var.project}-lake-reader"
description = "Acesso de leitura ao Data Lake (Silver e Gold apenas)"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["s3:GetObject", "s3:ListBucket"]
Resource = [
aws_s3_bucket.data_lake.arn,
"${aws_s3_bucket.data_lake.arn}/silver/*",
"${aws_s3_bucket.data_lake.arn}/gold/*"
]
}
]
})
}
# ─────────────────────────────────────────────
# Policy de escrita no Bronze (pipelines de ingestão)
# ─────────────────────────────────────────────
resource "aws_iam_policy" "lake_bronze_writer" {
name = "${var.project}-bronze-writer"
description = "Escrita restrita à camada Bronze"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["s3:PutObject", "s3:GetObject", "s3:ListBucket"]
Resource = [
aws_s3_bucket.data_lake.arn,
"${aws_s3_bucket.data_lake.arn}/bronze/*"
]
},
# Leitura explicitamente negada no Gold para pipelines de ingestão
{
Effect = "Deny"
Action = ["s3:PutObject", "s3:DeleteObject"]
Resource = "${aws_s3_bucket.data_lake.arn}/gold/*"
}
]
})
}import boto3
import json
import hashlib
from datetime import datetime
from pathlib import Path
class BronzeIngestion:
"""
Responsável por carregar dados brutos para a camada Bronze do Data Lake.
Princípios aplicados:
- Idempotência: mesmo dado nunca gera arquivo duplicado (hash no nome)
- Particionamento: convenção Hive para compatibilidade com Athena/Glue
- Preservação: dado é escrito exatamente como recebido (sem transformação)
"""
def __init__(self, bucket_name: str, region: str = "us-east-1"):
self.s3 = boto3.client("s3", region_name=region)
self.bucket = bucket_name
def _build_s3_key(self, source: str, data: dict) -> str:
"""
Monta a chave S3 com particionamento Hive e hash para idempotência.
Resultado: bronze/orders/year=2024/month=01/day=15/abc123.json
"""
now = datetime.utcnow()
# Hash garante que o mesmo payload nunca sobrescreve arquivo existente
# e permite reprocessamento sem duplicação
content_hash = hashlib.md5(
json.dumps(data, sort_keys=True).encode()
).hexdigest()[:8]
return (
f"bronze/{source}/"
f"year={now.year}/"
f"month={now.month:02d}/"
f"day={now.day:02d}/"
f"{content_hash}.json"
)
def upload(self, source: str, data: dict) -> str:
"""
Faz upload do dado bruto para o Bronze.
Args:
source: nome da fonte (ex: 'orders', 'users', 'products')
data: payload a ser armazenado sem modificação
Returns:
s3_key do arquivo criado
"""
s3_key = self._build_s3_key(source, data)
self.s3.put_object(
Bucket=self.bucket,
Key=s3_key,
Body=json.dumps(data, ensure_ascii=False),
ContentType="application/json",
# Metadados para rastreabilidade — essencial em produção
Metadata={
"source": source,
"ingested_at": datetime.utcnow().isoformat(),
"schema_version": "1.0",
},
)
print(f"✅ Bronze: s3://{self.bucket}/{s3_key}")
return s3_key
def upload_batch(self, source: str, records: list[dict]) -> list[str]:
"""Processa um batch de registros, retorna lista de keys criadas."""
return [self.upload(source, record) for record in records]
# ─────────────────────────────────────────────
# Uso
# ─────────────────────────────────────────────
if __name__ == "__main__":
ingestion = BronzeIngestion(bucket_name="my-data-lake-prod")
# Simula dados de pedidos vindos de uma API
order = {
"order_id": "ORD-12345",
"customer_id": "CUST-789",
"total": 249.90,
"status": "delivered",
"created_at": "2024-01-15T14:32:00Z",
}
ingestion.upload(source="orders", data=order)-- ─────────────────────────────────────────────
-- Tabela externa apontando para a camada Silver
-- Athena não move dados — query diretamente no S3
-- ─────────────────────────────────────────────
CREATE EXTERNAL TABLE silver.orders (
order_id STRING,
customer_id STRING,
total DOUBLE,
status STRING,
created_at TIMESTAMP
)
-- Particionamento Hive: Athena usa para pruning automático
-- Uma query com WHERE year=2024 AND month=01 lê só essa partição
PARTITIONED BY (
year INT,
month INT,
day INT
)
STORED AS PARQUET
LOCATION 's3://my-data-lake-prod/silver/orders/'
TBLPROPERTIES (
'parquet.compress' = 'SNAPPY',
-- Atualiza automaticamente o catálogo ao adicionar novas partições
'projection.enabled' = 'true'
);
-- Registra as partições existentes no Glue Data Catalog
MSCK REPAIR TABLE silver.orders;
-- ─────────────────────────────────────────────
-- Query de exemplo: receita por mês (só lê Gold)
-- ─────────────────────────────────────────────
SELECT
year,
month,
COUNT(DISTINCT order_id) AS total_orders,
SUM(total) AS gross_revenue,
AVG(total) AS avg_order_value
FROM gold.orders_monthly
WHERE year = 2024
GROUP BY year, month
ORDER BY month;Antes de usar este setup em produção, verifique:
- Versionamento habilitado no bucket
- Acesso público bloqueado
- Criptografia em repouso configurada
- Lifecycle policies ativas em todas as camadas
- IAM policies seguem princípio do menor privilégio
- Logs de acesso habilitados (S3 Server Access Logging)
- Particionamento validado com query de teste no Athena
- Alertas de custo configurados no AWS Budgets
→ Glue ETL Basics — Transformação Bronze → Silver com AWS Glue e PySpark