Skip to content

Deploy dual-region DTM

Region-aware answers on private IPs are DTM’s flagship trick, and dual-region is also its highest-silent-failure setup: a missing peering route or a closed cluster port leaves the second node joining forever, and a missing identity grant makes Azure discovery quietly do nothing. This tutorial gates on the prerequisites first, proves each layer before building the next, and ends with a fallback you have actually watched happen.

Two panels with the same cast. Target state: the northeurope (10.0.0.0/16) and westeurope (10.1.0.0/16) VNets each hold a client VM, a DTM node (dtm-ne 10.0.250.4, dtm-we 10.1.250.4), and an app backend (10.0.1.10, 10.1.1.10); bidirectional peering carries TCP+UDP 7946 between node subnets with 53 open from client subnets; one shared Key Vault in the primary region serves bootstrap secrets to both nodes; each region's client gets its local answer, and each node probes on the record's 5s interval. The fallback drill: the westeurope backend is stopped and marked unhealthy in about 15 seconds (5s interval times 3 probes, plus the record TTL for client caches), and the westeurope client's answer flips to A 10.0.1.10, a cross-region fallback, while northeurope still answers locally. Two panels with the same cast. Target state: the northeurope (10.0.0.0/16) and westeurope (10.1.0.0/16) VNets each hold a client VM, a DTM node (dtm-ne 10.0.250.4, dtm-we 10.1.250.4), and an app backend (10.0.1.10, 10.1.1.10); bidirectional peering carries TCP+UDP 7946 between node subnets with 53 open from client subnets; one shared Key Vault in the primary region serves bootstrap secrets to both nodes; each region's client gets its local answer, and each node probes on the record's 5s interval. The fallback drill: the westeurope backend is stopped and marked unhealthy in about 15 seconds (5s interval times 3 probes, plus the record TTL for client caches), and the westeurope client's answer flips to A 10.0.1.10, a cross-region fallback, while northeurope still answers locally.

1. Gate on the prerequisites before spending money

Section titled “1. Gate on the prerequisites before spending money”

One network fact must be true before you deploy, and one decision must be made in the wizard.

  • Non-overlapping address spaces between the two regions’ VNets, and no subnet NSG, UDR, or hub firewall that blocks traffic between them. This is the number-one cause of a second node stuck joining.
  • Peering: decide before you deploy. The dual-region plan creates the bidirectional peering for you (the “Create a VNet peering between the two VNets” checkbox, on by default). If your VNets are already connected, by an existing peering or through a hub, clear that checkbox (createVnetPeering = false) so the deployment does not try to create a second peering.

You do not need to pre-create NSG rules. The plan creates both NSGs, allowing 7946 TCP and UDP between the two node IPs and 53 from your DNS client CIDR, per the port table.

Verify cross-region routability with any existing VM pair across the peering (ICMP, or any port that is already listening). Do not test 7946 yet: no DTM node exists until step 2, so that port will not answer. Ten minutes here saves an afternoon of stuck-joining troubleshooting.

Deploy the dual-region plan (Marketplace, or its Bicep dialect) with one node per region and one shared Key Vault in the primary region. The bootstrap is behavioral and worth knowing: the primary node generates the cluster’s secrets (the initial administrator password and the cluster encryption key) and publishes them to the vault; the secondary reads the same vault and retries until they appear, so expect the second node to reach ready a few minutes after the first. Retrieve the admin password with the deployment’s recoverAdminAccessCommand output (retrieveAdminPasswordCommand in the Bicep dialect); first login covers it.

3. Verify cluster formation, then prove replication

Section titled “3. Verify cluster formation, then prove replication”

Readiness first, on both nodes (the secondary sits at 503 until its first full copy of cluster state arrives):

Terminal window
curl -sk https://10.0.250.4:8443/readyz # northeurope node: 200
curl -sk https://10.1.250.4:8443/readyz # westeurope node: 200

Then membership:

Terminal window
curl -sk https://10.0.250.4:8443/api/v1/nodes \
-H "Authorization: Bearer $DTM_TOKEN"
# two members, both "status": "alive", each with its region

If only one member appears, now is the moment to test the gossip port directly, from a VM in one region toward the other node’s IP: nc -vz 10.1.250.4 7946. It answers only once the node is up, which is why this check belongs here and not in step 1.

Now prove replication the way that actually matters, write on one node and resolve from the other:

