Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

README.md

🪣 S3 Data Lake Setup

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.


📐 Arquitetura

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
Loading

🧠 Conceitos Fundamentais

Por que S3 como Data Lake?

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.

Medallion Architecture no S3

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

Particionamento

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.

Storage Classes e Lifecycle

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)

📁 Estrutura de Arquivos

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

🏗️ Terraform — Infraestrutura como Código

main.tf — Bucket S3 com configurações de segurança

# ─────────────────────────────────────────────
# 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
}

lifecycle.tf — Transição automática entre Storage Classes

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
    }
  }
}

iam.tf — Controle de acesso por camada

# ─────────────────────────────────────────────
# 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/*"
      }
    ]
  })
}

🐍 Python — Ingestão para o Bronze

upload_bronze.py — Upload idempotente com particionamento por data

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)

🗃️ Athena — Tabelas Externas sobre o S3

-- ─────────────────────────────────────────────
-- 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;

✅ Checklist de Produção

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

🔗 Próximo Módulo

Glue ETL Basics — Transformação Bronze → Silver com AWS Glue e PySpark