Skip to content

CR: Nonprod Resource Group Restructure

Jira: HM-8556 Status: In Progress Scheduled: TBD — phases 0–4 can proceed without a maintenance window; phase 5 requires one Owner: Leon Geldenhuys Risk: MEDIUM (phases 0–4 are LOW; phase 5 is HIGH) | Duration: ~7–9 working days | Rollback: varies per phase

Summary

rg-eshars-nonprod has grown to 222 resources across 41 resource types and 5 Azure regions — a “junk drawer” that mixes VMs, SQL servers, networking, app services, key vaults, storage, monitoring, and unrelated sandbox resources. This makes RBAC too broad, cost reporting opaque, and deployments risky (any ARM deployment targets the whole group).

This change splits rg-eshars-nonprod into purpose-built resource groups following the Microsoft Cloud Adoption Framework pattern of grouping by workload tier + environment. Target structure aligns with the Landing Zone Architecture — shared networking lives in the hub RG (rg-eshars-core-np); there is no per-environment -network RG. Prod is out of scope for now; ad-hoc RGs (rg-eshars-nonprod-centralUS, etc.) are left in place.

Target Structure

New Resource GroupContents~Count
rg-eshars-nonprod-data-cusSQL servers + databases, elastic pools, SQL private endpoints, Data Factory~34
rg-eshars-nonprod-compute-cusVMs, NICs, disks, VM extensions (application compute only)~62
rg-eshars-nonprod-ops-cusAutomation accounts, WAC gateway VMs (vm-ops-nonprod), hybrid workers, ops tooling~4
rg-eshars-nonprod-web-cusApp Service plans, web apps + slots, Function Apps + slots, App Insights, Log Analytics~39
rg-eshars-nonprod-security-cusKey Vaults, managed identities, certificates, KV private endpoints~12
rg-eshars-nonprod-storage-cusStorage accounts, blob private endpoints~12
rg-eshars-nonprod-monitoring-cusMetric alerts, action groups, dashboards, autoscale settings~15
rg-eshars-nonprod-backup-cusRecovery Services vaults~2

Scope

Resource GroupChangePhase
rg-eshars-nonprodSource — resources migrated out over phases 0–6All
rg-eshars-nonprod-data-cusNew — receives SQL + storage-adjacent resources3
rg-eshars-nonprod-compute-cusNew — receives application VMs + disks6
rg-eshars-nonprod-ops-cusNew — receives automation accounts, WAC gateway VM, ops tooling1
rg-eshars-nonprod-web-cusNew — receives App Services + Functions + App Insights4
rg-eshars-nonprod-security-cusNew — receives Key Vaults + managed identities1
rg-eshars-nonprod-storage-cusNew — receives storage accounts2
rg-eshars-nonprod-monitoring-cusNew — receives alerts + dashboards1
rg-eshars-nonprod-backup-cusNew — receives Recovery Services vaults6
rg-eshars-nonprod-network-cusRetire — migrate DNS zones to rg-eshars-core-np, then delete5

Resources requiring team decision before proceeding:

ResourceIssue
db-eshars-nonprod/db-nursing-prod, /ep-eshars-prodProd databases on a nonprod SQL server — clarify intent before Phase 3
eshars-prod-central-sp01, ASP-eshars-prod-CentralUsProd App Service plan in nonprod RG — move to rg-eshars-prod
vm-tmhp-prod-2-nsg, vm-tmhp-prod-2151Prod VM resources in nonprod RG — move to rg-eshars-prod
devops24, DevOps-24SQL + VM — confirm still in use or delete
interviewapiacr, authentica-interview, wa-hellowrld-test, kv-flask-app-npNon-eshars/sandbox resources — move to rg-authentica-sandbox or delete
WindowsServer*UpgradeDisk*, vm-sftp-nonprod-001-temp-nic, stale snapshotsOrphaned — confirm and delete