Terminal window
# create zone example.internal. and record app -> 10.0.1.10 on the NE node
# (UI, API, or Terraform; see the quickstart), then:
dig @10.1.250.4 app.example.internal +short
# 10.0.1.10 (written in northeurope, answered from westeurope)

DTM maps a client to a region by the subnet its query arrives from. Two ways to build that map; pick one:

  • Path A, Azure discovery: the deployment ships with azure.region_discovery_enabled on; add a discovery scope and run it (POST /api/v1/discovery/scopes, then POST /api/v1/discovery/run, both admin; add ?dry_run=true first to preview the diff without writing). Note the periodic loop only detects drift after this; you apply changes from the UI or with another run, unless you set azure.region_discovery_auto_apply: true. The prerequisite is the Reader grant at each scope, and a missing grant fails silently: discovery runs and maps nothing. The antidote is the identity probe surface, the Cluster permissions card on the Lifecycle page (or GET /api/v1/lifecycle/identity), which shows a per-backend read-only probe refreshed every 60 seconds; check it before blaming the mapper.
  • Path B, manual mappings: explicit and grant-free:
Terminal window
curl -sk -X POST https://10.0.250.4:8443/api/v1/subnet-mappings \
-H "Authorization: Bearer $DTM_TOKEN" -H "X-DTM-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{"subnet": "10.0.0.0/16", "region_id": "northeurope", "source": "manual"}'
# repeat for 10.1.0.0/16 -> westeurope

Regions and subnet mapping explains precedence when both sources coexist.

Verify with the right endpoints: GET /api/v1/subnet-mappings lists the mappings (subnet to region); GET /api/v1/regions returns the deduplicated region IDs only, so use it as the roll-up check, not the detail view.

5. Health-check the record, then make it region-aware

Section titled “5. Health-check the record, then make it region-aware”

First, attach a health check to the record’s values (a TCP probe on the app port with a 5 second interval, per health checks; the default interval is 15 seconds, and the explicit 5 keeps step 7’s drill timing tight). This is not optional garnish: without probes, values never go unhealthy and region fallback can never trigger.

Then make the record dual-region: add the westeurope backend as a second value, tag each value with its region (a blank region auto-detects from the subnet map), and set the policy to region (local first, with an optional region_preference fallback order) or geo-failover (region filtering plus round-robin within the selected region). Load-balancing methods covers the choice, and its two standing prerequisites: subnet mappings in place, and every node able to probe backends in both regions (peering is not transitive; if probes cannot cross, health goes dark from one side).

Verify: GET /api/v1/health/status on each node shows both backends healthy from that node’s own vantage point.

From a client in each region, against that region’s node:

Terminal window
# northeurope client
dig @10.0.250.4 app.example.internal +short # 10.0.1.10
# westeurope client
dig @10.1.250.4 app.example.internal +short # 10.1.1.10

Same name, different region, different (local) answer: that is the feature.

Stop the westeurope backend and keep querying from the westeurope client. The timing is the standard probe math: with the 5 second interval you set in step 5 and the template’s fixed failure threshold of 3, the local node marks its backend unhealthy after about 15 seconds and answers flip to 10.0.1.10, plus the record TTL as the client-cache floor. Restore the backend; two consecutive successful probes bring it back.

Two semantics to observe while you watch, both by design:

  • Health is per node, never replicated. Each node answers from its own probe view, so run the drill’s digs against the client’s local node; the northeurope node’s view of the westeurope backend is a different (and independently correct) story.
  • If every value is unhealthy, DTM fails open: it treats all values as candidates again (so answers never go empty), and your record’s region policy still picks from them. An answer that might reach a recovering backend beats an empty answer that strands every client.

Verify: you saw the flip, the timing matched the math, and recovery returned the local answer.

Point each region’s VNet DNS at its local node first with the remote node second, so normal resolution stays regional and a node loss fails over to the other region’s node:

Terminal window
az network vnet update -g rg-ne -n vnet-ne --dns-servers 10.0.250.4 10.1.250.4
az network vnet update -g rg-we -n vnet-we --dns-servers 10.1.250.4 10.0.250.4

(Clients pick the change up on restart or lease renewal.) Then extend your backups and observability to cover both nodes; a dual-region cluster with single-region monitoring is half-finished.

Need a hand? Email [email protected].

Last validated: 2026-07-26