15Unity Catalog topology
The Architecture route decided that there is a catalog per environment. This route decides the part Unity Catalog actually enforces: which metastore, which workspaces can see which catalog, who owns production objects, and who holds the two permissions that reach storage directly. Binding and ownership are what the whole access model rests on — get them wrong and every grant above them is advisory.
Metastore, catalogs, schemas
Three levels and one attachment point. The attachment point — which workspaces reach which catalog — is where environment separation stops being a naming convention and becomes a boundary.
#One metastore per region
metastore one per region, account-level
|
|-- catalog dwh_dev visible from: dev workspace (read-write)
|-- catalog dwh_test visible from: dev + CI workspaces (read-write)
|-- catalog dwh_prod visible from: prod workspace (read-write)
| | BI workspace (READ-ONLY)
| |-- schema bronze / silver / gold
| | '-- table · view · volume · function · model
|
'-- storage credential + external location
metastore-level securables. NOT inside a catalog — which is why
"who may create one" is an account question, not a catalog one.dwh_dev and dwh_prod keep their managed files in different containers, and deleting a catalog deletes a bounded thing. A metastore with root storage puts every environment's managed data in one place, so the separation exists in Unity Catalog and nowhere underneath it.Doing it
- Count the regions you must serve, not the teams that want their own space. One metastore per region, no more.
- Create the metastore with no root storage; set a managed storage location per catalog.
- Make metastore admin a group with two or three members — it can grant anything on anything, so it is not a working account.
- Before agreeing to a workspace in a second region, price the Delta Share and the loss of the direct join.
Defaults
- One metastore per region; one region if the business allows it.
- No metastore root storage — managed storage declared per catalog.
- Metastore admin is a small group with a documented way in and out.
Gotchas
- A second metastore appears the day someone creates a workspace in a second region. No error, no warning — just a fresh empty metastore whose tables can never be joined to the warehouse. It is discovered by an analyst writing a join, not by an alert, and the fix is a share or a migration.
- Metastore root storage. Every catalog's managed data lands in one container, so 'dev is separate from prod' is true in the catalog list and false at the storage layer, and cleanly deleting a decommissioned environment's data is no longer possible.
- Using the metastore-admin account for daily work. It bypasses every ownership boundary below it, so grants it issues are invisible to a review built around catalog owners, and the audit trail records that an admin did it and nothing about why.
- Assuming the metastore is per workspace. It is account-level and shared by every workspace attached to it — so an external location or storage credential 'created in dev' exists for the production workspace as well. You find out when a dev engineer lists the production landing container and is surprised it worked.
#Binding is what stops dev reading prod
-- dwh_prod stops being visible from every workspace on the metastore
ALTER CATALOG dwh_prod SET ISOLATION MODE = ISOLATED;# read-write for the production job workspace,
# read-only for the BI workspace that must never write to gold
# DATABRICKS_HOST carries the scheme: https://adb-....azuredatabricks.net
curl -sS -X PATCH \
-H "Authorization: Bearer $DATABRICKS_TOKEN" \
-H "Content-Type: application/json" \
"$DATABRICKS_HOST/api/2.1/unity-catalog/bindings/catalog/dwh_prod" \
-d '{
"add": [
{"workspace_id": 2468013579, "binding_type": "BINDING_TYPE_READ_WRITE"},
{"workspace_id": 1357924680, "binding_type": "BINDING_TYPE_READ_ONLY"}
]
}'gold. Read-only binding makes that structural instead of depending on which privileges someone remembered not to grant.Doing it
- Set every catalog to ISOLATED, then add workspaces explicitly. Default-open is the first thing to reverse, before there is anything worth reading.
- Bind the BI workspace read-only, and any analyst workspace too.
- Manage the workspace list in Terraform or the bundle deploy — a click is invisible to review and gets reproduced differently in the next environment.
Defaults
- ISOLATED on every catalog; the workspace list held as code.
- Read-only binding for every workspace that only consumes.
- Binding on top of grants, never instead of them — it restricts where a catalog is visible, not who may read it there.
- Binding reviewed as part of every new-workspace request, because a new workspace starts with none.
Gotchas
- Setting the isolation mode and stopping there. The catalog vanishes from every workspace except its owner's — including the job workspace that needs it — and the failure reads as a missing catalog, so the first instinct is to check whether the deploy dropped it.
- New workspaces inherit nothing. A workspace created in month eight sees no isolated catalog and every open one, so the boundary you demonstrated in month one is now describing a workspace count that has changed.
- Switching a shared catalog to read-only binding during working hours. Existing jobs do not fail at the start — they read happily for twenty minutes and fail at the write, mid-run, leaving a half-loaded target.
- Treating binding as the access model. Binding removes workspaces from the conversation; it removes no privileges. The failure looks like finished work — dwh_prod is ISOLATED, the diagram is clean, and every analyst in the bound workspace still holds the SELECT on gold they were given in month two, on the columns nobody has masked yet. Nothing surfaces it except an access review.
Ownership and the storage doors
Two questions decide whether the access model holds: who owns each production object, and who can create a path to the bytes underneath it.
#Production is owned by groups
CREATE owns it. In production that means the deploy service principal owns most things, which is fine — provided it was decided rather than inherited. Everything else in dwh_prod is owned by a group.ALTER CATALOG dwh_prod OWNER TO `dwh_platform_owners`;
ALTER SCHEMA dwh_prod.gold OWNER TO `dwh_gold_owners`;
ALTER TABLE dwh_prod.gold.fct_order_line OWNER TO `dwh_gold_owners`;
ALTER TABLE dwh_prod.gold.dim_customer OWNER TO `dwh_gold_owners`;
-- zero rows = pass. Run it in CI, not once a quarter.
SELECT table_name, table_owner
FROM dwh_prod.information_schema.tables
WHERE table_schema = 'gold'
AND table_owner NOT IN ('dwh_gold_owners', 'sp_dwh_deploy');gold.fct_order_line because they created it in a hurry in month two. They leave, the identity is deprovisioned, and the table now has an owner that does not resolve: nobody can alter its schema, nobody can grant on it, and repairing it requires the metastore admin — the one account you were trying not to use for daily work. It surfaces on a Tuesday, in the middle of a release that needs an ALTER TABLE.Doing it
- Set OWNER TO a group in the same DDL that creates the object, not in a cleanup pass afterwards.
- Use account-level groups only; check that the group in your GRANT is not a workspace-local one with the same name.
- Run the owner query in CI against dwh_prod and fail the build on any row.
- Give each owning group at least two members, and confirm both are still employed at every access review.
Defaults
- Groups own everything in dwh_prod, including 'temporary' objects.
- The deploy service principal owns what it creates, deliberately, and its identity is written down.
- Account-level SCIM groups only; workspace-local groups are not part of the model.
- Ownership asserted in CI, because it drifts through CREATE rather than through ALTER.
Gotchas
- An orphaned owner after offboarding. The securable becomes unmodifiable and only a metastore admin can repair it — discovered by a failing release, never by a review.
- Ownership drift is silent and continuous: every new table starts owned by whoever ran the statement, so a scheme fixed by hand is wrong again after the next sprint. That is why the check belongs in CI.
- An owner can remove the protection on their own object. MANAGE comes with ownership, so the group owning dim_customer can drop the column mask on email. If the owning group is wider than the group cleared for personal data, the mask is decoration.
- Workspace-local groups. The GRANT appears to succeed in the workspace, and the scheme works right up to the first metastore-level securable or the second workspace, at which point the group simply is not there.
- The ownership check passing because it saw nothing. information_schema returns only the objects the querying principal can see, so a CI service principal without USE SCHEMA on dwh_prod.gold gets zero rows — the same zero rows that mean pass. Assert the row count of the schema first, then assert the violation count is zero; a check that cannot fail is worse than no check, because it is now on the evidence list.
#External locations are the other door
- A landing zone another system writes into. The webshop drops JSON there, Autoloader reads it, and bronze.shop_order is still a MANAGED table — the files are external, the table is not.
- A dataset a non-Databricks engine must read in place, where a managed copy would mean two masters and no agreement about which of them is wrong.
| Object | What it is | Create privilege | What a grant on it gives |
|---|---|---|---|
| Storage credential | the cloud identity Unity Catalog uses to reach storage | CREATE STORAGE CREDENTIAL on the metastore | the ability to define locations over anything that identity can reach |
| External location | a path, plus the credential that reads it | CREATE EXTERNAL LOCATION on the metastore | READ FILES · WRITE FILES · CREATE EXTERNAL TABLE · CREATE EXTERNAL VOLUME |
CREATE EXTERNAL LOCATION shop_landing
URL 'abfss://landing@example.dfs.core.windows.net/shop_order/'
WITH (STORAGE CREDENTIAL cred_landing)
COMMENT 'Webshop JSON drop zone. Backs the external volume bronze.landing_shop; read by the Autoloader that loads bronze.shop_order.';
ALTER EXTERNAL LOCATION shop_landing OWNER TO `dwh_platform_owners`;
-- the ingestion service principal reads it. No human gets anything.
GRANT READ FILES ON EXTERNAL LOCATION shop_landing TO `sp_dwh_ingest`;dim_customer.email is enforced when someone queries the table. It is not enforced on the files. Anyone holding READ FILES on an external location covering a table's path can read the Delta files directly — every masked column, every filtered row — and no table access event is recorded, because they never touched the table.CREATE STORAGE CREDENTIAL is not a convenience permission.Doing it
- Grant CREATE EXTERNAL LOCATION and CREATE STORAGE CREDENTIAL to one named group; everyone else raises a request against a written path.
- Scope each credential to the narrowest container the cloud provider allows, then scope the external location narrower still.
- Grant READ FILES to service principals only. A human who needs the data needs a table.
- Sweep SHOW EXTERNAL LOCATIONS quarterly and make each entry name the process that reads it, in its COMMENT.
Defaults
- Two metastore-level create privileges, one group, written into the permissions concept.
- READ FILES to service principals; humans read tables.
- Managed everywhere data is not written by another system — the access-control argument and the maintenance argument point the same way.
Gotchas
- READ FILES over a table's path bypasses column masks, row filters and the table audit event in one move. Nothing errors, nothing appears in a table-grant review, and the person doing it usually believes they are being efficient.
- A storage credential scoped to the storage account instead of one container. It works on day one, so nobody revisits it, and every external location created with it afterwards can be pointed anywhere in that account.
- External locations and credentials left visible to every workspace. The catalog boundary you built holds; the storage boundary underneath it does not, and the dev workspace can reach the production landing zone.
- READ FILES granted to a person 'just for debugging'. It is never removed, it does not show up in any table-privilege review, and it is invisible to a governance scheme built entirely on table grants.
Metadata that survives a redeploy
The catalog is the only documentation an analyst reads at 02:00. It is worth exactly as much as the deployment pipeline that maintains it.
#Grain, owner and cadence, in the DDL
COMMENT, in a fixed format, deployed as DDL. Anything a consumer could misread goes in a column comment.CREATE OR REPLACE TABLE gold.fct_order_line (
order_line_sk STRING COMMENT 'Surrogate key: sha2 of order_id + line_number',
customer_sk STRING COMMENT 'FK -> gold.dim_customer. INFORMATIONAL ONLY, not enforced. UNKNOWN where the customer did not resolve (rule 7).',
order_ts TIMESTAMP COMMENT 'Order timestamp from silver.sales_order. Second clustering key — do not drop it from a rebuild.',
amount_eur DECIMAL(18,2) COMMENT 'ROUND(quantity * unit_price * fx_rate, 2) (rule 2). Includes CANCELLED lines — revenue must filter them (rule 1).',
status STRING COMMENT 'OPEN | SHIPPED | CANCELLED'
-- remaining columns exactly as declared in the model
)
CLUSTER BY (customer_sk, order_ts)
COMMENT 'Grain: one row per order line. Owner: dwh_gold_owners. Cadence: hourly, freshness SLA 26h.';SELECT table_name, comment
FROM dwh_prod.information_schema.tables
WHERE table_schema = 'gold'
AND (comment IS NULL
OR comment NOT LIKE 'Grain:%'
OR comment NOT LIKE '%Owner:%'
OR comment NOT LIKE '%Cadence:%');pii tag lives in the tag because a policy reads it.CREATE OR REPLACE TABLE. Nobody notices, because the person who typed them is not the person who runs the deploy and the table still works. Months of accumulated knowledge disappear in one release, with no diff, no error and nothing to review.amount_eur includes cancelled lines is the difference between a wrong revenue dashboard and none.Doing it
- Fix the table-comment format — Grain / Owner / Cadence — and check it by pattern rather than by review.
- Comment every key, every measure that carries a business rule, and every _tech_ column.
- Deploy comments in the same DDL as the table. If a comment is worth typing, it is worth a pull request.
- Treat AI-suggested comments as a draft to edit: they describe the values, not the rule, and a plausible wrong comment is worse than an empty one.
Defaults
- Table COMMENT format fixed and pattern-checked in CI.
- Column comments carry the rule, not a restatement of the column name.
- Comments in DDL, in the repo, reviewed. The UI is for reading metadata, not writing it.
- Tags for machines, comments for humans — and never the same fact in both.
Gotchas
- CREATE OR REPLACE TABLE wipes UI-typed comments. The release is green, the table is fine, and the documentation is gone with no diff and no error to notice.
- A cadence in the comment that nobody updates when the schedule changes. Wrong documentation outlives missing documentation, because the on-call engineer at 02:00 believes it — tie the comment to the schedule change in the same pull request.
- Comments that restate the column name. 'customer_sk: the customer surrogate key' costs a reader's attention and buys nothing; the useful version says it is informational-only and what UNKNOWN means.
- Documenting the table on a wiki instead. The page is correct on the day it is written; the COMMENT is correct on the day the DDL deploys — and only one of them is next to the data when somebody is actually looking at it.
Straight to the documentation
The generic platform material lives in the official docs and is not restated here. These are the pages worth having open while you work through this route.
- What is Unity Catalog? — the object model in full — read it once so the hierarchy above is a reminder rather than a lesson
- Limit catalog access to specific workspaces — isolation mode, read-only bindings, and the API that holds the workspace list
- Manage Unity Catalog object ownership — what an owner can do implicitly — the reason ownership is a control and not a label
- Create an external location — follow it before granting anyone CREATE EXTERNAL LOCATION, so the scoping decisions are made once
- COMMENT ON — the syntax for retrofitting comments onto tables that already exist