Skip to content

Deploy a cluster with Terraform

This page deploys the DTM cluster itself with Terraform, from the Azure Marketplace image, into a VNet you already own.

You want Use
Azure to create the resource group, subnet and peering for you The Marketplace wizard, whose ARM templates create them
To deploy into a subnet your network team already owns, as code This page
Bicep rather than Terraform Deploy a cluster with Bicep

The samples here deliberately do not create VNets, subnets or peerings. They take the resource id of a subnet that already exists, because in the estates that reach for Terraform the network is owned by someone else, and because a sample that creates network topology is a sample that fights your landing zone. The Marketplace ARM templates remain the path that builds the network for you.

Accept the Marketplace terms once per subscription. Azure refuses to create a VM from a Marketplace image until the offer’s terms are accepted for that subscription:

Once per subscription
az vm image terms accept \
--publisher hayami --offer dtm --plan dtm-payg-v1

Without it the apply fails at VM create with MarketplacePurchaseEligibilityFailed or VMMarketplaceInvalidInput.

You will also need a subnet resource id, a free static private IP inside it, and an SSH public key for break-glass access.

A Marketplace VM needs both an image reference and a matching purchase plan block. Azure refuses the create without the plan, and the plan is also what puts the offer into the attested instance document DTM checks at startup, so a VM built from the image without one is created successfully and then never becomes ready:

main.tf (excerpt)
locals {
marketplace_publisher = "hayami"
marketplace_offer = "dtm"
marketplace_sku = "dtm-payg-v1"
use_marketplace_image = var.image_id == ""
}
resource "azurerm_linux_virtual_machine" "dtm" {
# ... size, identity, os_disk, network_interface_ids ...
# Shared Image Gallery or managed image, for estates that mirror the image
# into their own gallery. Exactly one of this and source_image_reference.
source_image_id = local.use_marketplace_image ? null : var.image_id
dynamic "source_image_reference" {
for_each = local.use_marketplace_image ? [1] : []
content {
publisher = local.marketplace_publisher
offer = local.marketplace_offer
sku = local.marketplace_sku
version = var.marketplace_image_version
}
}
dynamic "plan" {
for_each = local.use_marketplace_image ? [1] : []
content {
name = local.marketplace_sku
publisher = local.marketplace_publisher
product = local.marketplace_offer
}
}
}

Both blocks are dynamic on purpose. If you point image_id at your own Shared Image Gallery copy, the plan block must disappear entirely: Azure rejects a purchase plan on an image that does not carry one.

Each is a self-contained directory: one main.tf and the cloud-init.yaml beside it, no modules and nothing else to fetch. Download both files into an empty directory and run terraform init && terraform apply.

Topology What it deploys Files
Single region One node, the cluster primary, with its own Key Vault main.tf · cloud-init.yaml
Dual region Two nodes, one per region, sharing one Key Vault main.tf · cloud-init.yaml
Add a node One more node joining a cluster you already have main.tf · cloud-init.yaml
Single region
mkdir dtm && cd dtm
curl -O https://docs.hayami.io/samples/terraform/single-region/main.tf
curl -O https://docs.hayami.io/samples/terraform/single-region/cloud-init.yaml
terraform init
terraform apply \
-var 'resource_group_name=rg-dtm' \
-var 'subnet_id=<subnet-resource-id>' \
-var 'private_ip=10.50.250.4' \
-var "ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)"

cloud-init.yaml is the node’s first-boot configuration. main.tf renders it with templatefile() and passes it as custom_data, so the two files must sit in the same directory. It is the same payload the Marketplace ARM and Bicep deployments use.

Two nodes, one per region, sharing a single Key Vault in the primary region. Node 1 generates the cluster secrets and node 2 reads them, so both join one encrypted cluster.

add-node references a cluster you already have (its subnet, its Key Vault, its current node IPs) and creates only the new node, as a read-only secondary that reads the shared secrets and joins over gossip.

A node, and the things only that node needs:

  • the VM, with a system-assigned managed identity and no public IP
  • a dedicated Premium SSD data disk for /var/lib/dtm, attached separately so replacing the VM leaves the database in place
  • a NIC with a static private IP, which becomes the stable resolver address
  • an NSG attached to the NIC, not the subnet, so the sample never takes over the security group on a subnet you own
  • a per-cluster Key Vault (single and dual region), and the four role assignments the node needs

The admin password and the gossip encryption key are generated on the VM at first boot and written to Key Vault. Neither is ever a Terraform variable, an output, or state. Read the initial password with the command the sample outputs, then change it.

Each VM deployed this way bills the DTM software charge per VM-hour on top of the Azure compute, storage and networking it uses. The current rate is on the Azure Marketplace listing; reservations apply where the listing says they do.

  • Upgrades replace the VM and keep its private IP. For a Terraform-deployed cluster that is a targeted terraform apply per node; see rolling upgrades.
  • Scaling out is the add-node sample above, or adding nodes.
  • Managing DNS moves to the Terraform provider once the cluster is up: provider setup.
Terminal window
curl -sk https://<node-ip>:8443/readyz -o /dev/null -w '%{http_code}\n'
# 200 = ready to serve; 503 = still starting or joining

Then follow the quickstart to create your first zone.

Need a hand? Email [email protected].

Last validated: 2026-09-15