Health checks and load balancing with Terraform
Two nested blocks turn a plain record into a traffic-managed
one: lb_policy decides which healthy values to return, and
health_check_template decides how DTM probes them. Both attach to
dtm_record, dtm_override_record, and dtm_auto_record (auto records
reject value_weights). lb_policy can also be set on dtm_zone as the
zone-wide default that records without their own policy inherit. There is
also a record-independent dtm_healthcheck resource (monitor-only).
Load-balancing policy
Section titled “Load-balancing policy”lb_policy { method = "region"}| Attribute | Description |
|---|---|
method |
round-robin, failover, latency, region, geo-failover, or weighted. See load-balancing methods. |
client_affinity |
Pin a client (by source-IP hash) to one value. |
affinity_ttl |
How long (seconds) the client-to-value mapping holds, up to a 24 hour maximum (larger values are clamped). Only meaningful with client_affinity = true. |
failover_order |
Explicit ordering for method = "failover". Values not listed fall to the end. |
region_preference |
Region preference order, honoured only when method = "region". It is accepted but ignored under geo-failover. |
value_weights |
For method = "weighted": map of record value to a percentage (0 to 100; listed weights may sum to at most 100). Unlisted values split the leftover evenly; 0 drains a value. Works on dtm_record and dtm_override_record. |
Weighted canary (90/10)
Section titled “Weighted canary (90/10)”lb_policy { method = "weighted" value_weights = { "10.0.1.10" = 90 # current "10.0.1.20" = 10 # canary }}Set a low TTL on the record: the split is applied per DNS answer, so resolver caching smears it unless answers expire quickly. See known limitations.
Failover with an explicit order
Section titled “Failover with an explicit order”lb_policy { method = "failover" failover_order = ["10.0.1.10", "10.0.1.11", "10.0.2.10"]}Region preference and client affinity
Section titled “Region preference and client affinity”lb_policy { method = "region" region_preference = ["northeurope", "westeurope"] client_affinity = true affinity_ttl = 300}Health-check template
Section titled “Health-check template”When set, DTM creates one health check per value from this template and keeps them in sync as values change.
health_check_template { protocol = "https" port = 443 path = "/healthz" interval_seconds = 15 timeout_seconds = 5 expected_status = [200]}| Attribute | Description |
|---|---|
protocol |
tcp, http, or https. |
port |
Target port (1-65535). |
path |
HTTP/HTTPS path, e.g. /healthz. |
interval_seconds |
Probe interval. Default 15, minimum 5. |
timeout_seconds |
Per-probe timeout. |
tls_skip_verify |
Skip TLS verification (self-signed HTTPS targets). |
expected_status |
HTTP status codes treated as healthy (default [200]). |
expected_body |
Substring the response body must contain. |
headers |
Static request headers sent on every probe (e.g. an Authorization bearer). Header values are secrets: the API returns them redacted, and the provider keeps the values you applied in state. |
ca_pem |
PEM CA bundle that verifies the target’s TLS certificate (server-cert pin). |
See health checks for tuning guidance on interval and thresholds.
Authenticated probes
Section titled “Authenticated probes”For auth-gated backends, add one of the oauth2, mtls, or managed_identity
sub-blocks (see authenticated health probes for the
behaviour). DTM requires HTTPS with verified TLS when credentials are attached:
use protocol = "https" and set ca_pem or leave tls_skip_verify off. A
template carrying one of those three sub-blocks over plain http is rejected.
A static headers token is not covered by that check, so set
protocol = "https" for those too. (An admin can
relax the certificate-verification half for the node the provider endpoint
targets with the dtm_health_probe_policy_config singleton resource; apply it
through each node’s endpoint to cover the cluster. The HTTPS requirement
always stands.)
# OAuth2 client-credentialshealth_check_template { protocol = "https" port = 443 path = "/healthz" interval_seconds = 15
oauth2 { token_url = "https://login.example.com/oauth2/token" client_id = var.probe_client_id client_secret = var.probe_client_secret # sensitive scope = "health.read" }}# Azure Managed Identity - no stored credentialshealth_check_template { protocol = "https" port = 443 path = "/healthz" interval_seconds = 15
managed_identity { resource = "api://<app-id-uri>" # client_id = "<user-assigned-mi-client-id>" # omit for the VM's system-assigned identity }}# Mutual TLShealth_check_template { protocol = "https" port = 443 path = "/healthz" interval_seconds = 15
mtls { cert_pem = file("client.pem") key_pem = file("client.key") # sensitive }}Probe secrets are write-only through the API: reads return client_secret,
key_pem, and all headers values redacted as ***. The provider preserves
the values you applied in state, so a refresh or plan shows no spurious secret
drift. To rotate a secret, change the value in your configuration and apply as
normal. The one exception is terraform import: an imported resource has no
previously applied value, so the redacted *** placeholder sits in state (and
shows as a diff) until your first apply restores the real values.
Monitor-only health checks
Section titled “Monitor-only health checks”dtm_healthcheck (role: editor or admin) manages a health check
independently of any record. It is monitor-only: the check probes its
target and reports status (the Health Checks page and the
dtm_health_check_status metric) but never gates a record’s DNS answers. To
make answers health-aware, use a health_check_template on the record, as
above.
resource "dtm_healthcheck" "web" { name = "web-http" protocol = "http" target = "10.0.1.10:80" interval_seconds = 15 timeout_seconds = 5 failure_threshold = 3 success_threshold = 2
http_config { method = "GET" path = "/healthz" expected_status = [200] }}| Attribute | Required | Description |
|---|---|---|
name |
Yes | Human-readable name. |
protocol |
Yes | tcp, http, or https. |
target |
Yes | host:port or a bare IP, for every protocol. No scheme and no path: for http/https the scheme comes from protocol and the request path from http_config.path. |
interval_seconds |
Yes | Probe interval (minimum 5). |
timeout_seconds |
Yes | Per-probe timeout. |
failure_threshold |
Yes | Consecutive failures before unhealthy. |
success_threshold |
Yes | Consecutive successes before healthy again. |
region_id |
No | Region tag for region-aware filtering. |
failure_rate_window_seconds |
No | Sliding window for rate-based failure detection; 0 (default) disables it. If you set it, you must also set failure_rate_percent. |
failure_rate_percent |
No | Percentage of failed probes within the window that flips the check unhealthy; must be set together with a non-zero failure_rate_window_seconds, and supplying only one of the pair is rejected with a 400. |
cross_region |
No | Allow the check result to drive failover across regions; leave unset to accept the server-side default. |
http_config (block) |
For HTTP/HTTPS | method, path, expected_status, expected_body, headers, tls_skip_verify, ca_pem, and the oauth2 / mtls / managed_identity auth blocks. headers values are secrets: returned redacted by the API and preserved in state, exactly as for record probes above. |
Authenticated monitor-only checks
Section titled “Authenticated monitor-only checks”http_config carries the same authenticated-probe fields as the record
template, field for field: ca_pem plus the oauth2, mtls, and
managed_identity blocks. The rules from
authenticated probes apply unchanged: at most one
bearer method (oauth2 or managed_identity, with mtls composable with
either), HTTPS with verified TLS whenever credentials are attached, and the
same secret round-trip (redacted reads, state-preserved values, and the
terraform import placeholder caveat).
resource "dtm_healthcheck" "secured_api" { name = "secured-api" protocol = "https" target = "api.internal:443" interval_seconds = 30 timeout_seconds = 3 failure_threshold = 3 success_threshold = 1
http_config { method = "GET" path = "/healthz" expected_status = [200] ca_pem = file("${path.module}/backend-ca.pem")
oauth2 { token_url = "https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token" client_id = "<app-id>" client_secret = var.probe_client_secret # sensitive scope = "api://<app-id>/.default" } }}