Data Engineering

CI/CD Pipelines für Databricks: Produktionsreifer Guide

Robuste CI/CD-Pipeline für Databricks-Projekte mit GitHub Actions, Asset Bundles und automatisierten Tests. Branching, Testing, Deployment in einem Setup.

Harbinger Team3. April 20266 Min. LesezeitAktualisiert 14.5.2026
  • databricks
  • cicd
  • github-actions
  • devops
  • asset-bundles
  • testing
Inhaltsverzeichnis20 Abschnitte

TL;DR: Notebook-first ist keine Engineering-Praxis. CI/CD für Databricks geht über Databricks Asset Bundles + GitHub Actions: PRs validieren Bundles und laufen Unit-Tests, Merge auf main deployt nach Staging mit Integrations-Tests, dann auf Prod. Trunk-based mit kurzen Feature-Branches.

Einer der häufigsten Schmerzpunkte von Data-Engineering-Teams, die Databricks adoptieren, ist fehlendes CI/CD. Notebooks werden direkt im UI editiert, "Deployment" heißt Files zwischen Ordnern kopieren, und Produktions-Incidents lassen sich auf jemanden zurückverfolgen, der Ad-hoc-Code im falschen Workspace ausgeführt hat.

Dieser Guide geht durch ein produktionsreifes CI/CD-Setup für Databricks mit Databricks Asset Bundles (DABs) und GitHub Actions. Am Ende hast du automatisiertes Testing, Environment-Promotion und Deployment, das deine Software-Engineering-Kollegen wirklich respektieren werden.


Das Problem mit Notebook-First-Entwicklung

Die meisten Databricks-Teams starten mit Notebooks. Sie sind schnell zum Iterieren, leicht zu teilen und brauchen kein Setup. Aber sie skalieren schlecht:

  • Keine Diff-Sichtbarkeit — Git zeigt base64-encoded JSON, nicht lesbares Python
  • Manuelle Promotion — "Deploy nach Prod" heißt Notebook zwischen Workspace-Ordnern ziehen
  • Kein automatisiertes Testing — wie testest du ein Notebook, bevor es auf Produktionsdaten läuft?
  • Environment-Drift — Prod- und Dev-Notebooks divergieren still

Die Lösung: behandle dein Databricks-Projekt wie ein richtiges Softwareprojekt — Code in .py-Files, Infrastructure as Code, automatisierte Tests, Deployment-Pipelines.


Projektstruktur

harbinger-pipelines/
├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── deploy.yml
├── databricks.yml
├── resources/
│   ├── jobs.yml
│   └── pipelines.yml
├── src/
│   ├── pipelines/
│   │   ├── bronze/
│   │   │   └── ingest_events.py
│   │   ├── silver/
│   │   │   └── clean_events.py
│   │   └── gold/
│   │       └── aggregate_signals.py
│   └── utils/
│       ├── schema_utils.py
│       └── quality_checks.py
├── tests/
│   ├── unit/
│   │   └── test_clean_events.py
│   └── integration/
│       └── test_pipeline_e2e.py
├── notebooks/
├── pyproject.toml
└── requirements.txt

Databricks Asset Bundles (DABs)

Asset Bundles sind Databricks' natives IaC-Framework. Sie ersetzen das ältere dbx-Tool und sind der aktuell empfohlene Ansatz.

Root databricks.yml

bundle:
  name: harbinger-pipelines

variables:
  environment:
    description: Deployment environment
    default: dev
  catalog:
    description: Unity Catalog name
    default: harbinger_dev

targets:
  dev:
    mode: development
    default: true
    workspace:
      host: https://adb-7405613637854743.3.azuredatabricks.net
    variables:
      environment: dev
      catalog: harbinger_dev

  staging:
    mode: development
    workspace:
      host: https://adb-7405613637854743.3.azuredatabricks.net
    variables:
      environment: staging
      catalog: harbinger_staging

  prod:
    mode: production
    workspace:
      host: https://adb-7405613637854743.3.azuredatabricks.net
    variables:
      environment: prod
      catalog: harbinger_prod
    run_as:
      service_principal_name: harbinger-prod-sp

Job-Definition (resources/jobs.yml)

resources:
  jobs:
    events_ingestion_job:
      name: "harbinger-events-ingestion-${var.environment}"
      schedule:
        quartz_cron_expression: "0 0 * * * ?"
        timezone_id: "UTC"
      tasks:
        - task_key: ingest_bronze
          python_wheel_task:
            package_name: harbinger_pipelines
            entry_point: ingest_events
          job_cluster_key: default

        - task_key: clean_silver
          depends_on:
            - task_key: ingest_bronze
          python_wheel_task:
            package_name: harbinger_pipelines
            entry_point: clean_events
          job_cluster_key: default

      job_clusters:
        - job_cluster_key: default
          new_cluster:
            spark_version: "15.4.x-scala2.12"
            node_type_id: "Standard_DS3_v2"
            autoscale:
              min_workers: 2
              max_workers: 8

GitHub Actions aufsetzen

CI-Workflow (.github/workflows/ci.yml)

Läuft bei jedem Pull-Request. Validiert Bundle-Konfiguration, führt Unit-Tests aus und macht einen Dry-Deploy.

name: CI

on:
  pull_request:
    branches: [main, staging]

jobs:
  lint-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

      - name: Install dependencies
        run: |
          pip install -r requirements-dev.txt
          pip install -e .

      - name: Run linting
        run: |
          ruff check src/ tests/
          mypy src/

      - name: Run unit tests
        run: pytest tests/unit/ -v --tb=short

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

      - name: Validate bundle
        env:
          DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST_DEV }}
          DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN_DEV }}
        run: databricks bundle validate --target dev

      - name: Deploy to dev
        env:
          DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST_DEV }}
          DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN_DEV }}
        run: databricks bundle deploy --target dev

Deploy-Workflow (.github/workflows/deploy.yml)

Läuft beim Merge auf main. Deployt nach Staging, läuft Integrations-Tests, promotet dann nach Prod.

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4

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

      - name: Deploy to staging
        env:
          DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }}
          DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN_STAGING }}
        run: databricks bundle deploy --target staging

      - name: Run integration tests
        env:
          DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }}
          DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN_STAGING }}
        run: pytest tests/integration/ -v --timeout=300

  deploy-prod:
    runs-on: ubuntu-latest
    needs: deploy-staging
    environment: production
    steps:
      - uses: actions/checkout@v4

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

      - name: Deploy to production
        env:
          DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }}
          DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN_PROD }}
        run: databricks bundle deploy --target prod

Testbaren Code schreiben

Der Schlüssel zu testbarem Databricks-Code: Business-Logik von Spark-I/O trennen.

# src/pipelines/silver/clean_events.py

from pyspark.sql import DataFrame
from pyspark.sql.functions import col, trim, upper, when

def clean_event_type(df: DataFrame) -> DataFrame:
    return df.withColumn(
        "event_type",
        when(upper(trim(col("event_type"))).isin(["CONFLICT", "WAR", "BATTLE"]), "CONFLICT")
        .when(upper(trim(col("event_type"))).isin(["NATURAL_DISASTER", "EARTHQUAKE", "FLOOD"]), "DISASTER")
        .otherwise("UNKNOWN")
    )

def filter_low_quality_events(df: DataFrame, min_severity: float = 0.5) -> DataFrame:
    return df.filter(col("severity") >= min_severity)
# tests/unit/test_clean_events.py

import pytest
from pyspark.sql import SparkSession
from src.pipelines.silver.clean_events import clean_event_type, filter_low_quality_events

@pytest.fixture(scope="session")
def spark():
    return SparkSession.builder.master("local[1]").appName("test").getOrCreate()

def test_clean_event_type_normalizes_aliases(spark):
    data = [("war", 5.0), ("FLOOD", 7.0), ("political", 3.0)]
    df = spark.createDataFrame(data, ["event_type", "severity"])
    result = clean_event_type(df)
    types = {row.event_type for row in result.collect()}
    assert "CONFLICT" in types
    assert "DISASTER" in types
    assert "UNKNOWN" in types

def test_filter_removes_low_severity(spark):
    data = [("CONFLICT", 0.3), ("DISASTER", 0.8), ("CONFLICT", 0.5)]
    df = spark.createDataFrame(data, ["event_type", "severity"])
    result = filter_low_quality_events(df, min_severity=0.5)
    assert result.count() == 2

Branching-Strategie

Wir empfehlen ein trunk-based-Development-Modell mit kurzen Feature-Branches:

main  (production)
  |
  +-- feature/add-gdelt-source     PR -> main
  +-- feature/optimize-silver      PR -> main
  +-- hotfix/fix-null-handling     PR -> main (fast-track)
  • main spiegelt immer den Produktions-Stand
  • Feature-Branches sind kurzlebig (1–3 Tage max.)
  • Keine langlebigen Dev/Staging-Branches — stattdessen Environment-Targets in DABs nutzen

Secrets-Management in CI/CD

Databricks-Token als GitHub-Actions-Secrets speichern (nie im Code):

gh secret set DATABRICKS_TOKEN_PROD --body "dapi..."
gh secret set DATABRICKS_TOKEN_STAGING --body "dapi..."
gh secret set DATABRICKS_HOST --body "https://adb-xxx.azuredatabricks.net"

In Databricks: Secret-Scopes nutzen statt Credentials in Job-Configs hardcoden.


Deployments monitoren

Nach dem Deploy den Bundle-State verifizieren:

# Check what is deployed in each target
databricks bundle summary --target prod

# Run a job manually to verify
databricks bundle run events_ingestion_job --target prod

Häufige Probleme und Fixes

ProblemUrsacheFix
bundle validate failed in CIFehlende DATABRICKS_HOST-Env-VarSecrets in GitHub-Repo-Settings setzen
Unit-Tests failen mit Spark-ErrorsJVM nicht im CI-Runner installiertjava-version: '11' in Setup-Action ergänzen
Deploy klappt, Job failedWheel nicht vor Job-Run hochgeladenbundle deploy lädt Artefakte hoch
Integrations-Tests laufen in TimeoutDev-Cluster Cold-Startkeep_alive: true für Test-Cluster nutzen
Prod-Deploy übersprungenStaging-Tests failedStaging-Tests fixen; das Gate nie überspringen

FAQ

Sollte ich Notebooks oder Python-Wheels deployen?

Python-Wheels für produktiven Code (testbar, versionierbar, mit Pinned Dependencies). Notebooks fürs Exploration- und Reporting-Layer, wo interaktive Iteration zählt.

Wie behandle ich Datenbank-Migrationen in CI/CD?

Schema-Änderungen via versionierte SQL-Migrationen (z. B. Alembic, Liquibase) oder Unity-Catalog-Grants als IaC. Niemals Schema-Änderungen direkt in Produktions-Pipelines.

Was, wenn Staging-Tests nicht-deterministisch sind?

Erst fixen, nicht überspringen. Flaky Integrations-Tests sind das größte Risiko in CI/CD — sie trainieren das Team, Failures zu ignorieren, und das eine Mal, wenn der Test echt failed, wird er weggeklickt.

Wie hosten DACH-Teams Databricks DSGVO-konform?

Azure Databricks in West Europe (Niederlande/Irland) oder Frankfurt für EU-Daten-Residency. Unity Catalog für RBAC, Service-Principals für Service-Account-Logins, Audit-Logs ins eigene Log-Analytics-Workspace.

Brauche ich databricks bundle deploy oder Terraform?

DABs sind enger an Databricks gebaut und idiomatischer für reine Databricks-Stacks. Terraform für Cross-Cloud-Networking, Identity-Provider, multi-region Setup.


Wrap-up

Ordentliches CI/CD für Databricks ist nicht optional — es ist das, was Teams unterscheidet, die confident shippen, von Teams, die Freitag-Deploys fürchten. Databricks Asset Bundles, kombiniert mit GitHub Actions, geben dir eine saubere, versionierte, environment-aware Deployment-Pipeline.

Klein anfangen: schon nur bundle validate zu deinen PR-Checks zu ergänzen, fängt Konfigurationsfehler ab, bevor sie Produktion erreichen.


Stand: 14. Mai 2026.

H

Geschrieben von

Harbinger Team

Cloud-, Data- und AI-Engineer in DACH. Schreibt seit 2018 über infrastruktur­kritische Tech-Entscheidungen — keine Marketing- Folien, sondern echte Trade-offs aus Production-Workloads.

Hat dir das geholfen?

Jede Woche ein neuer Artikel über DACH-Cloud, Data und AI — direkt in dein Postfach. Kein Spam, kein Marketing-Sprech.

Kein Spam. 1-Klick-Abmeldung. Datenschutz bei Loops.so.