Skip to content

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 login done; az account show confirms 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_ID and SNOWOPS_SANDBOX_TENANT_ID env vars set
  • (Part C only) SNOWOPS_AAD_BREAKGLASS_GROUP_OBJECT_ID set to a real AAD group object ID in the sandbox tenant (B4 binds Storage Blob Data roles by object ID; a synthetic GUID fails with PrincipalNotFound). Reuse the same group the F3/B3 tests consume.
  • Working directory: repo root

Steps

Part A — terraform fmt + validate (offline, ~2 min)

  1. Confirm formatting + structural validity of the module on its own (B4 is a composition module — init loads 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.

  1. 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.

  1. Run the B4-targeted offline Terratest:
cd tests/terratest
go test -v -timeout 5m ./modules/azure/... -run 'TestClientStateBackendValidate'

Expected: --- PASS: TestClientStateBackendValidate.


Part B — full offline Terratest suite + pre-commit (offline, ~3 min)

  1. Run the whole offline suite to confirm B4 hasn't regressed F0–F6 / H1–H3 / B2 / B3:
cd tests/terratest
go test -v -timeout 10m ./...

Expected: 21 top-level tests pass (the 20 from v0.27 plus TestClientStateBackendValidate).

  1. 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.

  1. 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>"
  1. Run the B4 integration test:
cd tests/terratest
go test -v -tags integration -timeout 30m ./modules/azure/... -run TestClientStateBackendModule
  1. Watch for key milestones:
  2. azurerm_storage_account.this: Creation complete (via the F6 child module) — the slow step.
  3. azurerm_storage_container.this["tfstate"]: Creation complete.
  4. azurerm_role_assignment.state["Storage Blob Data Contributor/..."]: Creation complete + the Reader one — ~5-30s each as ARM propagates.
  5. All output assertions PASS (SA ARM ID, SA name, backend_config carries use_azuread_auth:true, both role-assignment keys, contract versioning_enabled:true).
  6. 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 init against 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.

  1. 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
  1. 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-approve
    

    Expected: terraform init succeeds (AAD auth + Blob Data Contributor works); apply acquires the blob lease and writes state. A 403 AuthorizationPermissionMismatch here means the signed-in identity is NOT in the granted group — the RBAC contract is doing its job.

  2. 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 validate passes for both module and example
  • Part A — TestClientStateBackendValidate passes
  • Part B — full offline Terratest suite passes (21 top-level tests)
  • Part B — pre-commit + pre-push hooks all green
  • Part C — TestClientStateBackendModule integration test passes end-to-end
  • Part C — both Storage Blob Data role-assignment keys present in role_assignment_ids
  • Part C — backend_config carries use_azuread_auth:true (no shared keys)
  • Part C — object_store_contract reports versioning_enabled:true
  • Part C — destroy succeeds (immutability disabled in fixture)
  • (Part D) a granted-group member can terraform init + apply against 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: