Package your project using Declarative Automation Bundle
Estimated read: 18 min
Seven sections in, resources/ holds four things: Section 2’s bronze pipeline.yml, Section 5’s
silver-gold pipeline definition, Section 5’s orchestration_job.yml, and Section 7’s
dq_dashboard.lvdash.json. Every one of them was created by hand – databricks pipelines create,
databricks jobs run-now, a UI click-through – against a single dev catalog, with no concept of
uat or prod anywhere in the project. This lecture introduces Databricks Asset Bundles (the
official name for what the manifest calls a “Declarative Automation Bundle”) – the mechanism that
turns those four hand-deployed pieces into one versioned, environment-aware deployable unit.
What a bundle actually is
A Databricks Asset Bundle is infrastructure-as-code for Databricks: a databricks.yml root
manifest declaring every resource a project owns – pipelines, jobs, dashboards – plus one or more
deployment targets, each mapping the same resource definitions onto a different workspace,
catalog, and set of overrides. databricks bundle deploy --target dev and databricks bundle
deploy --target uat run the exact same YAML through two different lenses, rather than two
independently maintained sets of pipeline and job configuration that inevitably drift apart from
each other over time.
This is the direct Databricks-native answer to a problem every legacy ETL team already knows by a
different name: a Talend job promoted from dev to UAT to prod by manually re-pointing connection
strings and re-exporting .zip files at each stage, with no guarantee the version that reached
prod matches what was actually tested in UAT. A bundle’s databricks.yml, checked into
steprightproject’s Git history alongside every transformation file this course has built, is the
single source of truth for what “the StepRight pipeline” means in any environment – diffable,
reviewable, and revertible the same way every other file in this repo already is.
Why now, and not from Section 2
Building pipeline.yml and orchestration_job.yml by hand first, then packaging them here, wasn’t
an oversight – it mirrors how a real legacy-to-Databricks migration often actually proceeds: prove
a pipeline works, prove the job orchestrates it correctly, prove the dashboard reads the right
data, and only then invest in the infrastructure-as-code layer that makes deploying all of it
repeatable across environments. Introducing a bundle before any of Sections 2 through 7 existed
would have meant learning Asset Bundle syntax and Lakeflow pipeline design at the same time, with
no working pipeline yet to anchor either one against.
Anatomy of databricks.yml
# databricks.yml
bundle:
name: steprightproject
include:
- resources/*.yml
variables:
catalog:
description: "Unity Catalog catalog for this target"
schema:
description: "Schema within the catalog"
default: step_right
notification_email:
description: "Email address for job failure notifications"
default: data-eng-team@company.com
targets:
dev:
mode: development
default: true
workspace:
host: https://your-workspace.cloud.databricks.com
variables:
catalog: dev
uat:
mode: production
workspace:
host: https://your-workspace.cloud.databricks.com
variables:
catalog: uat
run_as:
service_principal_name: stepright-uat-sp
prod:
mode: production
workspace:
host: https://your-workspace.cloud.databricks.com
variables:
catalog: prod
run_as:
service_principal_name: stepright-prod-sp
Four top-level blocks, each doing one job: bundle names the project; include tells the CLI
where to find resource definitions (every .yml file already sitting in resources/, picked up
automatically, the same glob-include instinct Section 2’s pipeline.yml used for its own
transformation files); variables declares the knobs every target can override; targets is
where dev, uat, and prod each supply their own values for those knobs.
What databricks bundle deploy actually does
Running deploy isn’t a metaphor for “make the workspace match the YAML” – it’s a concrete
sequence: the CLI uploads every file the bundle references (transformation code, notebooks, the
resource definitions themselves) into a workspace file path under
/Workspace/Users/<deploying-identity>/.bundle/steprightproject/<target>/files, then calls the
Databricks REST API to create or update each declared resource (a pipeline, a job, a dashboard) so
it points at that uploaded code. A second deploy against the same target is idempotent – it
diffs the current deployed state against the YAML and only changes what actually differs, the same
way terraform apply or git push only moves what changed, not a full teardown-and-recreate every
time.
One resource file per concern, not one giant databricks.yml
include: [resources/*.yml] is what makes it possible to keep resources/pipeline_bronze.yml,
resources/pipeline_silver_gold.yml, resources/orchestration_job.yml, and
resources/dq_dashboard.yml as separate files rather than pasting every resource definition into
one sprawling databricks.yml. This mirrors the same one-file-per-concern discipline
transformations/ already follows – gold_reporting.py and silver_files.py stay separate files
for the same reason a pipeline resource and a job resource stay in separate YAML files: each is
independently readable, and a diff touching only the job doesn’t show up as noise in a pipeline
file’s review.
${var.X} substitution, beyond catalog
Bundle variable substitution isn’t limited to catalog names – any string value in any resource
file under resources/ can reference ${var.<name>}, ${bundle.target} (the active target’s
name, useful for a resource’s display name), or ${workspace.current_user.userName} (useful in
mode: development contexts). A few examples worth previewing before Lecture 2 rewrites every
resource file in full:
# A job's notification target, driven by the notification_email variable
email_notifications:
on_failure: ["${var.notification_email}"]
# A schema reference built from two variables at once
catalog: ${var.catalog}
schema: ${var.schema}
# A resource name that shows which target deployed it, useful where mode: development's
# automatic per-user prefixing doesn't apply (uat and prod both run in mode: production)
name: "stepright_daily_pipeline_${bundle.target}"
Every one of these resolves at deploy time, per target – the exact same resource file produces
catalog: uat when deployed to uat and catalog: prod when deployed to prod, with zero
hand-editing between the two.
Targets can point at the same workspace or different ones
Nothing about a bundle target requires a separate Databricks workspace per environment – StepRight
keeps all three targets pointed at the same workspace.host in the example above, relying on
Unity Catalog’s dev/uat/prod catalogs for isolation rather than separate workspaces
entirely. A larger organization with stricter network or compliance boundaries between
environments would instead give each target its own workspace.host, pointing at genuinely
separate workspaces – the YAML shape is identical either way; only the host value under each
target’s workspace block changes. StepRight’s single-workspace, catalog-isolated approach is the
simpler, and for a project this size, entirely sufficient choice.
Guarding against an accidental prod deploy
databricks bundle deploy with no --target flag deploys to whichever target is marked
default: true – dev here, deliberately, so a bare deploy command run out of habit can never
accidentally reach prod. Reaching uat or prod always requires spelling out
--target uat or --target prod explicitly, a small but real safeguard against muscle memory
causing a production deploy nobody intended to run.
catalog and schema as bundle variables
schema stays step_right in every target – Section 1, Lecture 3 already established that the
schema, not the catalog, is what isolates this project from everything else in the workspace.
catalog, by contrast, is exactly what changes: dev for the target used throughout Sections 2
through 7, uat and prod as two catalogs that don’t exist yet anywhere in the workspace.
Deploying to a new target for the first time is what actually creates them – Lecture 2 covers the
bootstrap resource that runs the CREATE CATALOG/CREATE SCHEMA/CREATE VOLUME statements from
Section 1, Lecture 3 against whichever catalog name the active target supplies.
mode: development vs. mode: production
mode: development (dev) |
mode: production (uat, prod) |
|
|---|---|---|
| Resource naming | Prefixed with the deploying user’s identity ([dev alice] stepright_daily_pipeline) |
Exact name, no prefix |
| Pipeline development setting | development: true by default – faster iteration, no retries on failure |
development: false – full retry and monitoring behavior |
| Concurrent deploys from different developers | Expected and safe – each developer’s prefix keeps their deployed resources separate | Not expected – one canonical deployment per target |
Typical run_as |
The deploying user | A service principal, not a personal identity |
mode: development’s automatic per-user prefixing is what lets multiple engineers each run
databricks bundle deploy --target dev against the same workspace without colliding – a second
developer’s dev deploy creates their own separately-named pipeline and job, not a conflicting
redeploy of the first developer’s resources. uat and prod deliberately opt out of that
behavior: there should be exactly one stepright_daily_pipeline in each of those catalogs, owned
by a service principal, not by whichever engineer happened to run the deploy command last.
Why run_as matters for uat and prod
A job or pipeline deployed under a personal identity breaks the moment that person leaves the team,
changes roles, or simply has their laptop credentials expire – a service principal
(stepright-uat-sp, stepright-prod-sp) is a non-human identity that owns the deployed resources
instead, the same reasoning behind using a service principal for any production automation rather
than a person’s own login. dev skips this deliberately – individual engineer identity is exactly
what mode: development wants for its per-user resource prefixing to make sense in the first
place.
A preview: one resource file, before and after
Lecture 2 rewrites all four resource files in full; one worked example here makes the pattern
concrete. Section 2, Lecture 2 left pipeline.yml with catalog and schema hardcoded:
# Before -- Section 2, Lecture 2
name: steprightproject-bronze-cdc
catalog: dev
schema: step_right
serverless: true
continuous: false
libraries:
- glob:
include: transformations/bronze_*.py
As a bundle resource, the same pipeline becomes:
# resources/pipeline_bronze.yml
resources:
pipelines:
bronze_cdc:
name: steprightproject-bronze-cdc
catalog: ${var.catalog}
schema: ${var.schema}
serverless: true
continuous: false
libraries:
- glob:
include: transformations/bronze_*.py
Two changes, both small: catalog: dev and schema: step_right become ${var.catalog} and
${var.schema}, and the whole definition nests one level deeper, under resources: pipelines:
bronze_cdc: – that nesting is what gives this pipeline a resource key (bronze_cdc), the
name other resources in the same bundle use to reference it directly, which matters the moment
orchestration_job.yml needs this pipeline’s ID.
Referencing one bundle resource from another
# resources/orchestration_job.yml (excerpt)
tasks:
- task_key: run_ingestion
pipeline_task:
pipeline_id: ${resources.pipelines.bronze_cdc.id}
${resources.pipelines.bronze_cdc.id} resolves to whatever pipeline ID the bundle actually created
for that resource key, in that target – the bundle framework tracks this automatically, so the job
resource never needs to know or hardcode an actual pipeline ID at all. This is the more idiomatic
alternative to the ${var.bronze_pipeline_id} placeholder Section 5, Lecture 4
used before a bundle existed to resolve it against – Lecture 2 makes this exact substitution for
real.
Installing and authenticating the CLI
databricks --version # v0.230.0 or newer required for full bundle support
databricks auth login --host https://your-workspace.cloud.databricks.com
The Databricks CLI is what interprets databricks.yml and turns it into actual API calls against a
workspace – databricks bundle deploy, validate, run, and destroy are the four commands
this section leans on most, covered as Lecture 3 actually deploys for the first time.
Validating before deploying anything
databricks bundle validate --target dev
validate parses databricks.yml and every included resource file, checks for schema errors and
unresolved variable references, and reports them without touching the workspace at all – the
bundle equivalent of a syntax check, worth running after every edit to any file under resources/
or to databricks.yml itself, well before ever running deploy.
Inspecting and rolling back a deployment
databricks bundle summary --target dev # what's currently deployed, and where
databricks bundle open --target dev # opens the deployed job/pipeline in the browser
databricks bundle destroy --target dev # tears down every resource this bundle deployed
summary is the fastest way to answer “what does dev actually have deployed right now” without
hunting through the Jobs and Pipelines UI by hand. destroy is the deliberate inverse of deploy
– it removes every resource the bundle created in that target, which matters for dev specifically:
a developer’s per-user-prefixed dev resources are meant to be disposable, torn down and
redeployed freely while iterating, in a way uat and prod resources never should be.
Secrets never belong in databricks.yml
Nothing this lecture’s databricks.yml example needs is actually a secret – catalog names, an
email address, a service principal’s name are all fine to commit in plain text. Credentials
themselves (a service principal’s client secret, an API token used by CI) are a different matter,
and belong in a secrets manager or CI provider’s own secret store, injected as an environment
variable at deploy time rather than written into any file this Git folder tracks. Lecture 6’s
CI/CD pipeline covers exactly where those credentials actually live and how they reach the
databricks bundle deploy command without ever touching version control.
What existed before this lecture, and what changes now
| Before (Sections 2-7) | After (this section) | |
|---|---|---|
| How a pipeline gets created | databricks pipelines create --json @pipeline.yml, by hand |
databricks bundle deploy, one command for every resource |
| Environment awareness | None – everything hardcoded to dev.step_right |
dev, uat, prod targets, one shared resource definition |
| Who owns deployed resources | Whoever ran the CLI command | A service principal, in uat and prod |
| Change history | Whatever the Git folder’s commit log shows for the YAML files themselves | The same, plus a reviewable diff of what actually gets deployed on every change |
Nothing about the pipelines, the job’s DAG, or the dashboard’s queries changes here – this lecture packages what already works, it doesn’t redesign it.
Why this matters far beyond one capstone project
A promotion path this clean – one shared resource definition, three targets, a service principal
owning anything beyond dev – is exactly the piece a legacy-to-Databricks migration needs once
pipelines move past a single engineer’s proof of concept. A migration project with dozens of
translated ETL jobs, each needing its own dev-to-prod promotion story, either builds this pattern
once and reuses it everywhere, or reinvents ad hoc promotion scripts per pipeline – the same
“many small consistent pieces over one clever combined solution” instinct this course has applied
repeatedly, now applied at the level of an entire project’s deployment story rather than one
function or one view.
Common mistakes
- Hardcoding
dev.step_rightinside a resource YAML file instead of referencing thecatalogvariable. A pipeline definition withcatalog: devwritten literally, rather thancatalog: ${var.catalog}, silently ignores whichever target actually gets deployed – Lecture 2 covers exactly this substitution for every resource fileresources/holds. - Skipping
databricks bundle validateand going straight todeploy. A YAML syntax error or an unresolved variable reference is far cheaper to catch locally, in seconds, than mid-deploy against a real workspace. - Running
databricks bundle destroyagainstuatorprodout of habit fromdeviteration.destroyremoves every resource the bundle owns in that target – muscle memory built up tearing down and redeploying disposabledevresources is exactly the habit that makes this command dangerous the one time--targetgets typed without thinking in a production context.
What’s next
Lecture 2 rewrites Section 2, Section 5, and Section 7’s resource files to use ${var.catalog}
and ${var.schema} throughout, adds the environment bootstrap job this lecture referenced, and
reconciles Section 5’s ${var.bronze_pipeline_id} placeholder with how bundle-managed pipelines
actually reference each other.
| ← Previous: StepRight - Integration Testing, Packaging and Deployment | Next: Define your Automation Bundle Resources and Setup Job → | |:—|—:|