Skip to content
meetadev for AndroidYour feed and Pulse alerts, in your pocket.Get it on Google PlayGet app
Blogs
Deploying a self hosted financial grade MongoDB replica set · Part 3 of 11

Three regions, one private network: the Azure foundation for a production MongoDB replica set

The database doesn't decide its own failure boundaries, the network does. Before the first MongoDB process starts, three Azure regions need to find each other privately, trust each other selectively, and resolve each other by name. This is how that foundation gets built.

17 May · 12 min readPublished 17 May 2026 · 12 min read · 127 views
Updated 20 Oct
Three regions, one private network: the Azure foundation for a production MongoDB replica set

A replica set is only as resilient as the network underneath it. MongoDB doesn't decide its own failure boundaries; the infrastructure does. If two of your three nodes share a network boundary, a single failure event can take both of them out simultaneously, regardless of how carefully you've configured your write concerns or election timeouts. The database inherits whatever topology you hand it.

This is the constraint that shapes everything in this article. The goal isn't three VMs running MongoDB - it's three VMs in three distinct failure domains, connected privately, isolated from the public internet, and able to resolve each other by hostname across regional boundaries. MongoDB comes later. The network has to be right first.

The architecture this builds toward is a three-node replica set spanning UAE North, South Africa North, and West Europe - two geopolitical zones, three Azure regions, one private mesh. By the end of this article, none of those nodes are running MongoDB yet. What they can do is reach each other by hostname across continents with zero public attack surface, which is the only acceptable starting condition for what comes next.

architecture.png
architecture.png

The network design has four components, each earning its place by supporting the failure model: VNets with non-overlapping address spaces, mesh peering connecting all three regions, a private DNS zone for stable hostname resolution, and NSGs enforcing the security boundary at the subnet level. Each decision is a consequence of the architecture, not a default.

VNET DESIGN AND CIDR RATIONALE


Three VNets, one per region. The address spaces are deliberately non-overlapping:

VNet

Region

Address Space

Subnet

vnet-mongodb-uae

UAE North

10.1.0.0/16

10.1.1.0/24

vnet-mongodb-southafrica

South Africa North

10.2.0.0/16

10.2.1.0/24

vnet-mongodb-westeurope

West Europe

10.3.0.0/16

10.3.1.0/24

The non-overlapping requirement isn't a preference, it's a hard constraint imposed by VNet peering. Azure cannot route between two VNets whose address spaces overlap. If you assign 10.1.0.0/16 to two regions and then attempt to peer them, the peering will be rejected. The CIDR ranges here were chosen specifically to leave that option open without conflict.

The /16 allocation per region is intentional headroom. Each replica set node only needs one IP address today, but the subnet sits inside a /16 that has room for future workloads in the same region —monitoring agents, application servers, bastion hosts without requiring a redesign of the address space. The /24 subnet carves out 256 addresses for the MongoDB tier specifically, keeping it isolated within the broader VNet.

SQL
az network vnet create \
  --name vnet-mongodb-uae \
  --resource-group rg-mongodb-uae-north \
  --location uaenorth \
  --address-prefix 10.1.0.0/16 \
  --subnet-name subnet-mongodb-uae \
  --subnet-prefix 10.1.1.0/24

The same command structure applies to South Africa North and West Europe, with 10.2.0.0/16 and 10.3.0.0/16 respectively. Three VNets, three subnets, no address space conflicts.

vnet SA.png
vnet SA.png

VNET MESH PEERING


Three VNets need to talk to each other. VNet peering is how Azure connects them; it creates a private, low-latency path between two VNets that bypasses the public internet entirely. Traffic between peered VNets stays on Microsoft's backbone network.

The detail most tutorials skip: peering in Azure is not bidirectional by default. Each peering link is one-directional. To connect two VNets so that traffic flows both ways, you need two links - one in each direction. Three VNets in a full mesh means three pairs, and three pairs means six links.

