Back to Blog
6 min read

Azure Private DNS Zones for Internal Name Resolution

Introduction

Azure Private DNS provides a reliable, secure DNS service for your virtual networks. It enables you to use custom domain names within your Azure infrastructure without deploying your own DNS servers. Private DNS zones are essential for Private Link endpoints and internal service discovery.

In this post, we will explore how to set up and manage Private DNS zones effectively.

Creating Private DNS Zones

Set up a private DNS zone for your organization:

# Create private DNS zone
az network private-dns zone create \
    --resource-group rg-dns \
    --name contoso.internal

# Link DNS zone to virtual network
az network private-dns link vnet create \
    --resource-group rg-dns \
    --zone-name contoso.internal \
    --name hub-vnet-link \
    --virtual-network /subscriptions/$SUBSCRIPTION_ID/resourceGroups/rg-networking/providers/Microsoft.Network/virtualNetworks/vnet-hub \
    --registration-enabled true

# Create DNS records
az network private-dns record-set a create \
    --resource-group rg-dns \
    --zone-name contoso.internal \
    --name api

az network private-dns record-set a add-record \
    --resource-group rg-dns \
    --zone-name contoso.internal \
    --record-set-name api \
    --ipv4-address 10.0.1.10

Terraform Configuration

Complete Private DNS setup with Terraform:

# Private DNS Zone
resource "azurerm_private_dns_zone" "internal" {
  name                = "contoso.internal"
  resource_group_name = azurerm_resource_group.dns.name

  tags = {
    Environment = "Production"
    ManagedBy   = "Terraform"
  }
}

# Link to Hub VNet with auto-registration
resource "azurerm_private_dns_zone_virtual_network_link" "hub" {
  name                  = "hub-vnet-link"
  resource_group_name   = azurerm_resource_group.dns.name
  private_dns_zone_name = azurerm_private_dns_zone.internal.name
  virtual_network_id    = azurerm_virtual_network.hub.id
  registration_enabled  = true

  tags = {
    Environment = "Production"
  }
}

# Link to Spoke VNets (without auto-registration)
resource "azurerm_private_dns_zone_virtual_network_link" "spokes" {
  for_each = toset(["app", "data", "dev"])

  name                  = "${each.key}-vnet-link"
  resource_group_name   = azurerm_resource_group.dns.name
  private_dns_zone_name = azurerm_private_dns_zone.internal.name
  virtual_network_id    = azurerm_virtual_network.spoke[each.key].id
  registration_enabled  = false

  tags = {
    Environment = "Production"
    Spoke       = each.key
  }
}

# A Records
resource "azurerm_private_dns_a_record" "services" {
  for_each = {
    "api"      = "10.0.1.10"
    "web"      = "10.0.1.20"
    "database" = "10.0.2.10"
    "cache"    = "10.0.2.20"
  }

  name                = each.key
  zone_name           = azurerm_private_dns_zone.internal.name
  resource_group_name = azurerm_resource_group.dns.name
  ttl                 = 300
  records             = [each.value]
}

# CNAME Records
resource "azurerm_private_dns_cname_record" "aliases" {
  for_each = {
    "www"     = "web.contoso.internal"
    "backend" = "api.contoso.internal"
  }

  name                = each.key
  zone_name           = azurerm_private_dns_zone.internal.name
  resource_group_name = azurerm_resource_group.dns.name
  ttl                 = 300
  record              = each.value
}

Private DNS Zones for Private Link

Create DNS zones for Azure Private Link services:

# Private DNS zones for Azure services
locals {
  private_link_dns_zones = {
    "blob"          = "privatelink.blob.core.windows.net"
    "file"          = "privatelink.file.core.windows.net"
    "queue"         = "privatelink.queue.core.windows.net"
    "table"         = "privatelink.table.core.windows.net"
    "dfs"           = "privatelink.dfs.core.windows.net"
    "sql"           = "privatelink.database.windows.net"
    "cosmos_sql"    = "privatelink.documents.azure.com"
    "keyvault"      = "privatelink.vaultcore.azure.net"
    "acr"           = "privatelink.azurecr.io"
    "eventhub"      = "privatelink.servicebus.windows.net"
    "servicebus"    = "privatelink.servicebus.windows.net"
    "cognitive"     = "privatelink.cognitiveservices.azure.com"
    "datafactory"   = "privatelink.datafactory.azure.net"
    "synapse"       = "privatelink.sql.azuresynapse.net"
    "synapse_dev"   = "privatelink.dev.azuresynapse.net"
  }
}

