<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>topic Re: Productionizing Databricks Pipelines with Declarative Automation Bundles and Azure DevOps in Community Articles</title>
    <link>https://community.databricks.com/t5/community-articles/productionizing-databricks-pipelines-with-declarative-automation/m-p/166332#M1459</link>
    <description>&lt;P&gt;clear explanation of DABs, nice work!!&lt;/P&gt;</description>
    <pubDate>Mon, 24 Aug 2026 17:13:40 GMT</pubDate>
    <dc:creator>VinayKumarB</dc:creator>
    <dc:date>2026-08-24T17:13:40Z</dc:date>
    <item>
      <title>Productionizing Databricks Pipelines with Declarative Automation Bundles and Azure DevOps</title>
      <link>https://community.databricks.com/t5/community-articles/productionizing-databricks-pipelines-with-declarative-automation/m-p/166220#M1448</link>
      <description>&lt;P&gt;A retail medallion pipeline packaged as a bundle, promoted through Dev, QA, and Production from a single repository — including the deployment-mode bug that made every stage go green while shipping a job that could never have run.&lt;/P&gt;&lt;P&gt;Most Databricks projects start the same way: a notebook that works, scheduled through the workspace UI. That is fine for one pipeline. It stops being fine somewhere around the third environment and the second engineer.&lt;/P&gt;&lt;P&gt;We hit that wall on a retail analytics build — sales, inventory, product, and customer feeds landing into a medallion architecture, promoted across Dev, QA, and Production. This walkthrough covers how we packaged it as a Declarative Automation Bundle and deployed it from Azure DevOps, including the parts that bit us.&lt;/P&gt;&lt;H2&gt;A note on naming&lt;/H2&gt;&lt;P&gt;Databricks renamed Asset Bundles to Declarative Automation Bundles as part of the Lakeflow consolidation. The DAB abbreviation survives; the expansion changed.&lt;/P&gt;&lt;P&gt;Along with it, Workflows became Lakeflow Jobs, and Delta Live Tables became Lakeflow Spark Declarative Pipelines — often shortened to Lakeflow pipelines.&lt;/P&gt;&lt;P&gt;Plain "Spark Declarative Pipelines" refers to the Apache Spark open-source framework that the Databricks product extends.&lt;/P&gt;&lt;P&gt;Most search results and blog posts still use the older names.&lt;/P&gt;&lt;H2&gt;1. What breaks without bundles&lt;/H2&gt;&lt;P&gt;Three specific failures, not a general lament.&lt;/P&gt;&lt;H3&gt;Environment drift&lt;/H3&gt;&lt;P&gt;Dev points at retail_dev, prod at retail_prod, and the only record of that difference lives in someone's head.&lt;/P&gt;&lt;P&gt;A cluster configuration gets tuned in prod and never backported. Six weeks later nobody can say why QA passes and prod does not.&lt;/P&gt;&lt;H3&gt;No change history&lt;/H3&gt;&lt;P&gt;A UI-edited job has no diff, no author, and no reviewer.&lt;/P&gt;&lt;P&gt;When a pipeline breaks at 2 a.m., what changed? There is no reliable answer.&lt;/P&gt;&lt;H3&gt;No rollback&lt;/H3&gt;&lt;P&gt;Without a versioned definition of the job, reverting means remembering what the settings used to be.&lt;/P&gt;&lt;P&gt;Declarative Automation Bundles address all three by making the job definition a file in your repository.&lt;/P&gt;&lt;H2&gt;2. What a bundle actually is&lt;/H2&gt;&lt;P&gt;A bundle is a directory with a databricks.yml at its root that declares your Databricks resources — Lakeflow Jobs, Lakeflow pipelines, dashboards, Model Serving endpoints, MLflow experiments, and registered models — alongside the source code they run.&lt;/P&gt;&lt;P&gt;The Databricks CLI reads that file and reconciles your workspace to match.&lt;/P&gt;&lt;P&gt;You get one command per environment, and the same bundle can be promoted from Dev to Production.&lt;/P&gt;&lt;P&gt;A simplified repository structure looks like this:&lt;/P&gt;&lt;LI-CODE lang="python"&gt;DAB-CI-CD/
