Create a Private Link Service using Bicep
Published Jul 01 2022 04:43 AM 6,656 Views

Create a Private Link Service using Bicep

This sample shows how to use Bicep to create an Azure Private Link Service that can be accessed by a third party via an Azure Private Endpoint. Bicep modules deploy all the Azure resources in the same resource group in the same Azure subscription. In a real-world scenario, service consumer and service provider resources will be hosted distinct Azure subscriptions under the same or a different Azure Active Directory tenant. You can find code on Azure Samples.





The following picture shows the high-level architecture created by the Bicep modules included in this sample:



Figure: network topology and infrastructure architecture.

Multiple Azure resources are defined in the Bicep modules:

  • Microsoft.Network/virtualNetworks: the module creates a virtual network for the service provider and one virtual network for the services consumer.
  • Microsoft.Network/loadBalancers: The internal standard load balancer that exposes the virtual machine that hosts the service.
  • Microsoft.Network/networkInterfaces: the module deploys two network interfaces, one for service provider virtual machine and one for the service consumer virtual machine.
  • Microsoft.Compute/virtualMachines: There are two virtual machines, one that hosts the service exposed via and one virtual machine that can be used to test the connection to the service via a private endpoint. This virtual machine simulates a third party consumer application and can resides in a separate Azure subscription under a distinct Azure Active Directory tenant. For the sake of simplicity, this demo deploys both virtual machines in the same Azure subscription. By default, this sample deploys two Ubuntu Linux virtual machines, but you can use imagePublisher, imageOffer, and imageSku to select a different operating system or version.
  • Microsoft.Storage/storageAccounts: this storage account is used to store the boot diagnostics logs of both the service provider and service consumer virtual machines. Boot Diagnostics is a debugging feature which allows you to view Console Output and Screenshot to diagnose virtual machine status. In a real-world scenario, the service provider and service consumer virtual machines would be located in separate subscriptions and should be configured to store the boot diagnostics logs in separate storage accounts. Both virtual machines access the storage account via a private endpoint, one in the service provider virtual network, and one in the service consumer virtual network.
  • Microsoft.Network/bastionHosts: a separate Azure Bastion is deployed in the service provider and service consumer virtual network to provide SSH connectivity to both virtual machines.
  • Microsoft.Compute/virtualMachines/extensions: The Azure Custom Script Extension is used on the service provider virtual machine to installs NGINX web server. The Log Analytics virtual machine extension for Linux is installed to both virtual machines to collect diagnostics logs and metrics to a shared Azure Log Analytics workspace. In a real-world scenario, the two virtual machines will collect logs and metrics into separate workspaces.
  • Microsoft.OperationalInsights/workspaces: a centralized Azure Log Analytics workspace is used to collect the diagnostics logs and metrics from all the Azure resources. In a real-world scenario, the service provider and service consumer may run into separate Azure subscriptions and use separate workspaces.
  • Microsoft.Network/privateLinkServices: The Azure Private Link Service used to expose the service, represented in this sample by a sample website hosted by the NGINX web server on the service provider virtual machine.
  • Microsoft.Network/natGateways: Outbound connectivity isn't available for any virtual machine in the backend pool of an internal standard load balancer. A Virtual Network NAT is created and associated to the backend subnet hosting the service provider virtual machine. A standard Public IP Address is created and associated to the NAT Gateway. With a NAT gateway the service provider virtual machine doesn't need a public IP address to access services via the public internet and can remain private. The Custom Script Extension of the service provider virtual machine needs outbound connectivity to download and execute a bash script that install NGINX locally.
  • Microsoft.Network/publicIpAddresses: An Azure Public IP Address is created for each Azure Bastion Host and for the NAT Gateway.
  • Microsoft.Network/privateEndpoints: The Azure Private Endpoint used to access the service via Azure Private Link. In addition, each virtual network contains a private endpoint to access the shared storage account. In real-world scenario, the service consumer and service provider would run in separate subscriptions, and they would use separate storage accounts for storing the boot diagnostics logs of virtual machines.
  • Microsoft.Network/privateDnsZones: an Azure Private DNS Zone is used for translating (or resolving) the fully qualified name of the storage account to the private IP address of the two private endpoints, one in the service provider virtual network and one in the service consumer virtual network, using A records.
  • Microsoft.Network/networkSecurityGroups: subnets hosting virtual machines and Azure Bastion Hosts are protected by Azure Network Security Groups that are used to filter inbound and outbound traffic.

You can find the architecture.vsdx file used for the diagram under the visio folder under Azure Samples.


Deploy the Bicep modules

You can deploy the Bicep modules in the bicep folder using either Azure CLI or Azure PowerShell.


Azure CLI



az group create \
  --name SampleRG \
  --location westeurope
