How to set up CI/CD for Azure Databricks
Terraform for the platform, Asset Bundles for the workload: where that line sits, what belongs on each side and why you do not move it later.

- 01
Draw the line between platform and workload
Enterprise CI/CD on Azure Databricks is not a choice between Terraform and Databricks Asset Bundles. It is a split into two layers that move at different speeds. Terraform delivers the landing zone and the governance contract; bundles move data and AI applications through dev, test, acceptance and production at sprint speed.
What belongs on each side:
Layer Owner Tool Cadence Azure foundation Platform engineering Terraform with AzureRM Infrequent, controlled Databricks platform Platform engineering Terraform with the Databricks provider Infrequent, controlled Analytics workload Data engineering and ML Databricks Asset Bundles Frequent, CI/CD-driven Release orchestration DevOps and platform team Azure DevOps or GitHub Actions Every change And what actually lives in each layer:
- Azure foundation: resource groups, VNets, private endpoints, storage accounts, Key Vault, managed identities, diagnostic settings and the workspace itself.
- Databricks platform: workspaces, the Unity Catalog metastore, workspace assignment, groups, service principals, cluster policies, SQL warehouses, external locations, storage credentials and permissions.
- Analytics workload: jobs, Lakeflow pipelines, notebooks, wheels, libraries, dashboards, model serving endpoints, variables and targets.
- Release orchestration: validation, tests,
terraform planandapply,bundle validate,deployandrun, and the approvals in between.
Terraform can manage most Databricks resources, but it carries state while doing so. Every notebook change you pull into it becomes a state change that has to pass through the platform team. A bundle is meant to work the other way around: a complete project definition with source files, resource definitions, tests and deployment configuration in one version.
Two older approaches still exist and both share the same limitation. With Git folders only, your code is in source control but your job and pipeline configuration is not. With Git plus jobs, the job pulls code from Git at run time, but task order, compute and schedule stay outside source control. For deployment across multiple workspaces that falls short.
Decisiondo not use one tool for everything. Terraform for what rarely changes and reaches far, bundles for what changes every sprint.NoteDatabricks Asset Bundles are now called Declarative Automation Bundles. You will meet both names in the documentation and in CLI output; it is the same mechanism. - 02
Give every environment its own workspace
Separate development, test, acceptance and production at workspace level, not with folders or name prefixes inside a single workspace. Only at workspace level can you genuinely keep identity, compute and data access apart.
Environment Workspace Identity Data isolation Dev Shared or per team Developers plus a dev deployment principal Dev catalogs and dev storage Test One integration workspace CI/CD service principal Test catalogs with test data Acceptance Workspace for release validation Release service principal Acceptance catalogs with production-like controls Production Locked down, no manual runs Its own production principal Production catalogs, workspace-catalog binding, restricted external locations Unity Catalog cuts straight through this. Workspaces in the same region share one metastore, so your permission model and your lineage live in one place. You draw the line between environments inside it with workspace-catalog binding: that ties a production catalog to production workspaces, even when somebody elsewhere holds explicit grants.
Identity belongs at account level, should come from your identity provider and should sit in groups. Give ownership of production objects to a group and let jobs run under a service principal. A job that runs as a person falls over the moment that person leaves.
Secrets do not belong in notebooks, pipelines or YAML. Use Databricks secrets, optionally with a Key Vault-backed scope. That scope is read-only from Databricks and uses the Key Vault access policy model, not Azure RBAC. Cut scopes along roles or applications, not along people.
Decisionbind your production catalog to your production workspaces. Without that binding, the split between environments is an agreement rather than a control. - 03
Put the platform layer in Terraform
Start with a dedicated repository or folder for infrastructure, with reusable modules and a separate folder per environment. Onboarding a new domain is then a folder with variables, not a copy job.
infra/ modules/ azure-databricks-workspace/ unity-catalog/ networking/ identity/ policies/ envs/ dev/ backend.tf main.tf terraform.tfvars test/ acc/ prod/Put your state in Azure Storage rather than locally. Local state does not collaborate, can hold sensitive values and is one bad cleanup away from gone. Remote state gives you central storage, locking and encryption. Use a separate state key per environment.
The order inside Terraform is fixed, because the second provider needs the first one. The AzureRM provider creates the resource group and the workspace with
azurerm_databricks_workspace. You then configure the Databricks provider against the URL that comes out of it. Only then can you lay the Unity Catalog foundation: metastore, access connector, storage credentials, external locations, catalogs, schemas and grants. Expect a Premium workspace and account admin rights before you automate that.The same layer holds the platform-wide controls: groups, service principals, permissions, cluster policies, SQL warehouses and workspace settings. Run
terraform fmt,terraform validateandterraform planper environment, and only thenterraform apply, with an approval in front of production.Decisionnever let production apply automatically on every commit. Show the plan, have it read, approve it, then apply.Pitfallwithout scheduled drift detection you only notice manual changes in the production workspace when a deployment overwrites them. Runterraform planagainst production on a schedule and treat every difference as an alert. - 04
Put the workload layer in Asset Bundles
A bundle has exactly one
databricks.ymlat its root. That file holds the name, the includes, the artifacts, the variables, the workspace settings, the permissions, the resources, the targets and the identity it runs as.databricks/ databricks.yml resources/ jobs.yml pipelines.yml dashboards.yml src/ notebooks/ python/ sql/ tests/ pyproject.tomlThe difference between environments sits in the targets, not in four copies of the same YAML:
bundle: name: customer360 include: - resources/*.yml variables: catalog: description: Target catalog in Unity Catalog schema: description: Target schema targets: dev: mode: development workspace: host: https://<dev-workspace>.azuredatabricks.net variables: catalog: dev_customer360 schema: ${workspace.current_user.short_name} prod: mode: production workspace: host: https://<prod-workspace>.azuredatabricks.net root_path: /Workspace/Shared/.bundle/${bundle.name}/${bundle.target} variables: catalog: prod_customer360 schema: core run_as: service_principal_name: spn-dbx-prod-deployThe two modes genuinely do something. In
development, resources get a per-developer prefix, Lakeflow pipelines are marked as development, schedules and triggers are paused and deployment locks are off so two people do not block each other. Inproductionall of that falls away: pipelines are no longer development, compute cannot be overridden and the deployment should run under a service principal.In your pipeline it comes down to three commands:
databricks bundle validate -t dev databricks bundle deploy -t dev databricks bundle run -t dev <job_name>Build and test your code before you deploy, and publish wheels or JARs with a version tied to your commit. That is what makes a rollback possible later: without a version number you do not know which build was running in production.
Decisionproduction gets a fixedroot_pathand arun_aswith a service principal. A path with a user name in it is a deployment that stops the day that user leaves. - 05
Choose your orchestrator and settle authentication
Azure DevOps and GitHub Actions do the same thing: check out, install the CLI, validate, deploy, run and gate production. The difference is the governance around it.
Azure DevOps GitHub Actions Fits Organisations with Azure Repos, service connections and release approvals Teams that keep everything in the repository, with GitHub Environments Authentication Service connection with workload identity federation, client secret or managed identity GitHub OIDC federation or secrets, with a Databricks service principal Bundle steps The same CLI commands The same CLI commands Governance Built-in approval patterns Branch and environment protection in the repository Operational load Pipelines and service connections Environments, secrets and reusable workflows An Azure DevOps pipeline that puts the two layers in order:
trigger: branches: include: - main variables: cliUrl: https://raw.githubusercontent.com/databricks/setup-cli/main/install.sh stages: - stage: Validate jobs: - job: ci steps: - checkout: self - script: | terraform fmt -check terraform validate databricks bundle validate -t dev pytest displayName: Validate infrastructure, bundle and code - stage: Terraform_Dev condition: succeeded() jobs: - job: tf_dev steps: - task: AzureCLI@2 inputs: azureSubscription: sc-dbx-dev scriptType: bash scriptLocation: inlineScript inlineScript: | terraform -chdir=infra/envs/dev init terraform -chdir=infra/envs/dev plan terraform -chdir=infra/envs/dev apply -auto-approve - stage: Bundle_Dev dependsOn: Terraform_Dev jobs: - job: dab_dev steps: - script: | curl -fsSL $(cliUrl) | sh databricks bundle deploy -t dev databricks bundle run -t dev run-unit-tests displayName: Install the CLI and deploy the bundleFor authentication, workload identity federation is the better choice in both worlds. In Azure DevOps it works with the
AzureCLI@2task, or through the OIDC variant withDATABRICKS_AUTH_TYPE=azure-devops-oidcalongsideDATABRICKS_HOST,DATABRICKS_CLIENT_IDandSYSTEM_ACCESSTOKEN. In GitHub the Databricks CLI exchanges the workflow's OIDC token for a Databricks token. Either way there is no secret in your pipeline for somebody to rotate.Decisionfederation over secrets, and a service principal over a personal token. A personal token in CI/CD hangs on one employee and sits outside your normal access management. - 06
Fix the path from commit to production
The order of the steps is the actual design. Infrastructure goes before workload, because a job pointing at a schema that does not exist yet fails in a way that says nothing about the cause.
- A developer creates a branch and changes code, tests,
databricks.yml, resource YAML or a Terraform module. - The pull request runs linting, unit tests,
terraform fmt,terraform validate,terraform plananddatabricks bundle validate. - Merging to main starts the deployment to non-production.
- Terraform applies the platform changes first, and only when infrastructure actually changed.
- The bundle deploys the workload to dev or test.
- The pipeline runs the bundle jobs for integration tests, data quality and permissions.
- An acceptance approval guards the move to production.
- Terraform runs production only after approval and only on infrastructure changes.
- The bundle deploys to production in
productionmode under the service principal. - Operations watch jobs, audit logs, data quality and drift.
Rollback for the workload is a
git revertfollowed by a new deploy, and for the platform aterraform planandapplyfrom the previous version. That is the argument for version numbers on your artifacts back in step 4: without that version you know what you want to roll back, but not to what.Decisionyou recover by redeploying an earlier commit, not by working by hand in the production workspace. That one manual fix is the drift you find a quarter later. - A developer creates a branch and changes code, tests,
Common mistakes
The six you meet in almost every environment, and what belongs there instead:
| Mistake | Why it hurts | Better |
|---|---|---|
| Creating jobs by hand in production | Drift, no review trail, not repeatable | Jobs in bundle YAML |
| All Databricks resources in Terraform | State churn and slower workload releases | Terraform for platform, bundles for workload |
| Personal tokens in CI/CD | Tied to an employee and hard to govern | Service principal with federation |
| One catalog for every environment | Dev and test can reach production data | Catalogs per environment or domain, with workspace binding |
| Secrets in notebooks or YAML | Exposure and no rotation | Key Vault-backed scopes or your pipeline's secret store |
| Working directly in the production workspace | Configuration drift | Production only through the pipeline |
What to do next
This setup assumes your Unity Catalog layout is already settled, because the catalogs and schemas your bundle addresses come from there. See How to set up Unity Catalog. If you also want the lineage your pipelines produce to be visible outside Databricks, continue with How to connect Azure Databricks to Microsoft Purview.
The process at a glance
Click a step for its key decision
Draw the line between platform and workload
do not use one tool for everything. Terraform for what rarely changes and reaches far, bundles for what changes every sprint.
Read next
Related patterns
Related use cases
Related best practices
