Define your Automation Bundle Resources and Setup Job
Estimated read: 13 min
Lecture 1 previewed the pattern with one pipeline. This lecture rewrites all four resource files in full – bronze pipeline, silver-gold pipeline, orchestration job, dashboard – and adds one new resource none of the previous seven sections needed: an environment bootstrap job that creates the catalog, schema, and volumes a brand-new target starts without.
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
resources/pipeline_silver_gold.yml
resources:
pipelines:
silver_gold:
name: steprightproject-silver-gold
catalog: ${var.catalog}
schema: ${var.schema}
serverless: true
continuous: false
libraries:
- glob:
include:
- transformations/silver_*.py
- transformations/gold_*.py
- transformations/gold_*.sql
Both pipeline resources carry the exact glob scoping Section 5, Lecture 1
designed when it split the original combined pipeline in two – nothing about that design changes
here, only where the definition lives and how it picks up catalog/schema per target.
resources/orchestration_job.yml, with real cross-resource references
resources:
jobs:
stepright_daily_pipeline:
name: "StepRight Daily Pipeline"
schedule:
quartz_cron_expression: "0 0 3 * * ?"
timezone_id: "America/New_York"
email_notifications:
on_failure: ["${var.notification_email}"]
parameters:
- name: run_date
default: ""
tasks:
- task_key: run_ingestion
pipeline_task:
pipeline_id: ${resources.pipelines.bronze_cdc.id}
- task_key: dq_check
depends_on: [{task_key: run_ingestion}]
python_wheel_task:
package_name: "steprightproject"
entry_point: "dq_check"
parameters: ["--run_date", ""]
- task_key: run_transformation
depends_on: [{task_key: dq_check}]
run_if: ALL_SUCCESS
pipeline_task:
pipeline_id: ${resources.pipelines.silver_gold.id}
- task_key: report
depends_on: [{task_key: run_transformation}]
python_wheel_task:
package_name: "steprightproject"
entry_point: "report"
parameters: ["--run_date", ""]
The only real change from Section 5, Lecture 4’s
version: ${var.bronze_pipeline_id} and ${var.silver_gold_pipeline_id} become
${resources.pipelines.bronze_cdc.id} and ${resources.pipelines.silver_gold.id} now that both
pipelines are bundle-managed resources in the same databricks.yml, sitting right alongside this
job – a direct reference beats a variable once the thing being referenced lives in the same
bundle, since the bundle framework resolves it automatically rather than requiring a human to keep
a variable’s value in sync with whatever pipeline ID actually got created.
Testing bootstrap idempotency, deliberately, before trusting it in CI
Before Lecture 6 ever calls environment_bootstrap automatically, run it twice by hand against
dev and confirm the second run produces no errors and no duplicate objects:
databricks bundle run environment_bootstrap --target dev
databricks bundle run environment_bootstrap --target dev # should succeed identically
This is the same “test the thing that’s supposed to almost never fail” discipline Section 5,
Lecture 4
applied to testing dq_check’s gate on purpose – an idempotent job that’s never actually been run
twice is only idempotent in theory, and CI is the wrong place to discover otherwise for the first
time.
Why a direct resource reference beats a variable, restated
It’s worth dwelling on why ${resources.pipelines.bronze_cdc.id} is strictly better than
${var.bronze_pipeline_id} here, not just different. A variable requires a human to know the
correct pipeline ID and keep it current in every target’s variables block – three separate
values to maintain, three separate places a typo or a stale ID can quietly break a job task without
any validation catching it until the job actually tries to run. A direct resource reference has no
such failure mode: the bundle framework deploys bronze_cdc first, learns its actual ID from the
API response, and substitutes that ID into orchestration_job.yml automatically, every time,
regardless of target. The variable version made sense back in Section 5 only because no bundle
existed yet to resolve a direct reference against – once one does, direct references are the
correct default for any resource depending on another resource in the same bundle.
Why the bootstrap job is separate from the daily pipeline job
environment_bootstrap could theoretically live as a fifth task on stepright_daily_pipeline,
gated to run only once – but bundling a rarely-run, environment-setup concern into a job that
otherwise runs every single night at 3 AM would mean every ordinary daily run at least evaluates
whether bootstrap needs to happen, for no benefit on any of the 364 days a year it doesn’t. Keeping
it a fully separate job, with no schedule of its own, means the daily pipeline’s DAG stays exactly
the four tasks Section 5 designed, and bootstrap runs exactly when someone (or Lecture 6’s CI/CD
pipeline) explicitly decides a new environment needs standing up.
resources/dq_dashboard.yml
resources:
dashboards:
dq_monitoring:
display_name: "StepRight Data Quality Monitoring"
file_path: resources/dq_dashboard.lvdash.json
warehouse_id: ${var.sql_warehouse_id}
The .lvdash.json file Section 7, Lecture 4
exported sits alongside this thin wrapper, which is what actually makes the dashboard a bundle
resource – deployed, versioned, and promoted across targets the same way every pipeline and job
here is, rather than a one-off export living outside the bundle’s reach.
The new resource: environment bootstrap
uat and prod don’t have a catalog, schema, or landing volume yet – Section 1, Lecture 3’s
CREATE CATALOG/CREATE SCHEMA/CREATE VOLUME statements only ever ran once, by hand, against
dev. A bootstrap job runs the same statements, parameterized, against whichever target actually
needs them:
# resources/environment_bootstrap_job.yml
resources:
jobs:
environment_bootstrap:
name: "StepRight Environment Bootstrap"
tasks:
- task_key: bootstrap
notebook_task:
notebook_path: ./notebooks/bootstrap_environment
base_parameters:
catalog: ${var.catalog}
schema: ${var.schema}
# notebooks/bootstrap_environment
catalog = dbutils.widgets.get("catalog")
schema = dbutils.widgets.get("schema")
spark.sql(f"CREATE CATALOG IF NOT EXISTS {catalog} COMMENT 'StepRight {catalog} catalog'")
spark.sql(f"CREATE SCHEMA IF NOT EXISTS {catalog}.{schema} COMMENT 'StepRight bronze, silver, and gold tables'")
spark.sql(f"CREATE VOLUME IF NOT EXISTS {catalog}.{schema}.landing COMMENT 'Governed landing zone'")
spark.sql(f"CREATE VOLUME IF NOT EXISTS {catalog}.{schema}.staging COMMENT 'Test data staging'")
for source in ["products", "inventory", "clickstream", "fulfillment"]:
dbutils.fs.put(f"/Volumes/{catalog}/{schema}/landing/{source}/.gitkeep", "", overwrite=True)
IF NOT EXISTS on every statement is what makes this genuinely safe to run repeatedly – against
dev, where the catalog and schema already exist, it’s a harmless no-op; against a fresh uat or
prod, it’s exactly what stands the environment up for the first time. This job runs on demand,
not on Section 5’s daily schedule – it has no schedule block at all, triggered manually or as
one explicit step in Lecture 6’s CI/CD pipeline the first time a new target gets deployed.
databricks.yml, updated with the new variable
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
sql_warehouse_id:
description: "SQL warehouse the dashboard queries run against"
sql_warehouse_id has no default – unlike schema, a SQL warehouse ID is genuinely different per
workspace and per target, with no sensible shared value, so each target’s variables block must
supply its own rather than inheriting a default that would be wrong somewhere.
Why the dashboard resource needs a warehouse ID and the pipelines don’t
A Lakeflow Declarative Pipeline provisions its own serverless compute per Lecture 1 and Section
2’s serverless: true setting – nothing external to reference. A dashboard has no compute of its
own; every visualization runs its underlying SQL against a SQL warehouse, an existing piece of
infrastructure the dashboard resource has to be told about explicitly via warehouse_id. That
warehouse isn’t something this bundle creates – it’s assumed to already exist in each target
workspace, the same way the workspace itself and its host URL are assumed to already exist rather
than being something databricks.yml provisions from nothing.
What didn’t need to change
Nothing in transformations/, tests/, or dq_helpers.py changes in this lecture – every
resource file here wraps existing, already-verified pipeline and job logic in bundle syntax without
touching what any of it actually computes. That’s worth noting explicitly: packaging a project for
deployment is infrastructure work, and infrastructure work that silently changes business logic
along the way is exactly the kind of scope creep Section 6, Lecture 2 warned against when
refactoring gold_daily_revenue – extract or repackage, verify identical behavior, and treat any
actual logic change as a separate, deliberate step.
The final shape of resources/
resources/
├── pipeline_bronze.yml
├── pipeline_silver_gold.yml
├── orchestration_job.yml
├── environment_bootstrap_job.yml
├── dq_dashboard.yml
└── dq_dashboard.lvdash.json
Six files, include: [resources/*.yml] picking up every .yml among them automatically – the
.lvdash.json itself isn’t matched by that glob and doesn’t need to be; it’s referenced by
file_path from dq_dashboard.yml, not included as a bundle resource definition in its own right.
Common mistakes
- Forgetting
IF NOT EXISTSon any bootstrap statement. Without it, a second run ofenvironment_bootstrapagainst an environment that already exists fails outright instead of safely no-op-ing – exactly the idempotency property that makes this job safe to include as a routine step in Lecture 6’s CI/CD pipeline rather than a manual, one-time-only ritual. - Referencing
${resources.pipelines.bronze_cdc.id}from a resource file that doesn’t declarebronze_cdcand isn’t in the same bundle. Cross-resource references only resolve within onedatabricks.yml’s combined set of included files – this works precisely becauseinclude: [resources/*.yml]pulls every file in this lecture into one bundle, not because the reference syntax reaches across bundles.
What’s next
Every resource this project has built across eight sections now lives in resources/, bundle-managed,
environment-aware. Lecture 3 actually deploys to dev for the first time – including the one real
wrinkle every one of these pipelines and the job already existing in dev, created by hand back in
Sections 2 and 5, creates for a first bundle deploy.
| ← Previous: Package your project using Declarative Automation Bundle | Next: Test Your Deployment Bundle in Dev Environment → | |:—|—:|