Manual Test Runbook — F2: Network Hub
Owner: Sagar | Time: ~45 min | Sandbox: snowops-sandbox-01
Promotes F2 (
modules/azure/network-hub/) from 🟦 Code Complete → 🟩 Shipped. Includes one part that costs ~$5 (Azure Firewall hourly). Skip Part C / D if not iterating on the firewall path.
Prerequisites
- Sandbox subscription access active (PIM activated if required)
-
az logindone;az account showconfirms the sandbox subscription is selected - Identity has Network Contributor on the sandbox subscription
- Local tooling:
terraform >= 1.6,go >= 1.22,az CLI >= 2.50 -
SNOWOPS_SANDBOX_SUBSCRIPTION_IDandSNOWOPS_SANDBOX_TENANT_IDenv vars set - Working directory: repo root
- Azure Firewall quota: at least 1 firewall available in the region
(
az network firewall list-skusconfirms availability; sandbox subs ship with default quota)
Steps
Part A — terraform fmt + validate (offline, ~1 min)
- Confirm formatting + structural validity of the module on its own:
terraform -chdir=modules/azure/network-hub fmt -check
terraform -chdir=modules/azure/network-hub init -backend=false -input=false
terraform -chdir=modules/azure/network-hub validate
Expected: Success! The configuration is valid.
- Confirm the offline Terratest validate suite still passes for the whole azure/ tree (catches accidental fixture drift):
cd tests/terratest
go build ./...
go vet ./...
go test -v -timeout 5m ./modules/azure/... -run 'TestNetworkHubValidate|TestF2NetworkContractConformance|TestContractsRejectBadLiterals'
Expected: 3 top-level tests pass; the network-empty-private-subnets
sub-test under TestContractsRejectBadLiterals is the F2-relevant negative
case.
Part B — full Terratest suite (offline, ~3 min)
- Run the entire offline suite to make sure F2 hasn't regressed F1, F6, or X1:
Expected: ~9 top-level tests pass (TestNoopHarness, TestBaselineValidate,
TestStateBackendValidate, TestSandboxValidate, TestF1ContractConformance,
TestF6ObjectStoreContractConformance, TestF2NetworkContractConformance,
TestNetworkHubValidate, TestContractsRejectBadLiterals with 5 sub-tests).
Part C — integration test (real Azure apply + destroy, ~35 min, ~$5)
Skip if iterating on offline changes only. The Azure Firewall takes ~10 min to create and ~10 min to destroy.
- Export sandbox env vars (same as X2 / F1 / F6):
export SNOWOPS_SANDBOX_SUBSCRIPTION_ID="<sandbox-subscription-guid>"
export SNOWOPS_SANDBOX_TENANT_ID="<sandbox-tenant-guid>"
- Run the F2 integration test (build tag
integration):
cd tests/terratest
go test -v -tags integration -timeout 60m ./modules/azure/... -run TestNetworkHubModule
- Watch for these key milestones in the output:
Plan: ~30 to add, 0 to change, 0 to destroy.— sanity check on plan size (hub vNet + 3 hub subnets + 2 spoke vNets + 4 spoke subnets + 5 NSGs + 5 NSG associations + 2 spoke route tables + 2 routes + 4 route-table associations + 2 peerings + firewall + firewall policy + public IP + 2 private DNS zones + 6 vNet links + RG = ~38 resources).azurerm_firewall.this[0]: Still creating...— expect this for ~10 min; the firewall is the slow path.- All output assertions PASS.
Destroy complete!— clean teardown.
Part D — manual spot-check (optional, ~5 min)
Run while Part C is between InitAndApply and Destroy if you want to confirm live routing / peering / DNS state in the portal or via CLI.
- List the hub vNet's subnets and confirm
AzureFirewallSubnetexists:
az network vnet subnet list \
--resource-group "snowops-f2-test-<suffix>-rg" \
--vnet-name "snowops-f2-test-<suffix>-hub-vnet" \
--query "[].{name:name, prefix:addressPrefix, nsg:networkSecurityGroup.id}" \
--output table
Expected: 3 rows (AzureFirewallSubnet, GatewaySubnet, management).
Only management has a non-null nsg — Azure forbids NSG attachment to
AzureFirewallSubnet and GatewaySubnet.
- Confirm bidirectional peerings on the hub:
az network vnet peering list \
--resource-group "snowops-f2-test-<suffix>-rg" \
--vnet-name "snowops-f2-test-<suffix>-hub-vnet" \
--query "[].{name:name, state:peeringState, remote:remoteVirtualNetwork.id}" \
--output table
Expected: 2 rows (peer-hub-to-apps, peer-hub-to-data), both Connected.
- Confirm the spoke route table forces
0.0.0.0/0to the firewall private IP:
az network route-table route list \
--resource-group "snowops-f2-test-<suffix>-rg" \
--route-table-name "rt-snowops-f2-test-<suffix>-apps-vnet-to-fw" \
--query "[].{name:name, prefix:addressPrefix, nextHop:nextHopType, nextHopIp:nextHopIpAddress}" \
--output table
Expected: one row, addressPrefix = 0.0.0.0/0, nextHopType =
VirtualAppliance, nextHopIpAddress matches the firewall's private IP
from the Terraform output.
-
Confirm Private DNS zone vNet links exist for both spokes:
az network private-dns link vnet list \ --resource-group "snowops-f2-test-<suffix>-rg" \ --zone-name "privatelink.blob.core.windows.net" \ --query "[].{name:name, vnet:virtualNetwork.id, regState:virtualNetworkLinkState}" \ --output tableExpected: 3 rows (
link-hub-*,link-apps-*,link-data-*), allCompleted.
Part E — synthetic egress probe (optional, ~5 min)
This is the §4.F2 stated test ("synthetic ping from spoke to allowed endpoint"). Skip if Parts A–D already gave you confidence. Requires a temp VM in a spoke and
default_allow_egress = true(or a manual firewall rule permitting outbound 53/UDP).
-
After Part C apply but before destroy, edit
tests/terratest/fixtures/network-hub/main.tflocally to setfirewall.default_allow_egress = true, thenterraform applyin the fixture directory to push the temporary allow-all rule. -
Deploy a tiny VM into
apps/workload: -
az vm run-command invoketo ping8.8.8.8and confirm egress:az vm run-command invoke \ --resource-group "snowops-f2-test-<suffix>-rg" \ --name "f2-probe-vm" \ --command-id "RunShellScript" \ --scripts "curl -s --max-time 5 https://api.ipify.org && echo"Expected: the returned public IP matches one of
firewall_public_ipsfrom the Terraform output. That is the proof that spoke traffic egresses via the firewall. -
Delete the probe VM, then
terraform destroywill tear down everything else.
Pass criteria
- Part A —
terraform validatepasses for the module - Part B — full offline Terratest suite passes (9 top-level tests)
- Part C —
TestNetworkHubModuleintegration test passes end-to-end - Hub vNet has 3 subnets including
AzureFirewallSubnet+GatewaySubnet - Both spokes peered to the hub, both
Connected - Each spoke has a route table with
0.0.0.0/0 → VirtualAppliance(<fw-ip>) - Private DNS zones linked to hub + both spokes
- Azure Firewall reachable; private IP non-empty; public IP non-empty
- All
Destroycalls complete without error - No orphaned resource groups remain (verify with
az group list -o table) - All test resources tagged
ephemeral = true(X7 cleanup safety net) - (Part E only) synthetic egress probe returns the firewall public IP
Teardown
The integration test runs terraform destroy automatically. If a failure mid-run
orphans resources, clean up manually:
# RG holds everything F2 creates
az group delete --name "snowops-f2-test-<suffix>-rg" --yes --no-wait
Azure Firewall destroy takes 10+ minutes — let it finish before re-running the test, otherwise the next apply may collide on the public IP allocation.
Sign-off
- Tester: _ | Date: _ | Result: PASS / FAIL / N/A
- Notes: