Azure Web App Slots and Private Endpoint
Overview
- Private Endpoint and Private Link
The Private Endpoint uses an IP address from your Azure VNet address space. Network traffic between a client on your private network and the Web App traverses over the VNet and a Private Link on the Microsoft backbone network, eliminating exposure from the public Internet.
If you just need a secure connection between your VNet and your Web App, a Service Endpoint is the simplest solution. If you also need to reach the web app from on-premises through an Azure Gateway, a regionally peered VNet, or a globally peered VNet, Private Endpoint is the solution.
Make sure you use PremiumV2, PremiumV3 app service plan or ASE, private endpoint feature is not available in shared or standard plan.
- Web App Deployment Slot
Deployment slots are live apps with their own host names. App content and configurations elements can be swapped between two deployment slots, including the production slot.
This is essential feature to support blue green deployment, live traffic goes to production slot, while you can test your new release in staging slot.
Deployment slot is available in standard plan, premium plan and ASE.
Even though Azure app service private endpoint is already GA, but there is a limitation, slots cannot use Private Endpoint.
System diagram
Here are the configurations for the demo.
Subscription A on the left has web app and staging slot, private endpoint of the web app is joined with private endpoint subnet. The private endpoint is integrated with private DNS zone through private DNS zone group.
Subscription B on the right has private DNS zone to host DNS record for the web app private endpoint.
In order to access the private endpoint, we create a vnet, subnet and an Azure VM in subscription B, private DNS zone is linked to the subnet in subscription B.
Demonstration
- Deploy Rest API Web App
There are multiple options to deploy Web App to Azure, using integration with git hub(including local git and github), using Azure Devops pipeline, using az cli, using visual studio or language specific build tool (Maven for Java).
I will use visual studio, make sure sing in from visual studio to authenticate to your Azure subscription.
I will deploy a sample Rest API web app, code is available from https://github.com/Azure-Samples/dotnet-core-api
Clone the source code or download the code file, open the solution from visual studio, then click publish.
After app service plan and app service are created, deploy the code to Azure.
Code is successfully deployed, and site is up and running.
- Add Private Endpoint for the Web App
Private endpoint private DNS zone integration manages the DNS record in the zone for you. Also the private DNS zone can be in another subscription.
$resourcegroupname="ronniepersonal"
$VNetname="VNet-$(Get-Random)"
$location="eastus2"
$privateendpointsubnetname = "privateEndpointSubnet"$appName = <xyz>
$appPlan = $appName
$slotName = "ronniestaging"$privateendpointsubnet = New-AzVirtualNetworkSubnetConfig -Name $privateendpointsubnetname `
-AddressPrefix "10.8.2.0/24" `
-PrivateEndpointNetworkPoliciesFlag Disabled$virtualNetwork = New-AzVirtualNetwork -Name $VNetname `
-ResourceGroupName $resourcegroupname `
-Location $location -AddressPrefix "10.8.0.0/16" `
-Subnet $privateendpointsubnet# Configure the Private Endpoint
$privateEndPointConnection = New-AzPrivateLinkServiceConnection -Name "myPrivateEndpointconnection" `
-PrivateLinkServiceID $webApp.Id `
-GroupId sites$privateEndpoint = New-AzPrivateEndpoint -Name "myPrivateEndpoint" `
-ResourceGroupName $resourcegroupname `
-Location $location `
-Subnet $subnet `
-PrivateLinkServiceConnection $privateEndPointConnection# Configure the Private DNS zone
# Private DNS Zone is created in another subscription
$dnsZoneResourceId = "/subscriptions/<xxxx>/resourceGroups/ronnietest/providers/Microsoft.Network/privateDnsZones/privatelink.azurewebsites.net"$dnsConfig = New-AzPrivateDnsZoneConfig -Name "privatelink.azurewebsites.net" `
-PrivateDnsZoneId $dnsZoneResourceId$dnsZoneGroup = New-AzPrivateDnsZoneGroup -Name "myZoneGroup" `
-ResourceGroupName $resourcegroupname `
-PrivateEndpointName $privateEndpoint.Name `
-PrivateDnsZoneConfig $dnsConfig
ARM template for all resources in subscription B,
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"virtualMachines_vmdnstest_name": {
"defaultValue": "vmdnstest",
"type": "String"
},
"networkInterfaces_vmdnstest375_name": {
"defaultValue": "vmdnstest375",
"type": "String"
},
"publicIPAddresses_vmdnstest_ip_name": {
"defaultValue": "vmdnstest-ip",
"type": "String"
},
"virtualNetworks_ronnietest_vnet_name": {
"defaultValue": "ronnietest-vnet",
"type": "String"
},
"networkSecurityGroups_vmdnstest_nsg_name": {
"defaultValue": "vmdnstest-nsg",
"type": "String"
},
"privateDnsZones_privatelink_azurewebsites_net_name": {
"defaultValue": "privatelink.azurewebsites.net",
"type": "String"
},
"localPassword": {
"type": "securestring"
}
},
"variables": {},
"resources": [
{
"type": "Microsoft.Network/networkSecurityGroups",
"apiVersion": "2020-05-01",
"name": "[parameters('networkSecurityGroups_vmdnstest_nsg_name')]",
"location": "eastus2",
"properties": {
"securityRules": [
{
"name": "SSH",
"properties": {
"protocol": "TCP",
"sourcePortRange": "*",
"destinationPortRange": "22",
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "*",
"access": "Allow",
"priority": 300,
"direction": "Inbound",
"sourcePortRanges": [],
"destinationPortRanges": [],
"sourceAddressPrefixes": [],
"destinationAddressPrefixes": []
}
}
]
}
},
{
"type": "Microsoft.Network/privateDnsZones",
"apiVersion": "2018-09-01",
"name": "[parameters('privateDnsZones_privatelink_azurewebsites_net_name')]",
"location": "global",
"properties": {
"maxNumberOfRecordSets": 25000,
"maxNumberOfVirtualNetworkLinks": 1000,
"maxNumberOfVirtualNetworkLinksWithRegistration": 100,
"numberOfRecordSets": 3,
"numberOfVirtualNetworkLinks": 1,
"numberOfVirtualNetworkLinksWithRegistration": 0,
"provisioningState": "Succeeded"
}
},
{
"type": "Microsoft.Network/publicIPAddresses",
"apiVersion": "2020-05-01",
"name": "[parameters('publicIPAddresses_vmdnstest_ip_name')]",
"location": "eastus2",
"sku": {
"name": "Basic"
},
"properties": {
"publicIPAddressVersion": "IPv4",
"publicIPAllocationMethod": "Dynamic",
"idleTimeoutInMinutes": 4,
"ipTags": []
}
},
{
"type": "Microsoft.Network/virtualNetworks",
"apiVersion": "2020-05-01",
"name": "[parameters('virtualNetworks_ronnietest_vnet_name')]",
"location": "eastus2",
"properties": {
"addressSpace": {
"addressPrefixes": [
"10.0.8.0/24"
]
},
"subnets": [
{
"name": "default",
"properties": {
"addressPrefix": "10.0.8.0/26",
"delegations": [],
"privateEndpointNetworkPolicies": "Enabled",
"privateLinkServiceNetworkPolicies": "Enabled"
}
}
],
"virtualNetworkPeerings": [],
"enableDdosProtection": false,
"enableVmProtection": false
}
},
{
"type": "Microsoft.Compute/virtualMachines",
"apiVersion": "2019-07-01",
"name": "[parameters('virtualMachines_vmdnstest_name')]",
"location": "eastus2",
"dependsOn": [
"[resourceId('Microsoft.Network/networkInterfaces', parameters('networkInterfaces_vmdnstest375_name'))]"
],
"properties": {
"hardwareProfile": {
"vmSize": "Standard_DS1_v2"
},
"storageProfile": {
"imageReference": {
"publisher": "Canonical",
"offer": "UbuntuServer",
"sku": "18.04-LTS",
"version": "latest"
},
"osDisk": {
"osType": "Linux",
"name": "[concat(parameters('virtualMachines_vmdnstest_name'), '_OsDisk_1_ea05c084bb024554b3fa3be735062cde')]",
"createOption": "FromImage",
"caching": "ReadWrite",
"managedDisk": {
"storageAccountType": "Standard_LRS"
},
"diskSizeGB": 30
},
"dataDisks": []
},
"osProfile": {
"computerName": "[parameters('virtualMachines_vmdnstest_name')]",
"adminUsername": "myvmuser",
"adminPassword": "[parameters('localPassword')]",
"linuxConfiguration": {
"disablePasswordAuthentication": false,
"provisionVMAgent": true
},
"secrets": [],
"allowExtensionOperations": true
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', parameters('networkInterfaces_vmdnstest375_name'))]"
}
]
},
"priority": "Spot",
"evictionPolicy": "Delete",
"billingProfile": {
"maxPrice": -1
}
}
},
{
"type": "Microsoft.Network/networkSecurityGroups/securityRules",
"apiVersion": "2020-05-01",
"name": "[concat(parameters('networkSecurityGroups_vmdnstest_nsg_name'), '/SSH')]",
"dependsOn": [
"[resourceId('Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroups_vmdnstest_nsg_name'))]"
],
"properties": {
"protocol": "TCP",
"sourcePortRange": "*",
"destinationPortRange": "22",
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "*",
"access": "Allow",
"priority": 300,
"direction": "Inbound",
"sourcePortRanges": [],
"destinationPortRanges": [],
"sourceAddressPrefixes": [],
"destinationAddressPrefixes": []
}
},
{
"type": "Microsoft.Network/privateDnsZones/SOA",
"apiVersion": "2018-09-01",
"name": "[concat(parameters('privateDnsZones_privatelink_azurewebsites_net_name'), '/@')]",
"dependsOn": [
"[resourceId('Microsoft.Network/privateDnsZones', parameters('privateDnsZones_privatelink_azurewebsites_net_name'))]"
],
"properties": {
"ttl": 3600,
"soaRecord": {
"email": "azureprivatedns-host.microsoft.com",
"expireTime": 2419200,
"host": "azureprivatedns.net",
"refreshTime": 3600,
"retryTime": 300,
"serialNumber": 1,
"minimumTtl": 10
}
}
},
{
"type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
"apiVersion": "2018-09-01",
"name": "[concat(parameters('privateDnsZones_privatelink_azurewebsites_net_name'), '/localvnet')]",
"location": "global",
"dependsOn": [
"[resourceId('Microsoft.Network/privateDnsZones', parameters('privateDnsZones_privatelink_azurewebsites_net_name'))]",
"[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworks_ronnietest_vnet_name'))]"
],
"properties": {
"registrationEnabled": false,
"virtualNetwork": {
"id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworks_ronnietest_vnet_name'))]"
}
}
},
{
"type": "Microsoft.Network/networkInterfaces",
"apiVersion": "2020-05-01",
"name": "[parameters('networkInterfaces_vmdnstest375_name')]",
"location": "eastus2",
"dependsOn": [
"[resourceId('Microsoft.Network/publicIPAddresses', parameters('publicIPAddresses_vmdnstest_ip_name'))]",
"[resourceId('Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroups_vmdnstest_nsg_name'))]"
],
"properties": {
"ipConfigurations": [
{
"name": "ipconfig1",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"publicIPAddress": {
"id": "[resourceId('Microsoft.Network/publicIPAddresses', parameters('publicIPAddresses_vmdnstest_ip_name'))]"
},
"subnet": {
"id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworks_ronnietest_vnet_name'), 'default')]"
},
"primary": true,
"privateIPAddressVersion": "IPv4"
}
}
],
"dnsSettings": {
"dnsServers": []
},
"enableAcceleratedNetworking": false,
"enableIPForwarding": false,
"networkSecurityGroup": {
"id": "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroups_vmdnstest_nsg_name'))]"
}
}
}
]
}
Now let’s logon to the vmdnstest, you can see both site FQDN and scm FQDN is resolved to private IP.
As expected, you won’t be able to access the primary site or the procdution slot any more from public network.
You should be able to access the site from the private network, in this case from vmdnstest, after you peer the vnet from both subscriptions.
- Staging Slot
Create staging slot using powershell
New-AzWebAppSlot -ResourceGroupName $resourcegroupname -Name $appName -Slot $slotName -AppServicePlan $appPlan
Now let’s deploy new version code to staging slot from Visual Studio.
The staging slot has the version 2 API.
- Swap
Azure document has detail on what happen when swap, which settings are swapped and which settings are not swapped.
https://docs.microsoft.com/en-us/azure/app-service/deploy-staging-slots#which-settings-are-swapped
Now the production slot points to v2 API, and staging slot points to v1 API.
Private endpoint is still applied to the latest production slot, and can not be accessed from public network. Staging is still public endpoint.
clouduser@vmdnstest:~$ nslookup xxxwebappsample-ronniestaging.azurewebsites.net
Server: 127.0.0.53
Address: 127.0.0.53#53Non-authoritative answer:
xxxwebappsample-ronniestaging.azurewebsites.net canonical name = waws-prod-bn1-089.sip.azurewebsites.windows.net.
waws-prod-bn1-089.sip.azurewebsites.windows.net canonical name = waws-prod-bn1-089-04f7.eastus2.cloudapp.azure.com.
Name: waws-prod-bn1-089-04f7.eastus2.cloudapp.azure.com
Address: x.x.97.8clouduser@vmdnstest:~$ nslookup xxxwebappsample.azurewebsites.net
Server: 127.0.0.53
Address: 127.0.0.53#53Non-authoritative answer:
xxxwebappsample.azurewebsites.net canonical name = xxxwebappsample.privatelink.azurewebsites.net.
Name: xxxwebappsample.privatelink.azurewebsites.net
Address: 10.8.2.4
Conclusion
In this article, you explored the web app deployment slots and expected limitation for the private endpoint.
We also get a better understanding of private DNS zone group which helps to manage DNS record for the private endpoint.
More details about private endpoint and private DNS zone integration can be found in another article. https://cloudjourney.medium.com/azure-private-endpoint-and-private-dns-zone-integration-26cda64ed2f
Reference Links
Tutorial: Host RESTful API with CORS — Azure App Service | Microsoft Docs
Publish to Azure App Service — Visual Studio | Microsoft Docs
PowerShell: Deploy Private Endpoint for Web App with PowerShell — Azure App Service | Microsoft Docs