Prerequisites

  • RBAC audit: who has role assignments scoped to rg-eshars-nonprod? (These must be re-scoped to new RGs)
  • Terraform state strategy agreed: terraform state mv vs. re-import after each phase
  • Team decisions on prod-in-nonprod resources (see table above)
  • Maintenance window agreed for phases 5 and 7 (Bastion, VPN GW, App GW)
  • All Azure DevOps pipeline service connections scoped to rg-eshars-nonprod identified
  • Azure portal access and az CLI authenticated to correct subscription

Procedure

Pre-checks (all phases)

Terminal window
# Confirm current resource count before starting
az resource list --resource-group rg-eshars-nonprod \
--subscription 9639039c-86c6-4859-b014-d28688863cd5 \
--query "length(@)" -o tsv
Terminal window
# Run prerequisite audit
.\eshars-ops\develop\scripts\setup-az-prerequisites.ps1 -Audit

Phase 0 — Cleanup (1–2 days, zero downtime)

Goal: Remove orphaned and misplaced resources. No moves, no file changes.

  1. Audit orphaned resources (temp NICs, upgrade disks, old snapshots):
    Terminal window
    az resource list --resource-group rg-eshars-nonprod \
    --subscription 9639039c-86c6-4859-b014-d28688863cd5 \
    --query "[?contains(name,'temp') || contains(name,'Upgrade') || contains(name,'Snapshot')].{name:name,type:type}" -o table
  2. Get team confirmation — review the list, mark resources safe to delete
  3. Delete confirmed orphans:
    Terminal window
    az resource delete --ids <resource-id> --subscription 9639039c-86c6-4859-b014-d28688863cd5
  4. Move sandbox resources (devops24, interviewapiacr, authentica-interview, wa-hellowrld-test, kv-flask-app-np) to rg-authentica-sandbox or delete
  5. Move confirmed prod resources (vm-tmhp-prod-2*, eshars-prod-central-sp01, ASP-eshars-prod-CentralUs) to rg-eshars-prod

Verification: Re-run resource count — number should be meaningfully lower.

Rollback: N/A — deletions of confirmed orphans are intentional. Prod-resource moves can be reversed with another az resource move.


Phase 1 — Monitoring & Security (1 day, zero downtime)

Goal: Move loosely coupled resources first — alerts and Key Vaults don’t affect runtime traffic.

  1. Create target resource groups:

    Terminal window
    az group create --name rg-eshars-nonprod-monitoring --location centralus --subscription 9639039c-86c6-4859-b014-d28688863cd5
    az group create --name rg-eshars-nonprod-security --location centralus --subscription 9639039c-86c6-4859-b014-d28688863cd5
  2. Move metric alerts, smart detection rules, action groups, dashboards, autoscale settings:

    Terminal window
    az resource move --destination-group rg-eshars-nonprod-monitoring \
    --ids <space-separated resource IDs> \
    --destination-subscription-id 9639039c-86c6-4859-b014-d28688863cd5
  3. Move Key Vaults and managed identities to rg-eshars-nonprod-security:

    ⚠️ Managed identity resource IDs change — managed identities get a new resource ID when moved. Any App Service with this managed identity configured as its identity must be updated — go to App Service → Identity → User-assigned and re-select the identity after the move.

  4. Verify KV URIs are unchanged (apps reference KV by DNS name, not RG):

    Terminal window
    az keyvault show --name kv-eshars-mobile-keys --query "properties.vaultUri" -o tsv

Files to update: eshars-ops/develop/infrastructure/core/envs/nonprod.tfvars if KV resource groups are parameterized.

Verification:

  • Alert rules appear in rg-eshars-nonprod-monitoring
  • Key Vaults appear in rg-eshars-nonprod-security
  • App Services can still read secrets from moved Key Vaults (check App Service logs / App Insights for 401/403)
  • Managed identity still assigned on App Services that use it

Rollback: Move resources back to rg-eshars-nonprod. KV URI is DNS-based — no app config changes needed.


