DE Track · Module 09

DevOps on Databricks

Bundles, CI/CD, testing, and the production-operations discipline that separates senior candidates.

Why this module carries senior-level weight

Anyone can demo a notebook. Senior candidates are judged on whether they can take a pipeline from a developer's branch to a governed production workspace with tests, approvals, rollback, and monitoring — and keep it running. Interviewers probe this area because it cannot be faked from tutorials: you either have run production hypercare or you have not. You have. Your job on this page is to map the Azure DevOps discipline you actually practiced at ADM onto Databricks-native tooling — bundles, Git folders, system tables — so you can speak both dialects.

Declarative Automation Bundles (formerly Databricks Asset Bundles)

Bundles are the Databricks-native infrastructure-as-code unit for workspace assets: a project folder containing source code plus YAML that declares the jobs, pipelines, and other resources the code needs, deployable to any target environment with one CLI command. Naming note for interviews: Databricks Asset Bundles were renamed Declarative Automation Bundles in March 2026. The rename is non-breaking — the CLI command is still databricks bundle and the YAML schema is unchanged — but using the current name (and knowing the old one) signals you are up to date. Most practitioners still say "DABs" or just "bundles."

Anatomy of databricks.yml

One root databricks.yml defines the bundle; resource definitions can be split into included files. The key blocks:

bundle:
  name: r2r_finance_pipelines