az deployment group create \
  --resource-group SampleRG \
  --template-file main.bicep \
  --parameters vmAdminUsername=<admin-user>






New-AzResourceGroup -Name SampleRG -Location westeurope
New-AzResourceGroupDeployment `
  -ResourceGroupName SampleRG `
  -TemplateFile ./main.bicep `
  -vmAdminUsername "<admin-user>"



Bash Script

You can also use the bash script under the bicep folder to deploy the infrastructure.




# Template

# Variables

# Name and location of the resource group for the Azure Kubernetes Service (AKS) cluster

# Subscription id, subscription name, and tenant id of the current subscription
subscriptionId=$(az account show --query id --output tsv)
subscriptionName=$(az account show --query name --output tsv)
tenantId=$(az account show --query tenantId --output tsv)

# Check if the resource group already exists
echo "Checking if [$resourceGroupName] resource group exists in the [$subscriptionName] subscription..."

az group show --name $resourceGroupName &>/dev/null

if [[ $? != 0 ]]; then
  echo "No [$resourceGroupName] resource group exists in the [$subscriptionName] subscription"
  echo "Creating [$resourceGroupName] resource group in the [$subscriptionName] subscription..."

  # Create the resource group
  az group create --name $resourceGroupName --location $location 1>/dev/null

  if [[ $? == 0 ]]; then
    echo "[$resourceGroupName] resource group created in the [$subscriptionName] subscription"
    echo "Failed to create [$resourceGroupName] resource group in the [$subscriptionName] subscription"
  echo "[$resourceGroupName] resource group already exists in the [$subscriptionName] subscription"

# Validate the Bicep template
if [[ $validateTemplate == 1 ]]; then
  if [[ $useWhatIf == 1 ]]; then
    # Execute a deployment What-If operation at resource group scope
    echo "Previewing changes deployed by [$template] Bicep template..."
    az deployment group what-if \
      --resource-group $resourceGroupName \
      --template-file $template \
      --parameters $parameters

    if [[ $? == 0 ]]; then
      echo "[$template] Bicep template validation succeeded"
      echo "Failed to validate [$template] Bicep template"
    # Validate the Bicep template
    echo "Validating [$template] Bicep template..."
    output=$(az deployment group validate \
      --resource-group $resourceGroupName \
      --template-file $template \
      --parameters $parameters)

    if [[ $? == 0 ]]; then
      echo "[$template] Bicep template validation succeeded"
      echo "Failed to validate [$template] Bicep template"
      echo $output

# Deploy the Bicep template
if [[ $install == 1 ]]; then
  echo "Deploying [$template] Bicep template..."
  az deployment group create \
    --resource-group $resourceGroupName \
    --only-show-errors \
    --template-file $template \
    --parameters $parameters 1>/dev/null

  if [[ $? == 0 ]]; then
    echo "[$template] Bicep template successfully provisioned"
    echo "Failed to provision the [$template] Bicep template"




Make sure to specify a value for the following parameters in the main.parameters.json file:

  • prefix: specifies a prefix for all the Azure resources.
  • authenticationType: specifies the type of authentication when accessing the Virtual Machine. SSH key is recommended. Allowed values: sshPublicKey and password.
  • vmAdminUsername: specifies the name of the administrator account of the virtual machine.
  • vmAdminPasswordOrKey: specifies the SSH Key or password for the virtual machine. SSH key is recommended. We suggest reading the password or SSH Key from Key Vault. For more information, see Use Azure Key Vault to pass secure parameter value during Bicep deployment.


Review deployed resources

Use the Azure portal, Azure CLI, or Azure PowerShell to list the deployed resources in the resource group.


Azure CLI



az resource list --resource-group SampleRG







Get-AzResource -ResourceGroupName SampleRG




Azure Portal






Figure: Azure Resources in the resource group.


Bicep Modules

The sample is composed of many Bicep modules, each deploying a different set of resources. The following table contains the Bicep module that deployed the Azure Private Link Service and related Azure Private Endpoint.



// Parameters
@description('Specifies the name of the Azure Private Link Service.')
param privatelinkServiceName string

@description('Specifies the name of the Azure Private Endpoint.')
param privateEndpointName string

@description('Specifies the name of the load balancer.')
param loadBalancerName string

@description('Specifies the name of the client virtual network.')
param virtualNetworkName string

@description('Specifies the name of the subnet used by the load balancer.')
param subnetName string

@description('Specifies the location.')
param location string = resourceGroup().location

@description('Specifies the resource tags.')
param tags object

// Resources
resource loadBalancer 'Microsoft.Network/loadBalancers@2021-08-01' existing = {
  name: loadBalancerName

resource vnet 'Microsoft.Network/virtualNetworks@2021-08-01' existing = {
  name: virtualNetworkName

resource subnet 'Microsoft.Network/virtualNetworks/subnets@2021-08-01' existing = {
  name: subnetName
  parent: vnet

resource privatelinkService 'Microsoft.Network/privateLinkServices@2021-05-01' = {
  name: privatelinkServiceName
  location: location
  tags: tags
  properties: {
    enableProxyProtocol: false
    loadBalancerFrontendIpConfigurations: [
        id: resourceId('Microsoft.Network/loadBalancers/frontendIpConfigurations',,[0].name)
    ipConfigurations: [
        name: 'ipConfig'
        properties: {
          privateIPAllocationMethod: 'Dynamic'
          privateIPAddressVersion: 'IPv4'
          subnet: {
          primary: false

resource privateEndpoint 'Microsoft.Network/privateEndpoints@2021-05-01' = {
  name: privateEndpointName
  location: location
  properties: {
    subnet: {
    privateLinkServiceConnections: [
        name: privateEndpointName
        properties: {
  dependsOn: [




Connect to the Service Consumer VM via Bastion

Connect to to service consumer virtual machine via Bastion from the Azure portal as follows:

  1. In the Azure portal the service consumer virtual network called PrefixClientVm.
  2. Select Connect on the Overview page.
  3. Select Bastion from the drop-down list.
  4. Specify the Username. Authentication Type, and Password or SSH Private Key.
  5. Click the Connect button.


Access the HTTP service privately from the service consumer VM

Here's how to connect to the http service from the VM by using the private endpoint.

  1. Open the MazingaPrivateLinkServicePrivateEndpoint.nic.GUID network interface associated to the private endpoint.
  2. Read the Private IP address in the Overview page.
  3. Run the curl <private IP address> command.

If everything works as expected, you should see a response message like the following returned by the NGINX website hosted by the service provider virtual machine via Azure Private Link service.

azadmin@MazingaClientVm:~$ curl
Hello World from host MazingaServiceVm !


Clean up resources

When you no longer need the resources that you created with the private link service, delete the resource group. This will remove all the Azure resources.


What is Bicep?

Bicep is a domain-specific language (DSL) that uses declarative syntax to deploy Azure resources. It provides concise syntax, reliable type safety, and support for code reuse. Bicep offers the best authoring experience for your infrastructure-as-code solutions in Azure.


What is Azure Private Link service?

Azure Private Link service is the reference to your own service that is powered by Azure Private Link. Your service that is running behind Azure Standard Load Balancer can be enabled for Private Link access so that consumers to your service can access it privately from their own VNets. Your customers can create a private endpoint inside their VNet and map it to this service. This article explains concepts related to the service provider side.



Figure: Azure Private Link Service.


Assuming that the user deploying the solution has Owner or Contributor role on the resource group or subscription, the private link connection in this sample will be automatically approved. In a real-world scenario, the private endpoint connection will need to be approved by the service provider. For more information, see the approval workflow under Azure Private Link service .



Figure: Azure Private Link service workflow.


Create your Private Link Service

  • Configure your application to run behind a standard load balancer in your virtual network. If you already have your application configured behind a standard load balancer, you can skip this step.
  • Create a Private Link Service referencing the load balancer above. In the load balancer selection process, choose the frontend IP configuration where you want to receive the traffic. Choose a subnet for NAT IP addresses for the Private Link Service. It is recommended to have at least eight NAT IP addresses available in the subnet. All consumer traffic will appear to originate from this pool of private IP addresses to the service provider. Choose the appropriate properties/settings for the Private Link Service.

Azure Private Link Service is only supported on Standard Load Balancer.


Share your service

After you create a Private Link service, Azure will generate a globally unique named moniker called "alias" based on the name you provide for your service. You can share either the alias or resource URI of your service with your customers offline. Consumers can start a Private Link connection using the alias or the resource URI.


Manage your connection requests

After a consumer initiates a connection, the service provider can accept or reject the connection request. All connection requests will be listed under the privateendpointconnections property on the Private Link service. On the Azure portal, you can see select the Private endpoint connections tab under the Azure Private Link service ro see all pending or approved private endpoint connections.



The following are the known limitations when using the Private Link service:

  • Supported only on Standard Load Balancer. Not supported on Basic Load Balancer.
  • Supported only on Standard Load Balancer where backend pool is configured by NIC when using VM/VMSS.
  • Supports IPv4 traffic only
  • Supports TCP and UDP traffic only


Next steps



If you have any feedback, please write a comment below or submit an issue or a PR on GitHub. If you found this article and companion sample useful, please like the article below and give a star to the project on GitHub, thanks.

Version history
Last update:
‎Jul 02 2022 10:09 AM
Updated by: