Atlas and Azure Private Link 🔗
What we're doing here 🤔
Goal: Your app connects to MongoDB Atlas over private networking (not the public internet) using an Atlas Private Endpoint–aware connection string.
Key idea 💡: With Private Endpoint–aware connection strings, the driver resolves an SRV record (example: _mongodb._tcp.cluster0-pl-0.<hash>.mongodb.net) that points to hostnames like pl-0-<region>.<hash>.mongodb.net. Your DNS must resolve those hostnames to the private IP of your Azure Private Endpoint NIC.
Useful background (light reading, not homework 😄):
- MongoDB Atlas Private Endpoints overview
- Azure Private Link overview
- SRV records (Wikipedia)
- DNS basics (Wikipedia)
Azure Private Link + Atlas ✅
0) Prereqs (don’t skip) 🧰
- Atlas cluster: Dedicated cluster M10+ (Private Endpoints are not supported on free/flex tiers).
- Atlas permissions: You’ll want Project Owner (or higher) in Atlas to configure Private Endpoints.
- Azure network: An Azure VNet + subnet where your app will run (VM, AKS node pool, container host, etc.).
- Database user: A MongoDB database user in Atlas.
- Tools (optional but helpful):
azCLI (Azure)atlasCLI (MongoDB)node18+ /npm(only if you want the test app)
1) Create the Atlas Private Link Service (Atlas-side) 🏗️
This step creates the Atlas-side Azure Private Link Service that your Azure Private Endpoint will connect to.
Atlas docs (workflow + concepts):
Option A: Atlas UI (recommended)
- Atlas → Security → Database & Network Access → Private Endpoint
- Click Add Private Endpoint
- Choose Azure
- Choose the same region as your Atlas cluster (or the region Atlas requires for your deployment)
- Wait until the Atlas Endpoint Service status becomes Available
At the end of this step, Atlas will show you Azure-side values — especially a resource ID that looks like:
.../providers/Microsoft.Network/privateLinkServices/pls_<...>
That is the target you’ll connect to from Azure.
Option B: Atlas CLI (repeatable)
atlas privateEndpoints azure create \
--region eastus \
--projectId <ATLAS_PROJECT_ID> \
--output json
This returns an Atlas endpoint service ID you’ll use later during the handshake step.
2) Create the Azure Private Endpoint (Azure-side) 🔌
Now you create the Azure Private Endpoint inside your VNet/subnet. This creates a NIC with a private IP in your subnet and connects it to Atlas’s Private Link Service.
Azure docs:
Option A: Azure Portal (most common)
- Azure Portal → Private Link Center → Private endpoints → + Create
- Choose your Subscription, Resource group, and Region
- Networking step: choose your Virtual network and Subnet
- Resource step: connect by Resource ID
- Paste the Atlas-provided Private Link Service resource ID (the one that ends in
.../privateLinkServices/pls_...) - Create the private endpoint
Option B: Azure CLI (scriptable)
Atlas often generates a ready-to-run command like this:
az network private-endpoint create \
--resource-group resource-group-name \
--name endpoint-name \
--vnet-name vnet-name \
--subnet subnet-xxxx1 \
--private-connection-resource-id /subscriptions/<...>/resourceGroups/<...>/providers/Microsoft.Network/privateLinkServices/pls_<...> \
--connection-name pls_<...> \
--manual-request true
Notes:
- You control:
--resource-group,--name,--vnet-name,--subnet - Atlas controls:
--private-connection-resource-id ...privateLinkServices/pls_...(don’t change it) - If Azure CLI requires a location, add:
--location <azure-region>(same region as your VNet)
Field mapping (Atlas-generated values → Azure CLI flags)
| CLI flag | What it is | Where the value comes from |
|---|---|---|
--resource-group <rg> |
RG where the private endpoint resource will live | You choose |
--name <name> |
Name of the Azure Private Endpoint resource | You choose |
--vnet-name <vnet> |
VNet to place the endpoint NIC into | You choose |
--subnet <subnet> |
Subnet inside that VNet | You choose |
--private-connection-resource-id .../privateLinkServices/pls_... |
Target Atlas Private Link Service | Atlas provides |
--connection-name <conn-name> |
Friendly name for the PE connection | Atlas suggests (any name is fine) |
--manual-request true |
Manual approval handshake | Atlas expects this |
3) Register the Azure Private Endpoint with Atlas (the “handshake”) 🤝
After Azure creates the Private Endpoint, you need two values from Azure:
- Azure Private Endpoint resource ID (the PE object ID)
- Private IP address assigned to the endpoint NIC
Get the values from Azure (Portal)
- Azure Private Endpoint → Properties → copy the Resource ID
- Looks like:
/subscriptions/.../resourceGroups/.../providers/Microsoft.Network/privateEndpoints/<your-endpoint-name>
- Looks like:
- Azure Private Endpoint → Network interface (or DNS configuration / NIC) → copy the Private IP
Option A: Atlas UI
Atlas → Database & Network Access → Private Endpoint, find your Azure entry and complete the wizard:
- Paste the Azure Private Endpoint Resource ID
- Paste the Private IP address
Then wait for Atlas to show the endpoint as Available.
Option B: Atlas CLI
atlas privateEndpoints azure interfaces create <endpointServiceId> \
--privateEndpointId "<AZURE_PRIVATE_ENDPOINT_RESOURCE_ID>" \
--privateEndpointIpAddress "<PRIVATE_IP>" \
--projectId <ATLAS_PROJECT_ID> \
--output json
4) DNS (the #1 reason “it looks green but won’t connect”) 🧠
This is where most Private Link setups actually succeed or fail.
Your app will connect using a Private Endpoint–aware SRV hostname like:
cluster0-pl-0.<hash>.mongodb.net
That SRV record resolves to one or more targets like:
pl-0-<region>.<hash>.mongodb.net
Inside Azure, those pl-0-... hostnames must resolve to the private IP of your Private Endpoint NIC.
Atlas docs for this behavior:
Option A (recommended): Azure Private DNS Zone 🗺️
- Create a Private DNS zone for the cluster’s subdomain:
- Prefer:
<hash>.mongodb.net(scoped + safer)
- Prefer:
- Link the private DNS zone to your VNet.
- Add A record(s) for the SRV targets returned by DNS:
- Record name:
pl-0-<region>(if zone is<hash>.mongodb.net) - Record value: the Private IP of your Private Endpoint NIC
- Record name:
If your SRV lookup returns multiple targets (or you have multiple endpoints), create A records for each pl-<n>-... hostname.
Option B (quick proof): /etc/hosts on the app machine 🧪
If you just want to prove connectivity quickly, you can map the SRV target hostname directly to the private IP temporarily:
sudo sh -c 'echo "<PRIVATE_IP> pl-0-<region>.<hash>.mongodb.net" >> /etc/hosts'
Not production DNS, but great for a fast “does this work?” ✅
5) Smoke testing (DNS first, then TCP) 🔍
Run these tests from the machine where your app will run (Azure VM/AKS node/bastion/etc.). Running from your laptop only works if you’re truly on that VNet (VPN/ExpressRoute) and using the same DNS.
5.1 Confirm status (control-plane) 🟢
- Atlas: Network Access → Private Endpoint shows Available
- Azure: Private Endpoint connection shows Approved (not Pending)
If either isn’t true, don’t bother testing DNS yet.
5.2 Get the SRV hostname from Atlas 🔗
Atlas → Database (Deployments/Clusters) → Connect → Connect your application
Pick the connection string that mentions Private Endpoint (or includes -pl-0-), like:
mongodb+srv://...@cluster0-pl-0.<hash>.mongodb.net/?...
The hostname after the @ is your SRV hostname.
5.3 Resolve the SRV record 📌
nslookup -type=SRV _mongodb._tcp.<SRV_HOSTNAME>
Example:
nslookup -type=SRV _mongodb._tcp.cluster0-pl-0.<hash>.mongodb.net
You should see one or more SRV targets and ports.
5.4 Resolve the SRV target to an IP 🧭
Pick one SRV target hostname and resolve it:
nslookup <SRV_TARGET_HOSTNAME>
✅ Success looks like: it returns a private IP (your Private Endpoint NIC IP).
If it returns a public IP, your DNS isn’t wired for Private Link yet.
5.5 Test TCP reachability 🔥
If you have nc:
nc -vz <SRV_TARGET_HOSTNAME> <PORT>
✅ Success looks like: “succeeded” / “connected”.
6) Optional: tiny Node.js “hello world” to prove it works 👋
Not the point of the post, but it’s the cleanest end-to-end confirmation.
MongoDB Node driver docs:
6.1 Setup
mkdir atlas-private-endpoint-hello
cd atlas-private-endpoint-hello
npm init -y
npm i mongodb dotenv
Create .env:
MONGODB_URI="mongodb+srv://<user>:<pass>@<cluster>-pl-0.<hash>.mongodb.net/?retryWrites=true&w=majority"
MONGODB_DB="pe_demo"
6.2 index.js
import "dotenv/config";
import { MongoClient } from "mongodb";
const uri = process.env.MONGODB_URI;
if (!uri) throw new Error("Missing MONGODB_URI in .env");
const dbName = process.env.MONGODB_DB || "pe_demo";
const client = new MongoClient(uri);
async function main() {
await client.connect();
const db = client.db(dbName);
const col = db.collection("hello");
// CREATE (upsert)
const doc = { _id: "hello", msg: "hello private endpoint", ts: new Date() };
await col.replaceOne({ _id: doc._id }, doc, { upsert: true });
// READ
console.log("READ:", await col.findOne({ _id: "hello" }));
// UPDATE
await col.updateOne(
{ _id: "hello" },
{ $set: { msg: "updated over private link 🚀", updatedAt: new Date() } }
);
console.log("UPDATED:", await col.findOne({ _id: "hello" }));
// DELETE
await col.deleteOne({ _id: "hello" });
console.log("DELETED count:", await col.countDocuments({ _id: "hello" }));
await client.close();
}
main().catch(async (err) => {
console.error(err);
try { await client.close(); } catch {}
process.exit(1);
});
Run it:
node index.js
What “done” looks like ✅
- Atlas private endpoint status: Available
- Azure private endpoint connection: Approved
nslookup -type=SRV _mongodb._tcp.<srv-host>returns SRV targetsnslookup <pl-0-...>returns a private IPnc -vz <pl-0-...> <port>connects- Node app connects and CRUD works 🎉
If you hit issues, 90% of the time it’s DNS. Get pl-0-... resolving privately first, then everything else becomes boring (the good kind of boring 😄).