Phase 1b — Ops Tooling (0.5 day, zero downtime)

Goal: Create rg-eshars-nonprod-ops and move automation accounts + WAC gateway VM into it. Run alongside or immediately after Phase 1 (same risk tier — no runtime traffic impact).

Note: vm-ops-nonprod is moved here (not to rg-eshars-nonprod-compute) because it is management-plane infrastructure (WAC gateway), not application compute.

  1. Create target resource group:

    Terminal window
    az group create --name rg-eshars-nonprod-ops --location centralus \
    --subscription 9639039c-86c6-4859-b014-d28688863cd5
  2. Create aa-eshars-nonprod automation account in the new RG:

    Terminal window
    az automation account create \
    --name aa-eshars-nonprod \
    --resource-group rg-eshars-nonprod-ops \
    --location centralus \
    --assign-identity SystemAssigned
  3. Create nonprod variants of the cleanup automation accounts:

    JobLogsCleanup and PeriodicCleanup currently live in rg-eshars-prod and contain both _PROD and _QA/_Regression/_Training/_UAT runbooks. We split them rather than move — prod runbooks stay in the prod accounts, nonprod runbooks go to new accounts.

    Terminal window
    az automation account create \
    --name JobLogsCleanup-Nonprod \
    --resource-group rg-eshars-nonprod-ops \
    --location centralus \
    --assign-identity SystemAssigned
    az automation account create \
    --name PeriodicCleanup-Nonprod \
    --resource-group rg-eshars-nonprod-ops \
    --location centralus \
    --assign-identity SystemAssigned

    Then upload nonprod runbooks from git (the deploy-runbooks.yml pipeline handles this automatically on push — directories JobLogsCleanup-Nonprod/ and PeriodicCleanup-Nonprod/ map to the new accounts in rg-eshars-nonprod-ops):

    Terminal window
    # Or manually:
    for rb in CleanRecords_QA CleanRecords_Regression CleanRecords_Training CleanRecords_UAT; do
    for account in JobLogsCleanup-Nonprod PeriodicCleanup-Nonprod; do
    src="JobLogsCleanup-Nonprod"
    [[ "$account" == "PeriodicCleanup-Nonprod" ]] && src="PeriodicCleanup-Nonprod"
    az automation runbook create -g rg-eshars-nonprod-ops \
    --automation-account-name "$account" --name "$rb" --type PowerShell72
    az automation runbook replace-content -g rg-eshars-nonprod-ops \
    --automation-account-name "$account" --name "$rb" \
    --content @"scripts/azure-runbooks/$src/$rb.ps1"
    az automation runbook publish -g rg-eshars-nonprod-ops \
    --automation-account-name "$account" --name "$rb"
    done
    done

    Finally, remove the nonprod runbooks from the prod accounts (they now live in the nonprod accounts):

    Terminal window
    for rb in CleanRecords_QA CleanRecords_Regression CleanRecords_Training CleanRecords_UAT; do
    az automation runbook delete -g rg-eshars-prod --automation-account-name JobLogsCleanup --name "$rb" -y
    az automation runbook delete -g rg-eshars-prod --automation-account-name PeriodicCleanup --name "$rb" -y
    done
  4. Move vm-ops-nonprod (WAC gateway VM) along with its NIC and OS disk:

    Terminal window
    az vm show --name vm-ops-nonprod --resource-group rg-eshars-nonprod \
    --query "{vmId:id, osDiskId:storageProfile.osDisk.managedDisk.id, nicIds:networkProfile.networkInterfaces[].id}" -o json
    az resource move --destination-group rg-eshars-nonprod-ops \
    --ids <vm-id> <nic-id> <os-disk-id> \
    --destination-subscription-id 9639039c-86c6-4859-b014-d28688863cd5
  5. Update deploy-runbooks.yml ACCOUNT_RG_MAP to point JobLogsCleanup and PeriodicCleanup at rg-eshars-nonprod-ops (see Files to update below).