Code
UAE → South Africa      South Africa → UAE
UAE → West Europe       West Europe → UAE
South Africa → Europe   West Europe → South Africa

This isn't an Azure quirk to work around; it's a deliberate design that gives you granular control. You can peer two VNets and allow traffic in one direction only, which has legitimate use cases in hub-and-spoke architectures. For a replica set mesh where every node needs to reach every other node, all six links are required.

The output from the first link of each pair is instructive:

JSON
"peeringState": "Initiated",
"peeringSyncLevel": "RemoteNotInSync"

Initiated means one side of the connection exists but the other doesn't yet. The moment the return link is created, both sides flip to:

JSON
"peeringState": "Connected",
"peeringSyncLevel": "FullyInSync"

That state transition is Azure confirming the full bidirectional path is established. All three pairs reached Connected and FullyInSync after their respective return links were created.

peer from uae.png
peer from uae.png

With peering in place, the three VNets can route to each other's address spaces. What they can't do yet is resolve each other by hostname - that requires DNS.

PRIVATE DNS ZONE


MongoDB replica set members are registered by hostname, not IP address. This is not a configuration preference, it is how the replica set configuration is stored internally and how members identify each other during elections, replication, and failover. If you register a member by IP and that IP changes - a VM reboot, a reallocation, a failover event, the replica set loses track of that member. Hostname resolution is the stable layer that makes the cluster resilient to the kind of infrastructure events it was designed to survive.

Azure Private DNS Zones provide that stable layer. A single zone — mongo.internal — is created once and linked to all three VNets. Any VM in any of the three regions can resolve any hostname in that zone privately, without the query ever leaving the Microsoft backbone.

SQL
az network private-dns zone create \
  --resource-group rg-mongodb-uae-north \
  --name mongo.internal

The zone is created in the UAE North resource group but its scope is global, it isn't tied to a region. What ties it to the VNets is the link, and a separate link is required for each VNet:

SQL
az network private-dns link vnet create \
  --resource-group rg-mongodb-uae-north \
  --zone-name mongo.internal \
  --name link-uae \
  --virtual-network /subscriptions/.../vnet-mongodb-uae \
  --registration-enabled false

The --registration-enabled false flag is deliberate. Auto-registration would create DNS records automatically based on VM hostnames as VMs are provisioned. That behaviour is useful for dynamic environments but wrong here - it creates records tied to Azure-assigned VM names rather than the stable, semantically meaningful hostnames the replica set will use. We manage the records manually, which means we control exactly what the replica set sees.

Three A records, one per node:

Hostname

Record

IP

node-uae.mongo.internal

A

10.1.1.4

node-southafrica.mongo.internal

A

10.2.1.4

node-westeurope.mongo.internal

A

10.3.1.4

These hostnames are what the replica set configuration will reference in Part 5. They don't change when VMs reboot. They don't change if private IPs are reallocated. They are the stable identity layer that sits between the replica set configuration and the infrastructure it runs on.

record sets.png
record sets.png
mongo.internal dns links.png
mongo.internal dns links.png

NETWORK SECURITY GROUPS


VNet peering opens routing between the three regions. It does not restrict it. Without an explicit security boundary, any resource in any of the three VNets can reach any other resource on any port. For a database cluster, that is an unacceptable default.

Network Security Groups are the boundary. An NSG is a stateful packet filter attached at the subnet level - every byte entering or leaving the subnet is evaluated against its rules in priority order. The first matching rule wins. Anything that doesn't match an explicit allow rule hits the default deny at priority 65500.

Three NSGs, one per subnet. The rule set on each is identical in intent:

Rule

Priority

Protocol

Port

Source

Action

allow-mongodb

100

TCP

27017

10.1.1.0/24, 10.2.1.0/24, 10.3.1.0/24

Allow

allow-ssh

200

TCP

22

102.213.48.0/24

Allow

DenyAllInBound

65500

Any

Any

Any

Deny

Two explicit rules. Everything else is denied.

