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.

AI Isn’t Magic: Why Predictive Accuracy Can Be Misleading

by Frans Lytzen | 15/04/2025

One of the biggest misconceptions in AI today is how well it can actually predict things – especially things that are rare. This is most directly applicable to Machine Learning (as they are just statistical models) but the same principle applies to LLMs. The fundamental problem is the same and AI is not magic. In reality, AI’s predictive power is more complicated. One of the key challenges? False positives—incorrect detections that can significantly undermine the value of AI-driven decision-making. Let’s explore why this happens and how businesses can better understand AI’s limitations.

From Figma Slides to Svelte Page in Under an Hour – How I Accidentally Proved My Own Point

by Marcin Prystupa | 10/04/2025

A quick case study on how I went from a Figma presentation to a working Svelte page in less than an hour – with the help of AI and some clever tooling.

Embracing the European Accessibility Act: A NewOrbit Perspective

by George Elkington | 12/03/2025

As the European Accessibility Act (EAA) approaches its enforcement date on June 28, 2025, businesses must prioritise accessibility to ensure compliance and inclusivity. The EAA sets new standards for software, e-commerce, banking, digital devices, and more, aiming to make products and services accessible to all, including people with disabilities and the elderly. Non-compliance could lead to significant penalties across the EU. At NewOrbit, we believe that accessibility is not just a legal requirement—it’s good design. Take advantage of our free initial review to assess your compliance and stay ahead of the deadline.

Contact Us

NewOrbit Ltd.
Hampden House
Chalgrove
OX44 7RW


020 3757 9100

NewOrbit Logo

Copyright © NewOrbit Ltd.