Manage DTM as code
Your DNS changes go through the web UI today; this tutorial moves them into pull requests. By the end, the provider is installed from the cluster’s own mirror, a service account (not a person) applies changes from CI, your existing zone is Terraform-owned without having been recreated, and you have rehearsed a token rotation before you need one under pressure.
One disambiguation up front: this covers the DTM Terraform provider, which manages DNS objects on a running cluster. Deploying the cluster infrastructure itself with Terraform is a different surface; see Deploy a cluster with Terraform.
1. Install the provider
Section titled “1. Install the provider”The provider ships inside the DTM image, at the same version as the server
running beside it, and your cluster serves it to terraform init over HTTPS.
There is no public registry and no separate download. Generate the CLI
configuration from any node:
dtm-server --print-terraformrcCopy the printed provider_installation block into your Terraform CLI
configuration file, then run terraform init. Two things have to be true
first: the provider mirror is off by default, so an admin turns it on, and the
API needs a certificate your workstation can verify. Both are in
provider setup.
2. Connect and prove it
Section titled “2. Connect and prove it”Export DTM_ENDPOINT and DTM_TOKEN (an admin token for this bootstrap
session only; the service-account resources in the next step are admin-only),
with an empty provider "dtm" {} block. Then prove connectivity with a data
source, because a plan of an empty configuration never calls the API and
proves nothing:
data "dtm_cluster_info" "this" {}output "alive_nodes" { value = data.dtm_cluster_info.this.alive_count }Verify: terraform plan prints your live node count. Auth, TLS, and
endpoint are all proven in one output.
3. Mint the CI identity, token never in state
Section titled “3. Mint the CI identity, token never in state”Create the automation identity in Terraform itself, using the ephemeral token resource and a write-only Key Vault sink; the full pattern and its semantics live in service accounts for CI:
variable "token_version" { type = number default = 1}
resource "dtm_service_account" "ci" { name = "terraform-ci" role = "editor" 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-ci-token" key_vault_id = var.kv_id value_wo = ephemeral.dtm_service_account_token.ci.token value_wo_version = var.token_version}Two semantics worth internalizing now: every service-account attribute
forces replacement (a changed credential is always a fresh credential, by
design), and bumping only the ephemeral version against an unchanged
account mints an empty token at apply, which is why the three versions
above always move together. If the CI pipeline should only ever touch this
one zone, add zone_ids = [dtm_zone.example.id] to the account to confine
its writes to that zone; scope narrows the role, never widens it, and
changing it forces replacement like any other attribute (see
zone scoping).
Verify the whole point of the pattern:
grep -c dtmsat_ terraform.tfstate# 04. Adopt the existing zone; do not recreate it
Section titled “4. Adopt the existing zone; do not recreate it”Bring the UI-created zone under Terraform with terraform import, never by
declaring and re-applying (which would collide with the live objects). The
dtm_zone data source resolves the zone ID by name; get the record ID from
the API, then import with the documented ID forms (<zone-id> and
<zone-id>:<record-id>, per
zones and records in Terraform):
curl --cacert corp-ca.pem \ https://dtm.internal:8443/api/v1/zones/<zone-id>/records \ -H "Authorization: Bearer $DTM_TOKEN" # note the web record's id
terraform import dtm_zone.example <zone-id>terraform import dtm_record.web "<zone-id>:<record-id>"Set protected = true on the adopted record so a stray bulk delete cannot
remove IaC-owned records.
Verify: terraform plan is clean (no changes), which is the proof the
imported state matches reality.
5. Make your first change as code
Section titled “5. Make your first change as code”Add a new health-checked, load-balanced record entirely in HCL (the health and load-balancing page covers every attribute):
resource "dtm_record" "api" { zone_id = dtm_zone.example.id name = "api" type = "A" ttl = 30
values { value = "10.0.1.20" } values { value = "10.0.1.21" }
lb_policy { method = "failover" }
health_check_template { protocol = "tcp" port = 443 interval_seconds = 5 }}Verify with the same discipline as any change:
dig @10.0.250.4 api.example.internal +shortcurl --cacert corp-ca.pem https://dtm.internal:8443/api/v1/health/status \ -H "Authorization: Bearer $DTM_TOKEN"6. Wire the pipeline
Section titled “6. Wire the pipeline”Any CI system works; the shape is identical: on pull request, read the
dtm-ci-token secret from Key Vault, export it as DTM_TOKEN, run
terraform plan and post it for review; on merge, terraform apply. GitHub
Actions sketch:
- name: Fetch DTM token run: echo "DTM_TOKEN=$(az keyvault secret show --vault-name kv-corp \ --name dtm-ci-token --query value -o tsv)" >> "$GITHUB_ENV"- run: terraform plan -input=false # apply on the merge branch insteadScope note: Terraform manages your DNS objects and runtime config; it does not manage backups or the deploy-time bootstrap secrets (the initial administrator password, the cluster encryption key, and the first-boot TLS material). Those stay with their own procedures.
7. Rehearse the rotation
Section titled “7. Rehearse the rotation”Rotate the CI token now, while nothing is on fire, with exactly one move:
bump var.token_version and apply. That single bump drives the account
replacement, the fresh token mint, and the Key Vault re-write together; the
account’s ID changes, which is expected.
Verify: the next pipeline run authenticates with the new token.
8. Retire the human bootstrap
Section titled “8. Retire the human bootstrap”Close the loop on step 2’s admin token: login tokens expire after 24 hours and have no revoke endpoint, so retirement means ceasing use and letting it expire; if you created a dedicated bootstrap user, disable it per users and access. Then confirm the audit trail tells the story you want auditors to read:
curl --cacert corp-ca.pem \ "https://dtm.internal:8443/api/v1/audit?user_name=terraform-ci&limit=50" \ -H "Authorization: Bearer $DTM_TOKEN"Changes from here on are attributed to terraform-ci, driven by reviewed
merges, with a clean terraform plan as your steady state.
Where next
Section titled “Where next”- Production readiness pass: the wider go-live sequence, if the cluster is still at defaults.
- Terraform resource reference: every resource, role, and import ID in one table.
- Cluster and runtime config: put the cluster-wide singletons under code too.
Need a hand? Email [email protected].
Last validated: 2026-07-26