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
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.
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