Skip to content
Back to Blog
1 min read

Azure Private DNS Zones for Internal Name Resolution

I wrote “2021-07-13-private-dns-zones” to share practical, production-minded guidance on this topic.

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.