Saturday, February 28, 2026

Connecting Azure to MongoDB Atlas via Private Endpoints

In this post, let's see how we can set up Azure Private Endpoints to connect to a MongoDB Atlas cluster.

When we connect to a MongoDB Atlas cluster, we typically use a connection string like this:

mongodb+srv://<CLUSTER_NAME>.bzmphh.mongodb.net

This goes over the public internet. If we are on Azure and want traffic to stay private and routed through Azure's backbone network, we need a Private Endpoint.

Setting up a Private Endpoint between Azure and Atlas involves both sides:
  • Atlas
    • A Private Link Service that our Azure VNet can connect to
  • Azure
    • A Private Endpoint in our VNet's subnet that gets a private IP
    • An Azure Private DNS Zone so our apps resolve the Atlas hostname to the private IP instead of the public one

Let's walk through how to achieve this step by step using PowerShell. Please note that we need az cli and atlas cli for this.

$atlasProjectId = "<ATLAS_PROJECT_ID>"
$atlasClusterName = "<ATLAS_CLUSTER_NAME>"
$atlasRegion = "<ATLAS_REGION>"

$subscription = "<AZURE_SUBSCRIPTION_ID>"
$resourceGroup = "<RESOURCE_GROUP>"
$location = "<REGION>"
$vnetName = "<VNET>"
$subnetName = "<SNET_FOR_PRIVATE_ENDPOINT>"

$peName = "<PE_NAME>"
$peNicName = "<PE_NAME>_nic"
First, we need to create the Private Link Service in Atlas. This is the resource that Azure will connect to.
$peService = atlas privateEndpoints azure create `
    --projectId $atlasProjectId `
    --region $atlasRegion -o json | ConvertFrom-Json

$endpointServiceId = $peService.id
$privateLinkServiceResourceId = $peService.privateLinkServiceResourceId
Now we create the Private Endpoint in our Azure VNet. 
az account set --subscription $subscription

$pe = az network private-endpoint create `
    --resource-group $resourceGroup `
    --location $location `
    --name $peName `
    --nic-name $peNicName `
    --vnet-name $vnetName `
    --subnet $subnetName `
    --private-connection-resource-id $privateLinkServiceResourceId `
    --connection-name "$peName-connection" `
    --manual-request true | ConvertFrom-Json

$peResourceId = $pe.id
Note the "--manual-request true" flag is required because Atlas needs to accept the connection on their side.

The Private Endpoint creates a NIC in our subnet. We need its private IP for our next step.
$pePrivateIp = az network nic show `
    --subscription $subscription `
    --resource-group $resourceGroup `
    --name $peNicName `
    --query "ipConfigurations[0].privateIPAddress" -o tsv
Now we need to ask Atlas to accept the connection.
atlas privateEndpoints azure interfaces create $endpointServiceId `
    --privateEndpointId $peResourceId `
    --privateEndpointIpAddress $pePrivateIp `
    --projectId $atlasProjectId
Now we need a Private DNS Zone so that apps inside the VNet resolve Atlas hostnames to our private IP. We can derive the DNS Zone Name from the cluster's connection string.
$cluster = atlas clusters describe $atlasClusterName `
    --projectId $atlasProjectId -o json | ConvertFrom-Json

$srvHost = $cluster.connectionStrings.standardSrv -replace "mongodb\+srv://", ""
$dnsZoneName = $srvHost.Substring($srvHost.IndexOf('.') + 1)
# Result: bzmphh.mongodb.net

az network private-dns zone create `
    --resource-group $resourceGroup `
    --name $dnsZoneName
Note that we use a specific subdomain (bzmphh.mongodb.net) rather than mongodb.net. This avoids hijacking DNS resolution for all MongoDB Atlas clusters, only our cluster's traffic goes through the private endpoint.

The DNS zone needs to be linked to our VNet so resources inside it can resolve the private records.
$vnetResourceId = az network vnet show `
    --subscription $subscription `
    --resource-group $resourceGroup `
    --name $vnetName `
    --query "id" -o tsv

az network private-dns link vnet create `
    --resource-group $resourceGroup `
    --zone-name $dnsZoneName `
    --name $vnetName `
    --virtual-network $vnetResourceId `
    --registration-enabled false
After registering the PE, Atlas generates private endpoint-specific connection strings. We can find those using the following:
$connectionStrings = atlas clusters connectionStrings describe $atlasClusterName `
    --projectId $atlasProjectId `
    -o json | ConvertFrom-Json
atlas clusters connectionStrings describe
This gives us everything we need, the PE-specific hostnames, ports, and replica set info.

