VNet Integration¶
Enable VNet integration for a Flask app so outbound traffic to SQL, Redis, and Key Vault flows through private network paths.
flowchart TD
A[Flask app on App Service] --> B[VNet integration subnet]
B --> C[Private Endpoint SQL]
B --> D[Private Endpoint Key Vault]
B --> E[Private Endpoint Redis]
B --> F[NSG and UDR controls] Prerequisites¶
- App Service Plan tier that supports VNet integration
- Existing virtual network with empty delegated subnet for App Service
- Permissions to configure VNet, subnet, NSG, and private endpoints
Main Content¶
1) Create delegated subnet for App Service integration¶
az network vnet subnet create \
--resource-group "$RG" \
--vnet-name "vnet-appservice" \
--name "snet-appservice-integration" \
--address-prefixes "10.10.1.0/24" \
--delegations "Microsoft.Web/serverFarms" \
--output json
| Flag | Description |
|---|---|
--resource-group "$RG" | Creates the subnet inside the specified resource group. |
--vnet-name "vnet-appservice" | Selects the virtual network that will host the integration subnet. |
--name "snet-appservice-integration" | Names the subnet dedicated to App Service VNet integration. |
--address-prefixes "10.10.1.0/24" | Assigns the address space used by the integration subnet. |
--delegations "Microsoft.Web/serverFarms" | Delegates the subnet so App Service plans can attach to it. |
--output json | Returns the created subnet definition as JSON. |
2) Connect web app to the integration subnet¶
az webapp vnet-integration add \
--resource-group "$RG" \
--name "$APP_NAME" \
--vnet "vnet-appservice" \
--subnet "snet-appservice-integration" \
--output json
| Flag | Description |
|---|---|
--resource-group "$RG" | Targets the resource group that contains the web app and VNet. |
--name "$APP_NAME" | Selects the web app that should use VNet integration. |
--vnet "vnet-appservice" | Chooses the virtual network that will carry outbound traffic. |
--subnet "snet-appservice-integration" | Attaches the app to the delegated integration subnet. |
--output json | Returns the integration result as JSON. |
3) Route all outbound traffic through VNet (optional)¶
az webapp config appsettings set \
--resource-group "$RG" \
--name "$APP_NAME" \
--settings WEBSITE_VNET_ROUTE_ALL=1 \
--output json
| Flag | Description |
|---|---|
--resource-group "$RG" | Targets the resource group that contains the web app. |
--name "$APP_NAME" | Selects the web app whose routing behavior you want to change. |
--settings WEBSITE_VNET_ROUTE_ALL=1 | Forces all outbound traffic from the app through the integrated virtual network. |
--output json | Returns the updated app-settings payload as JSON. |
4) Apply NSG baseline for least-privilege egress¶
Allow required backend ports (for example 1433, 6380, and 443), deny broad outbound, and enable flow logs for troubleshooting.
5) Configure private endpoints and private DNS zones¶
Create private endpoints for SQL, Key Vault, and Redis in a dedicated subnet, then link private DNS zones to the VNet.
Expected zones:
privatelink.database.windows.netprivatelink.vaultcore.azure.netprivatelink.redis.cache.windows.net
6) Set Flask environment-based config on App Service¶
az webapp config appsettings set \
--resource-group "$RG" \
--name "$APP_NAME" \
--settings \
SQL_SERVER_FQDN="<sql-private-fqdn>" \
SQL_DATABASE_NAME="<db-name>" \
REDIS_HOST="<redis-private-fqdn>" \
REDIS_PORT="6380" \
KEY_VAULT_URI="https://<kv-name>.vault.azure.net/" \
--output json
| Flag | Description |
|---|---|
--resource-group "$RG" | Targets the resource group that contains the web app. |
--name "$APP_NAME" | Selects the web app that should receive the private-endpoint connection settings. |
--settings SQL_SERVER_FQDN="<sql-private-fqdn>" SQL_DATABASE_NAME="<db-name>" REDIS_HOST="<redis-private-fqdn>" REDIS_PORT="6380" KEY_VAULT_URI="https://<kv-name>.vault.azure.net/" | Writes the SQL, Redis, and Key Vault hostnames and related settings the app will use over the VNet path. |
--output json | Returns the updated app-settings payload as JSON. |
7) Use os.environ plus managed identity in Flask code¶
import os
import pyodbc
from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()
token = credential.get_token("https://database.windows.net/.default").token
token_bytes = token.encode("utf-16-le")
connection = pyodbc.connect(
"Driver={ODBC Driver 18 for SQL Server};"
f"Server=tcp:{os.environ['SQL_SERVER_FQDN']},1433;"
f"Database={os.environ['SQL_DATABASE_NAME']};"
"Encrypt=yes;TrustServerCertificate=no;",
attrs_before={1256: token_bytes},
)
8) Validate DNS and endpoint state in CI¶
- name: Validate VNet integration and private endpoints
run: |
az webapp vnet-integration list \
--resource-group "$RG" \
--name "$APP_NAME" \
--output table
az network private-endpoint list \
--resource-group "$RG" \
--output table
Inbound versus outbound
VNet integration controls outbound from App Service to private dependencies. It does not place your app inbound endpoint directly inside the VNet.
Verification¶
az webapp vnet-integration listshows expected VNet/subnet mapping.- SQL, Key Vault, and Redis hostnames resolve to private IP addresses.
- Flask app reaches private backends without public network access.
Troubleshooting¶
App cannot reach private backend¶
- Validate NSG and route table configuration for integration subnet.
- Confirm private endpoints are approved and healthy.
- Verify backend firewall and private access policy configuration.
DNS still resolves public addresses¶
- Check private DNS zone links to VNet.
- Validate custom DNS forwarders if you use enterprise DNS.
Connectivity broke after route-all¶
- Verify required Azure destinations remain reachable via your egress path.
- Re-test with route-all disabled to isolate routing policy impact.
Run It in the Portal¶
Portal view: Networking blade (Virtual network integration not yet configured)¶

The Networking blade is the Portal surface where this recipe's outbound VNet connection becomes visible. In the pre-recipe state shown here, Virtual network integration: Not configured is the key row; after az webapp vnet-integration add, this is the row to revisit to confirm the app is attached to the delegated subnet. The Inbound traffic configuration card remains separate, which matches the recipe's point that VNet integration changes outbound connectivity rather than inbound exposure on its own.