My other posts
Centralized Private Endpoints with Azure Bicep
Table of contents
Introduction
In this post, we'll be looking at improving the management of Private Endpoints using Bicep templates. While the focus for this post will be on a Hub-and-Spoke topology, where there is a central VNET for managing VPN and other networking resources, this could also be simplified easily for simpler networks.
Here's a sample diagram for what we'll be achieving with this post:
To briefly explain what is needed, for each resource that you want to access from the hub vnet (and/or VPN), you need:
- A resource that supports Private Endpoints (such as storage accounts, key vaults, CosmosDb accounts, etc.)
- A Virtual Network with at least one Subnet, with space for the Network Interface Card created by the Private Endpoint
- A Private DNS Zone for the specific type of Private Endpoint to be deployed. For example, a Private DNS Zone for
privatelink.blob.core.windows.net
can be used for accessing blobs in a storage account, but cannot be used for accessing file shares in the same storage account or other resources like key vaults. - A network link for the Private DNS Zone, so that the zone is connected to the Virtual Network that will allow access to the resource (in this case, the hub network).
- A Private Endpoint with a Private Link connection, which creates the connection between the resource and the subnet selected, for the specified group(s).
- And finally, a Private DNS Zone Group, which connects the Private Endpoint with the Private DNS Zone, so that the resource can be resolved via the DNS name.
Not covered in this post is the setup of DNS. There are two options for this, either using the Azure DNS Private Resolver service, or one or more Virtual Machines configured as DNS servers with DNS forwarding zones configured for the Private DNS Zones you want to access.
Microsoft provides detailed explanations on the differences between the approaches, if you are keen to learn more.
Required components
To make things a bit simpler, I'll assume that you already have a Virtual Network with a Subnet for this sample, so we'll need the resource ID for both as part of the deployment.
Private DNS Zone
We'll start with this resource as it's needed for the Virtual Network link and the Private Endpoints' DNS.
param privateDnsZoneName string
param tags object
resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
name: privateDnsZoneName
tags: tags
location: 'global'
}
The privateDnsZoneName
here is expected to be one of the zone names that you can find in the Private DNS zone name column in this Microsoft Learn article.
Private DNS Zone link
The next component needed is a Virtual Network Link for the Private DNS Zone. This can be added easily as a child resource through Bicep native features:
param privateDnsZoneLinkName string
param vnetId string
resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
//...
resource link 'virtualNetworkLinks' = {
name: privateDnsZoneLinkName
tags: tags
location: 'global'
properties: {
registrationEnabled: false
virtualNetwork: { id: vnetId }
}
}
}
Private Endpoint
We can now move to the Private Endpoints resource. Keep in mind that you'd use the same Private DNS Zone for multiple Private Endpoints, which we can design easily with Bicep. First, the resource itself:
param location string
param privateEndpointName string
param subnetId string
param resourceId string
param groupIds string[]
var uniqueId = uniqueString(privateEndpointName, resourceId)
resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-05-01' = {
name: '${privateEndpointName}-${uniqueId}'
tags: tags
location: location
properties: {
customNetworkInterfaceName: '${privateEndpointName}-${uniqueId}-nic'
subnet: { id: subnetId }
privateLinkServiceConnections: [
{
name: '${privateEndpointName}-${uniqueId}-link'
properties: {
groupIds: groupIds
privateLinkServiceId: resourceId
}
}
]
}
}
You'll notice that I've used uniqueString
to generate a set of 13 random characters. This is just to prevent having to give each private endpoint a name manually.
You may also wonder about the groupIds
property, those values come from the same Microsoft Learn article, documented in the Subresource
column.
Private DNS Zone Group
The last resource to be deployed is the DNS configuration for the Private Endpoint, which is a subresource of the Private Endpoint.
resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-05-01' = {
//...
resource privateEndpointDns 'privateDnsZoneGroups' = {
name: '${privateEndpointName}-${uniqueId}-dns'
properties: {
privateDnsZoneConfigs: [
{
name: '${privateEndpointName}-${uniqueId}-dns-config'
properties: {
privateDnsZoneId: privateDnsZone.id
}
}
]
}
}
}
Simple addition of new resources
Up until this point, we have all the resources that are needed to deploy a new Private Endpoint with all of its dependencies. But we are missing the links that ensure we don't duplicate code (or forget to deploy a resource). Fortunately, Bicep's features allow us to make this dynamic very easily.
Let's start by moving the Private Endpoint resource to it's own file, PrivateEndpoint.bicep
:
param privateEndpointName string
param subnetId string
param resourceId string
param privateDnsZoneId string
param groupIds string[]
param location string
param tags object
var uniqueId = uniqueString(privateEndpointName, resourceId)
resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-05-01' = {
name: '${privateEndpointName}-${uniqueId}'
tags: tags
location: location
properties: {
customNetworkInterfaceName: '${privateEndpointName}-${uniqueId}-nic'
subnet: { id: subnetId }
privateLinkServiceConnections: [
{
name: '${privateEndpointName}-${uniqueId}-link'
properties: {
groupIds: groupIds
privateLinkServiceId: resourceId
}
}
]
}
resource privateEndpointDns 'privateDnsZoneGroups' = {
name: '${privateEndpointName}-${uniqueId}-dns'
properties: {
privateDnsZoneConfigs: [
{
name: '${privateEndpointName}-${uniqueId}-dns-config'
properties: {
privateDnsZoneId: privateDnsZoneId
}
}
]
}
}
}
And let's put the Private DNS Zone into PrivateDnsZone.bicep
:
param privateDnsZoneName string
param privateDnsZoneLinkName string
param vnetId string
param tags object
resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
name: privateDnsZoneName
tags: tags
location: 'global'
resource link 'virtualNetworkLinks' = {
name: privateDnsZoneLinkName
tags: tags
location: 'global'
properties: {
registrationEnabled: false
virtualNetwork: { id: vnetId }
}
}
}
We also know that one Private DNS Zone can be linked with many Private Endpoints, so we can add those next using a for
loop. At the end of PrivateDnsZone.bicep
, add:
param privateEndpointName string
param subnetId string
param resourceIds string[]
param groupIds string[]
param location string
module privateEndpoints 'PrivateEndpoint.bicep' = [for (item, index) in resourceIds: {
name: '${privateDnsZoneName}-links-${index}'
params: {
location: location
tags: tags
groupIds: groupIds
privateDnsZoneId: privateDnsZone.id
privateEndpointName: privateEndpointName
resourceId: item
subnetId: subnetId
}
}]
The only thing missing now, is to make the main.bicep
do a for
loop on the zones to be created. We'll use an array of objects to store this data, and you'll see why soon.
param vnetId string
param subnetId string
param privateEndpointPrefixName string
param privateEndpointsData array
param location string = resourceGroup().location
param tags object
module privateDnsZones 'PrivateDnsZone.bicep' = [for item in privateEndpointsData: {
name: item.privateDnsZoneName
params: {
privateDnsZoneLinkName: item.privateDnsZoneLinkName
privateDnsZoneName: item.privateDnsZoneName
vnetId: vnetId
groupIds: item.groupIds
privateEndpointName: privateEndpointPrefixName
resourceIds: item.resourceIds
subnetId: subnetId
location: location
tags: tags
}
}]
If you were to create a bicepparam file for this file, you could now write this:
using './main.bicep'
param location = ''
param vnetId = ''
param subnetId = ''
param privateEndpointPrefixName = ''
param privateEndpointsData = [
{
privateDnsZoneName: 'privatelink.blob.core.windows.net'
privateDnsZoneLinkName: 'vnetlink-blob'
resourceIds: [ 'YourBlobStorageAccountId' ]
groupIds: [ 'blob' ]
}
{
privateDnsZoneName: 'privatelink.vaultcore.azure.net'
privateDnsZoneLinkName: 'vnetlink-kv'
resourceIds: [ 'YourKeyVaultId1', 'YourKeyVaultId2' ]
groupIds: [ 'vault' ]
}
]
param tags = {}
The relationship between the Private DNS Zones and the Private Endpoints is now very clear. Adding new zones and endpoints is a matter of reusing the same structure.
The Bicep visualizer (in Visual Studio Code) helps reaffirm this relationship:
Conclusion
Used correctly, Bicep is a very powerful language that allows us to manage resources at scale efficiently. I have published the source code for this article on my GitHub samples repository.
Bicep projects can (and should) also be treated with CI/CD (Continuous Integration and Continuous Delivery), and if you are interested in doing that with Azure DevOps, see this blog post of mine.
I hope you found this post useful, and if you have any questions, comments, ideas... feel free to leave a comment!