Now we parse the Atlas connection strings and create the DNS records in our Private DNS Zone.
$peConnStr = $connectionStrings.privateEndpoint[0]

$srvHostFull = $peConnStr.srvConnectionString -replace "mongodb\+srv://", ""
$srvPrefix = $srvHostFull.Split('.')[0]

$connPart = ($peConnStr.connectionString -replace "mongodb://", "").Split('?')[0].TrimEnd('/')
$hostPortEntries = $connPart.Split(',')
$aRecordHostFull = $hostPortEntries[0].Split(':')[0]
$aRecordName = $aRecordHostFull -replace "\.$dnsZoneName$", ""
$ports = $hostPortEntries | ForEach-Object { $_.Split(':')[1] }

$queryParams = ($peConnStr.connectionString -split '\?')[1]
$txtValue = ($queryParams -split '&' | Where-Object {
    $_ -match "authSource|replicaSet"
}) -join '&'
Now create the actual records:
# A Record
az network private-dns record-set a add-record `
    --resource-group $resourceGroup `
    --zone-name $dnsZoneName `
    --record-set-name $aRecordName `
    --ipv4-address $pePrivateIp

# SRV Records
foreach ($port in $ports) {
    az network private-dns record-set srv add-record `
        --resource-group $resourceGroup `
        --zone-name $dnsZoneName `
        --record-set-name "_mongodb._tcp.$srvPrefix" `
        --target $aRecordHostFull `
        --priority 0 --weight 0 --port $port
}

# TXT Record
az network private-dns record-set txt add-record `
    --resource-group $resourceGroup `
    --zone-name $dnsZoneName `
    --record-set-name $srvPrefix `
    --value "`"$txtValue`""
Once done, I can see something like this in our Private DNS Zone.
Private DNS Zone: Recordsets

Why Not Just an A Record?

If the private endpoint gives us a single private IP, why do we need three types of DNS records?

When our app uses a connection string like:
mongodb+srv://<CLUSTER_NAME>-pl-0.bzmphh.mongodb.net
The MongoDB driver doesn't just do a simple hostname lookup. It performs three DNS queries:
  • SRV Record
    • This tells the driver which hosts and ports to connect to. For private endpoints, the port is 1024 (not the standard 27017). The SRV record returns:
<CLUSTER_NAME>-pl-0-0.bzmphh.mongodb.net:1024
<CLUSTER_NAME>-pl-0-0.bzmphh.mongodb.net:1025
<CLUSTER_NAME>-pl-0-0.bzmphh.mongodb.net:1026
Without SRV records, the driver wouldn't know which port to use and would default to 27017, which won't work over Private Link.
  • TXT Record
    • This provides the replica set name and auth database:
authSource=admin&replicaSet=atlas-5x58u7-shard-0
Without this, the driver wouldn't know which replica set to join or where to authenticate.
  • A Record
    • This resolves the hostname to the Private IP address of our Private Endpoint. This is what actually routes traffic through Azure Private Link instead of the public internet.
What If You Skip SRV and TXT?

You could technically use a "mongodb://" connection string instead of "mongodb+srv://" and hardcode everything:
mongodb://<CLUSTER_NAME>-pl-0-0.bzmphh.mongodb.net:1024,<CLUSTER_NAME>-pl-0-0.bzmphh.mongodb.net:1025,<CLUSTER_NAME>-pl-0-0.bzmphh.mongodb.net:1026/?authSource=admin&replicaSet=atlas-5x58u7-shard-0
But that means your application configuration now contains infrastructure details, ports, replica set names, and host entries. If anything changes on Atlas's side, you'd need to update and redeploy your app. With "mongodb+srv://", your app connection string is just "mongodb+srv://<CLUSTER_NAME>-pl-0-0.bzmphh.mongodb.net", clean and stable. If something changes, you update the DNS records (infrastructure), not the app config.

So all three are required for "mongodb+srv://" to work over Private Link.

Once everything is set up, you can run the following and verify inside the VNet:
# Check DNS resolution
nslookup <CLUSTER_NAME>-pl-0-0.bzmphh.mongodb.net

# Test connection
mongosh 'mongodb+srv://<CLUSTER_NAME>-pl-0.bzmphh.mongodb.net' \
    --username dbadmin --password 'yourpassword'
The A record should resolve to your private IP, and mongosh should connect without going over the public internet.

Note: The domain (bzmphh.mongodb.net), -pl-x suffix, and port numbers shown here are examples. Update them to match the values for your own Atlas cluster and Private Endpoint setup.

Hope this helps.

Happy Coding.

Regards,
Jaliya

No comments:

Post a Comment