# macOS
brew tap databricks/tap && brew install databricks
# Windows
winget install Databricks.DatabricksCLI
# Linux / WSL
curl -fsSL https://raw.githubusercontent.com/databricks/setup-cli/main/install.sh | sh
databricks -v # shows the version
Module 15: CI/CD & Deployment
The CLI, bundles, local development and CI/CD.
Topics in this module
- Databricks CLI
- Databricks SDKs
- Databricks Declarative Automation Bundles (formerly Asset Bundles)
- Git Repository Structure
- Local Development with VS Code
- Dependency and Environment Management
- Development, Test, and Production Environments
- GitHub Actions and Azure DevOps
- CI/CD Pipeline Design
- Automated Tests and Quality Gates
- Deployment Configuration
- Infrastructure as Code with Terraform
- Secrets and Environment Variables
- Release Management and Rollbacks
Deploy code and jobs from Git, the same way every time. This module is about the tools and the flow. The full worked project is Module 26 and 27.
- Most of this runs on your computer or in CI, not in a notebook. Code blocks show terminal commands and config files
- Free Edition: the docs don't say whether API access is available. Community reports say the CLI works with a personal access token (Settings → Developer) and that OAuth login did not. Treat everything here that needs the CLI as try it and see
- Assumes you know Git and Python (Module 26 starts from there)
Check this first
In your workspace, open Settings → Developer → Access tokens. If you can create a token, you can follow topics 1–3, 5 and 8. If not, read them as reference.
Databricks CLI
By the end: install the Databricks CLI and authenticate.
Install (once, on your computer):
Authenticate, choose one:
databricks auth login --host https://<workspace-url> # OAuth in the browser (recommended)
export DATABRICKS_HOST=https://<workspace-url> # token, for scripts and CI
export DATABRICKS_TOKEN=<token>
Try it:
databricks current-user me
databricks jobs list
databricks workspace list /
databricks fs ls dbfs:/Volumes/<catalog>/dbx_course/raw
| Credential | Use |
|---|---|
| OAuth user login | You, on your computer |
| OAuth with a service principal | CI and automation |
| Personal access token | Legacy. Short-lived and rotated if you must use one |
- Profiles live in
~/.databrickscfg:-p <profile>picks one - Order of lookup: bundle settings, environment variables, then profiles
- The CLI wraps the REST API: every command is an API call
Free Edition login
Community posts report OAuth is not supported for this host on Free Edition. If you see it, use a personal access token.
Databricks SDKs
By the end: call Databricks from Python with the SDK.
# pip install databricks-sdk
from databricks.sdk import WorkspaceClient
w = WorkspaceClient() # uses the same auth as the CLI (env vars or profile)
for job in w.jobs.list():
print(job.job_id, job.settings.name)
run = w.jobs.run_now(job_id=123) # start a job
result = run.result() # wait for it to finish
print(result.state.result_state)
| Use the | When |
|---|---|
| CLI | Shell scripts, CI steps, quick checks |
| SDK | Python logic around Databricks: loops, retries, custom tooling |
| REST API | Other languages, or calls the SDK doesn't wrap |
WorkspaceClient()with no arguments reads the default auth chain. Don't hard-code tokens- The SDK is in Beta: pin a version (
databricks-sdk==x.y.z) - The SDK has clients for jobs, clusters, tables, secrets, files, and more
- This is what Airflow and other tools use underneath (Module 28)
Databricks Declarative Automation Bundles (formerly Asset Bundles)
By the end: describe jobs and pipelines as code with a bundle.
Declarative Automation Bundles (earlier Asset Bundles) package code and resource definitions in a repo. The CLI command is still databricks bundle.
databricks bundle init # create a project from a template
databricks bundle validate # check the config
databricks bundle deploy -t dev # create or update jobs, pipelines
databricks bundle run my_job -t dev
databricks bundle destroy -t dev # remove what the bundle created
# databricks.yml
bundle:
name: orders-etl
resources:
jobs:
daily_orders:
name: daily-orders
tasks:
- task_key: ingest
notebook_task:
notebook_path: ./src/ingest.py
environment_key: default
environments:
- environment_key: default
spec:
environment_version: "3"
targets:
dev:
mode: development
default: true
prod:
mode: production
workspace:
host: https://<prod-workspace-url>
- One file,
databricks.yml, withbundle,resources,targets,variables - Needs CLI v0.218.0 or newer
- Targets override settings per environment.
-t prodselects one - Resource types include jobs, pipelines, schemas, volumes, dashboards
Check the environment version
Serverless job environments are set by environment_version. Take the current value from a template made with databricks bundle init, not from this page.
Git Repository Structure
By the end: lay out a Git repo that is easy to test and deploy.
| Rule | Why |
|---|---|
Code in a package under src/ |
Importable, testable, buildable as a wheel |
| Notebooks thin, logic in functions | Testing without a cluster |
resources/ split per job or pipeline |
Small, reviewable files |
.gitignore for data, .databricks/, __pycache__, outputs |
Nothing sensitive in Git |
| One environment per target, not per branch | Same code everywhere |
- Branches:
main(production), short-lived feature branches, pull requests with review - Commit messages and pull requests say why
- Never commit tokens,
.envfiles or data
Same repo, any environment
A target changes where it deploys, never the code.
Local Development with VS Code
By the end: develop locally in VS Code and run against Databricks.
Databricks extension for VS Code (also works in Cursor):
- Run a local Python file on serverless compute or a cluster
- Define, deploy and run bundles from the UI
- Debug with Databricks Connect: your code runs locally, Spark runs in Databricks
- Run pytest, and sync files to the workspace
pip install databricks-connect # use the version that matches your serverless environment
from databricks.connect import DatabricksSession
spark = DatabricksSession.builder.serverless().getOrCreate()
print(spark.range(5).count())
| Where it runs | What |
|---|---|
| Your computer | Python, your IDE, tests, debugger |
| Databricks | Spark operations and data |
- Unit tests can run locally with Spark Connect, or locally without Spark at all for pure Python
dbutilsanddisplayneed small substitutes when running outside a notebook- Match the Databricks Connect version to the serverless environment version
Free Edition
Not confirmed in the docs. If it works with your token, great. If not, develop in the workspace and use Git folders (Module 26.8).
Dependency and Environment Management
By the end: manage dependencies and environments reproducibly.
| Layer | Tool |
|---|---|
| Local virtual env | python -m venv .venv, pip install -r requirements-dev.txt |
| Pinned versions | pandas==2.2.3, not pandas |
| Project metadata | pyproject.toml (name, version, dependencies) |
| Job libraries on serverless | The job environment (dependencies list) or %pip install |
| Reusable code | A built wheel (.whl) (Module 26.7) |
# serverless environment in a bundle
environments:
- environment_key: default
spec:
environment_version: "3"
dependencies:
- pandas==2.2.3
- ./dist/*.whl
- Serverless uses environment versions (not Databricks Runtime): pick one and test against it
- Environments cache packages, so restarts are quicker
- Keep dev-only tools (
pytest,ruff) in a separate requirements file - Rebuild the wheel on each release. Never edit installed packages in place
Pin everything that runs
Unpinned dependencies are the usual reason a job breaks with no code change.
Development, Test, and Production Environments
By the end: separate development, test and production.
| Way to separate | Pro | Con |
|---|---|---|
Catalogs per environment (dev, test, prod) |
Clean isolation | Needs more than Free Edition offers |
Schemas per environment (dbx_dev, dbx_prod) |
Works in one catalog | Weaker isolation |
| Workspaces per environment | Strongest | Costs and admin overhead |
- Pass the catalog and schema as variables (topic 11), never hard-coded
- Development mode (bundle
mode: development): resource names get a[dev <you>]prefix, schedules are paused - Production mode: checks that the Git branch matches, wants
run_asandpermissionsset - Production data is changed only by deployed code run as a service principal, not by people
Free Edition
One workspace and no separate catalogs. Use schemas (dbx_dev, dbx_prod) to practise the pattern.
GitHub Actions and Azure DevOps
By the end: run CI/CD with GitHub Actions (or Azure DevOps).
# .github/workflows/ci.yml
name: ci
on:
pull_request:
types: [opened, synchronize]
jobs:
test-and-deploy-dev:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11" }
- run: pip install -r requirements-dev.txt
- run: ruff check . && pytest tests/unit
- uses: databricks/setup-cli@main
- run: databricks bundle validate -t dev
env:
DATABRICKS_HOST: ${{ vars.DATABRICKS_HOST }}
DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
- run: databricks bundle deploy -t dev
env:
DATABRICKS_HOST: ${{ vars.DATABRICKS_HOST }}
DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
# .github/workflows/release.yml
on:
push:
branches: [main]
concurrency: deploy-prod # one deployment at a time
jobs:
deploy-prod:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: databricks/setup-cli@main
- run: databricks bundle deploy -t prod
env:
DATABRICKS_HOST: ${{ vars.DATABRICKS_HOST }}
DATABRICKS_TOKEN: ${{ secrets.SP_TOKEN }}
| Trigger | Action |
|---|---|
| Pull request | Lint, unit tests, deploy to dev, run checks |
Push to main |
Deploy to prod |
| Tag | Release deploy (topic 14) |
- Azure DevOps and GitLab use the same CLI commands in their own YAML
- Credentials come from repository secrets. For production use a service principal, ideally with OIDC (workload identity federation) instead of a long-lived token
- Pin the CLI action version in real projects
Never print secrets
Secrets are masked in logs only when they come from the secrets store. Don't echo them.
CI/CD Pipeline Design
By the end: design a CI/CD pipeline from commit to production.
| Stage | Goal | Fails when |
|---|---|---|
| Lint, format, types | Style and obvious bugs | Rules broken |
| Unit tests | Logic is right | A test fails |
| Build | A versioned artifact (wheel) | Build error |
| Deploy to test | Config is valid, resources create | bundle validate/deploy fails |
| Integration tests | The job really runs on test data | Job fails or outputs are wrong |
| Approve and deploy to prod | A tagged, reviewed version ships | Branch or approval rule |
- Build once, deploy the same artifact to every environment
- Fast stages first
- Make deployment repeatable and automatic: no manual edits in the workspace
- Keep a manual approval before production if your team needs it
Automated Tests and Quality Gates
By the end: enforce quality with automated gates.
| Gate | Blocks the merge when |
|---|---|
Lint and format (ruff, black) |
Style rules fail |
| Unit tests | Any test fails |
| Coverage threshold | Coverage drops below your target |
bundle validate |
Config is invalid |
| Integration tests | The test job fails |
| Pull request review | Not approved |
| Secret scanning | A credential is committed |
- Set the gates as required checks on the
mainbranch - Fast gates run on every commit. Slow ones on pull requests
- Keep gates reliable: a flaky test that people rerun until green trains people to ignore the gate
- Pre-commit hooks catch issues before they reach CI
A gate people trust
Few checks that always mean something beat many that nobody believes.
Deployment Configuration
By the end: configure each environment with variables, targets and permissions.
variables:
catalog:
description: Target catalog
default: main
schema:
description: Target schema
targets:
dev:
mode: development
default: true
variables:
schema: dbx_dev
prod:
mode: production
variables:
schema: dbx_prod
run_as:
service_principal_name: <application-id>
permissions:
- group_name: data-engineers
level: CAN_VIEW
resources:
jobs:
daily_orders:
name: daily-orders
parameters:
- name: catalog
default: ${var.catalog}
- name: schema
default: ${var.schema}
| Syntax | Meaning |
|---|---|
${var.schema} |
A bundle variable |
${bundle.target}, ${bundle.name} |
Current target and bundle name |
${workspace.current_user.userName} |
The deploying user |
--var="schema=x" or BUNDLE_VAR_schema |
Override at the command line |
- Variables resolve at deploy time. For per-run values use job parameters (Module 10.7)
- Check the resolved config:
databricks bundle validate --output json - Production: set
run_as(a service principal) andpermissionsexplicitly - Deploy production to a read-only folder so people can't edit deployed code
Infrastructure as Code with Terraform
By the end: recognise where Terraform fits next to bundles.
terraform {
required_providers { databricks = { source = "databricks/databricks" } }
}
resource "databricks_job" "daily" {
name = "daily-orders"
# ...
}
- Terraform describes infrastructure as code with the Databricks provider: state, plan, apply
- Typical split: Terraform creates the platform (workspaces, catalogs, permissions, service principals). Bundles deploy each project's jobs and pipelines
- Don't manage the same resource with both
- Keep Terraform state in a remote backend, with locking
Not hands-on
Free Edition has no account console, so there's nothing platform-level to manage. Read this as reference.
Secrets and Environment Variables
By the end: keep secrets and settings out of code.
| Kind | Where it lives |
|---|---|
| Secrets (tokens, passwords, keys) | Databricks secret scope, CI secret store |
| Per-environment settings (catalog, paths) | Bundle variables, job parameters |
| Non-secret defaults | Config file in the repo |
| Cloud access | Unity Catalog credentials, not keys in code |
databricks secrets create-scope my_app
databricks secrets put-secret my_app api_key --string-value "<value>"
api_key = dbutils.secrets.get(scope="my_app", key="api_key")
print(api_key) # shows [REDACTED]
- Reading a secret in a notebook or job shows
[REDACTED]in output. Only literal values are masked: a transformed secret can leak - Give each scope an ACL: grant
READto the groups or service principals that need it - Name scopes after an application or role, not a person
- Environment variables (
DATABRICKS_HOST,DATABRICKS_TOKEN) are for tools and CI; pull app secrets from scopes - Rotate secrets. Tokens should be short-lived
A committed secret is a leaked secret
Remove it from Git history and rotate it. Deleting the file in a later commit doesn't help.
Release Management and Rollbacks
By the end: release with tags and roll back safely.
git tag -a v1.4.0 -m "Release 1.4.0"
git push origin v1.4.0
| Problem | Roll back by |
|---|---|
| Bad code release | Redeploy the previous tag: git checkout v1.3.2 && databricks bundle deploy -t prod |
| Bad config | Revert the commit, redeploy |
| Bad data written | Delta time travel and RESTORE (Module 5.10), then rerun after the fix |
| Failed run | Repair run (Module 10.12) |
- Use semantic versions (
1.4.0): breaking, feature, fix - Keep a short changelog
- Rolling back code doesn't undo data it already wrote: plan for both
- Deploy often and in small steps: small releases are easy to revert
- Make jobs idempotent so a re-run after a rollback gives correct tables
Practise a rollback
Do one on purpose in dev. The first time shouldn't be during an incident.
What you can do now
- CLI, SDK and bundles
- Repo structure, local development, dependencies
- Environments, CI/CD design and quality gates
- Deployment config, Terraform's role, secrets
- Releases and rollbacks