The MongoDB rule is intentionally narrow. Port 27017 is allowed only from the three subnet ranges that contain the replica set nodes - not from the broader VNet address space, not from a service tag, not from any source that isn't a known replica set subnet. A VM provisioned in the same VNet outside those subnets cannot reach port 27017. This is not defence in depth as an afterthought - it is the primary access control for the database tier.

The SSH rule reflects a setup-phase tradeoff. Port 22 is restricted to a /24 range rather than a single IP because the deployment environment uses a dynamic public IP. The tradeoff is documented here because it belongs in the article, not hidden in a config file: a /24 allows 256 possible source addresses rather than one, which is a wider surface than a static IP would provide. The mitigation is that this rule exists for the duration of the implementation phase. Once MongoDB is installed, hardened, and the cluster is operational, SSH access gets locked down further or removed entirely - the VMs have no business being SSH-accessible in steady state.

CSS
az network nsg rule create \
  --nsg-name nsg-mongodb-uae \
  --resource-group rg-mongodb-uae-north \
  --name allow-mongodb \
  --priority 100 \
  --protocol Tcp \
  --destination-port-ranges 27017 \
  --source-address-prefixes 10.1.1.0/24 10.2.1.0/24 10.3.1.0/24 \
  --access Allow --direction Inbound

After the rules are created, the NSG is associated with its subnet:

SQL
az network vnet subnet update \
  --vnet-name vnet-mongodb-uae \
  --name subnet-mongodb-uae \
  --resource-group rg-mongodb-uae-north \
  --network-security-group nsg-mongodb-uae

Association is the step that makes the rules active. An NSG with no subnet association exists but enforces nothing. The subnet update attaches it - from that point, every packet entering subnet-mongodb-uae is evaluated against the rule set.

nsg-mongodb-uae → Inbound security rules. You should see allow-mongodb (priority 100) and allow-ssh (priority 200) .png
nsg-mongodb-uae → Inbound security rules. You should see allow-mongodb (priority 100) and allow-ssh (priority 200) .png

VM PROVISIONING


The network is ready. Three subnets, secured, peered, DNS-enabled. What they don't have yet is a compute resource in each one. Three Ubuntu 22.04 VMs, one per region, sized at Standard_B2s - 2 vCPUs, 4GB RAM. Sufficient for a production-grade MongoDB deployment at this scale, and cost-efficient for a long-running lab environment.

Each VM is provisioned directly into its regional subnet with a public IP for SSH access during the implementation phase. Authentication is SSH key only - no passwords.

SQL
az vm create \
  --name vm-mongodb-uae \
  --resource-group rg-mongodb-uae-north \
  --location uaenorth \
  --image Ubuntu2204 \
  --size Standard_B2s \
  --vnet-name vnet-mongodb-uae \
  --subnet subnet-mongodb-uae \
  --nsg nsg-mongodb-uae \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_rsa.pub \
  --public-ip-sku Standard

The same command structure deploys to South Africa North and West Europe with their respective resource groups, VNet names, and subnet names. Each VM lands in its subnet and picks up a private IP from the subnet's address range:

VM

Region

Private IP

Public IP

vm-mongodb-uae

UAE North

10.1.1.4

20.46.46.187

vm-mongodb-southafrica

South Africa North

10.2.1.4

40.123.254.137

vm-mongodb-westeurope

West Europe

10.3.1.4

20.86.123.179

Private IPs are assigned sequentially from the subnet range — .4 is the first allocatable address in a /24, with .1 through .3 reserved by Azure for gateway and DNS infrastructure. The private IPs are what the DNS A records point to. The public IPs exist only for SSH access and will be the subject of a deliberate access review once the cluster is operational.

vms running.png
vms running.png

VERIFICATION


Infrastructure claims mean nothing without proof. The test is simple: SSH into one node and resolve the other two by hostname. If DNS resolution works and the pings return, the peering is routing correctly, the DNS zone is linked and resolving, and the NSG is allowing ICMP across subnet boundaries.

From the UAE North VM:

