Private linking resources from inside an Azure VM to multiple regional networks

Private linking resources from inside an Azure VM to multiple regional networks

ยท

7 min read

We are using Private Linking - as described here - in multiple scenarios, globally distributed:

Private Linked resources in multiple regions

Regular deployments are usually handled by build agents in those isolated regional networks which (๐Ÿ˜€ of course only for emergency debugging) also can be used as jump boxes.

This can make some out-of-the-ordinary maintenance jobs, which are not covered with cross-regional deployment pipelines, a bit cumbersome as one practically has to connect to each of these jump boxes.

To make this easier, I created a script in PowerShell, which allows to connect to a type of resource in all those networks at once from a standalone Azure VM.

A sample configuration with Key Vault:

VM connected to Key Vault instances in multiple regions with Private Link

I considered virtual network peering from my VMs virtual network into the virtual networks of those regions but wanted to avoid to get blocked by potential IP address space overlaps. Hence I implemented linking those regional resources into my VMs virtual network and also maintaining private DNS records in a private DNS zone connected to this virtual network and with that not messing up the original environment.

In the following paragraphs I describe the most relevant parts of the script:

Setting the stage

When working with Private Linking and Private DNS the private DNS zone name and the subresource or groupId has to be used according to this table. Hence the script takes that groupId as input parameter as well as 2 RegEx patterns to identify resources and resource groups to be linked:

param (

    [Parameter(Mandatory)]
    [ValidateSet(
        "blob",
        "configurationStores",
        "namespace",
        "registry",
        "sites",
        "Sql",
        "sqlServer",
        "table",
        "vault"
    )]
    [string]
    $GroupId,

    [Parameter(Mandatory)]
    [string]
    $ResourceNamePattern,

    [Parameter(Mandatory)]
    [string]
    $ResourceGroupNamePattern,

    [switch]
    $SkipDeletes,
    [switch]
    $SkipCreate
)

With these input parameters the relevant private DNS zone is assigned and the relevant resource ids are obtained:

# determine resource Ids

switch ($GroupId) {
    "blob" {
        $dnsZone = "privatelink.blob.core.windows.net"
        $resourceType = "Microsoft.Storage/storageAccounts"
    }
    "configurationStores"    {
        $dnsZone = "privatelink.azconfig.io"
        $resourceType = "Microsoft.AppConfiguration/configurationStores"
    }
    "namespace" {
        $dnsZone = "privatelink.servicebus.windows.net"
        $resourceType = "Microsoft.ServiceBus/namespaces"
    }
    "registry" {
        $dnsZone = "privatelink.azurecr.io"
        $resourceType = "Microsoft.ContainerRegistry/registries"
    }
    "sites"    {
        $dnsZone = "privatelink.azurewebsites.net"
        $resourceType = "Microsoft.Web/sites"
    }
    "Sql"    {
        $dnsZone = "privatelink.documents.azure.com"
        $resourceType = "Microsoft.AzureCosmosDB/databaseAccounts"
    }
    "sqlServer"    {
        $dnsZone = "privatelink.database.windows.net"
        $resourceType = "Microsoft.Sql/servers"
    }
    "table" {
        $dnsZone = "privatelink.table.core.windows.net"
        $resourceType = "Microsoft.Storage/storageAccounts"
    }
    "vault" {
        $dnsZone = "privatelink.vaultcore.azure.net"
        $resourceType = "Microsoft.KeyVault/vaults"
    }
}

$resources = @()

az resource list --resource-type $resourceType -o json | ConvertFrom-Json | `
    ? { $_.name -match $ResourceNamePattern -and $_.resourceGroup -match $ResourceGroupNamePattern } | % {
    $resources += @{id = $_.id; name = $_.name }
}

if ($resources.Count -eq 0) {
    Write-Error "No resources found matching the pattern"
    Exit
}

Extracting VM metadata

To retrieve information about the Azure VM one is currently working on, the metadata information endpoint can be used:

$vmInfo = Invoke-RestMethod -Headers @{"Metadata" = "true" } -Method GET -NoProxy -Uri "http://169.254.169.254/metadata/instance?api-version=2021-02-01"

$nicId = az vm show --subscription $vmInfo.compute.subscriptionId --resource-group $vmInfo.compute.resourceGroupName --name $vmInfo.compute.name --query networkProfile.networkInterfaces[0].id --output tsv

$subnetId = az network nic show --ids $nicId --query ipConfigurations[0].subnet.id --output tsv

$vnetInfo = $subnetId.split('/')[0..8]
$vnetId = [string]::Join("/", $vnetInfo)

This extracts the Ids of the virtual network and the subnet the current Azure VM is connected to.

vnetId is derived from subnetId by breaking down the segments as neither az network nic show nor az network vnet subnet show return a ready-to-use vnetId; not a nice approach working with those segments, but maybe someone can leave me a comment on how to improve readability here

Wait a minute - why mixing PowerShell and Azure CLI?

I decided to use Azure CLI instead of Azure PowerShell as CLI allows working with resources in subscriptions outside the current context by simply specifying the remote --subscription and --resource-group. With Azure PowerShell I would be needing to switch back and forth - from/to subscription which holds the VM and to/from subscription which holds the regional networks and resources.

the script assumes that Azure CLI session / context is set to the subscription holding the resources and that Azure VM is in another subscription

All the steps in this script can be easily achieved with Azure CLI and bash / zsh combined with jq.

Private DNS zone

First we need a private DNS zone connected to the VMs virtual network which will receive the private IP address entries of the private link endpoints:

Write-Host "create/check private DNS zone" $dnsZone "for group" $groupId

if (!$(az network private-dns zone list -g $vmInfo.compute.resourceGroupName --subscription $vmInfo.compute.subscriptionId --query "[?name == '$dnsZone'].id" -o tsv)) {
    az network private-dns zone create -g $vmInfo.compute.resourceGroupName --subscription $vmInfo.compute.subscriptionId `
        -n $dnsZone
}

