top of page

5 _ [LAB - Hotelaria] Construção da Camada Bronze Append-Only com Databricks Asset Bundles


Após concluir a ingestão Source → Landing, avançamos para a construção da camada Bronze. O objetivo desta etapa foi transformar os arquivos Parquet armazenados em Unity Catalog Volumes em tabelas Delta gerenciadas no Unity Catalog, mantendo a Bronze como uma camada append-only, auditável e reprocessável.

A arquitetura até este ponto ficou assim:

Neon PostgreSQL
    ↓
Foreign Catalog: neon_hotelaria.public
    ↓
Job Source → Landing
    ↓
Unity Catalog Volume
    /Volumes/hotelaria_dev/landing/neon_postgresql/<tabela>/
    ↓
Job Landing → Bronze
    ↓
hotelaria_dev.bronze.<tabela>

1. Objetivo da Bronze

A camada Bronze representa os dados brutos estruturados, já dentro do Lakehouse, mas ainda sem aplicação de regras de negócio, deduplicação final ou modelagem analítica.

A decisão arquitetural foi:

Bronze = append-only

Ou seja:

não usar merge
não atualizar registros existentes
não deletar registros
não deduplicar como regra principal
apenas adicionar novas cargas

Essa decisão preserva histórico, rastreabilidade e capacidade de auditoria. Se uma mesma linha vier novamente da Landing por causa do lookback da marca d’água, ela será gravada novamente na Bronze com novos metadados de ingestão.

A deduplicação e a aplicação de regra de negócio ficam para a Silver.


2. Por que a Landing fica em Volume e a Bronze em tabela Delta?

A Landing foi criada em Unity Catalog Volume porque ela armazena arquivos físicos de extração, como Parquet, organizados por tabela, data e batch. Volumes são objetos do Unity Catalog voltados para governança de arquivos e dados não tabulares ou semitabulares. Já tabelas governam dados tabulares. (Documentação Databricks)

Exemplo de Landing:

/Volumes/hotelaria_dev/landing/neon_postgresql/hoteis/ingestion_date=2026-05-09/batch_timestamp=20260509143000/

A Bronze foi criada como tabela Delta porque a partir dessa camada queremos governança tabular, consultas SQL, evolução de schema, versionamento Delta e integração com as próximas camadas do Lakehouse.

Exemplo de Bronze:

hotelaria_dev.bronze.hoteis
hotelaria_dev.bronze.hospedes
hotelaria_dev.bronze.reservas

3. Papel dos contratos na Bronze

Antes de criar a Bronze, criamos contratos YAML por tabela:

contracts/neon_hotelaria/
├── consumos.yml
├── faturas.yml
├── hoteis.yml
├── hospedes.yml
├── quartos.yml
├── reservas.yml
└── reservas_ota.yml

Cada contrato define:

nome da tabela
origem
caminho da Landing
destino da Bronze
modo de carga
coluna de watermark
chave primária
schema esperado
regras mínimas de qualidade

Exemplo conceitual:

table_name: hoteis

landing:
  path: /Volumes/hotelaria_dev/landing/neon_postgresql/hoteis
  file_format: parquet
  retention_days: 15

bronze:
  full_name: hotelaria_dev.bronze.hoteis
  load_mode: append_only

incremental:
  enabled: true
  watermark_column: update_date
  lookback_minutes: 1440

primary_key:
  - hotel_id

Com isso, o pipeline da Bronze não precisa depender de regras espalhadas no código. Ele lê os contratos e entende de onde carregar e para onde gravar cada tabela.


4. Controle de arquivos processados

Para evitar reprocessar os mesmos arquivos da Landing, usamos uma tabela de controle:

hotelaria_dev.control.processed_files

Ela registra:

source_system
source_table
source_file_path
source_file_name
file_modification_time
file_size
ingestion_id
processed_at
target_table
status
created_at

A lógica é simples:

1. Listar arquivos Parquet da Landing
2. Consultar quais arquivos já foram processados
3. Carregar apenas arquivos novos
4. Gravar na Bronze em modo append
5. Registrar os arquivos carregados em processed_files

Assim, se o job da Bronze rodar novamente sem novos arquivos, ele não duplica dados.


5. Script comum para leitura dos contratos

Criamos o arquivo:

src/common/contracts.py

Objetivo inicial:

ler contratos YAML
carregar todos os contratos
buscar contrato por tabela

A primeira versão usava file para descobrir o caminho do projeto. Porém, quando executado no Databricks como notebook_task, essa variável não existe.


6. Erro encontrado: file is not defined

Ao executar o job landing_to_bronze, ocorreu o erro:

NameError: name '__file__' is not defined

Esse erro aconteceu porque arquivos .py executados como notebooks no Databricks não possuem a variável file, comum em scripts Python tradicionais.

O trecho problemático era:

current_dir = os.path.dirname(os.path.abspath(__file__))

A correção foi usar o caminho do notebook no contexto do Databricks:

dbutils.notebook.entry_point
    .getDbutils()
    .notebook()
    .getContext()
    .notebookPath()
    .get()

