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.
Which deployment path is yours
Section titled “Which deployment path is yours”| 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.
Before the first apply
Section titled “Before the first apply”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:
az vm image terms accept \ --publisher hayami --offer dtm --plan dtm-payg-v1Without 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.
The two blocks that matter
Section titled “The two blocks that matter”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:
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.
The samples
Section titled “The samples”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 |
mkdir dtm && cd dtmcurl -O https://docs.hayami.io/samples/terraform/single-region/main.tfcurl -O https://docs.hayami.io/samples/terraform/single-region/cloud-init.yaml
terraform initterraform 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.
Dual region
Section titled “Dual region”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.
Adding a node later
Section titled “Adding a node later”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.
What the samples create
Section titled “What the samples create”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.
Day two
Section titled “Day two”- Upgrades replace the VM and keep its private IP. For a
Terraform-deployed cluster that is a targeted
terraform applyper 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.
Verify the cluster
Section titled “Verify the cluster”curl -sk https://<node-ip>:8443/readyz -o /dev/null -w '%{http_code}\n'# 200 = ready to serve; 503 = still starting or joiningThen follow the quickstart to create your first zone.
Where next
Section titled “Where next”- Deploy a cluster with Bicep: the same samples for Bicep-first estates.
- Deploy from the Azure Marketplace: the wizard, and what it creates for you.
- Network access (NSG): which ports the cluster needs and from where.
Need a hand? Email [email protected].
Last validated: 2026-09-15