Files to update:

  • .github/workflows/deploy-runbooks.yml — add per-account RG map, add aa-eshars-nonprod entry
  • scripts/azure-runbooks/docs/jit-vm-access.md — update RG references

Verification:

  • az automation account show -g rg-eshars-nonprod-ops -n JobLogsCleanup returns account
  • az automation account show -g rg-eshars-nonprod-ops -n PeriodicCleanup returns account
  • az automation account show -g rg-eshars-nonprod-ops -n aa-eshars-nonprod returns account
  • vm-ops-nonprod accessible via Bastion after move
  • Existing runbook schedules still firing (check job history next day)
  • deploy-runbooks.yml pipeline passes on a test push

Rollback: Move accounts back to rg-eshars-nonprod with another az resource move.


Phase 2 — Storage (0.5 day, zero downtime)

Goal: Move storage accounts. Connection strings don’t include RG names so no app reconfiguration needed.

  1. Create target resource group:

    Terminal window
    az group create --name rg-eshars-nonprod-storage --location centralus --subscription 9639039c-86c6-4859-b014-d28688863cd5
  2. Move storage accounts + blob private endpoints:

    Terminal window
    az resource move --destination-group rg-eshars-nonprod-storage \
    --ids <storage account resource IDs> \
    --destination-subscription-id 9639039c-86c6-4859-b014-d28688863cd5

Verification:

  • Storage accounts appear in rg-eshars-nonprod-storage
  • Function App storage bindings still work (eshars-functions-nonprod)
  • Diagnostic settings still flow to diagnostic storage accounts
  • No storage-related errors in App Insights

Rollback: Move storage accounts back to rg-eshars-nonprod.


Phase 3 — Data Tier (1 day, ~30s read-only lock per SQL server)