include:
  - resources/*.yml

variables:
  catalog:
    description: Target Unity Catalog
    default: fin_dev

targets:
  dev:
    mode: development
    default: true
    workspace:
      host: https://adb-dev.azuredatabricks.net
  staging:
    workspace:
      host: https://adb-stg.azuredatabricks.net
    variables:
      catalog: fin_staging
  prod:
    mode: production
    workspace:
      host: https://adb-prod.azuredatabricks.net
    variables:
      catalog: fin_prod
    run_as:
      service_principal_name: sp-r2r-deploy

The lifecycle is three commands: databricks bundle validate (schema and reference checks, runnable in CI without touching a workspace), databricks bundle deploy -t staging (sync code and create/update declared resources in that target), and databricks bundle run my_job -t staging (trigger and wait, ideal for smoke tests). Deploys are idempotent — the bundle is the source of truth and drift is overwritten.

Honesty framing that works in your favor: your production deployments at ADM shipped through Azure DevOps pipelines, not bundles — bundles are study and lab knowledge for you. Say exactly that, then add that bundles solve the same problem you solved by hand: parameterized per-environment deployment with validation gates. Mapping a tool you know deeply onto one you have labbed is a senior move; pretending production experience you don't have is a disqualifier.

Git folders and the source-control workflow

Git folders (the feature long called Repos) clone a Git repository into the workspace so notebooks and source files are edited on real branches. The reference workflow: each engineer works in their own Git folder on a feature branch; commits and pushes happen from the workspace UI or local IDE; a pull request triggers CI; merge to main triggers deployment. Two production rules matter:

CI/CD pipeline design

The canonical Databricks delivery pipeline has four gates, and it is the same shape whether you implement it in Azure DevOps or GitHub Actions:

  1. Validate & test on the runner — lint (ruff), static checks, unit tests with a local SparkSession, databricks bundle validate. No workspace needed; fails fast and free.
  2. Deploy to stagingbundle deploy -t staging under a service-principal identity (OAuth/OIDC, not PATs where avoidable).
  3. Integration test in stagingbundle run the job against staging data on a small cluster or serverless; assert row counts, schema, and key data-quality expectations on the output.
  4. Promote to prod — on merge to main or a tagged release, deploy to the prod target behind a manual approval gate (an "environment" approval in Azure DevOps or GitHub).

An Azure DevOps sketch — the tool you actually used — adapted to drive bundles:

# azure-pipelines.yml
trigger:
  branches: { include: [ main ] }

stages:
- stage: Test
  jobs:
  - job: unit
    steps:
    - script: pip install -r requirements-dev.txt
    - script: ruff check src/
    - script: pytest tests/unit --junitxml=results.xml
    - script: databricks bundle validate -t staging

- stage: Staging
  dependsOn: Test
  jobs:
  - deployment: deploy_staging
    environment: databricks-staging       # approvals/checks live here
    strategy:
      runOnce:
        deploy:
          steps:
          - script: databricks bundle deploy -t staging
          - script: databricks bundle run r2r_gl_load -t staging
          - script: pytest tests/integration  # asserts on staging output

- stage: Prod
  dependsOn: Staging
  jobs:
  - deployment: deploy_prod
    environment: databricks-prod          # manual approval gate
    strategy:
      runOnce:
        deploy:
          steps:
          - script: databricks bundle deploy -t prod

The GitHub Actions version is structurally identical: a pull_request workflow for stage 1, a push-to-main workflow using databricks/setup-cli for stages 2–4, with protected environments providing the approval gate and OIDC federation providing keyless auth to Azure.

At ADM you own this delivery chain for Finance R2R analytics: CI/CD through Azure DevOps with release gates, plus the operational wrapper — monitoring, runbooks, and hypercare — around pipelines orchestrated across ADF, Databricks Jobs, and Redwood RunMyJobs (with Power BI refresh in the chain). When asked "describe your CI/CD setup," narrate the gates concretely: what ran on PR, what an approval looked like, what blocked a bad release — then note you have replicated the flow with databricks bundle in your lab so you can speak to the native tooling too.

"Walk me through how a code change reaches production in your ideal Databricks setup."

Strong outline: feature branch in a Git folder or local IDE → PR triggers lint + unit tests (local SparkSession, no cluster cost) + bundle validate → merge deploys the bundle to staging as a service principal → integration run on real-but-small staging data with assertions on outputs → manual approval → idempotent bundle deploy -t prod, schedules owned by a service principal, humans have read-only access in prod. Close with rollback: redeploy the previous Git tag — the bundle makes the workspace match the repo, so rollback is just deploying older code.

Testing PySpark

Distinguish three layers — interviewers love asking where data-quality checks belong, and the answer is "a different layer than unit tests":

LayerWhat it verifiesWhere it runsWhen it runs
Unit testsTransformation logic: given this input DataFrame, the function returns that outputCI runner with a local SparkSession, or your IDE via Databricks ConnectEvery PR, seconds, no cluster
Data-quality testsThe data itself: nulls, uniqueness, referential integrity, freshnessInside the pipeline — SDP expectations, constraint checks, or a DQ task in the jobEvery production run, on real data
Integration testsThe wiring: job config, dependencies, permissions, end-to-end output on sample dataStaging workspace, small job cluster or serverlessPer deploy to staging

For unit tests, structure code so transformations are pure functions of DataFrames, then test with pyspark.testing.assertDataFrameEqual (built in since Spark 3.5 — no more hand-rolled collect-and-sort comparisons):

# src/transforms.py
def derive_variance(actuals_df, budget_df):
    return (actuals_df.join(budget_df, ["cost_center", "period"], "left")
            .withColumn("variance", col("actual_amt") - col("budget_amt")))

# tests/unit/test_transforms.py
from pyspark.testing import assertDataFrameEqual

def test_variance(spark):  # spark = local SparkSession fixture
    actuals = spark.createDataFrame([("CC10", "2026-05", 120.0)],
                                    "cost_center string, period string, actual_amt double")
    budget  = spark.createDataFrame([("CC10", "2026-05", 100.0)],
                                    "cost_center string, period string, budget_amt double")
    expected = spark.createDataFrame([("CC10", "2026-05", 120.0, 100.0, 20.0)],
        "cost_center string, period string, actual_amt double, budget_amt double, variance double")
    assertDataFrameEqual(derive_variance(actuals, budget), expected)

Databricks Connect is the alternative runtime for the same tests: the pytest fixture builds a DatabricksSession instead of a local one, and execution happens on remote serverless compute — useful when logic depends on Databricks-specific behavior, at the cost of speed and a workspace dependency in CI. Default to local Spark in CI; reserve Connect for interactive development and the rare DBR-dependent test. Data-quality checks, by contrast, run on every production execution because they guard against bad inputs, which no amount of unit testing can prevent.

Pitfall: testing notebooks by running them end to end as the only "test." Notebook-runner tests are slow, flaky, expensive, and tell you only that something failed somewhere. If your logic lives in %run-chained notebooks with widget state, it is effectively untestable. Refactor to importable functions first; the testing problem mostly dissolves. The second classic trap: letting unit tests depend on real catalog tables — your CI now fails whenever someone touches dev data.

Environment strategy: workspaces vs catalogs

Under Unity Catalog there are two viable isolation models, and a deliberate hybrid is the common enterprise answer:

Workspace per environmentCatalog per environment
IsolationStrong: separate compute, network config, workspace ACLs per envData-level only: one workspace, dev/staging/prod catalogs separated by grants
Cost & overheadHigher: three workspaces to administer, bind, and pay attention toLower: one workspace, simpler day-to-day
Blast radiusA dev mistake cannot touch prod compute or configA misconfigured grant or a job pointed at the wrong catalog can cross environments
PromotionBundle targets point at different hosts; identical code, different workspaceBundle targets override only the catalog variable; same host
Best fitRegulated/enterprise prod (use workspace-catalog binding so the prod catalog is reachable only from the prod workspace)Small teams, fast iteration, lower-stakes domains

Since UC metastores are per-region and shared across workspaces, the hybrid is natural: separate workspaces for prod vs non-prod, environment-named catalogs, and workspace-catalog bindings ensuring fin_prod is simply invisible outside the production workspace. Whatever the topology, parameterize the catalog in code (a bundle variable or job parameter) so promotion never requires an edit.

Secrets management

Never put credentials in notebooks, job parameters, or repo YAML. Databricks secret scopes hold key-value secrets accessed via dbutils.secrets.get(scope, key); values are redacted in notebook output. On Azure, prefer Azure Key Vault-backed scopes: the scope is a read-only mirror of a Key Vault, so rotation, access policies, and audit stay in Key Vault where the security team already lives — the pattern that matches your Azure estate. Two caveats worth volunteering: anyone with READ on a scope can print a secret if they try (redaction is cosmetic, ACLs are the control), and for service-to-service auth the stronger 2026 answer is to avoid long-lived secrets entirely — UC storage credentials with managed identities for data access, OAuth/OIDC federation for CI pipelines.

Terraform: account and workspace infrastructure

Bundles deploy into workspaces; the Databricks Terraform provider builds what bundles assume already exists — workspaces themselves, the UC metastore, catalogs and external locations, storage credentials, groups and service principals, cluster policies, network configuration. The clean division of labor: platform team owns Terraform (infrastructure, governance scaffolding, slow-changing), data teams own bundles (jobs and pipelines, fast-changing, per-project). Be plain about your level here: your Terraform is working knowledge — you can read provider configs and explain the division of responsibilities, but infrastructure provisioning was not your hands-on lane. Saying so, then articulating the bundle/Terraform boundary crisply, lands better than bluffing through a state-file question.

Observability in production

Production discipline is what you sell hardest, because you have lived it. The Databricks-native toolkit:

Monitoring, runbooks, and hypercare via Azure DevOps are on your resume at ADM for a reason: R2R feeds Executive Committee and CFO-level reporting, where a late P&L or Budget-vs-Actual refresh is visible at the top of the company. You also coordinated failure handling across a multi-tool chain — ADF, Databricks jobs, RunMyJobs, Power BI refresh — where "the pipeline failed" first means working out which pipeline. Use that: cross-system triage is exactly the maturity the system-tables story formalizes, and you can say you would rebuild your monitoring on system.lakeflow queries rather than UI spot checks.

Runbooks and hypercare

The least glamorous, most differentiating topic on this page. A runbook is the per-pipeline operational contract: what the pipeline does and its SLA, how failures present, triage steps in order (check the job run page → task error → upstream source availability → data-quality output), safe rerun procedure (which tasks are idempotent, what a backfill needs), escalation path with names, and known failure modes with fixes. The test of a good runbook is that the on-call who did not build the pipeline resolves the incident without paging the author. Hypercare is the deliberately elevated support window after go-live — daily run reviews, tightened alert thresholds, the build team on point rather than standard support, explicit exit criteria (for example, N consecutive clean cycles and no Sev-1s) before handover to steady-state operations. Treating hypercare as a planned phase with an exit gate, rather than "we'll watch it for a while," is a signal of operational maturity that most candidates cannot give — you can, from your ADM delivery work.

"A production pipeline failed overnight and a CFO-level dashboard is stale. What do you do?"

Strong outline: (1) Communicate first — notify consumers of the stale state and an ETA before debugging, because trust decays faster than data. (2) Triage via the runbook: job run page → failing task error → upstream availability → data-quality check output. (3) Restore service — rerun from the failed task if idempotent (repair run), scoped backfill if not; fixing the dashboard beats root-causing first. (4) Root-cause afterward and feed it back: a new alert or DQ expectation so this failure mode is caught earlier, and a runbook update. Anchor it in your actual ADM context — Finance R2R consumers, multi-tool orchestration — and it stops sounding hypothetical.

Thread to pull across the whole module: everything is declarative and versioned. Bundles declare workspace assets, Terraform declares infrastructure, SDP declares pipelines, runbooks declare operations. If you frame DevOps on Databricks as "move every click into reviewed, versioned configuration," every sub-topic on this page becomes one coherent answer.