Manual Test Runbook — B2: Client-Bootstrap (AAD App Reg + Federated OIDC)
Owner: Sagar | Time: ~5 min (Parts A + B) · +25 min (optional Part C tenant apply) · +20 min (optional Part D live OIDC probe) | Sandbox: snowops-sandbox-01
Promotes B2 (
modules/azure/client-bootstrap/) from 🟦 Code Complete → 🟩 Shipped. Parts A + B are 100% offline. Part C costs ~$0 (AAD objects + role assignments are control-plane only). Part D requires a sandbox GitHub repo and adds ~$0.
Prerequisites
- Sandbox subscription access active (PIM activated if required)
-
az logindone;az account showconfirms the sandbox subscription is selected - Identity has Application.ReadWrite.OwnedBy (AAD Graph) + User Access Administrator on the sandbox subscription (needed for sub-scope role assignments)
- Local tooling:
terraform >= 1.6,go >= 1.22,az CLI >= 2.50 -
SNOWOPS_SANDBOX_SUBSCRIPTION_IDandSNOWOPS_SANDBOX_TENANT_IDenv vars set - (Part D only) sandbox GitHub org + repo controlled by the tester; ability to push a workflow file
- Working directory: repo root
Steps
Part A — terraform fmt + validate (offline, ~2 min)
- Confirm formatting + structural validity of the module on its own:
terraform -chdir=modules/azure/client-bootstrap fmt -recursive -check
terraform -chdir=modules/azure/client-bootstrap init -backend=false -input=false
terraform -chdir=modules/azure/client-bootstrap validate
Expected: Success! The configuration is valid.
- Same for the example, which wires both providers + every variable surface:
terraform -chdir=modules/azure/client-bootstrap/examples/basic fmt -check
terraform -chdir=modules/azure/client-bootstrap/examples/basic init -backend=false -input=false
terraform -chdir=modules/azure/client-bootstrap/examples/basic validate
Expected: Success! The configuration is valid.
- Run the B2-targeted offline Terratest:
Expected: --- PASS: TestClientBootstrapValidate.
Part B — full offline Terratest suite + pre-commit (offline, ~3 min)
- Run the whole offline suite to confirm B2 hasn't regressed F0/F1/F2/F3/F4/F5/F6/H1/H2/H3:
Expected: 19 top-level tests pass (the 18 from v0.25 plus
TestClientBootstrapValidate).
- Run pre-commit + pre-push hooks on every new file:
pre-commit run --files \
modules/azure/client-bootstrap/versions.tf \
modules/azure/client-bootstrap/variables.tf \
modules/azure/client-bootstrap/main.tf \
modules/azure/client-bootstrap/outputs.tf \
modules/azure/client-bootstrap/README.md \
modules/azure/client-bootstrap/examples/basic/versions.tf \
modules/azure/client-bootstrap/examples/basic/variables.tf \
modules/azure/client-bootstrap/examples/basic/main.tf \
modules/azure/client-bootstrap/examples/basic/outputs.tf \
tests/terratest/fixtures/client-bootstrap/main.tf \
tests/terratest/fixtures/client-bootstrap/variables.tf \
tests/terratest/fixtures/client-bootstrap/outputs.tf \
tests/terratest/modules/azure/client_bootstrap_validate_test.go \
tests/terratest/modules/azure/client_bootstrap_test.go \
docs/runbooks/test/B2.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 AAD apply + destroy, ~25 min, ~$0)
Skip if iterating on offline changes only. Costs nothing — AAD objects and role assignments are control-plane resources.
- Confirm sandbox env vars (same as F1 / F2 / F3 / F4 / F5):
export SNOWOPS_SANDBOX_SUBSCRIPTION_ID="<sandbox-subscription-guid>"
export SNOWOPS_SANDBOX_TENANT_ID="<sandbox-tenant-guid>"
- Run the B2 integration test:
cd tests/terratest
go test -v -tags integration -timeout 30m ./modules/azure/... -run TestClientBootstrapModule
- Watch for key milestones:
Plan: 8 to add, 0 to change, 0 to destroy.— 1 application + 1 SP + 3 federated credentials + 1 sub-scope role assignment + 1 ACR role assignment + 1 KV crypto role + 1 KV cert role + 1 extra (rg-reader) = 10. (Numbers will vary if Terraform inlines preconditions differently.)azuread_application.this: Creation complete after ~5s— fast.- Each
azuread_application_federated_identity_credential.this[...]: Creation complete— sub-second per cred. azurerm_role_assignment.this[...]: Creation complete— ~5-30s per role assignment as ARM propagates.- All output assertions PASS.
-
Destroy complete!— clean teardown (LIFO: role assignments → fed creds → SP → app). -
(Spot-check) Inspect the app registration in the AAD portal before destroy fires (use a separate terminal during the test sleep):
# Find the app reg by display name prefix
az ad app list --display-name-starts-with snowops-b2-test --query '[].{name:displayName, appId:appId, id:id}' -o table
# Inspect its federated credentials
az ad app federated-credential list --id <appId>
Expected: 3 federated credentials with the standard SnowOps subjects.
Part D — live OIDC round-trip from a real GitHub repo (optional, ~20 min, ~$0)
Verifies the GH-side trust path end-to-end against a real workflow. Skip if you're only validating module mechanics.
-
Stand the fixture up manually (so it isn't auto-destroyed by the test):
cd /tmp && mkdir b2-probe && cd b2-probe cat > main.tf <<'EOF' terraform { required_version = ">= 1.6.0, < 2.0.0" required_providers { azuread = { source = "hashicorp/azuread"; version = "~> 3.0" } azurerm = { source = "hashicorp/azurerm"; version = "~> 4.0" } } } provider "azuread" { tenant_id = var.tenant_id } provider "azurerm" { subscription_id = var.subscription_id tenant_id = var.tenant_id features {} } variable "subscription_id" { type = string } variable "tenant_id" { type = string } variable "github_org" { type = string } variable "github_repo" { type = string } module "probe" { source = "<absolute path to repo>/modules/azure/client-bootstrap" display_name = "snowops-b2-probe" federated_credentials = { "main-branch" = { subject = "repo:${var.github_org}/${var.github_repo}:ref:refs/heads/main" } } subscription_id = var.subscription_id subscription_role_assignments = ["Reader"] } output "secrets" { value = module.probe.gh_oidc_secrets } EOF terraform init terraform apply -var subscription_id=$SNOWOPS_SANDBOX_SUBSCRIPTION_ID \ -var tenant_id=$SNOWOPS_SANDBOX_TENANT_ID \ -var github_org=<sandbox-org> \ -var github_repo=<sandbox-repo> -
Read the secrets values from the output:
Paste each into the sandbox GitHub repo's Settings → Secrets and variables → Actions: -
AZURE_CLIENT_ID-AZURE_TENANT_ID-AZURE_SUBSCRIPTION_ID -
Commit a probe workflow to
mainin the sandbox repo:# .github/workflows/b2-probe.yml name: b2-probe on: push: branches: [main] workflow_dispatch: permissions: id-token: write contents: read jobs: probe: runs-on: ubuntu-latest steps: - uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - run: az account show - run: az group list -o tableExpected:
azure/login@v2succeeds (federation works);az account showprints the sandbox sub;az group listsucceeds (Reader role landed). -
Tear down:
Pass criteria
- Part A —
terraform validatepasses for both module and example - Part A —
TestClientBootstrapValidatepasses - Part B — full offline Terratest suite passes (19+ top-level tests)
- Part B — pre-commit + pre-push hooks all green
- Part C —
TestClientBootstrapModuleintegration test passes end-to-end - Part C — app registration visible via
az ad app list - Part C — 3 federated credentials visible via
az ad app federated-credential list - Part C — no
azuread_application_password/azuread_service_principal_passwordresources in the plan output (federation only) - Part C — destroy succeeds;
az ad app list --display-name-starts-with snowops-b2-testreturns empty - (Part D)
azure/login@v2succeeds from a real GH workflow - (Part D)
az account showprints the sandbox subscription - (Part D)
az group listsucceeds (Reader role landed)
Teardown
The integration test runs terraform destroy automatically. If a failure
mid-run orphans AAD objects:
# Find leftover app regs
az ad app list --display-name-starts-with snowops-b2-test --query '[].{name:displayName, appId:appId}' -o table
# Hard-delete each (no soft-delete window for AAD apps)
az ad app delete --id <appId>
Role assignments on the subscription are cleaned by the app deletion (AAD cascades the principal removal). Spot-check with:
Expected: empty.
Sign-off
- Tester: _ | Date: _ | Result: PASS / FAIL / N/A
- Notes: