Manual Test Runbook — B4: Client State Backend (F6 + Blob RBAC)
Owner: Sagar | Time: ~5 min (Parts A + B) · +8 min (optional Part C sandbox apply) · +10 min (optional Part D live backend round-trip) | Sandbox: snowops-sandbox-01
Promotes B4 (
modules/azure/client-state-backend/) from 🟦 Code Complete → 🟩 Shipped. Parts A + B are 100% offline. Part C costs ~$0 — an empty state storage account's capacity + transactions are negligible, and the fixture provisions no Private Endpoint. Part D exercises the real AAD-auth backend lease path.
Prerequisites
- Sandbox subscription access active (PIM activated if required)
-
az logindone;az account showconfirms the sandbox subscription is selected - Identity has User Access Administrator (or Owner) on the sandbox subscription — needed to write the Storage Blob Data role assignments
- Local tooling:
terraform >= 1.6,go >= 1.22,az CLI >= 2.50 -
SNOWOPS_SANDBOX_SUBSCRIPTION_IDandSNOWOPS_SANDBOX_TENANT_IDenv vars set - (Part C only)
SNOWOPS_AAD_BREAKGLASS_GROUP_OBJECT_IDset to a real AAD group object ID in the sandbox tenant (B4 binds Storage Blob Data roles by object ID; a synthetic GUID fails withPrincipalNotFound). Reuse the same group the F3/B3 tests consume. - Working directory: repo root
Steps
Part A — terraform fmt + validate (offline, ~2 min)
- Confirm formatting + structural validity of the module on its own (B4 is a composition module —
initloads the child F6 module from../state-backend):
terraform -chdir=modules/azure/client-state-backend fmt -recursive -check
terraform -chdir=modules/azure/client-state-backend init -backend=false -input=false
terraform -chdir=modules/azure/client-state-backend validate
Expected: Success! The configuration is valid.
- Same for the example:
terraform -chdir=modules/azure/client-state-backend/examples/basic fmt -check
terraform -chdir=modules/azure/client-state-backend/examples/basic init -backend=false -input=false
terraform -chdir=modules/azure/client-state-backend/examples/basic validate
Expected: Success! The configuration is valid.
- Run the B4-targeted offline Terratest:
Expected: --- PASS: TestClientStateBackendValidate.
Part B — full offline Terratest suite + pre-commit (offline, ~3 min)
- Run the whole offline suite to confirm B4 hasn't regressed F0–F6 / H1–H3 / B2 / B3:
Expected: 21 top-level tests pass (the 20 from v0.27 plus
TestClientStateBackendValidate).
- Run pre-commit + pre-push hooks on every new file:
pre-commit run --files \
modules/azure/client-state-backend/versions.tf \
modules/azure/client-state-backend/variables.tf \
modules/azure/client-state-backend/main.tf \
modules/azure/client-state-backend/outputs.tf \
modules/azure/client-state-backend/README.md \
modules/azure/client-state-backend/examples/basic/versions.tf \
modules/azure/client-state-backend/examples/basic/variables.tf \
modules/azure/client-state-backend/examples/basic/main.tf \
modules/azure/client-state-backend/examples/basic/outputs.tf \
tests/terratest/fixtures/client-state-backend/main.tf \
tests/terratest/fixtures/client-state-backend/variables.tf \
tests/terratest/fixtures/client-state-backend/outputs.tf \
tests/terratest/modules/azure/client_state_backend_validate_test.go \
tests/terratest/modules/azure/client_state_backend_test.go \
docs/runbooks/test/B4.md
pre-commit run --hook-stage pre-push --all-files
Expected: every hook PASS; FINAL_EXIT=0 from the pre-push run.
Part C — integration test (real sandbox apply + destroy, ~8 min, ~$0)
Skip if iterating on offline changes only.
- Confirm sandbox env vars + the real group object ID:
export SNOWOPS_SANDBOX_SUBSCRIPTION_ID="<sandbox-subscription-guid>"
export SNOWOPS_SANDBOX_TENANT_ID="<sandbox-tenant-guid>"
export SNOWOPS_AAD_BREAKGLASS_GROUP_OBJECT_ID="<real-sandbox-group-object-id>"
- Run the B4 integration test:
cd tests/terratest
go test -v -tags integration -timeout 30m ./modules/azure/... -run TestClientStateBackendModule
- Watch for key milestones:
azurerm_storage_account.this: Creation complete(via the F6 child module) — the slow step.azurerm_storage_container.this["tfstate"]: Creation complete.azurerm_role_assignment.state["Storage Blob Data Contributor/..."]: Creation complete+ the Reader one — ~5-30s each as ARM propagates.- All output assertions PASS (SA ARM ID, SA name, backend_config carries
use_azuread_auth:true, both role-assignment keys, contractversioning_enabled:true). Destroy complete!— clean LIFO teardown (immutability disabled in the fixture, so the container deletes immediately).
Part D — live backend round-trip (optional, ~10 min, ~$0)
Proves the data-plane RBAC actually works: a principal in the granted group can
terraform initagainst the backend over AAD auth (no shared keys). Run against a manually stood-up account so the test's auto-destroy doesn't race you.
- Stand up the example with your own identity's group as contributor:
terraform -chdir=modules/azure/client-state-backend/examples/basic init
terraform -chdir=modules/azure/client-state-backend/examples/basic apply \
-var subscription_id=$SNOWOPS_SANDBOX_SUBSCRIPTION_ID \
-var tenant_id=$SNOWOPS_SANDBOX_TENANT_ID \
-var client_name=snowopsb4probe \
-var deploy_sp_object_id=$SNOWOPS_AAD_BREAKGLASS_GROUP_OBJECT_ID
-
Grab the backend config + drive a throwaway consumer stack at it:
terraform -chdir=modules/azure/client-state-backend/examples/basic output backend_config SA=$(terraform -chdir=modules/azure/client-state-backend/examples/basic output -raw storage_account_name) cd /tmp && mkdir b4-consumer && cd b4-consumer cat > main.tf <<EOF terraform { backend "azurerm" { resource_group_name = "snowopsb4probe-tfstate-rg" storage_account_name = "$SA" container_name = "prod" key = "b4-probe.tfstate" use_azuread_auth = true } } resource "null_resource" "probe" {} EOF # Requires your signed-in identity to be a member of the granted group. terraform init terraform apply -auto-approveExpected:
terraform initsucceeds (AAD auth + Blob Data Contributor works);applyacquires the blob lease and writes state. A403 AuthorizationPermissionMismatchhere means the signed-in identity is NOT in the granted group — the RBAC contract is doing its job. -
Tear down:
cd /tmp/b4-consumer && terraform destroy -auto-approve && cd - && rm -rf /tmp/b4-consumer terraform -chdir=modules/azure/client-state-backend/examples/basic destroy \ -var subscription_id=$SNOWOPS_SANDBOX_SUBSCRIPTION_ID \ -var tenant_id=$SNOWOPS_SANDBOX_TENANT_ID \ -var client_name=snowopsb4probe \ -var deploy_sp_object_id=$SNOWOPS_AAD_BREAKGLASS_GROUP_OBJECT_ID
Pass criteria
- Part A —
terraform validatepasses for both module and example - Part A —
TestClientStateBackendValidatepasses - Part B — full offline Terratest suite passes (21 top-level tests)
- Part B — pre-commit + pre-push hooks all green
- Part C —
TestClientStateBackendModuleintegration test passes end-to-end - Part C — both Storage Blob Data role-assignment keys present in
role_assignment_ids - Part C —
backend_configcarriesuse_azuread_auth:true(no shared keys) - Part C —
object_store_contractreportsversioning_enabled:true - Part C — destroy succeeds (immutability disabled in fixture)
- (Part D) a granted-group member can
terraform init+applyagainst the backend over AAD auth - (Part D) a non-member is rejected with
403 AuthorizationPermissionMismatch
Teardown
The integration test runs terraform destroy automatically. If a failure
mid-run orphans resources:
# Find + delete the test resource group (cascades the SA + containers)
az group list --query "[?starts_with(name,'snowops-b4-test-')].name" -o tsv \
| xargs -I{} az group delete -n {} --yes --no-wait
Note: the test fixture sets
enable_immutability = false, so containers delete immediately. If you ran a manual apply with locked immutability, the container cannot be deleted until the WORM retention period elapses — by design.
Sign-off
- Tester: _ | Date: _ | Result: PASS / FAIL / N/A
- Notes: