Skip to content

Private Network Deploy

Use this recipe after 02. First Deploy when the app must reach Azure services through VNet integration, private endpoints, and managed identity.

flowchart TD
    INTERNET[Internet users] -->|HTTPS| APP[App Service\nNode.js 20 LTS]

    subgraph VNET[Virtual Network]
        subgraph INT[Integration subnet\nDelegated to Microsoft.Web/serverFarms]
            APP
        end
        subgraph PE[Private endpoint subnet]
            PE_STORAGE[Private Endpoint: Storage]
            PE_KV[Private Endpoint: Key Vault]
        end
    end

    APP -.->|System-assigned identity| ENTRA[Microsoft Entra ID]
    PE_STORAGE --> STORAGE[Storage Account]
    PE_KV --> KV[Key Vault]
    APP --> DNS[Private DNS zones]

    style APP fill:#0078d4,color:#fff
    style VNET fill:#E8F5E9,stroke:#4CAF50
    style DNS fill:#E3F2FD

Overview

flowchart TD
    A[Prepare network variables] --> B[Create VNet and subnets]
    B --> C[Add App Service VNet integration]
    C --> D[Enable managed identity]
    D --> E[Create private endpoints and DNS links]
    E --> F[Grant RBAC and configure app settings]
    F --> G[Validate connectivity]

Prerequisites

  • Completed 02. First Deploy
  • Azure CLI authenticated with permission to manage networking and RBAC
  • Existing Node.js App Service app
  • App Service plan tier that supports VNet integration

Main Content

Step 1: Set advanced deployment variables

RG="rg-express-tutorial"
LOCATION="koreacentral"
APP_NAME="app-express-tutorial-abc123"
VNET_NAME="vnet-express-tutorial"
INTEGRATION_SUBNET_NAME="snet-appsvc-integration"
PE_SUBNET_NAME="snet-private-endpoints"
STORAGE_NAME="stexpresstutorialabc123"
KEY_VAULT_NAME="kv-express-tutorial-abc123"
Command/Parameter Purpose
RG="rg-express-tutorial" Reuses the resource group that contains the deployed app.
LOCATION="koreacentral" Sets the Azure region for the networking resources.
APP_NAME="app-express-tutorial-abc123" Identifies the target App Service app.
VNET_NAME="vnet-express-tutorial" Names the virtual network used for private connectivity.
INTEGRATION_SUBNET_NAME="snet-appsvc-integration" Names the delegated subnet used by App Service VNet integration.
PE_SUBNET_NAME="snet-private-endpoints" Names the subnet reserved for private endpoints.
STORAGE_NAME="stexpresstutorialabc123" Sets the storage account name used in the example.
KEY_VAULT_NAME="kv-express-tutorial-abc123" Sets the Key Vault name used in the example.

Step 2: Create the VNet and required subnets

az network vnet create --resource-group $RG --name $VNET_NAME --location $LOCATION --address-prefixes 10.0.0.0/16
az network vnet subnet create --resource-group $RG --vnet-name $VNET_NAME --name $INTEGRATION_SUBNET_NAME --address-prefixes 10.0.1.0/24 --delegations Microsoft.Web/serverFarms
az network vnet subnet create --resource-group $RG --vnet-name $VNET_NAME --name $PE_SUBNET_NAME --address-prefixes 10.0.2.0/24 --disable-private-endpoint-network-policies true
Command/Parameter Purpose
az network vnet create Creates the virtual network for the advanced deployment.
--resource-group $RG Places the VNet in the selected resource group.
--name $VNET_NAME Sets the VNet name.
--location $LOCATION Creates the VNet in the selected Azure region.
--address-prefixes 10.0.0.0/16 Defines the VNet CIDR range.
az network vnet subnet create Creates a subnet inside the VNet.
--vnet-name $VNET_NAME Targets the named VNet.
--name $INTEGRATION_SUBNET_NAME Names the delegated integration subnet.
--address-prefixes 10.0.1.0/24 Defines the integration subnet CIDR range.
--delegations Microsoft.Web/serverFarms Delegates the subnet to App Service.
--name $PE_SUBNET_NAME Names the private endpoint subnet.
--address-prefixes 10.0.2.0/24 Defines the private endpoint subnet CIDR range.
--disable-private-endpoint-network-policies true Disables policies that block private endpoint NICs.

Step 3: Integrate the web app with the VNet

az webapp vnet-integration add --resource-group $RG --name $APP_NAME --vnet $VNET_NAME --subnet $INTEGRATION_SUBNET_NAME
Command/Parameter Purpose
az webapp vnet-integration add Routes outbound app traffic through the delegated integration subnet.
--resource-group $RG Selects the resource group containing the app.
--name $APP_NAME Targets the web app to integrate.
--vnet $VNET_NAME Chooses the virtual network used for integration.
--subnet $INTEGRATION_SUBNET_NAME Chooses the delegated integration subnet.

Step 4: Enable system-assigned managed identity

az webapp identity assign --resource-group $RG --name $APP_NAME
Command/Parameter Purpose
az webapp identity assign Enables a system-assigned managed identity for the web app.
--resource-group $RG Selects the resource group containing the app.
--name $APP_NAME Targets the App Service app receiving the identity.

Step 5: Create backend services and private endpoints

az storage account create --resource-group $RG --name $STORAGE_NAME --location $LOCATION --sku Standard_LRS --kind StorageV2
az keyvault create --resource-group $RG --name $KEY_VAULT_NAME --location $LOCATION --sku standard
STORAGE_ID="$(az storage account show --resource-group $RG --name $STORAGE_NAME --query id --output tsv)"
KEY_VAULT_ID="$(az keyvault show --resource-group $RG --name $KEY_VAULT_NAME --query id --output tsv)"
az network private-endpoint create --resource-group $RG --name pe-storage-blob --vnet-name $VNET_NAME --subnet $PE_SUBNET_NAME --private-connection-resource-id $STORAGE_ID --group-id blob --connection-name pe-storage-blob-connection
az network private-endpoint create --resource-group $RG --name pe-keyvault --vnet-name $VNET_NAME --subnet $PE_SUBNET_NAME --private-connection-resource-id $KEY_VAULT_ID --group-id vault --connection-name pe-keyvault-connection
Command/Parameter Purpose
az storage account create Creates a storage account that the app will reach through a private endpoint.
--resource-group $RG Places the storage account in the selected resource group.
--name $STORAGE_NAME Sets the storage account name.
--location $LOCATION Creates the storage account in the selected region.
--sku Standard_LRS Uses standard locally redundant storage.
--kind StorageV2 Creates a general-purpose v2 storage account.
az keyvault create Creates a Key Vault for secret access over private networking.
--name $KEY_VAULT_NAME Sets the Key Vault name.
--sku standard Uses the standard Key Vault service tier.
STORAGE_ID="$(...)" Stores the storage account resource ID in a shell variable.
az storage account show Reads the storage account metadata.
--query id Returns only the resource ID.
--output tsv Formats the ID as plain text for shell assignment.
KEY_VAULT_ID="$(...)" Stores the Key Vault resource ID in a shell variable.
az keyvault show Reads the Key Vault metadata.
az network private-endpoint create Creates a private endpoint in the dedicated subnet.
--name pe-storage-blob Names the storage private endpoint.
--vnet-name $VNET_NAME Places the endpoint in the selected VNet.
--subnet $PE_SUBNET_NAME Uses the subnet reserved for private endpoints.
--private-connection-resource-id $STORAGE_ID Connects the endpoint to the storage account resource.
--group-id blob Targets the Blob service subresource.
--connection-name pe-storage-blob-connection Names the storage private link connection object.
--name pe-keyvault Names the Key Vault private endpoint.
--private-connection-resource-id $KEY_VAULT_ID Connects the endpoint to the Key Vault resource.
--group-id vault Targets the Key Vault private link subresource.
--connection-name pe-keyvault-connection Names the Key Vault private link connection object.
az network private-dns zone create --resource-group $RG --name privatelink.blob.core.windows.net
az network private-dns zone create --resource-group $RG --name privatelink.vaultcore.azure.net
az network private-dns link vnet create --resource-group $RG --zone-name privatelink.blob.core.windows.net --name link-storage-dns --virtual-network $VNET_NAME --registration-enabled false
az network private-dns link vnet create --resource-group $RG --zone-name privatelink.vaultcore.azure.net --name link-keyvault-dns --virtual-network $VNET_NAME --registration-enabled false
az network private-endpoint dns-zone-group create --resource-group $RG --endpoint-name pe-storage-blob --name storage-zone-group --private-dns-zone privatelink.blob.core.windows.net --zone-name blob
az network private-endpoint dns-zone-group create --resource-group $RG --endpoint-name pe-keyvault --name keyvault-zone-group --private-dns-zone privatelink.vaultcore.azure.net --zone-name vault
Command/Parameter Purpose
az network private-dns zone create Creates a private DNS zone for private endpoint name resolution.
--resource-group $RG Places the DNS zone in the selected resource group.
--name privatelink.blob.core.windows.net Creates the private DNS zone for Azure Storage blob endpoints.
--name privatelink.vaultcore.azure.net Creates the private DNS zone for Key Vault endpoints.
az network private-dns link vnet create Links a private DNS zone to the VNet.
--zone-name privatelink.blob.core.windows.net Selects the storage private DNS zone.
--name link-storage-dns Names the storage DNS VNet link.
--virtual-network $VNET_NAME Links the storage DNS zone to the App Service VNet.
--registration-enabled false Disables auto-registration because private endpoint records are service-managed.
--zone-name privatelink.vaultcore.azure.net Selects the Key Vault private DNS zone.
--name link-keyvault-dns Names the Key Vault DNS VNet link.
az network private-endpoint dns-zone-group create Associates a private endpoint with a private DNS zone.
--endpoint-name pe-storage-blob Targets the storage private endpoint.
--name storage-zone-group Names the storage DNS zone group resource.
--private-dns-zone privatelink.blob.core.windows.net Attaches the storage private DNS zone.
--zone-name blob Uses the blob zone group label.
--endpoint-name pe-keyvault Targets the Key Vault private endpoint.
--name keyvault-zone-group Names the Key Vault DNS zone group resource.
--private-dns-zone privatelink.vaultcore.azure.net Attaches the Key Vault private DNS zone.
--zone-name vault Uses the Key Vault zone group label.

Step 7: Grant RBAC and configure app settings

PRINCIPAL_ID="$(az webapp identity show --resource-group $RG --name $APP_NAME --query principalId --output tsv)"
az role assignment create --assignee-object-id $PRINCIPAL_ID --assignee-principal-type ServicePrincipal --role "Storage Blob Data Contributor" --scope $STORAGE_ID
az role assignment create --assignee-object-id $PRINCIPAL_ID --assignee-principal-type ServicePrincipal --role "Key Vault Secrets User" --scope $KEY_VAULT_ID
az webapp config appsettings set --resource-group $RG --name $APP_NAME --settings STORAGE_ACCOUNT_URL="https://$STORAGE_NAME.blob.core.windows.net" KEY_VAULT_URI="https://$KEY_VAULT_NAME.vault.azure.net/"
Command/Parameter Purpose
PRINCIPAL_ID="$(...)" Retrieves the managed identity object ID used for role assignments.
az webapp identity show Reads the managed identity details for the web app.
--query principalId Returns only the managed identity principal ID.
--output tsv Formats the principal ID as plain text for shell assignment.
az role assignment create Creates an RBAC role assignment for the managed identity.
--assignee-object-id $PRINCIPAL_ID Targets the app's managed identity object ID.
--assignee-principal-type ServicePrincipal Tells Azure RBAC that the assignee is a service principal.
--role "Storage Blob Data Contributor" Grants blob data access without secrets.
--scope $STORAGE_ID Applies the storage role at the storage account scope.
--role "Key Vault Secrets User" Grants secret read access in Key Vault.
--scope $KEY_VAULT_ID Applies the Key Vault role at the vault scope.
az webapp config appsettings set Writes environment variables into App Service configuration.
--settings Passes the app settings to store.
STORAGE_ACCOUNT_URL="https://$STORAGE_NAME.blob.core.windows.net" Stores the storage hostname that private DNS resolves privately.
KEY_VAULT_URI="https://$KEY_VAULT_NAME.vault.azure.net/" Stores the Key Vault URI used by the app.

Step 8: Use managed identity in Node.js and validate

const { DefaultAzureCredential } = require("@azure/identity");
const { BlobServiceClient } = require("@azure/storage-blob");
const { SecretClient } = require("@azure/keyvault-secrets");

const credential = new DefaultAzureCredential();

const blobServiceClient = new BlobServiceClient(process.env.STORAGE_ACCOUNT_URL, credential);
const secretClient = new SecretClient(process.env.KEY_VAULT_URI, credential);
Command/Code Purpose
new DefaultAzureCredential() Uses the App Service managed identity in Azure without storing credentials in code.
new BlobServiceClient(process.env.STORAGE_ACCOUNT_URL, credential) Connects to Azure Storage through the standard blob hostname and managed identity.
new SecretClient(process.env.KEY_VAULT_URI, credential) Connects to Key Vault through the standard vault hostname and managed identity.
az webapp vnet-integration list --resource-group $RG --name $APP_NAME --output table
az network private-endpoint list --resource-group $RG --output table
Command/Parameter Purpose
az webapp vnet-integration list Confirms the app is attached to the expected VNet subnet.
--resource-group $RG Selects the app resource group.
--name $APP_NAME Targets the web app being validated.
--output table Formats the validation output for quick inspection.
az network private-endpoint list Confirms the private endpoints are provisioned in the resource group.

Verification

  • az webapp vnet-integration list shows the expected VNet and delegated subnet.
  • az network private-endpoint list shows private endpoints in a healthy state.
  • The app can use DefaultAzureCredential to reach Storage and Key Vault without secrets.
  • Storage and Key Vault hostnames resolve through the linked private DNS zones.

Troubleshooting

Private endpoint exists but the app still uses public resolution

  • Confirm the private DNS zones are linked to the same VNet used for App Service integration.
  • Verify the private endpoint DNS zone groups were created successfully.

Managed identity requests return 403

  • Wait a few minutes for RBAC propagation.
  • Verify the role assignments target the correct principalId and resource scope.

App cannot connect after VNet integration

  • Confirm the integration subnet is delegated to Microsoft.Web/serverFarms.
  • Review NSG and route table rules if your VNet uses custom egress controls.

Run It in the Portal

Portal view: Access Restrictions blade (public reachability before separate inbound hardening)

Access Restrictions blade reached from the Networking page with Save and Refresh actions. The App access section explains that public access applies to both the main site and the advanced (SCM) tool site, and that "Deny public network access will block all incoming traffic except that comes from private endpoints"; the Public network access control offers three radio buttons — Enabled from all networks (with a note that selecting it will clear all current access restrictions), Enabled from select virtual networks and IP addresses, and Disabled — and shows an info banner reading "Enabled (using default behavior)". The Site access and rules section has Main site (active) and Advanced tool site tabs and describes rules being evaluated in priority order with the "Unmatched rule action" controlling un-rule-matched traffic. The Unmatched rule action selector has Allow (selected) and Deny radio buttons. Add and Delete buttons appear above a Filter rules search box and an Action : All filter chip with a removable X, followed by a rules table with columns Priority, Name, Source, Action, and HTTP headers. The table contains a single rule with Priority 2147483647, Name "Allow all", Source "Any", Action "Allow" (green checkmark), and HTTP headers "Not configured".

The Access Restrictions blade is the Portal surface that shows whether this app is still publicly reachable while you work through the recipe's VNet integration, managed identity, and storage private endpoint steps. In the visible default state, Public network access is open and the rules table contains only Allow all, so this screenshot works as a before-state reminder that those outbound/private-connectivity steps do not automatically change inbound access. Use this blade only as a public-reachability check around the recipe, not as evidence that the recipe itself has already added access-restriction rules.

See Also

Sources