├── resources/
│   └── retail_pipeline_job.yml
├── src/
│   └── notebooks/
│       ├── bronze/
│       │   ├── ingest_customers
│       │   ├── ingest_inventory
│       │   ├── ingest_products
│       │   └── ingest_sales
│       ├── silver/
│       │   ├── transform_customers_scd
│       │   ├── transform_inventory
│       │   ├── transform_products
│       │   └── transform_sales
│       └── gold/
│           ├── agg_store_revenue
│           ├── agg_product_performance
│           ├── agg_inventory_health
│           └── agg_executive_summary
├── tests/
│   └── test_data_quality
├── setup_test_data.ipynb
├── azure-pipelines.yml
├── databricks.yml
└── .gitignore&lt;/LI-CODE&gt;&lt;P&gt;Nothing in that tree is created by hand in the workspace.&lt;/P&gt;&lt;P&gt;The job, folder layout, and notebooks are all provisioned by&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;databricks bundle deploy.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="figure-1-workspace-tree.png" style="width: 165px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/30195i028599A4DF042ED4/image-size/medium?v=v2&amp;amp;px=400" role="button" title="figure-1-workspace-tree.png" alt="figure-1-workspace-tree.png" /&gt;&lt;/span&gt;&lt;/STRONG&gt;&lt;/P&gt;&lt;P&gt;The same structure as it appears inside the Databricks workspace after deployment. The bundle is Git-linked, so the workspace copy is a deployment target rather than a place to edit.&lt;/P&gt;&lt;H2&gt;3. Walking through databricks.yml&lt;/H2&gt;&lt;H3&gt;Bundle identity&lt;/H3&gt;&lt;LI-CODE lang="python"&gt;bundle:
  name: retail-modernization-dab&lt;/LI-CODE&gt;&lt;P&gt;This name feeds the deployment path, so changing it later moves your remote state to a new location.&lt;/P&gt;&lt;P&gt;Pick it once.&lt;/P&gt;&lt;H3&gt;Splitting out resources&lt;/H3&gt;&lt;LI-CODE lang="python"&gt;include:
  - resources/*.yml&lt;/LI-CODE&gt;&lt;P&gt;Keeping job definitions in their own files under&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;resources/&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;is worth doing from day one.&lt;/P&gt;&lt;P&gt;A single databricks.yml holding four bronze tasks, four silver tasks, four gold tasks, and a quality check becomes unreadable fast.&lt;/P&gt;&lt;H3&gt;Variables for what differs per environment&lt;/H3&gt;&lt;LI-CODE lang="python"&gt;variables:
  catalog:
    description: "Unity Catalog name for the environment"
    default: "retail_dev"

  schema_bronze:
    description: "Bronze layer schema"
    default: "bronze"

  schema_silver:
    description: "Silver layer schema"
    default: "silver"

  schema_gold:
    description: "Gold layer schema"
    default: "gold"&lt;/LI-CODE&gt;&lt;P&gt;&lt;SPAN&gt;The catalog is the only value that genuinely changes between environments; the layer schemas stay constant.&lt;/SPAN&gt;&lt;/P&gt;&lt;P&gt;&lt;SPAN&gt;Declaring all four keeps the notebooks free of hardcoded names. They can read:&lt;/SPAN&gt;&lt;/P&gt;&lt;LI-CODE lang="python"&gt;${var.catalog}.${var.schema_bronze}&lt;/LI-CODE&gt;&lt;P&gt;and work anywhere.&lt;/P&gt;&lt;H3&gt;Variable precedence&lt;/H3&gt;&lt;P&gt;Variable precedence, highest to lowest, is worth knowing before you debug a value that "isn't taking":&lt;/P&gt;&lt;OL&gt;&lt;LI&gt;--var="catalog=retail_qa"&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;on the CLI&lt;/LI&gt;&lt;LI&gt;Environment variables prefixed&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;BUNDLE_VAR_&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;— e.g.&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;BUNDLE_VAR_catalog&lt;/LI&gt;&lt;LI&gt;A&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;variable-overrides.json&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;file, if present&lt;/LI&gt;&lt;LI&gt;A&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;variables:&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;mapping inside the target&lt;/LI&gt;&lt;LI&gt;The&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;default:&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;in the top-level variable definition&lt;/LI&gt;&lt;/OL&gt;&lt;H3&gt;Where the bundle lands&lt;/H3&gt;&lt;LI-CODE lang="markup"&gt;workspace:
  root_path: /Workspace/Users/${workspace.current_user.userName}/.bundle/${bundle.name}/${bundle.target}&lt;/LI-CODE&gt;&lt;P&gt;This is correct for development — every engineer gets an isolated copy and nobody collides.&lt;/P&gt;&lt;P&gt;Do not ship this for production.&lt;/P&gt;&lt;P&gt;A user-scoped production path means your production pipeline is owned by one person's account and can become problematic when that person leaves the organization.&lt;/P&gt;&lt;P&gt;For production, deploy to a shared location and run as a service principal.&lt;/P&gt;&lt;P&gt;Production mode will also validate against user-scoped paths, which is covered below.&lt;/P&gt;&lt;LI-CODE lang="python"&gt;targets:

  # DEVELOPMENT
  dev:
    mode: development
    default: true
    workspace:
      host: https://YOUR-DEV-WORKSPACE.azuredatabricks.net
    variables:
      catalog: "retail_dev"

  # QA
  qa:
    mode: development
    workspace:
      host: https://YOUR-QA-WORKSPACE.azuredatabricks.net
    variables:
      catalog: "retail_qa"

  # PRODUCTION
  prod:
    mode: production
    workspace:
      host: https://YOUR-PROD-WORKSPACE.azuredatabricks.net
      root_path: /Workspace/Shared/.bundle/${bundle.name}/${bundle.target}
    run_as:
      service_principal_name: ${var.prod_service_principal}
    variables:
      catalog: "retail_prod"&lt;/LI-CODE&gt;&lt;H3&gt;The mistake we shipped&lt;/H3&gt;&lt;P&gt;Our first working version had&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;mode: development&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;on all three targets.&lt;/P&gt;&lt;P&gt;Everything deployed cleanly and the DevOps pipeline went green, so it looked correct.&lt;/P&gt;&lt;P&gt;It was not.&lt;/P&gt;&lt;P&gt;mode: development&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;does considerably more than its name suggests. It:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;Prefixes resources with a development identifier.&lt;/LI&gt;&lt;LI&gt;Tags deployed jobs and pipelines with a development Databricks tag.&lt;/LI&gt;&lt;LI&gt;Marks Lakeflow pipelines as development.&lt;/LI&gt;&lt;LI&gt;Pauses schedules and triggers on deployed jobs and quality monitors.&lt;/LI&gt;&lt;LI&gt;Enables concurrent runs on deployed jobs.&lt;/LI&gt;&lt;LI&gt;Disables the deployment lock.&lt;/LI&gt;&lt;LI&gt;Permits cluster overrides.&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;So our "production" job was effectively a development deployment, with its schedule paused and its deployment lock disabled.&lt;/P&gt;&lt;P&gt;A green CI/CD pipeline told us nothing because deployment success is not deployment correctness.&lt;/P&gt;&lt;P&gt;Production mode runs the opposite set of checks.&lt;/P&gt;&lt;P&gt;It validates production-oriented settings such as pipeline development status, Git branch configuration, deployment paths, run-as configuration, and permissions.&lt;/P&gt;&lt;P&gt;It also prevents cluster overrides.&lt;/P&gt;&lt;P&gt;Those validations are the feature. Let them stop you.&lt;/P&gt;&lt;H2&gt;4. The pipeline: Bronze, Silver, Gold&lt;/H2&gt;&lt;P&gt;Four retail domains flow through three layers.&lt;/P&gt;&lt;P&gt;Bronze ingests with Auto Loader and stamps audit metadata.&lt;/P&gt;&lt;P&gt;Silver cleanses and applies merge logic.&lt;/P&gt;&lt;P&gt;Gold aggregates to business KPIs.&lt;/P&gt;&lt;P&gt;A final task validates quality before anything downstream consumes the data.&lt;/P&gt;&lt;H3&gt;Bronze&lt;/H3&gt;&lt;P&gt;The Bronze layer contains:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;ingest_customers&lt;/LI&gt;&lt;LI&gt;ingest_inventory&lt;/LI&gt;&lt;LI&gt;ingest_products&lt;/LI&gt;&lt;LI&gt;ingest_sales&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;Auto Loader reads CSV files from the landing zone, adds an ingestion timestamp and source file name, and appends the data to Delta.&lt;/P&gt;&lt;P&gt;There is no business logic in Bronze.&lt;/P&gt;&lt;H3&gt;Silver&lt;/H3&gt;&lt;P&gt;The Silver layer contains:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;transform_customers_scd&lt;/LI&gt;&lt;LI&gt;transform_inventory&lt;/LI&gt;&lt;LI&gt;transform_products&lt;/LI&gt;&lt;LI&gt;transform_sales&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;These tasks filter invalid rows, standardize identifiers to uppercase, cast types, deduplicate, and then perform MERGE operations.&lt;/P&gt;&lt;P&gt;Customers use SCD Type 1.&lt;/P&gt;&lt;P&gt;Inventory derives a reorder flag.&lt;/P&gt;&lt;H3&gt;Gold&lt;/H3&gt;&lt;P&gt;The Gold layer contains:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;agg_store_revenue&lt;/LI&gt;&lt;LI&gt;agg_product_performance&lt;/LI&gt;&lt;LI&gt;agg_inventory_health&lt;/LI&gt;&lt;LI&gt;agg_executive_summary&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;These produce daily revenue and transaction counts by store, product performance by category, latest-snapshot stock health, and a company-level rollup for leadership dashboards.&lt;/P&gt;&lt;H3&gt;Tests&lt;/H3&gt;&lt;P&gt;The&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;test_data_quality&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;task checks:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;Row counts&lt;/LI&gt;&lt;LI&gt;Null values&lt;/LI&gt;&lt;LI&gt;Referential integrity across the Gold tables&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;The Silver merges are idempotent, which matters more than it sounds.&lt;/P&gt;&lt;P&gt;It is what makes a re-run after a partial failure safe rather than duplicating rows.&lt;/P&gt;&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="figure-2-job-dag.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/30196i98318A7B020F6C8F/image-size/large?v=v2&amp;amp;px=999" role="button" title="figure-2-job-dag.png" alt="figure-2-job-dag.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;P&gt;A successful run. Bronze tasks execute in parallel; each Silver task depends only on its own Bronze task, so&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;transform_sales&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;starts as soon as&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;ingest_sales&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;finishes instead of waiting for all four. The whole run completes in under three minutes on serverless compute. Usernames and job identifiers are redacted.&lt;/P&gt;&lt;H3&gt;Why per-task dependencies beat layer barriers&lt;/H3&gt;&lt;P&gt;It is tempting to make all four Silver tasks depend on all four Bronze tasks.&lt;/P&gt;&lt;P&gt;Don't.&lt;/P&gt;&lt;P&gt;Wiring each Silver task to its own Bronze task means one slow domain does not hold up the other three, and a single failed ingest only blocks its own branch instead of the entire layer.&lt;/P&gt;&lt;H2&gt;5. Azure DevOps: validate, then promote&lt;/H2&gt;&lt;P&gt;The pipeline runs four stages on every merge to main.&lt;/P&gt;&lt;H3&gt;Validate&lt;/H3&gt;&lt;P&gt;Install a pinned CLI, run unit tests, then bundle validate against every target.&lt;/P&gt;&lt;P&gt;This catches YAML errors, missing references, and broken variable substitutions before anything is deployed.&amp;nbsp;&lt;/P&gt;&lt;H3&gt;Deploy to Dev&lt;/H3&gt;&lt;P&gt;Run:&lt;/P&gt;&lt;LI-CODE lang="python"&gt;databricks bundle deploy --target dev&lt;/LI-CODE&gt;&lt;P&gt;This provides fast feedback that the resources provision cleanly.&lt;/P&gt;&lt;H3&gt;Deploy to QA&lt;/H3&gt;&lt;P&gt;Deploy the bundle and then run the job:&lt;/P&gt;&lt;LI-CODE lang="python"&gt;databricks bundle deploy --target qa
databricks bundle run retail_etl_pipeline --target qa&lt;/LI-CODE&gt;&lt;P&gt;The data quality task has to pass here before production is reachable.&lt;/P&gt;&lt;H3&gt;Deploy to Production&lt;/H3&gt;&lt;P&gt;Production is gated behind an Azure DevOps environment approval check.&lt;/P&gt;&lt;P&gt;Promotion becomes a decision rather than a side effect of merging.&lt;/P&gt;&lt;P&gt;Everything the pipeline needs is in the repository — the bundle configuration, job definition, notebooks, and pipeline YAML itself.&lt;/P&gt;&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="figure-3-devops-repo.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/30197iA6B75C502685F9E8/image-size/large?v=v2&amp;amp;px=999" role="button" title="figure-3-devops-repo.png" alt="figure-3-devops-repo.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;P&gt;The repository on main. Because the bundle is the deployment, every change to a job arrives as a reviewable commit. The databricks.yml and azure-pipelines.yml sit next to the notebooks they govern. Organization, project, and author details are redacted.&lt;/P&gt;&lt;P&gt;One structural note before the YAML: each target already declares its own&amp;nbsp;&lt;EM&gt;workspace.host&lt;/EM&gt;, so the pipeline passes only credentials, never&amp;nbsp;DATABRICKS_HOST.&lt;/P&gt;&lt;P&gt;Setting a host environment variable that disagrees with the target's declared host is a common and confusing source of authentication failures.&lt;/P&gt;&lt;H3&gt;Azure DevOps pipeline&lt;/H3&gt;&lt;LI-CODE lang="python"&gt;trigger:
  branches:
    include:
      - main

pool:
  vmImage: ubuntu-latest

variables:
  DATABRICKS_CLI_VERSION: '1.13.0'

stages:

  # CI
  - stage: Validate
    displayName: 'Validate bundle'
    jobs:
      - job: ValidateBundle
        steps:
          - checkout: self

          - script: |
              curl -fsSL https://raw.githubusercontent.com/databricks/setup-cli/v$(DATABRICKS_CLI_VERSION)/install.sh | sh
              databricks --version
            displayName: 'Install Databricks CLI'

          - script: |
              pip install -r requirements-dev.txt
              pytest tests/unit -q
            displayName: 'Unit tests'

          - script: |
              for t in dev qa prod; do
                echo "Validating target: $t"
                databricks bundle validate --target $t
              done
            displayName: 'Validate all targets'
            env:
              DATABRICKS_CLIENT_ID: $(CI_CLIENT_ID)
              DATABRICKS_CLIENT_SECRET: $(CI_CLIENT_SECRET)

  # Dev
  - stage: DeployDev
    dependsOn: Validate
    jobs:
      - deployment: DeployToDev
        environment: 'dev'
        strategy:
          runOnce:
            deploy:
              steps:
                - checkout: self

                - script: |
                    curl -fsSL https://raw.githubusercontent.com/databricks/setup-cli/v$(DATABRICKS_CLI_VERSION)/install.sh | sh
                    databricks bundle deploy --target dev
                  displayName: 'Deploy to Dev'
                  env:
                    DATABRICKS_CLIENT_ID: $(DEV_CLIENT_ID)
                    DATABRICKS_CLIENT_SECRET: $(DEV_CLIENT_SECRET)

  # QA
  - stage: DeployQA
    dependsOn: DeployDev
    jobs:
      - deployment: DeployToQA
        environment: 'qa'
        strategy:
          runOnce:
            deploy:
              steps:
                - checkout: self

                - script: |
                    curl -fsSL https://raw.githubusercontent.com/databricks/setup-cli/v$(DATABRICKS_CLI_VERSION)/install.sh | sh
                    databricks bundle deploy --target qa
                    databricks bundle run retail_etl_pipeline --target qa
                  displayName: 'Deploy and run in QA'
                  env:
                    DATABRICKS_CLIENT_ID: $(QA_CLIENT_ID)
                    DATABRICKS_CLIENT_SECRET: $(QA_CLIENT_SECRET)

  # Production
  - stage: DeployProd
    dependsOn: DeployQA
    jobs:
      - deployment: DeployToProd
        environment: 'prod'
        strategy:
          runOnce:
            deploy:
              steps:
                - checkout: self

                - script: |
                    curl -fsSL https://raw.githubusercontent.com/databricks/setup-cli/v$(DATABRICKS_CLI_VERSION)/install.sh | sh
                    databricks bundle deploy --target prod
                  displayName: 'Deploy to Prod'
                  env:
                    DATABRICKS_CLIENT_ID: $(PROD_CLIENT_ID)
                    DATABRICKS_CLIENT_SECRET: $(PROD_CLIENT_SECRET)&lt;/LI-CODE&gt;&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="figure-4-ci-run.png" style="width: 400px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/30198i4B4B23BB655C1C61/image-size/medium?v=v2&amp;amp;px=400" role="button" title="figure-4-ci-run.png" alt="figure-4-ci-run.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;P&gt;One run, end to end. Commit c47cf215 on main produced four green stages in 2 minutes 36 seconds. Each stage targets a different workspace, so a failure stops promotion before it reaches the next environment. The commit hash makes any deployed state traceable back to the exact code that produced it.&lt;/P&gt;&lt;H3&gt;Three choices worth explaining&lt;/H3&gt;&lt;H4&gt;QA runs the job, not just the deploy&lt;/H4&gt;&lt;P&gt;Our original pipeline only ever called&lt;EM&gt;&amp;nbsp;bundle deploy.&lt;/EM&gt;&lt;/P&gt;&lt;P&gt;Every stage went green while proving nothing about whether the pipeline produced correct data.&lt;/P&gt;&lt;P&gt;Deploy succeeding means the configuration was valid and the resources were created.&lt;/P&gt;&lt;P&gt;Adding&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;bundle run&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;in QA means the data quality task actually has to pass before production is reachable.&lt;/P&gt;&lt;H4&gt;Production is gated&lt;/H4&gt;&lt;P&gt;Auto-deploying to production on every merge to main is a fast way to have a bad morning.&lt;/P&gt;&lt;P&gt;Azure DevOps environments support approval checks. Attach one to production and promotion becomes deliberate.&lt;/P&gt;&lt;H4&gt;The CLI version is pinned&lt;/H4&gt;&lt;P&gt;We originally piped&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;install.sh&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;from the main branch without pinning the version.&lt;/P&gt;&lt;P&gt;That makes CI non-reproducible.&lt;/P&gt;&lt;P&gt;A CLI release can change validation behavior and break a build on a commit that touched nothing.&lt;/P&gt;&lt;P&gt;The setup script accepts a release tag, so pin it and bump deliberately.&lt;/P&gt;&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="figure-5-prod-deployed.png" style="width: 400px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/30199iFF84C88CB0594AE0/image-size/medium?v=v2&amp;amp;px=400" role="button" title="figure-5-prod-deployed.png" alt="figure-5-prod-deployed.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;P&gt;The result: the bundle as deployed to production, provisioned entirely by the pipeline with no manual workspace configuration. Because&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;run_as&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;points at a service principal, the deploy path and resource owner are the service principal rather than an individual. Path and owner are redacted here.&lt;/P&gt;&lt;H2&gt;6. Authentication: OAuth, not PATs&lt;/H2&gt;&lt;P&gt;Our first pass used personal access tokens per environment.&lt;/P&gt;&lt;P&gt;Two problems:&lt;/P&gt;&lt;OL&gt;&lt;LI&gt;A PAT is tied to a human.&lt;/LI&gt;&lt;LI&gt;If it is not flagged as a secret variable in Azure DevOps, it can be exposed in the variables panel or in screenshots.&lt;/LI&gt;&lt;/OL&gt;&lt;P&gt;For unattended CI/CD, Databricks provides several authentication options, including managed identities and OAuth machine-to-machine authentication.&lt;/P&gt;&lt;P&gt;Managed identity requires an appropriate Azure-hosted execution environment. On Microsoft-hosted agents such as&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;ubuntu-latest&lt;/EM&gt;, OAuth M2M is a practical choice.&lt;/P&gt;&lt;P&gt;For OAuth M2M, generate an OAuth secret for the service principal and configure the required variables.&lt;/P&gt;&lt;LI-CODE lang="python"&gt;DATABRICKS_HOST
DATABRICKS_CLIENT_ID
DATABRICKS_CLIENT_SECRET&lt;/LI-CODE&gt;&lt;P&gt;The CLI can use these through unified authentication.&lt;/P&gt;&lt;P&gt;Our bundle supplies the workspace host through each target, so the pipeline primarily needs to provide the credentials.&lt;/P&gt;&lt;H3&gt;How many service principals?&lt;/H3&gt;&lt;P&gt;Databricks supports using a service principal across participating workspaces.&lt;/P&gt;&lt;P&gt;Many teams instead use one service principal per environment to limit blast radius and accept the additional credential variables.&lt;/P&gt;&lt;P&gt;Either approach can be defensible.&lt;/P&gt;&lt;P&gt;Decide deliberately rather than by accident.&lt;/P&gt;&lt;P&gt;We use a single read-mostly service principal for validation and separate service principals for environment-specific deployments.&lt;/P&gt;&lt;H3&gt;Three things that will bite you&lt;/H3&gt;&lt;H4&gt;Mark every credential as a secret&lt;/H4&gt;&lt;P&gt;An Azure DevOps variable is only masked if you configure it as secret.&lt;/P&gt;&lt;P&gt;A plain variable holding a token can be exposed in the variables panel or captured in screenshots.&lt;/P&gt;&lt;P&gt;Source credentials from Azure Key Vault through a variable group where appropriate.&lt;/P&gt;&lt;H4&gt;Unset DATABRICKS_TOKEN when migrating&lt;/H4&gt;&lt;P&gt;A leftover&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;DATABRICKS_TOKEN&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;or&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;DATABRICKS_USERNAME&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;can conflict with OAuth and produce authentication failures that look nothing like a configuration problem.&lt;/P&gt;&lt;P&gt;If you are moving from PATs, remove the old variables rather than leaving them alongside the new authentication method.&lt;/P&gt;&lt;H4&gt;OAuth secrets expire&lt;/H4&gt;&lt;P&gt;OAuth secrets have expiration limits.&lt;/P&gt;&lt;P&gt;Put the expiry date in a calendar or another operational reminder.&lt;/P&gt;&lt;P&gt;Otherwise, your pipeline can fail on a commit that changed nothing.&lt;/P&gt;&lt;H2&gt;7. Gotchas worth knowing first&lt;/H2&gt;&lt;H3&gt;Renaming a resource key can destroy and recreate the job&lt;/H3&gt;&lt;P&gt;The key in&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;resources/*.yml&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;is the identity the CLI tracks in state.&lt;/P&gt;&lt;P&gt;Rename:&lt;/P&gt;&lt;LI-CODE lang="python"&gt;retail_etl_pipeline&lt;/LI-CODE&gt;&lt;P&gt;to&lt;/P&gt;&lt;LI-CODE lang="python"&gt;retail_pipeline&lt;/LI-CODE&gt;&lt;P&gt;and you can end up with a new job and a new ID.&lt;/P&gt;&lt;P&gt;Run history may no longer be associated with the new resource, and alerts can point at the old resource.&lt;/P&gt;&lt;P&gt;Change the job's display name freely; leave the resource key stable.&lt;/P&gt;&lt;P&gt;If you have already orphaned a resource,&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;databricks bundle deployment bind&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;can be used to associate an existing resource with bundle state instead of creating a duplicate.&lt;/P&gt;&lt;H3&gt;Bundles use Terraform state&lt;/H3&gt;&lt;P&gt;Bundle deployments maintain state.&lt;/P&gt;&lt;P&gt;There is remote state under the workspace root path and local bundle state associated with the target.&lt;/P&gt;&lt;P&gt;Deleting the workspace folder by hand, or deploying the same bundle from a machine with stale state, can result in orphaned or duplicated resources.&lt;/P&gt;&lt;H3&gt;Add .databricks to .gitignore&lt;/H3&gt;&lt;P&gt;The local CLI working directory should never be committed.&lt;/P&gt;&lt;P&gt;for example:&lt;/P&gt;&lt;LI-CODE lang="python"&gt;.databricks/
.bundle/&lt;/LI-CODE&gt;&lt;H3&gt;Deployment locks matter more than they look&lt;/H3&gt;&lt;P&gt;Development mode disables the deployment lock.&lt;/P&gt;&lt;P&gt;Two engineers deploying the same development target concurrently can therefore interfere with each other.&lt;/P&gt;&lt;P&gt;A user-scoped development root path helps isolate individual development deployments.&lt;/P&gt;&lt;P&gt;Resist reaching for&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;--force-lock&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;after a failed pipeline run. It can create state-management problems and duplicate resources when used incorrectly.&lt;/P&gt;&lt;H3&gt;bundle destroy is not a dry run&lt;/H3&gt;&lt;P&gt;It removes deployed resources.&lt;/P&gt;&lt;P&gt;Try it in development first.&lt;/P&gt;&lt;H3&gt;Rollback is a redeploy&lt;/H3&gt;&lt;P&gt;Rollback is not a special command.&lt;/P&gt;&lt;P&gt;Check out the previous commit or tag and run:&lt;/P&gt;&lt;LI-CODE lang="python"&gt;databricks bundle deploy --target prod&lt;/LI-CODE&gt;&lt;P&gt;again.&lt;/P&gt;&lt;P&gt;Tag your production releases so there is something concrete to return to.&lt;/P&gt;&lt;P&gt;Note that production mode validates Git-related configuration, so plan your tagging and branching strategy together.&lt;/P&gt;&lt;H2&gt;8. Try it yourself&lt;/H2&gt;&lt;P&gt;Install the CLI using a pinned version:&lt;/P&gt;&lt;LI-CODE lang="python"&gt;curl -fsSL https://raw.githubusercontent.com/databricks/setup-cli/v1.13.0/install.sh | sh
databricks --version&lt;/LI-CODE&gt;&lt;P&gt;&lt;SPAN&gt;Scaffold a bundle:&lt;/SPAN&gt;&lt;/P&gt;&lt;LI-CODE lang="python"&gt;databricks bundle init&lt;/LI-CODE&gt;&lt;P&gt;&lt;SPAN&gt;Authenticate as a service principal:&lt;/SPAN&gt;&lt;/P&gt;&lt;LI-CODE lang="python"&gt;export DATABRICKS_HOST=https://YOUR-WORKSPACE.azuredatabricks.net
export DATABRICKS_CLIENT_ID=YOUR-SERVICE-PRINCIPAL-APPLICATION-ID
export DATABRICKS_CLIENT_SECRET=YOUR-OAUTH-SECRET&lt;/LI-CODE&gt;&lt;P&gt;&lt;SPAN&gt;Validate, deploy, and run:&lt;/SPAN&gt;&lt;/P&gt;&lt;LI-CODE lang="python"&gt;databricks bundle validate --target dev
databricks bundle deploy --target dev
databricks bundle run retail_etl_pipeline --target dev&lt;/LI-CODE&gt;&lt;P&gt;Clean up:&lt;/P&gt;&lt;LI-CODE lang="python"&gt;databricks bundle destroy --target dev&lt;/LI-CODE&gt;&lt;H3&gt;Prerequisites&lt;/H3&gt;&lt;P&gt;You need:&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;A Unity Catalog-enabled workspace&lt;/LI&gt;&lt;LI&gt;Permission to create the required catalogs and schemas&lt;/LI&gt;&lt;LI&gt;A current Databricks CLI&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;The CLI version used in this example is 1.13.0.&lt;/P&gt;&lt;P&gt;Bundle behavior and templates have changed across older CLI releases, so check:&lt;/P&gt;&lt;LI-CODE lang="python"&gt;databricks --version&lt;/LI-CODE&gt;&lt;P&gt;before following this tutorial.&lt;/P&gt;&lt;H2&gt;9. What we got out of it&lt;/H2&gt;&lt;P&gt;The pipeline definition lives in Git with the code it runs.&lt;/P&gt;&lt;P&gt;Promotion is one command per environment.&lt;/P&gt;&lt;P&gt;Rollback is a checkout and redeploy.&lt;/P&gt;&lt;P&gt;And the&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;mode: development&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;bug is exactly the kind of thing a reviewer can catch in a diff and never catch in a UI.&lt;/P&gt;&lt;P&gt;The broader lesson from our build is simple:&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;A green CI/CD pipeline is not evidence that anything works.&lt;/STRONG&gt;&lt;/P&gt;&lt;P&gt;Ours was green while deploying a production job that could never have run on schedule.&lt;/P&gt;&lt;P&gt;Make the pipeline assert something real — run the job, check the data — or you have automated the appearance of correctness.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;Note:&lt;/STRONG&gt;&lt;/P&gt;&lt;P&gt;Sample data in this project is synthetic. No real retail or customer data was used.&lt;/P&gt;&lt;P&gt;Screenshots have been redacted to remove workspace identifiers, user accounts, and credentials.&lt;/P&gt;</description>
      <pubDate>Sat, 22 Aug 2026 09:03:59 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/productionizing-databricks-pipelines-with-declarative-automation/m-p/166220#M1448</guid>
      <dc:creator>Isaac_18</dc:creator>
      <dc:date>2026-08-22T09:03:59Z</dc:date>
    </item>
    <item>
      <title>Re: Productionizing Databricks Pipelines with Declarative Automation Bundles and Azure DevOps</title>
      <link>https://community.databricks.com/t5/community-articles/productionizing-databricks-pipelines-with-declarative-automation/m-p/166332#M1459</link>
      <description>&lt;P&gt;clear explanation of DABs, nice work!!&lt;/P&gt;</description>
      <pubDate>Mon, 24 Aug 2026 17:13:40 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/productionizing-databricks-pipelines-with-declarative-automation/m-p/166332#M1459</guid>
      <dc:creator>VinayKumarB</dc:creator>
      <dc:date>2026-08-24T17:13:40Z</dc:date>
    </item>
    <item>
      <title>Re: Productionizing Databricks Pipelines with Declarative Automation Bundles and Azure DevOps</title>
      <link>https://community.databricks.com/t5/community-articles/productionizing-databricks-pipelines-with-declarative-automation/m-p/166510#M1464</link>
      <description>&lt;BLOCKQUOTE&gt;&lt;HR /&gt;&lt;a href="https://community.databricks.com/t5/user/viewprofilepage/user-id/248220"&gt;@Isaac_18&lt;/a&gt;&amp;nbsp;wrote:&lt;P&gt;&lt;SPAN&gt;For production, deploy to a shared location and run as a service principal.&lt;/SPAN&gt;&lt;/P&gt;&lt;HR /&gt;&lt;/BLOCKQUOTE&gt;&lt;P&gt;Doing this will give you a warning on the deployment as it will grant everyone read/write access to the underlying assets.&lt;/P&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&lt;SPAN class=""&gt;&lt;SPAN class=""&gt;&lt;SPAN class=""&gt;&lt;SPAN&gt;Warning: the bundle root path /Workspace/Shared/... is writable by all workspace users &lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&amp;nbsp;&lt;/DIV&gt;&lt;/DIV&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&lt;SPAN class=""&gt;&lt;SPAN class=""&gt;&lt;SPAN class=""&gt;&lt;SPAN&gt;The bundle is configured to use /Workspace/Shared, which will give read/write access to all users. If this is intentional, add CAN_MANAGE for 'group_name: users' permission to your bundle configuration. If the deployment should be restricted, move it to a restricted folder such as /Workspace/Users/&amp;lt;username or principal name&amp;gt;. &lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&amp;nbsp;&lt;/DIV&gt;&lt;/DIV&gt;</description>
      <pubDate>Wed, 26 Aug 2026 11:00:40 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/productionizing-databricks-pipelines-with-declarative-automation/m-p/166510#M1464</guid>
      <dc:creator>Talarfon</dc:creator>
      <dc:date>2026-08-26T11:00:40Z</dc:date>
    </item>
    <item>
      <title>Re: Productionizing Databricks Pipelines with Declarative Automation Bundles and Azure DevOps</title>
      <link>https://community.databricks.com/t5/community-articles/productionizing-databricks-pipelines-with-declarative-automation/m-p/166562#M1466</link>
      <description>&lt;P&gt;You're right — thanks for catching this.&lt;/P&gt;&lt;P&gt;Corrected prod target:&lt;/P&gt;&lt;LI-CODE lang="python"&gt;prod:
  mode: production
  workspace:
    host: https://&amp;lt;your-prod-workspace&amp;gt;.azuredatabricks.net
    root_path: /Workspace/Users/${var.prod_service_principal}/.bundle/${bundle.name}/${bundle.target}
  run_as:
    service_principal_name: ${var.prod_service_principal}
  permissions:
    - service_principal_name: ${var.prod_service_principal}
      level: CAN_MANAGE&lt;/LI-CODE&gt;&lt;P&gt;&lt;SPAN&gt;That keeps the point I was actually making — production shouldn't be owned by a human account that disappears at offboarding — without making the root world-writable. The path belongs to the service principal that deploys and &lt;/SPAN&gt;runs the bundle, and mode: production is satisfied because both run_as and permissions are set.&lt;/P&gt;</description>
      <pubDate>Thu, 27 Aug 2026 06:38:26 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/productionizing-databricks-pipelines-with-declarative-automation/m-p/166562#M1466</guid>
      <dc:creator>Isaac_18</dc:creator>
      <dc:date>2026-08-27T06:38:26Z</dc:date>
    </item>
  </channel>
</rss>