resource "azurerm_private_dns_zone" "private_link" {
  for_each = local.private_link_dns_zones

  name                = each.value
  resource_group_name = azurerm_resource_group.dns.name

  tags = {
    Purpose = "Private Link"
    Service = each.key
  }
}

resource "azurerm_private_dns_zone_virtual_network_link" "private_link" {
  for_each = local.private_link_dns_zones

  name                  = "hub-link-${each.key}"
  resource_group_name   = azurerm_resource_group.dns.name
  private_dns_zone_name = azurerm_private_dns_zone.private_link[each.key].name
  virtual_network_id    = azurerm_virtual_network.hub.id
  registration_enabled  = false
}

Creating Private Endpoints with DNS

Integrate private endpoints with DNS zones:

from azure.mgmt.network import NetworkManagementClient
from azure.identity import DefaultAzureCredential

credential = DefaultAzureCredential()
network_client = NetworkManagementClient(credential, subscription_id)

def create_private_endpoint_with_dns(config):
    """Create private endpoint and configure DNS."""

    # Create private endpoint
    pe = network_client.private_endpoints.begin_create_or_update(
        resource_group_name=config["resource_group"],
        private_endpoint_name=config["pe_name"],
        parameters={
            "location": config["location"],
            "properties": {
                "subnet": {
                    "id": config["subnet_id"]
                },
                "privateLinkServiceConnections": [{
                    "name": f"{config['pe_name']}-connection",
                    "properties": {
                        "privateLinkServiceId": config["target_resource_id"],
                        "groupIds": config["group_ids"]
                    }
                }]
            }
        }
    ).result()

    # Get private IP from the endpoint
    private_ip = pe.custom_dns_configs[0].ip_addresses[0]

    # Create DNS record in private DNS zone
    from azure.mgmt.privatedns import PrivateDnsManagementClient
    dns_client = PrivateDnsManagementClient(credential, subscription_id)

    dns_client.record_sets.create_or_update(
        resource_group_name=config["dns_resource_group"],
        private_zone_name=config["dns_zone_name"],
        relative_record_set_name=config["dns_record_name"],
        record_type="A",
        parameters={
            "ttl": 300,
            "a_records": [{"ipv4_address": private_ip}]
        }
    )

    return pe, private_ip

# Create private endpoint for Storage Account
storage_pe = create_private_endpoint_with_dns({
    "resource_group": "rg-privatelink",
    "pe_name": "pe-storage-blob",
    "location": "eastus",
    "subnet_id": f"/subscriptions/{subscription_id}/resourceGroups/rg-networking/providers/Microsoft.Network/virtualNetworks/vnet-hub/subnets/subnet-privatelink",
    "target_resource_id": f"/subscriptions/{subscription_id}/resourceGroups/rg-storage/providers/Microsoft.Storage/storageAccounts/mystorageaccount",
    "group_ids": ["blob"],
    "dns_resource_group": "rg-dns",
    "dns_zone_name": "privatelink.blob.core.windows.net",
    "dns_record_name": "mystorageaccount"
})

Auto-Registration

Enable automatic DNS registration for VMs:

# VNet link with auto-registration
resource "azurerm_private_dns_zone_virtual_network_link" "auto_register" {
  name                  = "vnet-auto-register"
  resource_group_name   = azurerm_resource_group.dns.name
  private_dns_zone_name = azurerm_private_dns_zone.internal.name
  virtual_network_id    = azurerm_virtual_network.hub.id
  registration_enabled  = true  # VMs will auto-register
}

# When a VM is created in this VNet, its hostname will be
# automatically registered in the DNS zone
resource "azurerm_linux_virtual_machine" "example" {
  name                = "vm-web-01"
  resource_group_name = azurerm_resource_group.compute.name
  location            = azurerm_resource_group.compute.location
  size                = "Standard_D2s_v3"

  # VM will be accessible as: vm-web-01.contoso.internal
  computer_name = "vm-web-01"

  network_interface_ids = [azurerm_network_interface.example.id]

  # ... other VM configuration
}

DNS Forwarding

Set up DNS forwarding for hybrid scenarios:

