Runbook: Databricks Environment Audit (Read-Only)
Owner: Jonathan Moss | Frequency: As Needed (new-environment onboarding); re-run quarterly Last Updated: 2026-06-23 | Last Run: —
Generic, reusable across any Databricks tenant (Azure / AWS / GCP). Every command is
list/get/query/SELECT— read-only, nothing mutates state. The only non-read action is authentication (a prerequisite, not a change to the environment).
Executive Summary — Pillar Scorecard#
Fill this in after running the audit. It's the one-page roll-up for stakeholders; everything below it is the working detail. Keep it to this page. Replace every italic placeholder.
Environment: tenant / workspaces audited Date: YYYY-MM-DD Auditor: name Coverage: phases run (A–K) · environments covered · what was skipped and why
Bottom line: 2–3 sentences — overall posture and the single most important thing to fix.
Scorecard (RAG by Well-Architected pillar)#
| Pillar | Status | Findings (H/M/L) | Headline issue |
|---|---|---|---|
| Operational excellence | 🟢/🟡/🔴 | 0/0/0 | e.g. 40% of jobs hand-created, not IaC |
| Security, privacy & compliance | 🟢/🟡/🔴 | 0/0/0 | e.g. app runs as shared SP, no per-user auth |
| Reliability | 🟢/🟡/🔴 | 0/0/0 | e.g. 6 prod jobs have no failure alert |
| Performance efficiency | 🟢/🟡/🔴 | 0/0/0 | e.g. 70% of DBUs still on classic compute |
| Cost optimization | 🟢/🟡/🔴 | 0/0/0 | e.g. 3 clusters with no auto-termination |
| Data & AI governance | 🟢/🟡/🔴 | 0/0/0 | e.g. hive_metastore still in active use |
| Interoperability & usability | 🟢/🟡/🔴 | 0/0/0 | e.g. apps lack error/empty states |
🟢 no material issues · 🟡 issues to address · 🔴 urgent / business risk
Top 5 findings (severity-ranked)#
| # | Severity | Pillar | Finding (resource ID) | Impact | Fix | Effort |
|---|---|---|---|---|---|---|
| 1 | High | Cost | job 12345 on all-purpose cluster | ~2–4× cost | move to job cluster | S |
Quick wins vs. strategic#
- Quick wins (≤1 day): e.g. enable auto-termination; add failure webhooks; stop abandoned apps
- Strategic (needs a project): e.g. migrate hive_metastore → UC; adopt DABs for all resources
Coverage gaps / caveats#
- Products not present (phases skipped): …
- Phases blocked by missing permissions or disabled system tables: …
Purpose#
Map and assess an unfamiliar Databricks tenant across the full platform surface — inventory, cost/waste, jobs, pipelines, deployment hygiene, serverless opportunities, model serving, vector search, apps, and Lakebase — and produce an actionable findings list. Built so someone unfamiliar with the environment can run it top-to-bottom and end up with a defensible picture of "what's here and what's hurting."
Best-Practice Anchor — Databricks Well-Architected Lakehouse#
Every check below maps to a pillar of the official Databricks Well-Architected Lakehouse
Framework (learn.microsoft.com/azure/databricks/lakehouse-architecture/well-architected), so
findings are grounded in Databricks' own guidance rather than opinion. The seven pillars:
| Pillar | What the audit checks for it |
|---|---|
| Operational excellence | IaC/DABs coverage, drift, alerting, monitoring, capacity/quota limits |
| Security, privacy & compliance | auth, least-privilege grants, secret handling, network exposure |
| Reliability | failure rates, retries, timeouts, HA, sync freshness, health states |
| Performance efficiency | serverless adoption (the pillar's #1 principle), Photon, right-sizing |
| Cost optimization | idle/always-on compute, scale-to-zero, tagging/chargeback, tier choice |
| Data & AI governance | UC migration, model versioning, lineage, grants |
| Interoperability & usability | app UX states, discoverability (BROWSE) |
Prerequisites#
- [ ] Databricks CLI ≥ v0.292.0 (
databricks --version) - [ ] An authenticated profile (
databricks auth profilesshowsValid: YES) - [ ] Ideally account-admin read + workspace-admin read. Partial access still works — note gaps.
- [ ] Unity Catalog system tables enabled (
system.billing,system.compute,system.lakeflow). If disabled, the SQL steps fall back to slower per-resource CLI loops (noted inline).
Conventions#
- Replace
$Pwith the profile under audit;$ACCTwith the account-console profile. - Run the whole runbook once per environment (dev / test / stage / prod) and once for the account.
- Names are literal. Never normalize hyphens to underscores. Backtick-quote any name part with a
hyphen in SQL:
`my-catalog`.schema.table. - ⚠️ Unity Catalog commands take POSITIONAL args, not flags.
schemas list <CATALOG>—schemas list --catalog-name Xdoes not exist and will fail. - Capture resource IDs with every finding so it stays actionable later.
Permissions Required (per phase)#
Databricks access spans four independent permission planes: account roles, workspace object
ACLs, Unity Catalog privileges, and system-table grants. This audit reads only metadata and
system tables — never business data — so SELECT on user tables is never required.
| Phase | What you read | Least-privilege grant | Simplest blanket role |
|---|---|---|---|
| 0 Auth | your identity | workspace user (can log in) | — |
| A1 Account inventory | workspaces, metastores, account users/groups | (no granular read role exists) | Account admin |
| A2 Workspace resources | clusters, jobs, pipelines, warehouses, pools, policies, serving | CAN_VIEW per object |
Workspace admin (per workspace) |
| A3 Catalogs/schemas/tables | UC metadata | BROWSE on catalogs (no USE/SELECT needed) |
Metastore admin |
| A3 External locations / storage creds | UC storage objects | BROWSE on each external location (owner for storage creds) |
Metastore admin |
| B Cost | system.billing, system.compute |
USE CATALOG on system + USE SCHEMA + SELECT on those schemas |
account admin and metastore admin |
| B/C Run the SQL | execute queries | CAN_USE on a SQL warehouse |
— |
| C1/C2 Job failures & durations | system.lakeflow |
USE CATALOG+USE SCHEMA+SELECT on system.lakeflow |
account admin and metastore admin |
| C3 Per-job config | jobs get |
CAN_VIEW per job |
Workspace admin |
| D1/D2 Pipeline config & events | pipelines get, events |
CAN_VIEW per pipeline |
Workspace admin |
| D3 Pipeline source code | notebooks/files in workspace | CAN_READ on those files/folders |
Workspace admin |
| E Synthesis | — | (covered by the above) | — |
Hard preconditions for the system-table phases (B, C):
- An account admin must have enabled the relevant schemas (
system.billing,system.compute,system.lakeflow, andsystem.accessfor audit logs). They're not all on by default. - The metastore must be on Unity Catalog Privilege Model v1.0, and you must query from a UC-enabled workspace.
- System tables only contain data for workspaces in the same cloud region. To audit a workspace in another region, run Phases B/C from a workspace deployed in that region.
Recommended "auditor" grant bundle (read-only, least privilege):
- Workspace admin (read) in each environment workspace — the practical way to
CAN_VIEWevery job/pipeline/cluster/notebook at once (A2, C3, D). USE CATALOGonsystem+USE SCHEMA+SELECTonsystem.billing,system.compute,system.lakeflow,system.access(B, C).CAN_USEon one SQL warehouse (to run the queries).BROWSEon all catalogs — or metastore admin if you also need external locations / storage credentials (A3).- Account admin — only required for Phase A1 (account-console inventory). This is the one genuinely high-privilege ask; if it's denied, drop A1 and audit per-workspace.
Grant to a group, not your user. Per UC best practice (and consistent with this audit's own findings), assign the bundle to an
auditorsgroup. Whoever grants it must themselves be both an account admin and a metastore admin.
Procedure#
Step 0 — Authenticate (prerequisite, not a mutation)#
databricks auth login --host <WORKSPACE_URL> --profile $P
databricks current-user me --profile $P
Expected: OAuth flow completes; current-user me returns your identity JSON.
If it fails: configuration does not support OAuth tokens → re-run the login to convert a
PAT/azure-cli profile. On Azure with auth_type = azure-cli, run az login --tenant <TENANT_ID>
first — the Databricks profile borrows that identity.
PHASE A — Inventory ("what exists")#
A1 — Account-level (run with $ACCT)#
databricks account workspaces list --profile $ACCT
databricks account metastores list --profile $ACCT
databricks account groups list --profile $ACCT
databricks account users list --profile $ACCT
Look for: more metastores than regions; users granted directly instead of via groups; orphaned
or empty workspaces; duplicate workspace configs.
If it fails: subcommand names vary by CLI build — confirm with databricks account --help. No
account-admin → skip A1, audit per-workspace only and flag the coverage gap.
A2 — Per-workspace resources (run for each environment)#
databricks clusters list --profile $P
databricks warehouses list --profile $P
databricks instance-pools list --profile $P
databricks cluster-policies list --profile $P
databricks jobs list --profile $P
databricks pipelines list --profile $P
databricks serving-endpoints list --profile $P
Expected: one block per resource type; capture counts + IDs.
If it fails: databricks sql-warehouses list does not exist — use warehouses list.
A3 — Unity Catalog (governance)#
databricks catalogs list --profile $P
databricks schemas list <CATALOG> --profile $P
databricks tables list <CATALOG> <SCHEMA> --profile $P
databricks external-locations list --profile $P
databricks storage-credentials list --profile $P
Look for: a hive_metastore catalog still in active use (un-migrated legacy data — major
governance gap); external locations with broad access; storage credentials shared too widely.
PHASE B — Cost / Waste#
Prefer system tables — one query beats iterating the CLI over every resource. Get the warehouse first:
databricks experimental aitools tools get-default-warehouse --profile $P
B1 — Top DBU spend by SKU, last 30 days#
databricks experimental aitools tools query "SELECT sku_name, usage_unit, ROUND(SUM(usage_quantity),1) AS dbus FROM system.billing.usage WHERE usage_date >= DATE_SUB(CURRENT_DATE,30) GROUP BY 1,2 ORDER BY dbus DESC LIMIT 20" --profile $P
This is the "what's going on" headline. TABLE_OR_VIEW_NOT_FOUND → billing schema not enabled;
flag "enable system tables" as a finding and use B3.
B2 — Idle / always-on clusters (classic leak)#
databricks experimental aitools tools query "SELECT cluster_name, auto_termination_minutes, worker_node_type, num_workers FROM system.compute.clusters WHERE delete_time IS NULL ORDER BY auto_termination_minutes DESC" --profile $P
Red flag: auto_termination_minutes = 0 or NULL → burning DBUs 24/7. Top remediation target.
B3 — Fallback: per-cluster config sweep (no system tables)#
databricks clusters list --profile $P --output json
Scan for autotermination_minutes (0/missing), spark_version (outdated runtime = security + perf
debt), oversized node_type_id.
Active incident triage (first 15 minutes)#
Use this path before you restart a failed job or pipeline. Preserve evidence first.
- Record the UTC time, workspace, workload name, resource ID, and the person who reported it.
- Record the last successful run, the failed run ID, and the exact failure time.
- Capture the current job or pipeline configuration and note changes since the last success.
- Capture the exact error before you retry:
- Jobs:
databricks jobs list-runs --job-id <ID> --limit 10 --profile $P --output json. - Pipelines:databricks pipelines list-pipeline-events <PIPELINE_ID> --max-results 50 --profile $P --output json. - Check whether the failure affects one workload or a shared dependency, then continue with C3 for jobs or D1/D2 for pipelines. Record every command and result in the findings table.
Do not restart until you capture the failed run ID, error, and current configuration. A retry can replace the useful failure context.
PHASE C — Jobs Reliability (deep dive)#
The job timeline contains hourly slices. The result_state value exists only on
the final slice. Count final slices as runs, and combine all slices before you
calculate a run duration.
Source: https://learn.microsoft.com/azure/databricks/admin/system-tables/jobs
C1 — Failure rates, last 30 days (system tables)#
databricks experimental aitools tools query "SELECT job_id, COUNT(*) AS runs, COUNT_IF(result_state <> 'SUCCEEDED') AS failures, ROUND(100.0 * COUNT_IF(result_state <> 'SUCCEEDED') / COUNT(*), 1) AS fail_pct FROM system.lakeflow.job_run_timeline WHERE period_start_time >= DATE_SUB(CURRENT_DATE, 30) AND result_state IS NOT NULL GROUP BY 1 HAVING failures > 0 ORDER BY fail_pct DESC, failures DESC LIMIT 30" --profile $P
Worst offenders by failure rate. Column names vary by region — if one errors, run
databricks experimental aitools tools discover-schema system.lakeflow.job_run_timeline --profile $P.
C2 — Long-running / drifting jobs (duration outliers)#
databricks experimental aitools tools query "WITH runs AS (SELECT job_id, run_id, (UNIX_TIMESTAMP(MAX(period_end_time)) - UNIX_TIMESTAMP(MIN(period_start_time))) / 60.0 AS duration_min FROM system.lakeflow.job_run_timeline WHERE period_start_time >= DATE_SUB(CURRENT_DATE, 30) AND period_end_time IS NOT NULL GROUP BY job_id, run_id) SELECT job_id, ROUND(AVG(duration_min), 1) AS avg_min, ROUND(MAX(duration_min), 1) AS max_min, COUNT(*) AS runs FROM runs GROUP BY job_id ORDER BY max_min DESC LIMIT 20" --profile $P
Red flag: max_min ≫ avg_min → runaway runs (likely missing a timeout_seconds).
C3 — Per-job config review (anti-pattern scan)#
For each high-risk job ID from C1/C2:
databricks jobs get <JOB_ID> --profile $P --output json
Score each job against the table below (these are the patterns that quietly create a poor experience):
| Check | Red flag in JSON | Why it hurts |
|---|---|---|
| Compute type | task has existing_cluster_id (all-purpose) instead of new_cluster/serverless |
~2–4× cost; resource contention; slow on cold start |
| Cluster reuse | every task defines its own new_cluster instead of a shared job_cluster_key |
Repeated 3–5 min cluster spin-ups per task |
| Failure alerts | email_notifications / webhook_notifications on_failure empty |
Silent failures — nobody knows it broke |
| Retries | task max_retries = 0 on a flaky/networked task |
Transient blip = full job failure |
| Timeout | no timeout_seconds at job or task level |
Hung run burns compute indefinitely (see C2) |
| Health rules | no health.rules (e.g. RUN_DURATION_SECONDS threshold) |
No early warning on slow drift |
| Schedule | schedule.pause_status = PAUSED unexpectedly, or wrong timezone_id |
Job silently not running / firing at the wrong hour |
| Trigger fit | continuous used where a schedule/trigger would do |
Always-on compute cost |
| Concurrency | max_concurrent_runs = 1 on a job that overlaps its own schedule |
Skipped/queued runs pile up |
| Permissions | CAN_MANAGE granted to individual user_name instead of group_name |
Unauditable access; breaks when people leave |
| Naming | name lacks an env prefix (e.g. [prod]) |
Cross-env confusion in multi-workspace setups |
Quick jq to surface the two most common offenders:
databricks jobs get <JOB_ID> --profile $P --output json | jq '{name, tasks: [.settings.tasks[] | {task_key, uses_all_purpose: (.existing_cluster_id != null), retries: .max_retries}], on_failure: .settings.email_notifications.on_failure}'
C4 — Fallback: recent runs per job (no system tables)#
databricks jobs list --profile $P
databricks jobs list-runs --job-id <ID> --limit 25 --profile $P
PHASE D — Pipelines Reliability (deep dive — SDP / formerly DLT)#
D1 — Inventory + health#
databricks pipelines list --profile $P
databricks pipelines get <PIPELINE_ID> --profile $P --output json
Score each pipeline against the table below:
| Check | Red flag | Why it hurts |
|---|---|---|
| Run mode | continuous: true where triggered would suffice |
Always-on compute cost |
| Dev mode | development: true on a production pipeline |
Clusters don't auto-terminate between updates |
| Serverless | serverless: false while expecting incremental MV refresh |
MVs fall back to full recompute every run |
| Photon | photon: false |
Leaves significant performance on the table |
| Channel | channel: PREVIEW in prod |
Unpinned runtime behavior in production |
| Notifications | notifications empty |
Failed updates go unnoticed |
| Edition | edition: ADVANCED when expectations aren't used (or vice-versa) |
Paying for unused tier / missing data-quality gate |
D2 — Recent failures + the REAL error message#
databricks pipelines list-pipeline-events <PIPELINE_ID> --max-results 50 --profile $P
⚠️ Read the real cause from error.exceptions[0].message, NOT the top-level .message
(which only says "Update X is FAILED"):
databricks pipelines list-pipeline-events <PIPELINE_ID> --max-results 50 --profile $P --output json | jq -r '.events[] | select(.level=="ERROR") | .error.exceptions[0].message' | sort | uniq -c | sort -rn
Note: a pipeline stuck
INITIALIZINGon serverless is a normal cold start (a few minutes), not a failure — don't flag it as broken.
D3 — Source-code anti-patterns (if pipeline source is accessible)#
Pull the pipeline's source notebooks/files and scan. Legacy DLT syntax is migration debt and a reliability/perf risk — flag every hit:
| Pattern (grep / rg) | Meaning | Migrate to |
|---|---|---|
import dlt |
Legacy DLT module | from pyspark import pipelines as dp |
@dlt.table, @dlt.view |
Legacy decorators | @dp.table, @dp.temporary_view |
dlt.apply_changes |
Legacy CDC | dp.create_auto_cdc_flow |
LIVE. prefix (SQL) |
Deprecated, errors in modern pipelines | bare name / STREAM(name) |
CREATE LIVE TABLE / CREATE STREAMING LIVE TABLE |
Legacy DDL | CREATE OR REFRESH MATERIALIZED VIEW / ... STREAMING TABLE |
CREATE OR REPLACE ... STREAMING TABLE |
Invalid in SDP | CREATE OR REFRESH ... |
UNION across streaming sources |
Anti-pattern | multiple Append Flows (@dp.append_flow) |
aggregation (GROUP BY/SUM) on a streaming table |
STs are append-only; aggregates won't recompute | Materialized View with a batch read |
no EXPECT / @dp.expect* anywhere |
No data-quality gate | add Expectations (warn/drop/fail) |
PARTITIONED BY + ZORDER |
Legacy layout | CLUSTER BY (Liquid Clustering) |
rg -n "import dlt|@dlt\.|dlt\.apply_changes|LIVE\.|CREATE LIVE TABLE|CREATE STREAMING LIVE TABLE|CREATE OR REPLACE (STREAMING TABLE|MATERIALIZED VIEW)" <pipeline_src_dir>
PHASE E — Cross-Cutting Reliability#
- Alerting coverage: cross-reference C3 (jobs) + D1 (pipelines) — what fraction of production workloads have any failure notification? A low number is usually the single biggest finding.
- Full-refresh exposure: note any pipeline/job that triggers a pipeline full refresh on a schedule — it reprocesses streaming sources from scratch and can cause data loss.
- Runtime/version drift: outdated
spark_version(jobs) andchannel: PREVIEW(pipelines) → security + reproducibility risk. - Tagging: are clusters/jobs/warehouses tagged (team/cost-center)? Untagged = no cost attribution.
PHASE F — Deployment / IaC Hygiene (DABs) · Operational excellence#
Run from each bundle root (the directory containing
databricks.yml). The audit question: is this environment managed as code, or hand-clicked?
databricks bundle validate --strict --profile $P
databricks bundle validate --strict -t <TARGET> --profile $P
databricks bundle summary -t <TARGET> --profile $P
Then cross-reference what's bundle-managed against the live inventory to find hand-created resources:
databricks bundle summary -t <TARGET> --profile $P # IaC-managed resources
databricks jobs list --profile $P # everything that exists
# anything in `jobs list` but absent from `bundle summary` was created by hand
| Red flag | Signal | Pillar impact |
|---|---|---|
Resource in UI but not in bundle summary |
exists in jobs/pipelines list, absent from summary |
No source of truth; lost on rebuild |
| Single target / shared profile across envs | one target:, dev & prod share workspace.profile |
Dev deploy can clobber prod |
Missing mode: production on prod target |
prod target lacks it (or runs development) |
Schedules paused/mangled; guardrails skipped |
No permissions: / grants: block |
resource YAML omits it | Ungoverned, unauditable ACLs |
| Secrets hardcoded | literal tokens in databricks.yml/app.yaml |
Secrets in source control |
| Drift after deploy | UI edits diverge from bundle summary |
Next deploy silently reverts fixes |
| Hardcoded catalog/schema/warehouse | literals instead of ${var...} |
Can't promote across envs |
Caveats (flagged by reader): there is no bundle list-targets command — read targets from
databricks.yml. pipelines list-pipelines / apps list aren't shown in the DABs skill — confirm
with databricks <group> --help.
PHASE G — Serverless Migration Opportunities · Performance efficiency + Cost#
"Use serverless architectures" is the #1 principle of the Performance Efficiency pillar. This phase finds classic compute that should move.
databricks experimental aitools tools query "SELECT sku_name, usage_type, ROUND(SUM(usage_quantity),1) AS dbus FROM system.billing.usage WHERE usage_date >= DATE_SUB(CURRENT_DATE,90) GROUP BY 1,2 ORDER BY dbus DESC" --profile $P
Workloads whose usage_type is not SERVERLESS_COMPUTE are the candidate spend. Then per
job/pipeline, the migration signal is in the spec JSON:
databricks jobs get <JOB_ID> --profile $P --output json # job_clusters/new_cluster = classic; environments/environment_key = serverless
databricks pipelines get <PIPELINE_ID> --profile $P --output json
Candidates: all-purpose clusters backing notebook/Python work; jobs on classic job_clusters;
spark_jar_task on classic; outdated DBR (13.x/14.x); no scale-to-zero.
Compatibility blockers (don't over-recommend): RDD/SparkContext APIs, %scala/%r cells,
custom Spark data-source JARs, unsupported spark.conf.set(...), DBFS mounts / Hive Metastore,
continuous-trigger streaming, ML libs not pre-installed on serverless.
Caveat: system.compute.clusters is a real table but not referenced by the serverless skill —
confirm columns (cluster_source, dbr_version, auto_termination_minutes) in-env before relying on them.
PHASE H — Model Serving (deepens A2) · Cost + Reliability + Governance#
databricks serving-endpoints list --profile $P -o json
databricks serving-endpoints get <NAME> --profile $P -o json
databricks serving-endpoints get <NAME> --profile $P -o json | jq '.config.served_entities[] | {entity_name, entity_version, scale_to_zero_enabled, workload_size, min_provisioned_throughput, max_provisioned_throughput}'
databricks serving-endpoints get-permissions <ENDPOINT_ID> --profile $P # takes ID, not name
databricks serving-endpoints export-metrics <NAME> --profile $P # Prometheus metrics
| Red flag | Signal |
|---|---|
| Always-on compute | scale_to_zero_enabled: false on low-traffic/dev endpoint |
| Over-provisioned PT | min_provisioned_throughput ≈ max, set high |
| No rate limits | no AI Gateway rate-limit block |
| Stale model | entity_version pinned old while newer @prod exists |
| No monitoring | no inference tables; export-metrics empty |
| Bad traffic split | traffic_config not summing to 100% |
| Broad permissions | CAN_QUERY/CAN_MANAGE to users/account users |
| Broken deploy | state.ready != READY / config_update stuck |
Cost: system.billing.usage WHERE billing_origin_product = 'MODEL_SERVING'.
Caveat: the skill exposes no CLI for UC registered-model/alias listing (it uses the MLflow client);
served_entities[].entity_name/entity_version from get covers the serving side.
PHASE I — Vector Search · Cost + Performance + Governance#
databricks vector-search-endpoints list-endpoints --profile $P
databricks vector-search-endpoints get-endpoint <ENDPOINT_NAME> --profile $P
databricks vector-search-indexes list-indexes <ENDPOINT_NAME> --profile $P
databricks vector-search-indexes get-index <CATALOG>.<SCHEMA>.<INDEX> --profile $P
| Red flag | Signal |
|---|---|
| Idle always-on endpoint | ONLINE with num_indexes low/0, no query activity |
| Stale/failed sync | status.ready=False, error in status.message, indexed_row_count ≪ source rows |
| Wrong refresh mode | pipeline_type: CONTINUOUS on a batch source |
| Embedding drift | self-managed embedding_vector_columns vs managed source |
| Wrong tier | STANDARD (~7× cost) where Storage-Optimized fits |
| Source lacks CDF | deletions/updates won't propagate (verify Delta props) |
Health states to watch on get-endpoint: YELLOW_STATE, RED_STATE, OFFLINE, stuck PROVISIONING.
Caveat: the VECTOR_SEARCH billing-SKU string and system-table mappings aren't documented in the
skill — confirm in-env before quoting cost numbers.
PHASE J — Databricks Apps · Security + Cost + Usability#
databricks apps list --profile $P -o json # verify with --help (not shown in skill)
databricks apps get <APP_NAME> --profile $P -o json # app_status.state, url, compute, resources[], service_principal
databricks apps logs <APP_NAME> --profile $P # OAuth auth only — silently fails on PAT
| Red flag | Signal |
|---|---|
| No per-user auth | reads user data but runs as shared SP (no user_api_scopes/OBO) |
| Over-broad SP grants | resources[] show wide UC/warehouse/secret access |
| Plaintext secrets | env literals instead of valueFrom: secret |
| Always-on/oversized | Large (1.0 DBU/h) where Medium (0.5) suffices |
| Crash loop | app_status.state != RUNNING; logs show post-deploy PERMISSION_DENIED |
| Stale/abandoned | old RUNNING apps, no recent deploys; near 100-app/workspace cap |
Limits: 100 apps/workspace; 10-min start; 120s proxy timeout (504 not in app logs); ephemeral
FS — only stdout/stderr survive.
UX (from databricks-app-design): missing loading/empty/error states; Genie/AI surfaces that hide
the generated SQL, execution identity, or "verify" disclaimer.
PHASE K — Lakebase (Postgres / OLTP) · Cost + Reliability + Security#
CLI group is
postgres(notlakebase); needs CLI ≥ v0.294.0. Discover first — the surface is narrow:databricks postgres -h.
databricks postgres list-branches projects/<PROJECT_ID> --profile $P
databricks postgres list-endpoints projects/<PROJECT_ID>/branches/<BRANCH_ID> --profile $P
databricks postgres list-databases projects/<PROJECT_ID>/branches/<BRANCH_ID> --profile $P
databricks postgres get-endpoint projects/<PROJECT_ID>/branches/<BRANCH_ID>/endpoints/<ENDPOINT_ID> --profile $P
databricks postgres get-synced-table "synced_tables/<CATALOG>.<SCHEMA>.<TABLE>" --profile $P
| Red flag | Signal |
|---|---|
| Over-provisioned | min_cu ≈ max_cu on low-traffic project |
| No scale-to-zero | always-on compute on dev/CI branches |
| Stale/failed sync | old refresh timestamp / failed pipeline in get-synced-table |
| No HA in prod | single endpoint, no secondaries |
| Public exposure | reachable without private networking; Data API on public schema, no RLS |
| Static credentials | committed postgresql://user:pass@... vs 1-hr OAuth; no sslmode=require |
| Branch sprawl | many no_expiry branches; near 10/project or 8 TB/branch; no PITR plan |
Caveats (flagged by reader): no documented list-projects/get-project or list-synced-tables
command — enumerate via the Python SDK (w.postgres), REST /database/, or Catalog Explorer UI.
Provisioned-era instances use the separate databricks database group. Never run
generate-database-credential during an audit — it mints a live DB token.
Cost: CU-hours (~2 GB RAM/CU) + synced-table DLT pipeline cost; both surface under
system.billing.usage (confirm exact SKU in-env).
Verification#
- [ ] One completed inventory block per environment + the account
- [ ] B1 (spend) and C1 (job failures) returned data, or their fallbacks ran
- [ ] Every high-risk job (C3) and pipeline (D1) scored against its anti-pattern table
- [ ] Phases F–K run only for products that exist in the environment (skip absent ones; note which were skipped)
- [ ] Each finding tagged with its Well-Architected pillar (so the report rolls up by pillar)
- [ ] Findings captured with resource IDs
Findings Template#
| # | Env | Resource (ID) | Phase | Severity | Finding | Recommended fix |
|---|---|---|---|---|---|---|
| 1 | prod | job 12345 | C3 | High | No on_failure alert + runs on all-purpose cluster |
Add webhook alert; move to job cluster |
Troubleshooting#
| Symptom | Likely cause | Fix |
|---|---|---|
| All commands fail with credentials error | Session expired | Re-run Step 0 for that profile |
TABLE_OR_VIEW_NOT_FOUND on system.* |
System tables not enabled | Use CLI fallbacks (B3/C4); flag as a finding |
schemas list rejects --catalog-name |
Positional-arg command | Use schemas list <CATALOG> |
| Parse error on a catalog/table name | Hyphen in name | Backtick-quote it |
Pipeline events .message is unhelpful |
Wrong field | Read error.exceptions[0].message |
Pipeline stuck INITIALIZING |
Serverless cold start | Wait — not a failure |
| PERMISSION_DENIED | Not admin in that workspace | Note the coverage gap; continue |
Rollback#
None — every step is read-only. Nothing is created or modified, so there is nothing to undo.
Escalation#
| Situation | Contact | Method |
|---|---|---|
| Need account-admin / system tables enabled | Tenant Databricks admin | — |
| Cloud-side networking / Private Link / storage | Cloud platform team | — |
History#
| Date | Run By | Notes |
|---|---|---|
| 2026-06-23 | — | Runbook authored (Inventory + Cost + Jobs + Pipelines). Not yet executed. |
| 2026-06-23 | — | Added per-phase permissions section. |
| 2026-06-23 | — | Extended to full platform: +DABs/IaC, Serverless migration, Model Serving, Vector Search, Apps, Lakebase. Anchored every phase to a Well-Architected pillar. |