Skip to content

Manual Test Runbook — M4: Microsoft Purview Baseline

Owner: Sagar  |  Time: ~7 min (Parts A + B) · +45 min (optional Part C live apply + scan)  |  Sandbox: snowops-sandbox-01

Promotes M4 (modules/azure/purview-baseline/) from 🟦 Code Complete → 🟩 Shipped. Parts A + B are offline ($0). Part C costs real money even while idle — the Purview Data Map bills per capacity unit-hour (~$0.4/CU-hour, region-dependent; ~$10/day order of magnitude at the 1-CU floor) plus scan vCore-hours while a scan runs. Apply, verify, destroy promptly.


Prerequisites

  • Sandbox subscription access active (PIM activated if required)
  • az login done; sandbox subscription selected
  • Identity has Owner OR (Contributor + User Access Administrator) on the sandbox sub — UAA is needed because the module grants the Purview identity Reader + Storage Blob Data Reader over the scan scopes
  • A sandbox storage account with a few blobs to scan (any test data; a CSV with fake emails/phone numbers makes classifications visible)
  • SNOWOPS_SANDBOX_SUBSCRIPTION_ID + SNOWOPS_SANDBOX_TENANT_ID exported
  • Local tooling: terraform >= 1.6, go >= 1.22, az CLI >= 2.50
  • Working directory: repo root

Steps

Part A — terraform fmt + validate + offline tests (~4 min)

  1. Formatting (offline, no registry access needed):
terraform -chdir=modules/azure/purview-baseline fmt -recursive -check
terraform -chdir=tests/terratest/fixtures/purview-baseline fmt -check

Expected: exit 0, no output.

  1. Module + example validate:
terraform -chdir=modules/azure/purview-baseline init -backend=false -input=false
terraform -chdir=modules/azure/purview-baseline validate

terraform -chdir=modules/azure/purview-baseline/examples/basic init -backend=false -input=false
terraform -chdir=modules/azure/purview-baseline/examples/basic validate

Expected: Success! for both. The example uses a synthetic J1 workspace ID + storage scan scope, so validate runs with no cloud creds.

Note: terraform init needs Terraform-registry network access to fetch the azurerm provider (the lock files pin 4.76.0). In a network-restricted sandbox init fails at provider download — run Part A from a normal workstation.

  1. Offline Terratest case + vet:
cd tests/terratest
go vet ./modules/azure/
go test -v -timeout 5m ./modules/azure/... -run TestPurviewBaselineValidate

Expected: vet silent; test PASS — exercises the account (SystemAssigned identity + named managed RG), diagnostics ON against a synthetic J1 ID, and 3 scan scopes × 2 default roles = 6 role assignments through the for_each.

Part B — context check: what M4 is and where the Terraform boundary sits (~3 min)

No commands — confirm the README story holds before going live:

  1. What Purview is. Microsoft Purview (Data Map / data governance) is the data-classification foundation: a catalog account whose scanner walks registered sources, applies built-in classifiers (credit cards, national IDs, secrets, …), and builds the map of what data exists and where.

  2. Where it sits in the M series. M1 (encryption policy), M2 (CMK), M3 (TLS policy), M6 (data residency) enforce how data is protected; M4 discovers + classifies what data exists — the prerequisite for M5 (DLP) and M7 (GDPR/DSAR evidence). "We know where our sensitive data lives" is this asset.

  3. The ARM vs data-plane boundary. Terraform (this module) owns the account + access: the Purview account, its system identity, the self-audit diagnostic setting → J1, and the Reader/Storage Blob Data Reader grants over scan_scopes. Collections, source registration, scans, classification rules, and the glossary are data-plane objects behind the catalog/scan endpoints (governance portal or Purview REST) — that is Part C below, by hand, exactly as the module README states. If anyone claims the module "configures scanning", that is wrong by design.

  4. Full offline suite still green:

cd tests/terratest && go test -count=1 -timeout 15m ./...

Part C — live apply + scan (sandbox, ~45 min, bills per CU-hour — destroy promptly)

  1. Apply the example with a REAL J1 workspace ID and a REAL sandbox storage account as the scan scope:
cd modules/azure/purview-baseline/examples/basic
terraform init -input=false
WS="<REAL J1 workspace ARM ID>"
SA="/subscriptions/$SNOWOPS_SANDBOX_SUBSCRIPTION_ID/resourceGroups/<rg>/providers/Microsoft.Storage/storageAccounts/<name>"
terraform apply -auto-approve \
  -var "log_analytics_workspace_id=$WS" \
  -var "scan_scope=$SA"

Expected: account + diagnostic setting + 2 role assignments apply clean (~5-10 min; the account create is slow). terraform output shows the principal_id, catalog_endpoint, scan_endpoint.

Flake: the role assignments can transiently fail on a freshly-created identity (AAD replication). principal_type is pinned, but if an apply 403s/PrincipalNotFounds on azurerm_role_assignment.scan, re-run — the second apply converges.

  1. Verify the ARM layer landed:
az purview account show -g snowops-governance-rg -n snowops-example-purview \
  --query "{state:properties.provisioningState, publicNetwork:properties.publicNetworkAccess, identity:identity.principalId, managedRg:properties.managedResourceGroupName}" -o jsonc

az monitor diagnostic-settings list \
  --resource "$(terraform output -raw purview_account_id)" -o table

az role assignment list --assignee "$(terraform output -raw principal_id)" \
  --query "[].{role:roleDefinitionName, scope:scope}" -o table

Expected: state: Succeeded; the snowops-purview-diag setting targeting the J1 workspace; Reader + Storage Blob Data Reader on $SA.

  1. Data plane (the part Terraform deliberately does not own). Open the governance portal — https://web.purview.azure.com (select the account) — then:

    • create a collection (or use the root collection),
    • Data Map → Sources → Register → Azure Blob Storage → pick the $SA account, credential = Purview MSI (this works because of the module's role grants),
    • New scan on the source → default scope + default rule set (AzureStorage_Default) → run once,
    • wait for the scan to complete (~5-15 min for a small account), then Data Catalog → Browse → the storage assets appear; any seeded PII-looking test data shows classifications on the asset's schema tab.
  2. Confirm the scan-status logs reached J1 (allow ~10 min ingestion lag):

    az monitor log-analytics query \
      --workspace "<J1 customer GUID>" \
      --analytics-query "PurviewScanStatusLogs | take 5" -o table
    

    Expected: rows for the scan you just ran (queued → running → succeeded). (Category names → tables vary by Purview platform version; if the table is empty after lag, check AzureDiagnostics | where ResourceProvider == 'MICROSOFT.PURVIEW'.)

  3. Destroy promptly (the account bills CU-hours while it sits):

    terraform destroy -auto-approve \
      -var "log_analytics_workspace_id=$WS" \
      -var "scan_scope=$SA"
    

    Expected: clean destroy — role assignments, diagnostic setting, account (Azure removes the managed RG + data map with it), holding RG. The scanned storage account and its data are untouched (Purview only ever had read).


Pass criteria

  • Part A — fmt exit 0; module + example validate; go vet clean; TestPurviewBaselineValidate passes
  • Part B — boundary story confirmed; full offline suite green
  • (Part C) account Succeeded; diag setting → J1; Reader + Storage Blob Data Reader on the scan scope; portal scan completes; classifications visible; scan-status logs in J1
  • (Part C) destroy clean; managed RG gone; source data untouched

Failure modes

  • Apply fails: managed RG already exists — Purview must create + own managed_resource_group_name; pick an unused name or leave it null.
  • Portal scan fails authentication — RBAC propagation lag on the MSI grants (retry after a few minutes), or the source isn't covered by scan_scopes. The module grants Storage roles only; other source types need scan_roles overrides or out-of-band data-plane grants (README, Failure modes).
  • Diagnostic setting apply fails "category not supported" — categories vary by Purview platform version; list them with az monitor diagnostic-settings categories list --resource <account-id> and set diagnostic_log_categories to match.
  • Cost surprise — the Data Map bills per CU-hour even idle. If a sandbox account was left standing, destroy it; the spend mechanism is CU-hours + scan vCore-hours, documented in the README Cost impact.

Cost impact

Part C: Data Map ~$0.4/capacity-unit-hour (region-dependent — check the Azure pricing page) for the life of the account, + scan vCore-hours for the one scan (small account ≈ minutes of one vCore). A same-day apply→verify→destroy run is roughly a few dollars; an account forgotten for a month is hundreds. Parts A/B are $0.

Removal path

terraform destroy (step 12) removes everything M4 created; Azure deletes the Purview-managed RG with the account. Verified clean in Part C. The J1 workspace and the scanned storage account are never touched.


Sign-Off

Field Value
Part A (fmt + validate + vet + Terratest case) ☐ PASS
Part B (context + offline suite) ☐ PASS
Part C (live apply + portal scan + destroy) ☐ PASS / ☐ skipped
Tester
Date
Result ☐ PASS