Skip to content

Workload Network — Spoke VNet for Workload VMs

Overview

A dedicated workload spoke VNet that sits alongside the core hub VNets but is purpose-built for VMs that need controlled outbound internet access or isolated network posture. The first tenant is the TMHP SFTP VM; other workload VMs with similar requirements will join the same spoke.

This is distinct from the core hub (rg-eshars-core-prod) which carries shared platform infrastructure (VPN Gateway, App Gateway, Bastion). The workload spoke carries only workload VM subnets.

Design

flowchart TB
    VPNUser(["👤 VPN client"])

    subgraph hub["rg-eshars-core-prod — hub"]
        HubVNet["vnet-eshars-core\n10.10.0.0/16\nVPN Gateway · App GW · Bastion · Private DNS zones"]
    end

    subgraph spoke["rg-eshars-prod-network-cus — workload spoke"]
        SpokeVNet["vnet-eshars-prod-cus\n10.11.0.0/16"]
        VmsSubnet["snet-vms-vnet-eshars-prod-cus\n10.11.1.0/24"]
        SpokeVNet --- VmsSubnet
    end

    subgraph legacy["rg-eshars-prod — legacy spoke 🔴 decommissioning"]
        LegacyVNet["rg-eshars-prod VNet\n10.0.0.0/16"]
        TmhpOld["vm-tmhp-prod\n(to be decommissioned)"]
        LegacyVNet --- TmhpOld
    end

    VPNUser -->|"P2S VPN"| HubVNet
    HubVNet <-->|"peering\nallowGatewayTransit ↔ useRemoteGateways"| SpokeVNet
    SpokeVNet <-.->|"peering (legacy, temporary)"| LegacyVNet

VPN clients connected to the hub gateway get routing to 10.11.0.0/16 via the hub → spoke peering.

Resource Inventory

ResourceProdNonprod
Resource Grouprg-eshars-prod-network-cusrg-eshars-nonprod-network-cus
VNetvnet-eshars-prod-cusvnet-eshars-nonprod-cus
VNet CIDR10.11.0.0/1610.22.0.0/16
VMs Subnetsnet-vms-vnet-eshars-prod-cussnet-vms-vnet-eshars-nonprod-cus
VMs Subnet CIDR10.11.1.0/2410.22.1.0/24
Subnet NSGnsg-vms-vnet-eshars-prod-cusnsg-vms-vnet-eshars-nonprod-cus
Hub peering (spoke→hub)peer-vnet-eshars-prod-cus-to-vnet-eshars-corepeer-vnet-eshars-nonprod-cus-to-vnet-eshars-core-np
Hub peering (hub→spoke)peer-vnet-eshars-core-to-vnet-eshars-prod-cuspeer-vnet-eshars-core-np-to-vnet-eshars-nonprod-cus

Subnet NSG

The subnet NSG is a broad allow for VPN inbound — individual VMs apply their own tighter NIC-level NSGs on top.

RulePriorityDirectionActionProtocolPortSourceDestination
Allow-VPN-Inbound100InboundAllow**172.16.201.0/24*
Deny-All-Inbound4096InboundDeny****

VNet Peerings

Spoke ↔ Hub (core)

Both legs are managed in the workload-network Terraform root so both sides live in the same state file.

Leguse_remote_gatewaysallow_gateway_transitEffect
Spoke → HubtrueVPN routes from hub gateway flow into spoke
Hub → SpoketrueHub gateway advertises routes into spoke

Spoke ↔ Legacy (prod only, temporary)

Peering to the legacy rg-eshars-prod VNet. Retained during migration so vm-tmhp-prod and vm-tmhp-sftp-prod-cus can communicate. Remove once vm-tmhp-prod is decommissioned.

The spoke is linked to the centralized private DNS zones in the core hub RG so private endpoints in the spoke resolve correctly.

ZoneLinked from
privatelink.database.windows.netCore hub RG
privatelink.vaultcore.azure.netCore hub RG
privatelink.redis.cache.windows.netCore hub RG

The VNet link name convention is link-spoke-{zone-with-dots-replaced-by-dashes}.

Service Endpoints

The VMs subnet has Microsoft.Sql service endpoint enabled. This allows the SQL VNet rule on db-eshars-prod to allowlist this subnet for direct SQL access without a private endpoint.

IaC File Structure

infrastructure/workload-network/
├── main.tf # RG, VNet, subnet, NSG, peerings (both legs), DNS zone links
├── variables.tf
├── locals.tf
├── outputs.tf # vnet_id, vnet_name, vms_subnet_id, vms_subnet_name, vms_subnet_cidr
└── environments/
├── prod.tfvars
├── prod.backend.hcl # key: workload-network-prod.tfstate
├── nonprod.tfvars
└── nonprod.backend.hcl # key: workload-network-nonprod.tfstate

Deployment

Terminal window
just init-wn prod
just plan-wn prod
just apply-wn prod

Cleanup — Legacy Peering Removal

Once vm-tmhp-prod is decommissioned, remove the legacy peering:

  1. Delete the legacy_vnet_peerings entry from environments/prod.tfvars
  2. Remove the moved blocks from main.tf for the old peering names (they are no longer needed once the peering is gone)
  3. Run just plan-wn prod to confirm only the two peering resources are destroyed
  4. Apply