Goal: Move SQL servers and databases. Apps connect by FQDN — no connection string changes needed.

  1. Create target resource group:

    Terminal window
    az group create --name rg-eshars-nonprod-data --location centralus --subscription 9639039c-86c6-4859-b014-d28688863cd5
  2. Move one SQL server at a time (moving a server moves all its databases automatically):

    Terminal window
    # Get the SQL server resource ID
    az sql server show --name db-eshars-nonprod --resource-group rg-eshars-nonprod \
    --subscription 9639039c-86c6-4859-b014-d28688863cd5 --query id -o tsv
    az resource move --destination-group rg-eshars-nonprod-data \
    --ids <sql-server-resource-id> \
    --destination-subscription-id 9639039c-86c6-4859-b014-d28688863cd5
  3. Repeat for remaining SQL servers (db-eshars-nonprod-west, db-eshars-developeraccess, sqlvaxaodxhnlxdnju)

  4. Move SQL private endpoints to rg-eshars-nonprod-data

  5. Move Data Factory (qaesharsdatafactory)

  6. Update Azure Runbooks — each runbook references rg-eshars-nonprod for SQL operations:

    • eshars-ops/develop/scripts/azure-runbooks/aa-eshars-prod/ — 12+ files — update ResourceGroupName parameters
  7. Update eshars-ops/develop/pipelines/environments/*.yml — update deployment target resource group

  8. Update eshars-ops/develop/infrastructure/core/envs/nonprod.tfvars

Verification:

  • SQL servers appear in rg-eshars-nonprod-data
  • All apps connect to databases (dev, qa, uat, trn, regression)
  • Azure Automation Runbooks execute successfully (test a DB copy job)
  • Azure DevOps pipeline deploys to at least one environment

Rollback: Move SQL servers back to rg-eshars-nonprod. Revert runbook and pipeline file changes.


Phase 4 — Web Tier (1 day, zero downtime)

Goal: Move App Services and Functions. Apps keep running during the move.

  1. Create target resource group:

    Terminal window
    az group create --name rg-eshars-nonprod-web --location centralus --subscription 9639039c-86c6-4859-b014-d28688863cd5
  2. Move App Service plans, web apps + slots, Function Apps + slots, App Insights:

    Terminal window
    az resource move --destination-group rg-eshars-nonprod-web \
    --ids <space-separated resource IDs> \
    --destination-subscription-id 9639039c-86c6-4859-b014-d28688863cd5

    Note: move the App Service plan first — web apps in a plan must move with it.

  3. Update deployment pipelines:

    • eshars-ops/develop/pipelines/environments/dev.yml
    • eshars-ops/develop/pipelines/environments/qa.yml
    • eshars-ops/develop/pipelines/environments/uat.yml
    • eshars-ops/develop/pipelines/environments/training.yml
    • eshars-ops/develop/pipelines/environments/regression.yml
    • eshars-ops/develop/pipelines/hisd-release.yml

Verification:

  • All environment URLs respond: dev, qa, uat, trn, regression
  • App Insights telemetry flowing for all environments
  • Azure DevOps pipeline deploys to all environments successfully

Rollback: Move App Service plans + apps back to rg-eshars-nonprod. Revert pipeline file changes.


Phase 5 — Networking cleanup (1 day, maintenance window for Bastion deploy)

Goal: Retire rg-eshars-nonprod-network-cus (DNS-only legacy RG), link the hub’s SQL DNS zone, delete orphaned VNet, and deploy Bastion in the hub.

Step 1 — Fix orphaned SQL DNS zone (zero downtime)

The hub’s privatelink.database.windows.net zone has records but no VNet links — nothing resolves via it today.

Terminal window
# Link to hub VNet
az network private-dns link vnet create \
--resource-group rg-eshars-core-np \
--zone-name privatelink.database.windows.net \
--name link-to-hub-vnet \
--virtual-network vnet-eshars-core-np \
--registration-enabled false
# Link to legacy spoke VNet (so spoke resources also resolve via hub zone)
az network private-dns link vnet create \
--resource-group rg-eshars-core-np \
--zone-name privatelink.database.windows.net \
--name link-to-np-spoke \
--virtual-network /subscriptions/9639039c-86c6-4859-b014-d28688863cd5/resourceGroups/rg-eshars-nonprod/providers/Microsoft.Network/virtualNetworks/rg-eshars-nonprod \
--registration-enabled false

Step 2 — Migrate DNS records from legacy RG (zero downtime)

rg-eshars-nonprod-network-cus holds 2 zones linked to legacy eastus2 VNets. Migrate their A records to the hub zones, then delete.

Terminal window
# List records to migrate
az network private-dns record-set a list \
--resource-group rg-eshars-nonprod-network-cus \
--zone-name privatelink.database.windows.net \
--query "[].{name:name, ip:aRecords[0].ipv4Address}" -o table
# For each record, add it to the hub zone (verify no duplicate first)
az network private-dns record-set a add-record \
--resource-group rg-eshars-core-np \
--zone-name privatelink.database.windows.net \
--record-set-name <name> \
--ipv4-address <ip>
# After verifying all records migrated:
az network private-dns zone delete \
--resource-group rg-eshars-nonprod-network-cus \
--name privatelink.database.windows.net --yes
# Repeat for privatelink.blob.core.windows.net (create zone in hub RG first if needed)
# Delete the now-empty RG
az group delete --name rg-eshars-nonprod-network-cus --yes

Step 3 — Delete orphaned VNet (zero downtime)

rg-eshars-nonprod-vnet (10.5.0.0/24, southcentralus) is in the wrong region, has no peerings, no resources, no delegation. Safe to delete.

Terminal window
az network vnet delete \
--resource-group rg-eshars-nonprod \
--name rg-eshars-nonprod-vnet

Step 4 — Deploy Bastion in hub (maintenance window: ~5 min)

The hub’s AzureBastionSubnet is already sized (10.20.5.0/26). Deploy a Basic or Standard Bastion host there. Decommission the legacy Bastion per 2026-04-17 Nonprod Bastion Decommission.

Terminal window
# Create public IP for Bastion
az network public-ip create \
--resource-group rg-eshars-core-np \
--name pip-bastion-eshars-core-np \
--sku Standard \
--location centralus
# Deploy Bastion host
az network bastion create \
--resource-group rg-eshars-core-np \
--name bastion-eshars-core-np \
--vnet-name vnet-eshars-core-np \
--public-ip-address pip-bastion-eshars-core-np \
--location centralus \
--sku Standard

Update scripts to reference new Bastion RG:

  • eshars-ops/develop/scripts/bastion/bastion-rdp-nonprod.ps1$BastionRG = "rg-eshars-core-np", $BastionName = "bastion-eshars-core-np"
  • eshars-ops/develop/scripts/bastion/bastion-rdp-nonprod.sh
  • eshars-ops/develop/scripts/setup-az-prerequisites.ps1$BastionRG

Verification:

  • nslookup sql-eshars-nonprod-cus.database.windows.net from within VNet resolves to private IP (not public)
  • rg-eshars-nonprod-network-cus RG no longer exists
  • rg-eshars-nonprod-vnet VNet no longer exists
  • bastion-rdp-nonprod.ps1 -List shows all VMs
  • RDP tunnel to vm-ops-nonprod succeeds via hub Bastion
  • setup-az-prerequisites.ps1 -Audit passes all checks

Rollback: DNS zone migration — re-link the old rg-eshars-nonprod-network-cus zones (recreate VNet links). Bastion — if hub Bastion fails, deploy a Basic SKU in the legacy RG temporarily.


Phase 6 — Compute (1 day, zero downtime)

Goal: Move VMs. Azure supports live cross-RG VM moves — VMs keep running.

  1. Create target resource groups:

    Terminal window
    az group create --name rg-eshars-nonprod-compute --location centralus --subscription 9639039c-86c6-4859-b014-d28688863cd5
    az group create --name rg-eshars-nonprod-backup --location centralus --subscription 9639039c-86c6-4859-b014-d28688863cd5
  2. Move each VM along with its NIC, OS disk, data disks, and extensions (Azure requires all associated resources in the same move operation):

    Terminal window
    # Get VM ID, OS disk ID, managed data disk IDs, and NIC IDs
    az vm show --name vm-ops-nonprod --resource-group rg-eshars-nonprod \
    --subscription 9639039c-86c6-4859-b014-d28688863cd5 \
    --query "{vmId:id, osDiskId:storageProfile.osDisk.managedDisk.id, dataDiskIds:storageProfile.dataDisks[].managedDisk.id, nicIds:networkProfile.networkInterfaces[].id}" -o json
    # Get VM extension resource IDs (must be included in the same move)
    az vm extension list --vm-name vm-ops-nonprod --resource-group rg-eshars-nonprod \
    --subscription 9639039c-86c6-4859-b014-d28688863cd5 \
    --query "[].id" -o json
    az resource move --destination-group rg-eshars-nonprod-compute \
    --ids <vm-id> <nic-ids> <os-disk-id> <data-disk-ids> <extension-ids> \
    --destination-subscription-id 9639039c-86c6-4859-b014-d28688863cd5
  3. Repeat for each application VM: vm-sql16-np, vm-cleanup-jobs, vm-jobscheduler-np-001, vm-jobscheduler, vm-db-api, vm-successed-nonprod Note: vm-ops-nonprod was already moved to rg-eshars-nonprod-ops in Phase 1b.

  4. Move Recovery Services vaults to rg-eshars-nonprod-backup

  5. Update VM RG values in bastion scripts:

    • eshars-ops/develop/scripts/bastion/bastion-rdp-nonprod.ps1 — update each RG = value in the $VMs hash to "rg-eshars-nonprod-compute"
    • eshars-ops/develop/scripts/bastion/bastion-rdp-nonprod.sh
    • eshars-ops/develop/scripts/setup-az-prerequisites.ps1 — update nonprodVMs RG values

Verification:

  • VMs running and accessible via Bastion (bastion-rdp-nonprod.ps1 vm-ops-nonprod)
  • setup-az-prerequisites.ps1 -Audit VNet alignment checks all pass
  • Backup status shows healthy in Recovery Services vaults
  • Azure Monitor agents (AMA, Dependency Agent) still reporting

Rollback: Move VMs back to rg-eshars-nonprod with another az resource move. Revert bastion script changes.


Files Updated Across All Phases

FilePhasesChange
eshars-ops/develop/infrastructure/core/envs/nonprod.tfvars1–6RG name references
eshars-ops/develop/pipelines/environments/dev.yml3, 4Deployment target RG
eshars-ops/develop/pipelines/environments/qa.yml3, 4Deployment target RG
eshars-ops/develop/pipelines/environments/uat.yml3, 4Deployment target RG
eshars-ops/develop/pipelines/environments/training.yml3, 4Deployment target RG
eshars-ops/develop/pipelines/environments/regression.yml3, 4Deployment target RG
eshars-ops/develop/pipelines/hisd-release.yml4Deployment target RG
eshars-ops/develop/scripts/bastion/bastion-rdp-nonprod.ps15, 6$BastionRG = "rg-eshars-core-np" + VM $RG values
eshars-ops/develop/scripts/bastion/bastion-rdp-nonprod.sh5, 6Bastion RG + VM RG values
eshars-ops/develop/scripts/setup-az-prerequisites.ps15, 6$BastionRG + nonprodVMs RG values
eshars-ops/develop/scripts/azure-runbooks/ (12+ files)3SQL server RG references
eshars-docs/develop/ops/docs/platform/infrastructure/architecture-shared-core.md5Landing Zone Architecture — updated to reflect end-state

Verification (End-to-End)

Run after all phases are complete:

  • az resource list --resource-group rg-eshars-nonprod — should be near-empty (only unclassified leftovers)
  • az group show --name rg-eshars-nonprod-network-cus — should return 404 (deleted)
  • All eshars environments respond: dev, qa, uat, trn, regression
  • nslookup sql-eshars-nonprod-cus.database.windows.net from within VNet resolves to private IP
  • bastion-rdp-nonprod.ps1 -List shows all VMs with correct ports
  • setup-az-prerequisites.ps1 -Audit passes all checks
  • Azure DevOps pipelines deploy to all environments
  • Azure Automation Runbooks execute (DB copy/scrub test)
  • terraform plan shows no unexpected drift (update state as needed)
  • Azure Cost Management shows resources attributed to new RGs

Risk Assessment

RiskLikelihoodImpactMitigation
Hub Bastion deploy failsLowLow — fall back to legacy Bastion temporarilyDeploy Basic SKU first; Standard SKU only after verifying
DNS zone migration causes resolution gapLowMedium — brief private endpoint downtime during cutoverKeep old zone VNet links until hub zone is verified working; remove old zone only after
VPN Gateway transit broken during peering changeLowMedium — VPN tunnel downVPN GW and peering are already configured; Phase 5 only adds DNS links, not peering changes
SQL read-only lock causes app errorsLowLow — ~30s transientOff-hours scheduling; brief blip, not an outage
Managed identity re-assignment missedLowMedium — KV access brokenVerify each App Service identity binding after Phase 1
Azure DevOps pipeline deploys to wrong RGMediumLow — deployment fails, no data lossUpdate pipeline files before first post-Phase-4 pipeline run
Terraform drift after movesHighLow — plan/apply mismatchAgree on state strategy (mv vs. import) before starting

Outcome

Phase 0 Completed:Phase 1 Completed:Phase 2 Completed:Phase 3 Completed:Phase 4 Completed:Phase 5 Completed:Phase 6 Completed:Phase 7 Completed:Final State:Notes: