Azure SQL Private End Points with Pulumi

by Frans Lytzen | 16/02/2021

Private End Points are the right way to connect PaaS Services to vNets so they can be accessed from other resources on the vNet. There are other, older, ways but they are not as good. Unfortunately, Private End Points require understanding quite a few things, including DNS resolution, split-brain DNS and a bunch of weird abstractions. This Microsoft post gives a good example of how to set it up with Azure SQL. This post does essentially the same thing, except it does not create a VM. Anything related to the VM is left out here.

This is part of a bigger project I am working on to set up a complex vNet-and-PasS setup in Azure with multiple services, regions and VPN clients.

Before we can add the private end point

This bit here sets up a network and a SQL Server so we can add the private end point.

1 const resourceGroup = new azure.resources.latest.ResourceGroup("fl-vnettest", {
2 location,
3 resourceGroupName: "fl-vnettest"
4 })
5
6 const vnet = new azure.network.latest.VirtualNetwork("fl-vnettest-vn", {
7 resourceGroupName: resourceGroup.name,
8 virtualNetworkName: "fl-vnettest-vn",
9 addressSpace: { addressPrefixes: ["10.0.0.0/16"] },
10 location
11 });
12
13 const azureSqlSubnet = new azure.network.latest.Subnet("azure-sql", {
14 subnetName: "azure-sql",
15 addressPrefix: "10.0.4.0/24",
16 virtualNetworkName: vnet.name,
17 resourceGroupName: resourceGroup.name,
18 privateEndpointNetworkPolicies: "Disabled"
19 });
20
21 const passSqlServer = new azure.sql.v20200801preview.Server("fl-vnettest-ss",
22 {
23 serverName: "fl-vnettest-ss",
24 administratorLogin,
25 administratorLoginPassword,
26 location,
27 resourceGroupName: resourceGroup.name,
28 tags,
29 minimalTlsVersion: "1.2",
30 });
31

Setting up the private end point

This is the important bit

You need to do this once per vnet, no matter how many SQL Servers you have in that vNet. You will need to do this for each type of service, i.e you'd need something similar for Storage etc. This is required in order to make DNS lookups for the SQL Server return the local IP address, rather than the public one. The tricky bit is that an Azure SQL Server will always have a public IP Address, even if no public access is allowed. When you set up the DNS resolution just right, a DNS lookup on your private network will return the private IP address and when you look it up on the public internet, you will see the public IP address. There is a lot more detail to this, but that is for another post. What is described here is the easiest and most reliable way to make this work. There are other ways, but they require more knowledge and more work.

1 // Sets up a private DNS Zone for SQL private link entries
2 const sqlPrivateDns = new azure.network.latest.PrivateZone("fl-vnettest-sqldns", {
3 privateZoneName: "privatelink.database.windows.net",
4 resourceGroupName: resourceGroup.name,
5 location: "global" //https://github.com/Azure/azure-cli/issues/6052
6 })
7
8 // Links the private DNS Zone to the vnet
9 const dnsVnetLink = new azure.network.latest.VirtualNetworkLink("fl-vnettest-vnl", {
10 privateZoneName: sqlPrivateDns.name,
11 resourceGroupName: resourceGroup.name,
12 virtualNetworkLinkName: "fl-vnettest-vnl",
13 registrationEnabled: true,
14 virtualNetwork: { id: vnet.id },
15 location: "global"
16 })
17

Set up a Private End Point for the SQL Server

You need to do this for each SQL Server in your vnet

1 // Create the private end point for the SQL Server
2 const sqlPrivateEndPoint = new azure.network.latest.PrivateEndpoint("fl-vnettest-sqlpep", {
3 privateEndpointName: "fl-vnettest-sqlpep",
4 resourceGroupName: resourceGroup.name,
5 location,
6 subnet: { id: azureSqlSubnet.id },
7 privateLinkServiceConnections: [
8 {
9 name: "sql",
10 privateLinkServiceId: passSqlServer.id,
11 groupIds: ["sqlServer"], // This particular group id is not documented by Azure that I can see - I found it by trying to do this manually in the portal
12 privateLinkServiceConnectionState: {
13 actionsRequired: "None",
14 description: "Auto-approved",
15 status: "Approved"
16 }
17 }
18 ]
19 })
20
21 // Connects the private DNS Zone and the private end point (I think)
22 const sqlPrivateDnsZoneGroup = new azure.network.latest.PrivateDnsZoneGroup("fl-vnettest-sqldnsgroup", {
23 privateDnsZoneGroupName: "fl-vnettest-sqldnsgroup",
24 privateEndpointName: sqlPrivateEndPoint.name,
25 resourceGroupName: resourceGroup.name,
26 name: "fl-vnettest-sqldns",
27 privateDnsZoneConfigs: [{
28 name: sqlPrivateDns.name,
29 privateDnsZoneId: sqlPrivateDns.id
30 }]
31 })
32
33

Connect a Web App

If you want to connect a web app to the vNet so it can talk to the SQL (not really the scope of this post, but hey) then do the following. Note, this is outbound from the Web App. Inbound traffic to the Web App is best handled by setting up a Private End Point for the Web App - though that currently requires a Premium App Service Plan. Service End Points is another option but they are much harder to use in my experience and I don't recommend them.

1 // Set up a subnet dedicated as an entry point for Web Apps
2 const frontEndSubnet = new azure.network.latest.Subnet("front-end", {
3 subnetName: "front-end",
4 addressPrefix: "10.0.1.0/24",
5 virtualNetworkName: vnet.name,
6 resourceGroupName: resourceGroup.name,
7 delegations: [
8 {
9 serviceName: "Microsoft.Web/serverfarms",
10 name: "front-end-delegation"
11 }
12 ]
13 });
14
15 // Add an app service plan and a web app
16 const appServicePlan = new azure.web.latest.AppServicePlan("fl-vnettest-as", {
17 name: "fl-vnettest-as",
18 location,
19 resourceGroupName: resourceGroup.name,
20 sku: {
21 family: "S",
22 capacity: 1,
23 size: "S1",
24 name: "S1"
25 }
26 });
27
28 const appService = new azure.web.latest.WebApp("fl-vnettest-wa",
29 {
30 name: "fl-vnettest-wa",
31 location,
32 resourceGroupName: resourceGroup.name,
33 clientAffinityEnabled: false,
34 httpsOnly: true,
35 serverFarmId: appServicePlan.id,
36 tags,
37 siteConfig: {
38 appSettings: [
39 // This is critical to make DNS lookups return the internal IP Address.
40 // Without it, your web app will get the public IP address of the SQL Server and will be denied access.
41 // 168.63.129.16 is a magic constant provided by Microsoft and works with the setup above.
42 // You will only need something different if you use your own DNS servers.
43 // See https://feedback.azure.com/forums/169385-web-apps/suggestions/38383642-web-app-and-private-dns-zone-support
44 { name: "WEBSITE_DNS_SERVER", value: "168.63.129.16" },
45 { name: "WEBSITE_VNET_ROUTE_ALL", value: "1" },
46 { name: "hostname", value: passSqlServer.name.apply(n => n + ".database.windows.net") },
47 {
48 name: "sql",
49 value: "Server=tcp:fl-vnettest-ss.database.windows.net,1433;Initial Catalog=fl-vnettest-db;Persist Security Info=False;User ID=" + administratorLogin + ";Password=" + administratorLoginPassword + ";MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
50 // Note: This isn't ideal for many reasons, but I haven't got the brain power to nest apply calls :)
51 }
52 ]
53 },
54 })
55
56 // Give the web app access to the vnet <- the thing you actually wanted to see :)
57 // Note that "Swift" is the "modern" way of doing this. Don't do what I do and spend ours trying to make WebAppVnetConnection work - it won't.
58 const webAppVNetConnection = new azure.web.latest.WebAppSwiftVirtualNetworkConnection("fl-vnettest-wa-vc", {
59 name: appService.name,
60 resourceGroupName: resourceGroup.name,
61 subnetResourceId: frontEndSubnet.id
62 })
63
64

References


Originally posted on Frans' blog.


Share this article

You Might Also Like

Explore more articles that dive into similar topics. Whether you’re looking for fresh insights or practical advice, we’ve handpicked these just for you.

Using AI to write API documentation

by Maciej Kołodziej | 20/10/2025

Writing API documentation is hard, but AI can make it collaborative. Maciej Kołodziej shows how using AI as a writing partner improves clarity, structure, and speed — turning technical docs into a smarter, iterative process.

AI Providers Comparison – Why Microsoft Azure Leads for Fintech and Healthcare

by Maciek Fil | 16/10/2025

Comparing leading AI providers, this blog shows why Microsoft Azure is the most enterprise-ready choice for fintech and healthcare. From compliance and governance to integration and partner networks, discover how Naitive helps organisations deploy AI safely and at scale.

Write Better AI Prompts with RISEN Framework

by Nathan Ball | 09/10/2025

Ever typed a prompt into ChatGPT or Copilot and got something… underwhelming? You’re not alone. Generic prompts often lead to generic outputs — vague, off-target, or just plain dull. That’s where structure matters. And RISEN helps you get it right.

Cookie Settings

Contact Us

NewOrbit Ltd.
Hampden House
Chalgrove
OX44 7RW


020 3757 9100

NewOrbit Logo

Copyright © NewOrbit Ltd.