SQL
azureuser@vm-mongodb-uae:~$ ping -c 3 node-southafrica.mongo.internal
PING node-southafrica.mongo.internal (10.2.1.4) 56(84) bytes of data.
64 bytes from 10.2.1.4: icmp_seq=1 ttl=64 time=104 ms
64 bytes from 10.2.1.4: icmp_seq=2 ttl=64 time=100 ms
64 bytes from 10.2.1.4: icmp_seq=3 ttl=64 time=99.7 ms
--- node-southafrica.mongo.internal ping statistics ---
3 packets transmitted, 3 received, 0% packet loss

azureuser@vm-mongodb-uae:~$ ping -c 3 node-westeurope.mongo.internal
PING node-westeurope.mongo.internal (10.3.1.4) 56(84) bytes of data.
64 bytes from 10.3.1.4: icmp_seq=1 ttl=64 time=104 ms
64 bytes from 10.3.1.4: icmp_seq=2 ttl=64 time=102 ms
64 bytes from 10.3.1.4: icmp_seq=3 ttl=64 time=102 ms
--- node-westeurope.mongo.internal ping statistics ---
3 packets transmitted, 3 received, 0% packet loss

Zero packet loss. Hostname resolution working across all three regions. UAE North to South Africa North at ~100ms, UAE North to West Europe at ~102ms - realistic cross-continental latencies on Microsoft's backbone, not the public internet.

The hostnames resolving to the correct private IPs confirms the DNS zone is linked to the peered VNets and the A records are serving correctly. The round-trip times confirm the peering paths are active. The zero packet loss confirms the NSG rules are allowing traffic between the subnet ranges as intended.

pings from UAE.png
pings from UAE.png

WHAT'S NEXT


Three VMs. Three regions. One private mesh. The foundation is in place.

What doesn't exist yet is any form of authentication between the nodes. Right now, if MongoDB were installed and started on all three, any process that could reach port 27017 from within those subnets could connect to it. The NSG restricts which subnets can reach the port - but within those subnets, there is no per-node identity, no certificate-based mutual authentication, no audit trail of which node initiated which connection.

That's the problem Part 3 solves. Before MongoDB is installed, a private certificate authority is established, and each node receives a unique x.509 certificate that becomes its identity for the lifetime of the cluster. The replica set will never accept a connection from a node that can't present a valid certificate signed by that authority - not from outside the network, and not from within it.

The network trusts the subnets. MongoDB will trust the certificates. Those are two different security layers enforcing two different threat boundaries, and both need to be in place before the first mongod process starts.

If you've followed this far and the pings returned clean, you have something most MongoDB tutorials skip entirely - a network foundation that was designed before the database was installed, with a failure model that informed every decision from CIDR allocation to NSG rule priority.

The infrastructure in this article took deliberate choices to build. Non-overlapping address spaces because peering demands it. Six peering links because the mesh requires bidirectionality. Manual DNS records because auto-registration creates the wrong hostnames. NSG rules scoped to subnet ranges because port 27017 has no business being reachable from anywhere else. None of those are defaults - they are decisions, and decisions are what separate a production deployment from a tutorial.

If you missed the architecture and threat model that drove these decisions, that's in the previous article: "What a wiped MongoDB cluster in Bahrain taught me about cloud architecture" — it covers why this topology exists, what threats it's designed to mitigate, and why the region selection looks the way it does. This article builds on that foundation without requiring you to have read it, but the thinking behind it lives there.

Part 3 is next. The network is ready. The certificates aren't. Before the first mongod process starts, each node needs an identity - and that identity comes from a private certificate authority we build ourselves. x.509, self-managed PKI, per-node certificates. That's what's next.

If this was useful, drop a comment or share your own topology decisions below - especially if you're deploying across different regions or have made different tradeoffs on the security boundary. And if you want to be notified when Part 3 lands, follow the series on meetadev . Let's connect on LinkedIn.

Osego CodesBronze tierWhat's up 2026

Comments