Terraform provider setup
The DTM Terraform provider is the recommended way to manage DTM. It turns zones, records, GSLB, health checks, forwarders, and cluster settings into declarative, reviewable, version-controlled HCL.
This page covers installing the provider, configuring the connection, and authenticating. The rest of the Terraform section covers the individual resources.
What Terraform does and does not manage
Section titled “What Terraform does and does not manage”The provider manages DTM’s DNS data plane and cluster configuration:
- Zones and records:
dtm_zone,dtm_record,dtm_override_record. - Traffic management and health:
dtm_healthcheck, plus the load-balancing and ALIAS/GSLB settings carried on records. - Forwarding and resolver policy:
dtm_forwarder,dtm_default_forwarder,dtm_blocklist,dtm_tsig_key(and stub zones). - Regions and discovery:
dtm_discovery_scopeand subnet mappings. - Cluster and runtime config singletons:
dtm_axfr_config,dtm_cache_config,dtm_dynamic_dns_config,dtm_forwarder_hardening_config,dtm_otlp_config,dtm_query_log_config,dtm_rate_limit_config,dtm_replication_config, anddtm_transport_config.
See the resource reference for the complete list with required roles and import formats.
Users, RBAC, and Entra SSO are manageable either way: as code with
dtm_user and dtm_sso_config (see
users and SSO as code) or via the
web UI and REST API. Two things
are deliberately not Terraform resources:
- Backup and restore operations. See backup and restore.
- The cluster’s bootstrap secrets: the initial administrator password, the cluster encryption key, and the management TLS material. These are established at deploy time rather than through DTM’s Terraform resources (see the note below).
Install the provider
Section titled “Install the provider”The provider ships inside the DTM image. Every node carries the provider for each supported workstation platform, at the same version as the DTM server running beside it, so the two cannot drift apart. It is not on a public registry and there is no separate download: your cluster is the source.
terraform init fetches it from the cluster over HTTPS, which takes two
steps: turn the mirror on, and make sure Terraform can verify the cluster’s
certificate.
1. Give the API a certificate Terraform can verify
Section titled “1. Give the API a certificate Terraform can verify”Do this first, because the rest of the page assumes it. Terraform checks the mirror’s TLS certificate against your workstation’s trust store, and provider installation has no option to skip that check. By default the DTM API presents a self-signed certificate that it regenerates every time the service starts, which no workstation will trust.
Give the API a stable certificate, and include the address you will point Terraform at in its subject alternative names: TLS certificates. If your own certificate authority issued it, trust that authority on the workstation running Terraform, as you would for any internal service.
2. Turn on the provider mirror
Section titled “2. Turn on the provider mirror”The mirror is off by default, and the endpoints it adds are unauthenticated by protocol, so leaving it off until you want it is deliberate. Enable it on any node and the whole cluster follows, with no restart:
curl -s -X PUT https://<node-ip>:8443/api/v1/config/terraform-mirror \ -H "Authorization: Bearer $DTM_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"enabled": true}'Setting it back to false turns the endpoints off again the same way. (If you
have not done step 1 yet, curl needs -k here to reach the API at all, which
is the same trust gap Terraform will hit.)
3. Point Terraform at the cluster
Section titled “3. Point Terraform at the cluster”Generate the Terraform CLI configuration from any node:
dtm-server --print-terraformrcCopy the printed provider_installation block into ~/.terraformrc (or
%APPDATA%\terraform.rc on Windows), adjusting the URL to an address your
workstation can reach and that the certificate covers. terraform init then
installs the provider from the cluster with no registry access.
Then declare the provider in a required_providers block:
terraform { required_providers { dtm = { source = "dtm.local/dtm/dtm" version = "~> 0.1" } }}Run terraform init to install it.
Configure the connection
Section titled “Configure the connection”The provider needs two things: the endpoint (the DTM API base URL) and a token (a JWT bearer token).
provider "dtm" { endpoint = "https://dtm.internal:8443" token = var.dtm_token}| Attribute | Required | Description |
|---|---|---|
endpoint |
Yes | Base URL of the DTM API, e.g. https://dtm.internal:8443. Falls back to the DTM_ENDPOINT environment variable. |
token |
Yes | JWT bearer token from POST /api/v1/auth/login. Sensitive. Falls back to the DTM_TOKEN environment variable. |
insecure_skip_verify |
No | Skip TLS certificate verification. Only for dev clusters with self-signed certificates; leave off in production. |
Authenticate with environment variables
Section titled “Authenticate with environment variables”The cleanest pattern is to keep the endpoint and token out of your HCL entirely and pass them as environment variables:
export DTM_ENDPOINT="https://dtm.internal:8443"# Login returns the JWT only as the dtm_token cookie (not in the body);# capture it from the cookie jar:export DTM_TOKEN=$(curl -sk -c - -X POST "$DTM_ENDPOINT/api/v1/auth/login" \ -H "Content-Type: application/json" \ -d '{"username": "terraform", "password": "'"$DTM_PASSWORD"'"}' \ -o /dev/null | awk '$6 == "dtm_token" {print $7}')# No credentials in HCL - the provider reads DTM_ENDPOINT and DTM_TOKEN.provider "dtm" {}Service accounts for CI
Section titled “Service accounts for CI”For unattended automation, a service account beats a user login: its
dtmsat_ token is long-lived (up to 10 years, or non-expiring), it carries a
least-privilege role of its own, and the pattern below keeps the token out of
Terraform state entirely.
Two pieces work together (both need an admin-role connection):
dtm_service_accountcreates the account. Every configurable attribute (name,role,description,expires_in_days,zone_ids,rotation) forces replacement: there is deliberately no update path, so a “changed” credential is always a fresh credential (a scope change invalidates the current token exactly like arotationbump; the ephemeral resource mints the fresh one in the same apply). The create-time token is discarded by the provider, never stored.- The ephemeral
dtm_service_account_tokenmints the actual token at apply time via the version-gated rotate endpoint, and hands it to exactly one write-only sink. Ephemeral values exist only in memory during the run.
variable "token_version" { type = number default = 1}
resource "dtm_service_account" "ci" { name = "externaldns" role = "editor"
# Bump this together with token_version to rotate. rotation = tostring(var.token_version)}
ephemeral "dtm_service_account_token" "ci" { service_account_id = dtm_service_account.ci.id version = var.token_version}
resource "azurerm_key_vault_secret" "dtm_token" { name = "dtm-token" key_vault_id = var.kv_id
# value_wo is write-only: the token is read from the ephemeral resource at # apply and is NEVER persisted to Terraform state. value_wo = ephemeral.dtm_service_account_token.ci.token value_wo_version = var.token_version}Your CI job then reads dtm-token from Key Vault and exports it as
DTM_TOKEN.
To rotate, bump var.token_version and apply: the account is recreated
(the rotation keeper changed), the ephemeral resource mints a fresh token
for it, and the write-only sink re-sends the new value. Bumping only the
ephemeral version against an unchanged account is the one anti-pattern to
avoid: ephemeral resources also open during plan, so an in-place bump can
burn the rotation at plan time and hand the apply an empty no-op token.
Keeping the three versions moving together, as above, avoids it by
construction.
Service accounts support admin, editor, and readonly roles, plus
expires_in_days (0 or unset never expires; maximum 3650) and optional
zone scoping: set zone_ids to confine the token’s writes to specific
zones, the right shape for a per-team pipeline:
resource "dtm_service_account" "team_a_ci" { name = "team-a-ci" role = "editor" zone_ids = [dtm_zone.team_a.id, dtm_zone.team_a_staging.id] rotation = tostring(var.token_version)}Scope narrows the role, never widens it (a scoped admin token is still
confined), reads are not zone-filtered, and rotation via the ephemeral
resource preserves the scope. Revocation is available through the
service-accounts API.
Required roles
Section titled “Required roles”Each resource’s required role is noted on its reference page and in the resource reference. In short:
editorcovers most data-plane resources: zones, records, ALIAS and override records, health checks, forwarders, stub zones, blocklist, subnet mappings, and TSIG keys.adminis required for the runtime configuration singletons (observability, rate limits, cache, replication, AXFR, dynamic DNS; AXFR and query logging replicate cluster-wide, while observability, rate limits, cache, replication timing, and dynamic DNS configure only the node the provider endpoint targets) and for Azure discovery scopes.
If a resource returns a 403, the token’s user does not have the role that
resource needs.
Apply your first resource
Section titled “Apply your first resource”resource "dtm_zone" "example" { name = "example.internal." default_ttl = 300}
resource "dtm_record" "web" { zone_id = dtm_zone.example.id name = "web" type = "A" ttl = 60
values { value = "10.0.1.10" } values { value = "10.0.1.11" }
lb_policy { method = "round-robin" }}terraform initterraform planterraform applyImporting existing objects
Section titled “Importing existing objects”Every resource with a server-assigned ID supports terraform import. The import
ID format is noted per resource in the resource reference.
Records, for example, import as <zone_id>:<record_id>:
terraform import dtm_record.web 8f3a...:c21b...