Com isso, conseguimos identificar o caminho publicado pelo Asset Bundle dentro do workspace e localizar a pasta contracts.


7. Script Bronze corrigido

Criamos o script:

src/bronze/load_landing_to_bronze.py

Responsabilidades:

resolver caminho do projeto no Databricks
ler contratos YAML
listar arquivos Parquet da Landing
identificar arquivos ainda não processados
validar schema mínimo
adicionar metadados técnicos
gravar tabela Delta na Bronze em append
registrar arquivos processados

Principais metadados adicionados à Bronze:

_metadata_row_hash
_metadata_bronze_ingestion_id
_metadata_bronze_loaded_at
_metadata_source_system
_metadata_source_table
_metadata_primary_key
_metadata_watermark_column
_metadata_load_mode

O metadatarow_hash é calculado com base nas colunas de origem previstas no contrato. Ele será útil nas próximas camadas para identificar alterações, deduplicações e versionamento lógico.


8. Criação do Job Landing → Bronze

Criamos o arquivo:

resources/jobs/job_landing_to_bronze.yml

Conteúdo do job:

resources:
  jobs:
    landing_to_bronze:
      name: "landing_to_bronze [${bundle.target}]"
      description: "Carga append-only da Landing em Volumes para Bronze Delta usando contratos YAML"

      tasks:
        - task_key: landing_to_bronze
          notebook_task:
            notebook_path: ../../src/bronze/load_landing_to_bronze.py
            base_parameters:
              target_catalog: ${var.target_catalog}
              source_system: ${var.source_system}
              table_name: all

O caminho do notebook segue a mesma regra usada no job da Landing:

resources/jobs/job_landing_to_bronze.yml
        ↓
../../src/bronze/load_landing_to_bronze.py

Esse caminho é necessário porque o arquivo .yml está dentro de resources/jobs, enquanto o notebook está dentro de src/bronze.


9. Validação e deploy com Databricks Asset Bundles

Depois de criar o job da Bronze, validamos o projeto localmente:

pytest tests/contracts/test_contracts.py

Resultado esperado:

5 passed

Depois validamos o Bundle:

databricks bundle validate --target dev --profile hotelaria-free

Em seguida, fizemos deploy:

databricks bundle deploy --target dev --profile hotelaria-free

E executamos o job:

databricks bundle run landing_to_bronze --target dev --profile hotelaria-free

A Databricks recomenda Bundles como abordagem para CI/CD porque eles permitem versionar recursos, jobs e pipelines junto com o código do projeto, além de possibilitar validação e deploy por automação externa como GitHub Actions. (Documentação Databricks)


Ajustes na esteira CI/CD com GitHub Actions

Durante esta etapa, também ajustamos a esteira CI/CD do projeto no GitHub Actions.

O objetivo era validar automaticamente:

contratos YAML
testes com pytest
Databricks Bundle validate

10. Criação dos GitHub Secrets

No GitHub, os secrets foram criados em:

Repository
→ Settings
→ Secrets and variables
→ Actions
→ Repository secrets

Criamos:

DATABRICKS_HOST
DATABRICKS_TOKEN

O DATABRICKS_HOST recebeu:

O DATABRICKS_TOKEN recebeu um token gerado no Databricks, não no GitHub.

GitHub Actions usa secrets para armazenar informações sensíveis, como tokens, e eles só ficam disponíveis para o workflow quando são referenciados explicitamente. (GitHub Docs)


11. Correção importante: token GitHub x token Databricks

Durante o processo houve uma confusão natural entre tokens do GitHub e tokens do Databricks.

Tokens como:

ghp_...
github_pat_...

são tokens do GitHub.

Eles servem para acessar GitHub, repositórios, packages, APIs do GitHub etc.

Para o workflow executar comandos como:

databricks current-user me
databricks bundle validate --target dev

é necessário um token do Databricks.

A autenticação por PAT no Databricks usa variáveis como DATABRICKS_HOST e DATABRICKS_TOKEN. (Documentação Databricks)


12. Erro de autenticação no GitHub Actions

O primeiro erro foi:

cannot configure default credentials

Depois, ao configurar os secrets, o erro mudou para:

Credential was not sent or was of an unsupported type for this API
HTTP Status: 401 Unauthorized
Endpoint: GET /api/2.0/preview/scim/v2/Me

Isso indicava que:

o host estava chegando ao workflow
o token estava chegando ao workflow
mas o token não tinha permissão/scope suficiente

13. Correção: API scope no token Databricks

Ao gerar o token no Databricks, a tela solicitou API scope(s).

A correção foi gerar um novo token Databricks com escopo adequado, como:

all-apis

ou, quando aplicável, escopos suficientes para permitir chamadas de workspace, jobs, Unity Catalog e identificação do usuário.

Depois disso, o workflow conseguiu autenticar e executar o databricks bundle validate.


14. Workflow de validação

O workflow de validação ficou em:

.github/workflows/validate.yml

Com a ideia:

name: validate

on:
  pull_request:
    branches:
      - main
      - master

  push:
    branches:
      - main
      - master

jobs:
  validate:
    runs-on: ubuntu-latest

    env:
      DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }}
      DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
      DATABRICKS_AUTH_TYPE: pat
      FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install Python dependencies
        run: |
          pip install pyyaml pytest

      - name: Install Databricks CLI
        run: |
          curl -fsSL https://raw.githubusercontent.com/databricks/setup-cli/v0.299.1/install.sh | sudo sh
          databricks version

      - name: Validate Databricks authentication
        run: |
          databricks current-user me

      - name: Validate contracts
        run: |
          pytest tests/contracts/test_contracts.py

      - name: Validate Databricks Bundle
        run: |
          databricks bundle validate --target dev

Esse workflow garante que alterações no projeto passem por validação antes de seguirem adiante.


15. Observação sobre Node.js 20 no GitHub Actions

Durante a execução, o GitHub Actions exibiu um aviso sobre depreciação do Node.js 20 em actions como:

actions/checkout@v4
actions/setup-python@v5

Esse aviso não era o erro principal da esteira. Ainda assim, adicionamos:

FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

para antecipar a execução com Node.js 24 quando suportado pelo runner.


Validações após a Bronze

Após a execução bem-sucedida do job landing_to_bronze, validamos as tabelas no Databricks SQL Editor.


16. Listar tabelas Bronze

show tables in hotelaria_dev.bronze;

Resultado esperado:

consumos
faturas
hoteis
hospedes
quartos
reservas
reservas_ota

17. Contar registros por tabela

select 'hoteis' as tabela, count(*) as total from hotelaria_dev.bronze.hoteis
union all
select 'hospedes', count(*) from hotelaria_dev.bronze.hospedes
union all
select 'quartos', count(*) from hotelaria_dev.bronze.quartos
union all
select 'reservas', count(*) from hotelaria_dev.bronze.reservas
union all
select 'consumos', count(*) from hotelaria_dev.bronze.consumos
union all
select 'faturas', count(*) from hotelaria_dev.bronze.faturas
union all
select 'reservas_ota', count(*) from hotelaria_dev.bronze.reservas_ota
order by tabela;

18. Validar arquivos processados

select
    source_table,
    count(*) as total_arquivos_processados,
    max(processed_at) as ultima_execucao
from hotelaria_dev.control.processed_files
group by source_table
order by source_table;

Essa consulta confirma se os arquivos da Landing foram registrados como processados.


19. Validar metadados da Bronze

select
    _metadata_source_system,
    _metadata_source_table,
    _metadata_load_mode,
    count(*) as total
from hotelaria_dev.bronze.hoteis
group by
    _metadata_source_system,
    _metadata_source_table,
    _metadata_load_mode;

Resultado esperado:

_metadata_source_system = neon_postgresql
_metadata_source_table  = hoteis
_metadata_load_mode     = append_only

Resultado alcançado

Ao final desta etapa, temos:

Landing funcionando em Unity Catalog Volume
Bronze criada como Delta append-only
Contratos YAML sendo usados como referência da carga
Controle de arquivos processados funcionando
Metadados técnicos aplicados na Bronze
Job landing_to_bronze criado via Asset Bundle
CI com GitHub Actions validando contratos e Bundle
Token Databricks corrigido com API scope adequado
Deploy local e validação CI funcionando

Estrutura atual do projeto

Laboratorio-ETL-end-to-end-with-assets-budles-and-DLT/
├── .github
│   └── workflows
│       └── validate.yml
├── contracts
│   └── neon_hotelaria
│       ├── consumos.yml
│       ├── faturas.yml
│       ├── hoteis.yml
│       ├── hospedes.yml
│       ├── quartos.yml
│       ├── reservas.yml
│       └── reservas_ota.yml
├── resources
│   ├── jobs
│   │   ├── job_landing_to_bronze.yml
│   │   └── job_source_to_landing.yml
│   └── pipelines
├── src
│   ├── bronze
│   │   └── load_landing_to_bronze.py
│   ├── common
│   │   └── contracts.py
│   └── landing
│       └── ingest_neon_to_landing.py
├── sql
│   ├── control
│   │   └── 01_seed_ingestion_watermark.sql
│   └── setup
│       └── 00_setup_catalog_schemas_volumes.sql
├── tests
│   └── contracts
│       └── test_contracts.py
├── databricks.yml
└── README.md

Decisão arquitetural desta etapa

A decisão final da Bronze foi:

Landing:
  arquivos Parquet em Unity Catalog Volume
  histórico físico para reprocessamento

Bronze:
  tabelas Delta no Unity Catalog
  append-only
  metadados técnicos
  sem merge, update ou delete

Controle:
  ingestion_watermark para Source → Landing
  processed_files para Landing → Bronze

Contratos:
  um YAML por tabela
  usados como referência técnica do pipeline

CI/CD:
  GitHub Actions validando contratos e Bundle
  autenticação Databricks via DATABRICKS_HOST e DATABRICKS_TOKEN

Com isso, encerramos a etapa de construção da Bronze com uma base organizada, testável, versionada e preparada para o próximo passo: o job orquestrador Source → Landing → Bronze.

Comentários


bottom of page