# Configure Azure Firewall as DNS proxy
def configure_dns_forwarding(firewall_policy_name, resource_group, dns_servers):
    """Configure Azure Firewall DNS settings for forwarding."""

    from azure.mgmt.network import NetworkManagementClient

    network_client = NetworkManagementClient(credential, subscription_id)

    # Get firewall policy
    policy = network_client.firewall_policies.get(resource_group, firewall_policy_name)

    # Update DNS settings
    policy.dns_settings = {
        "servers": dns_servers,  # On-premises DNS servers
        "enable_proxy": True
    }

    result = network_client.firewall_policies.begin_create_or_update(
        resource_group,
        firewall_policy_name,
        policy
    ).result()

    return result

# Configure forwarding to on-premises DNS
configure_dns_forwarding(
    "firewall-policy-hub",
    "rg-networking",
    ["192.168.1.10", "192.168.1.11"]  # On-prem DNS servers
)

# Alternative: Using Azure DNS Private Resolver (Preview)
def create_dns_resolver(resource_group, name, vnet_id, location):
    """Create DNS Private Resolver for hybrid DNS."""

    resolver = network_client.dns_resolvers.begin_create_or_update(
        resource_group,
        name,
        {
            "location": location,
            "properties": {
                "virtualNetwork": {
                    "id": vnet_id
                }
            }
        }
    ).result()

    return resolver

Querying DNS Records

Manage DNS records programmatically:

from azure.mgmt.privatedns import PrivateDnsManagementClient

dns_client = PrivateDnsManagementClient(credential, subscription_id)

# List all record sets in a zone
def list_dns_records(resource_group, zone_name):
    records = dns_client.record_sets.list(resource_group, zone_name)

    for record in records:
        print(f"\nRecord: {record.name}")
        print(f"  Type: {record.type.split('/')[-1]}")
        print(f"  TTL: {record.ttl}")

        if record.a_records:
            for a in record.a_records:
                print(f"  A: {a.ipv4_address}")

        if record.cname_record:
            print(f"  CNAME: {record.cname_record.cname}")

        if record.txt_records:
            for txt in record.txt_records:
                print(f"  TXT: {txt.value}")

list_dns_records("rg-dns", "contoso.internal")

# Create SRV record for service discovery
srv_record = dns_client.record_sets.create_or_update(
    resource_group_name="rg-dns",
    private_zone_name="contoso.internal",
    relative_record_set_name="_http._tcp",
    record_type="SRV",
    parameters={
        "ttl": 300,
        "srv_records": [
            {
                "priority": 10,
                "weight": 60,
                "port": 80,
                "target": "web-01.contoso.internal"
            },
            {
                "priority": 10,
                "weight": 40,
                "port": 80,
                "target": "web-02.contoso.internal"
            }
        ]
    }
)

Monitoring DNS

Monitor DNS zone activity:

# Enable diagnostic logging
az monitor diagnostic-settings create \
    --resource /subscriptions/$SUBSCRIPTION_ID/resourceGroups/rg-dns/providers/Microsoft.Network/privateDnsZones/contoso.internal \
    --name dns-diagnostics \
    --workspace /subscriptions/$SUBSCRIPTION_ID/resourceGroups/rg-monitoring/providers/Microsoft.OperationalInsights/workspaces/log-analytics-ws \
    --logs '[
        {
            "category": "RecordSetChangeAuditLogs",
            "enabled": true
        },
        {
            "category": "VirtualNetworkLinkChangeAuditLogs",
            "enabled": true
        }
    ]'
# Query DNS audit logs
def query_dns_changes(workspace_id, zone_name, days=7):
    """Query Log Analytics for DNS changes."""

    query = f"""
    AzureDiagnostics
    | where ResourceProvider == "MICROSOFT.NETWORK"
    | where Resource contains "{zone_name}"
    | where TimeGenerated > ago({days}d)
    | project TimeGenerated, OperationName, Resource, CallerIpAddress, ResultType
    | order by TimeGenerated desc
    """

    from azure.monitor.query import LogsQueryClient

    logs_client = LogsQueryClient(credential)
    result = logs_client.query_workspace(workspace_id, query)

    for row in result.tables[0].rows:
        print(f"{row[0]}: {row[1]} - {row[4]}")

    return result

Conclusion

Azure Private DNS zones are fundamental for internal name resolution and Private Link integration. They provide a managed DNS solution that eliminates the need for custom DNS infrastructure while supporting advanced scenarios like auto-registration and hybrid DNS forwarding.

Key points to remember: each zone can be linked to multiple VNets, only one VNet link per zone can have auto-registration enabled, and Private Link services require specific DNS zone names to work correctly. Proper DNS planning is essential for a well-functioning Azure network architecture.

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.