Einleitung
In diesem Artikel geht es um das Aufsetzen eines Datenprojekts mit Databricks Asset Bundles. Wir schauen uns das Basis Setup an und eine typische Projektstruktur, gehen anschließend auf einige wichtige Details und mögliche Stolpersteine und zeigen wie eine CI/CD-Integration mit GitLab funktionieren könnte.
Was ist Databricks?
Databricks wurde 2013 von den Originalentwicklern von Apache Spark gegründet und hat sich seither zu einer führenden Data-Plattform für die moderne Datenverarbeitung entwickelt. Die Plattform vereint Funktionen zum Speichern, Verarbeiten, Abfragen und Visualisieren von Daten und unterstützt dabei das Konzept des Lakehouse – einer Architektur, die die Vorteile von Data Warehouses und Data Lakes kombiniert.
Databricks richtet sich an verschiedene Nutzergruppen: Data Engineers, Data Scientists, Data Analysts sowie Business-Anwender ohne tiefere technische Kenntnisse können unter einer einheitlichen Oberfläche zusammenarbeiten. Dafür stehen unter anderem interaktive Notebooks mit Multi-Language-Unterstützung (z. B. Python, SQL, Scala), ein leistungsstarker SQL-Editor, Dashboards, Workflows für Data Pipelines sowie integrierte Machine-Learning-Tools wie MLflow zur Verfügung.
Die Plattform ist als vollständig verwaltete PaaS-Lösung in allen großen Public Clouds verfügbar – Azure, AWS und Google Cloud – und erlaubt so eine nahtlose Integration in bestehende Cloud-Umgebungen.
Im weiteren Verlauf liegt der Fokus auf dem Bereich Data Engineering und CI/CD mit Databricks: Ich zeige anhand eines typischen Projektaufbaus, wie man mit Databricks Asset Bundles eine Datenpipeline erstellt und diese in einen CI/CD-Workflow integriert.
Was sind Databricks Asset Bundles?
Ein Databricks Bundle ist eine Sammlung von Coderessourcen und Metadaten, die als ein einziges Bundle in einem Databricks Workspace ausgerollt werden können. Ein Bundle wird lokal entwickelt und kann anschließend in ein Repository eingecheckt werden (wir verwenden GitLab). Alternativ kann man direkt per CLI ausrollen. Grob beinhaltet ein Bundle folgende Ressourcen:
- Databricks Workspace- und Projektkonfiguration
- Python-Code (z.B. Notebooks oder Module für ETL-Pipelines)
- Definitionen und Einstellungen für Databricks Ressourcen, wie Job-Definitionen für Serverless Jobs
- Unit-Tests and Integration-Tests
Da Businesslogik nicht im Fokus steht werden wir ein minimales Bundle Setup verwenden, das eines der von Databricks
bereitgestellten samples
-Datensätze (nyctaxi
) einbezieht.
Wie sieht ein typischer Projektaufbau mit Databricks aus?
Im Folgenden wird davon ausgegangen, dass ein gültiger Cloud-Account und ein Databricks Workspace bereits existieren.
Basis-Setup für Python-Projekt:
- Pyenv zu Verwaltung und lokaler Installation verschiedener Python Versionen
- Poetry zur Erstellung von virtuellen Python-Umgebungen
- Ruff als Tool zu Linting & Codeanalyse (Diese Tools erhöhen die Qualität und Wartbarkeit von Python-Pipelines)
- Typer zur Erstellung von Kommandozeilenanwendungen (Typer CLI). Das ist optional, wie ich weiter unten ausführe.
Als Erstes installieren wir eine Python Version in unserem Projektverzeichnis:
pyenv local 3.11.10
Hinweis: hier sollte die gleiche Python Version verwendet werden wie im Databricks Workspace. Letztere kann man leicht
herausfinden, wenn man in einem Notebook den Befehl: !python --version
ausführt.
Der Befehl erstellt eine .python-version
Datei, die man normalerweise nicht ins Repository eincheckt. Alternativ kann
man natürlich eine systemübergreifende Python Version installieren.
Als Nächstes erstellen wir eine virtuelle Umgebung, die wir auf unser Projektverzeichnis beschränken:
poetry config virtualenvs.in-project true
Databricks Bundle Setup:
Unsere Ziel-Projektstruktur sieht in etwa so aus:
databricks_bundle_example/
├── .databricks/ # Bundle-spezifische Konfigurationen und Artefakte pro Umgebung
│
├── .venv/ # Virtuelle Umgebung (optional, lokal)
│
├── src/ # Der eigentliche Code (z.B. ETL-Logik, Helper-Module)
│
├── resources/ # Job-Definitionen für Pipelines
│ └── databricks_bundle_demo.job.yml # Beispielhafte Job-Definition im YAML-Format
│
├── tests/ # Pytest-basierte Unit- oder Integrationstests
│
├── databricks.yml # Hauptdefinition des Databricks Bundles
├── .gitlab-ci.yml # CI/CD-Konfiguration für GitLab
└── pyproject.toml # Zentrale Konfigurationsdatei für das Python-Projekt
Ich möchte nicht auf jede einzelne Datei eingehen, nur einige wichtige Anmerkungen:
databricks.yml
:
Feld | Bedeutung |
---|---|
variables | Können in verschiedenen Stages verwendet werden, um Stage-spzifische Werte zu übergeben |
sync | Steuert, welche lokalen Dateien beim Deployment ins Databricks-Dateisystem (meist /Workspace/... ) hochgeladen werden. Die exclude -Einträge überschreiben include -Einträge, d. h. Dateien in exclude werden nicht synchronisiert, selbst wenn sie in include stehen. |
targets | Definiert verschiedene Deployment-Umgebungen wie dev_local , dev , prod . Ermöglicht isolierte Entwicklungsumgebungen sowie zentralisierte Produktionsdeployments. Jede Umgebung kann eigene Parameter (z. B. Kataloge, Cluster-Modi) enthalten. |
root_path | Gibt den genauen Pfad im Databricks Workspace an, in dem die Bundle-Inhalte abgelegt werden. Das ist wichtig zur Trennung von Umgebungen und Nutzern und verhindert Ressourcenkollisionen. Wird root_path nicht definiert, kann die Pipeline scheitern, weil nötige Artefakte nicht gefunden werden. |
host | Workspace URL, hat die Form: https://<workspace-id>.azuredatabricks.net |
Hier ist ein beispielhafter Aufbau einer databricks.yml
:
bundle:
name: databricks_bundle_demo
uuid: 57dddb64-fdcd-45bd-b00c-70de62f98c72
variables:
schema_prefix:
description: Prefix for local development schema
default: ""
catalogue:
description: Catalogue to store schema and tables
branch_ref:
description: Branch reference name used for naming test deployments
default: ""
schedule_pause_status:
description: Status of schedule triggers 'UNPAUSED' = enabled, 'PAUSED' = disabled
default: "UNPAUSED"
sync:
include:
- src/**/*.py
exclude:
- "test/*"
- "resources/*"
- ".*"
- "*.lock"
- "*.md"
- "*.toml"
- "databricks.yml"
include:
- resources/*.yml
permissions:
- group_name: users
level: CAN_VIEW
- user_name: "${workspace.current_user.userName}"
level: CAN_MANAGE
targets:
dev_local:
mode: development
default: true
artifacts:
wheel:
type: whl
files:
- source: ./dist/*.whl
variables:
catalogue: "demo_dev_local"
schema_prefix: "${workspace.current_user.short_name}_"
schedule_pause_status: "PAUSED"
workspace:
host: https://adb-185960349365378.18.azuredatabricks.net
dev:
mode: development
default: false
variables:
catalogue: "demo_dev"
schema_prefix: "${workspace.current_user.short_name}_"
schedule_pause_status: "PAUSED"
workspace:
host: https://adb-185960349365378.18.azuredatabricks.net
root_path: /Shared/.bundle/${bundle.name}/dev/${workspace.current_user.short_name}/
prod:
mode: production
workspace:
host: https://adb-185960349365378.18.azuredatabricks.net
root_path: /Workspace/Users/${workspace.current_user.name}/.bundle/${bundle.name}/${bundle.target}
permissions:
- user_name: ${workspace.current_user.name}
level: CAN_MANAGE
databricks_bundle_demo.job.yml
:
Feld | Bedeutung |
---|---|
environments | Definiert, mit welchen Libraries/Settings ein Task ausgeführt wird. Wir nutzen Serverless |
./../dist/*.whl | Zeigt auf das gebaute Python-Wheel mit deinem Code. Wichtig für Python-Wheel-Jobs. |
entry_point | Name des Python-Moduls bzw. Scripts im Paket, das gestartet werden soll. |
pause_status | "UNPAUSED" bedeutet, dass der Schedule aktiv ist. In dev meist "PAUSED" . |
tasks | Hier werden einzelne Job-Schritte, benötigte Variablen und Abhängigkeiten definiert. Im Databricks Portal kann das als Prozessdiagramm visualisiert werden |
Hier ist ein Beispiel für eine databricks_bundle_demo.job.yml
:
resources:
jobs:
databricks_bundle_demo_job:
name: databricks_bundle_demo_job
max_concurrent_runs: 1
schedule:
timezone_id: "UTC"
quartz_cron_expression: "0 15 0 ? * *"
pause_status: "UNPAUSED"
environments:
- environment_key: Serverless
spec:
client: "2"
dependencies:
- ./../dist/*.whl
tasks:
- task_key: task_get_taxis
python_wheel_task:
package_name: databricks_bundle_example
entry_point: demo_script
environment_key: Serverless
main.py
:
Ich möchte nicht näher auf den Inhalt des Codes eingehen, sondern nur ein Beispiel geben:
from loguru import logger
from pyspark.sql import SparkSession, DataFrame
def task_get_taxis():
"""
Request taxis from databricks example data sample.
"""
logger.info("running task_retrieve_roads_raw_data")
get_taxis(get_spark()).show(5)
logger.info("loaded roads data successful!")
def get_taxis(spark: SparkSession) -> DataFrame:
return spark.read.table("samples.nyctaxi.trips")
def get_spark() -> SparkSession:
try:
from databricks.connect import DatabricksSession
return DatabricksSession.builder.getOrCreate()
except ImportError:
return SparkSession.builder.getOrCreate()
In diesem Beispiel werden keine Parameter in der Funktion übergeben, weshalb es nicht notwendig ist, diese als CLI-Befehl zu kennzeichen. Anders sieht es aus, wenn man Parameter übergeben möchte, wie das folgende Beispiel zeigt:
import typer
from loguru import logger
from pyspark.sql import SparkSession, DataFrame
cli = typer.Typer()
def main():
cli(standalone_mode=False)
@cli.command()
def task_get_taxis(schema_path: str = typer.Option(...)):
"""
Request taxis from databricks example data sample.
"""
logger.info("running task_get_taxis")
taxies_data = get_taxis(get_spark())
logger.info("loaded taxies data successful!")
write_taxies_data(taxies_data, schema_path)
...
Für dieses Beispiel würde der entsprechende Task-Abschnitt in der Job-Definition so aussehen:
...
tasks:
- task_key: task_get_taxis
python_wheel_task:
package_name: databricks_bundle_example
entry_point: demo_script
parameters: [ "task-get-taxis", "--schema-path", "${var.catalogue}.${var.schema_prefix}" ]
environment_key: Serverless
...
Die Annotation @cli.command()
ist wichtig. Databricks ruft das Script über CLI auf, d.h.
wenn man in der Task-Definition Parameter einträgt, dann sind das CLI Parameter und keine Python Parameter. Deswegen
muss man im Python-Code die Aufrufargumente parsen. Typer-CLI
macht an dieser Stelle eine CLI-Applikation aus unserem
Code, sodass dieser mit den übergebenen Parametern in der Pipeline laufen kann.
pyproject.toml
:
Das ist die zentrale Konfigurationsdatei für unser Projekt und wird bei einem Poetry-Setup vorausgesetzt. Hier ist ein Beispiel, wie die Datei in unserem Projektkontext aussehen könnte:
[tool.poetry]
name = "databricks_bundle_example"
version = "0.1.0"
packages = [{ include = "src" }]
authors = ["denis.khaskin@codecentric.de"]
[tool.poetry.scripts]
demo_script = 'src.databricks_bundle_demo.main:main'
[tool.poetry.dependencies]
python = "^3.11"
loguru = "^0.7.2"
databricks-connect = "15.4.2"
[tool.poetry.group.dev.dependencies]
pre-commit = "^3.6.2"
freezegun = "1.5.1"
ruff = "^0.3.7"
[tool.ruff]
line-length = 120
exclude = ["*.ipynb"]
[tool.ruff.format]
quote-style = "single"
indent-style = "space"
docstring-code-format = false
[tool.ruff.lint]
unfixable = ["F401"] # E.g. disable fix for unused imports (`F401`).
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Der Eintrag demo_script
korrespondiert zu dem Eintrag entry_point
in dem entsprechenden Task in der Jobdefinition,
sodass der auszuführende Code zur Laufzeit gefunden wird.
Wie richte ich eine GitLab CI/CD-Pipeline für Databricks ein?
Bevor wir eine Pipeline in GitLab aufsetzen ist es wichtig eine Authentifizierung Methode festzulegen, mit der sich die
Pipeline gegenüber Databricks authentifizieren soll, um den Code auszurollen und den Job zu starten.
Wir verwenden dazu OAuth2-konform ein Service Principal, der für automatisierte Deployments mit GitLab CI/CD genutzt
wird.
Wichtig ist, dass dieses direkt im Databricks Portal und nicht im Portal des jeweiligen Cloud-Anbieters erstellt wird.
Dazu wechselt man in Databricks zu den Einstellungen (rechts oben auf den Nutzer klicken).
Dann navigiert man zu "Identity and access-Service principals-Manage".
Dort kann man ein neues Service principal erstellen.
Ist es erstellt, wechselt man zu "Secrets" und erstellt ein neues secret.
Die erzeugten Secret
-Daten sollte man sich sofort speichern, sie werden später nicht mehr angezeigt.
Anschließend müssen die erzeugten Secret
-Daten in GitLab (oder einem anderen Anbieter der Wahl) als Variablen
hinterlegt werden.
Folgend ein Beispiel wie es in GitLab aussehen könnte:
Key | Value | Masked | Expanded | Environments |
---|---|---|---|---|
DATABRICKS_CLIENT_ID | ***** | Nein | Ja | All (default) |
DATABRICKS_CLIENT_SECRET | ***** | Ja | Ja | All (default) |
DATABRICKS_HOST | ***** | Nein | Ja | All (default) |
DATABRICKS_SERVERLESS_WAREHOUSE_ID | ***** | Nein | Ja | All (default) |
Die ersten beiden Variablen stammen direkt aus dem erzeugten secret
. Databricks Host kann bei dem jeweiligen
Cloud-Anbieter oder direkt in der URL des Portals eingesehen werden. Für die Warehouse-ID navigiert man im Portal zu
SQL-warehouses, klickt auf das genutzte (sofern bereits erstellt, sonst ein neues erstellen), die ID steht direkt hinter
dem Namen.
Sind die Variablen gesetzt können wir die CI/CD-Pipeline aufbauen. Nachfolgend das Beispiel einer .gitlab-ci.yml
(ich
gehe bewusst nicht auf alle Details ein, sondern setze hier ein Grundwissen voraus):
image: gitlab.codecentric.de:4567/data_ml_ai/databricks_pipeline_build_image:latest
stages:
- check
- unittest
- build
- deploy_dev
- deploy_prod
python-lint-and-format:
stage: check
before_script:
- poetry install
script:
- poetry run pre-commit run --all-files
rules:
- if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "merge_request_event"'
validate-bundle:
stage: check
script:
- databricks bundle validate
rules:
- if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "merge_request_event"'
unit-tests:
stage: unittest
before_script:
- poetry install
script:
- poetry run pytest --capture=no .
rules:
- if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "merge_request_event"'
build:
stage: build
before_script:
- poetry install
script:
- poetry build
artifacts:
paths:
- dist
rules:
- if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push"'
debug-auth:
stage: check
script:
- echo $DATABRICKS_HOST
- echo $DATABRICKS_CLIENT_ID
- echo ${#DATABRICKS_CLIENT_SECRET} # nur Länge anzeigen
- echo $DATABRICKS_TENANT_ID
rules:
- when: always
rollout-dev:
stage: deploy_dev
environment:
name: dev
script:
- databricks version
- databricks auth describe
- databricks bundle deploy --target dev --var="branch_ref=$CI_COMMIT_REF_SLUG" --force --auto-approve
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push"'
run-dev:
stage: deploy_dev
needs:
- rollout-dev
environment:
name: dev
script:
- databricks bundle run databricks_bundle_demo_job --target dev --var="branch_ref=$CI_COMMIT_REF_SLUG"
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push"'
deploy-prod:
stage: deploy_prod
environment:
name: prod
script:
- databricks bundle deploy --target prod
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push"'
Man beachte, dass hier ein selbst gebautes Docker image verwendet wird, in dem Databricks-CLI und andere Abhängigkeiten
bereits installiert sind.
Man erkennt, dass die Pipeline nicht nur das Projekt baut, sondern auch im Databricks Workspace ausrollt und den Job
startet. Dazu
ist das erstellte Service Principal notwendig.
Nach einem erfolgreichen Lauf sollte man im Portal unter "Workflows" den entsprechenden Eintrag mit dem Tag
gitlab_deployment
(oder je nachdem was man verwendet) sehen können. Geht man weiter in die Jobdetails können einzelne
Tasks und ihre Ergebnisse eingesehen werden.
Fazit
An dem hier vorgestellten Aufbau sieht man, dass es relativ einfach ist, ein Datenprojekt mit Databricks aufzusetzen. In Kombination mit den Portalwerkzeugen wie Notebooks oder SQL-Editor ist es sehr einfach eine Funktionalität zu entwickeln: Man kann nahezu alles ausprobieren, bevor man es ins Repository eincheckt. Auf der anderen Seite bieten Databricks Asset Bundles die Möglichkeit, Code unter modernen Softwarearchitektur-Prinzipien zu entwickeln – mit klarer Trennung von Umgebungen, CI/CD-Automatisierung und reproduzierbaren Serverless Workflows.
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Blog-Autor*in
Denis Khaskin
Senior IT-Consultant und Developer
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.