Write-Host "create private DNS link"

az network private-dns link vnet create -g $vmInfo.compute.resourceGroupName --subscription $vmInfo.compute.subscriptionId `
    -n $("vnet-" + $groupId + "-dns-link") `
    -z $dnsZone -v $vnetId -e false

be aware that sometimes a VM restart is required when switching a virtual network from one private DNS link to another

The script then iterates through a the set of resources ...

  • creates names unique within the Azure VMs subscription + resource group for private link, private endpoint and DNS zone group
  • creates a private endpoint and link from the target resource to VMs subnet (could also be another subnet in the VMs virtual network)
  • creates a private DNS zone group which automatically maintains the private DNS zone record for the private endpoint within the private DNS zone connected to the VMs virtual network
Write-Host "create/check private DNS zone" $dnsZone "for group" $groupId

if (!$(az network private-dns zone list -g $vmInfo.compute.resourceGroupName --subscription $vmInfo.compute.subscriptionId --query "[?name == '$dnsZone'].id" -o tsv)) {
    az network private-dns zone create -g $vmInfo.compute.resourceGroupName --subscription $vmInfo.compute.subscriptionId `
        -n $dnsZone
}

Write-Host "create private DNS link"

az network private-dns link vnet create -g $vmInfo.compute.resourceGroupName --subscription $vmInfo.compute.subscriptionId `
    -n $("vnet-" + $groupId + "-dns-link") `
    -z $dnsZone -v $vnetId -e false

Write-Host "private link resources"

foreach ($resource in $resources) {
    Write-Host "link" $resource.name

    $linkName = $GroupId + "-" + $resource.name + "-" + $vmInfo.compute.name + "-link"
    $endpointName = $GroupId + "-" + $resource.name + "-" + $vmInfo.compute.name + "-pep"
    $groupName = $GroupId + "-" + $resource.name + "-" + $vmInfo.compute.name + "-zonegroup"

    az network private-endpoint create --connection-name $linkName `
        --name $endpointName `
        -g $vmInfo.compute.resourceGroupName `
        --subscription $vmInfo.compute.subscriptionId `
        --private-connection-resource-id $resource.id `
        --group-id $GroupId `
        --subnet $subnetId

    az network private-endpoint dns-zone-group create `
        -g $vmInfo.compute.resourceGroupName `
        --subscription $vmInfo.compute.subscriptionId `
        --endpoint-name $endpointName `
        --name $groupName `
        --private-dns-zone $dnsZone `
        --zone-name $dnsZone            
}

That's it. To connect to other types of resources "just" $dnsZone and $groupId need to be exchanged as well as the determination of the target resource's resourceId.

In our scenarios we also have globally distributed resources like

  • CosmosDB
  • API Management
  • Container Registry

(compared to regional resources like Key Vault, Storage, SQL, Service Bus).

For those global resources it makes no sense to connect to all endpoints in all regions. Hence the input parameters would need to be adjusted to only cover these resources in one region - most probably the main region.

Housekeeping

At beginning of the script existing virtual network peerings (which in my case could potentially cause conflicts with private links), existing private endpoints and private DNS links are deleted from the VMs virtual network to give the script a clean slate to work on:

Write-Host "delete existing (VM's) virtual network links"

az network vnet peering list --vnet-name $vnetInfo[8] -g $vnetInfo[4] --subscription $vnetInfo[2] --output json | ConvertFrom-Json | % {
    az network vnet peering delete --ids $_.Id
}

Write-Host "delete existing (VM's) private endpoint links"

az network private-endpoint list -g $vmInfo.compute.resourceGroupName --subscription $vmInfo.compute.subscriptionId --output json | ConvertFrom-Json | % {
    az network private-endpoint delete --ids $_.Id
}

Write-Host "delete existing (VM's) private DNS links"

if ($(az network private-dns zone list -g $vmInfo.compute.resourceGroupName --subscription $vmInfo.compute.subscriptionId --query "[?name == '$dnsZone'].id" -o tsv)) {
    az network private-dns link vnet list -g $vmInfo.compute.resourceGroupName --subscription $vmInfo.compute.subscriptionId --zone-name $dnsZone -o json | ConvertFrom-Json | % {
        if ($_.virtualNetwork.id -eq $vnetId) {
            Write-Host "deleting link: $($_.id)"
            az network private-dns link vnet delete -g $vmInfo.compute.resourceGroupName --subscription $vmInfo.compute.subscriptionId --zone-name $dnsZone --name $_.name --yes
        }
    }
}

This section of the script would only delete private endpoints and links created in the context of the VM. In case such endpoints and private DNS links exist in other subscriptions or resource groups, az network private-endpoint list + delete as well as az network private-dns link vnet list + delete need to be enhanced.

Conclusion

For me this is a very handy approach to switch with my Azure VM between various isolated environments.

Did you find this article valuable?

Support The ancient IT guy blog by becoming a sponsor. Any amount is appreciated!

ย