Skip to content

Deploy a cluster with Bicep

This page deploys the DTM cluster itself with Bicep, from the Azure Marketplace image, into a VNet you already own. It is the sibling of Deploy a cluster with Terraform; the samples are the same deployments in the other language.

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

The samples here deliberately do not create VNets, subnets or peerings. They take the resource id of a subnet that already exists, which is the normal shape for an Azure Landing Zone where the platform team owns connectivity. The Marketplace ARM templates remain the path that can create the subnet for you (the wizard does not create VNet peerings either).

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 deployment 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. 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.bicep (excerpt)
var imagePublisher = 'hayami'
var imageOffer = 'dtm'
var imageSku = 'dtm-payg-v1'
resource vm 'Microsoft.Compute/virtualMachines@2023-09-01' = {
name: vmName
location: location
plan: empty(imageResourceId) || imageResourceIdPurchasePlan
? {
name: imageSku
publisher: imagePublisher
product: imageOffer
}
: null
properties: {
storageProfile: {
imageReference: empty(imageResourceId)
? {
publisher: imagePublisher
offer: imageOffer
sku: imageSku
version: imageVersion
}
: {
id: imageResourceId
}
// ... osDisk, dataDisks ...
}
// ... osProfile with customData, networkProfile ...
}
}

The image reference is gated on empty(imageResourceId), and the plan on imageResourceIdPurchasePlan as well. If you point imageResourceId at your own Azure Compute Gallery copy of the Marketplace image, the VM keeps the plan: a gallery copy carries the purchase plan on its image definition, and a VM created from it must pass it (Microsoft Learn). That is the default, imageResourceIdPurchasePlan = true, and the Marketplace terms above still apply. Set it false only for an image that carries no purchase plan at all: Azure rejects a plan on such an image, and ARM omits the property entirely for a null value.

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

Topology What it deploys Files
Single region One node, the cluster primary, with its own Key Vault main.bicep · cloud-init.yaml
Dual region Two nodes, one per region, sharing one Key Vault main.bicep · cloud-init.yaml
Add a node One more node joining a cluster you already have main.bicep · cloud-init.yaml
Single region
mkdir dtm && cd dtm
curl -O https://docs.hayami.io/samples/bicep/single-region/main.bicep
curl -O https://docs.hayami.io/samples/bicep/single-region/cloud-init.yaml
az deployment group create \
--resource-group rg-dtm \
--template-file main.bicep \
--parameters existingSubnetResourceId=<subnet-resource-id> \
privateIPAddress=10.50.250.4 \
adminPasswordOrKey="$(cat ~/.ssh/id_ed25519.pub)"

cloud-init.yaml is the node’s first-boot configuration. main.bicep loads it with loadTextContent() at compile time, so it must sit beside the template when you deploy or the build fails. It is the same payload the Marketplace ARM deployment uses.

Preview any of them with az deployment group what-if before applying.

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 the cluster over the replication port (7946).

The single-region and dual-region samples carry Hayami’s customer usage attribution: an empty nested deployment whose name is Hayami’s tracking id. Microsoft uses it to attribute the Azure usage the deployment creates to Hayami. It collects nothing from your deployment and creates no resources. The add-node sample carries none, because it grows a cluster whose own deployment was already attributed. Microsoft’s notice for template deployments:

When you deploy this template, Microsoft can identify the installation of Hayami Ltd software with the deployed Azure resources. Microsoft can correlate these resources used to support the software. Microsoft collects this information to provide the best experiences with their products and to operate their business. The data is collected and governed by Microsoft’s privacy policies, located at https://www.microsoft.com/trustcenter.

To opt out, pass enableCustomerUsageAttribution=false. Nothing else in the deployment changes.

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 on the NIC, so the sample never takes over the security group on a subnet you own. It denies SSH (22) from in-VNet and peered sources (deny-ssh-vnet, priority 4010); for break-glass SSH, add a temporary allow below priority 4000 and remove it afterwards. The OS login is SSH key only: authenticationType accepts sshPublicKey alone
  • a per-cluster Key Vault (single and dual region), and the role assignments the node needs

The admin password and the replication encryption key are generated on the VM at first boot and written to Key Vault. Neither is ever a template parameter or a deployment artifact, so neither appears in your deployment history. Read the initial password with the command the sample outputs, then change it. That command runs on the node and reads the vault with the node’s own identity, so it works however the vault is exposed. You need permission to run commands on the VM (for example Virtual Machine Contributor), and no role on the vault. A read of a private vault from your own machine is refused. Before the node’s first boot has stored the password, the command fails with HTTP 404; wait and run it again.

The Key Vault is private by default. The single- and dual-region samples take keyVaultNetworkAccess, which sets how the node reaches the vault at its first boot:

  • Private (the default): public network access is disabled. A private endpoint in the node subnet is resolved through a privatelink.vaultcore.azure.net private DNS zone, which the sample creates and links to the subnet’s VNet.
  • ServiceEndpoint: the public endpoint, behind a firewall that admits only the node subnet. The subnet must already have the Microsoft.KeyVault service endpoint; the sample does not configure one.
  • Public: the public endpoint is open.

Access is role-based in every mode. If your landing zone already owns a privatelink.vaultcore.azure.net zone centrally, pass its resource ID in keyVaultPrivateDnsZoneId, with your VNet already linked to it: Azure cannot link a VNet to two zones of the same name. The same modes apply on every deployment path; see Key Vault network access.

Set enableLogsIngestion = true together with enableObservability = true. The deployment then creates the DTMLogs_CL table schema, a data collection endpoint and a data collection rule in the workspace’s region, and gives each node identity Monitoring Metrics Publisher on that rule only. Set logsIngestionDcePublicNetworkAccess = 'Disabled' when you add the endpoint to an Azure Monitor Private Link Scope.

The outputs logsIngestionEndpoint, logsIngestionDcrImmutableId and logsIngestionStream are the values for logs_ingestion_endpoint, logs_ingestion_dcr_immutable_id and logs_ingestion_stream. Set them on each node, restart the nodes one at a time, and turn the query log on (Ship the query log to Log Analytics).

Each VM deployed this way bills the DTM software charge per VM-hour on top of the Azure compute, storage and networking it uses: US$0.12 per node per VM-hour at any VM size, with 1-, 2- and 3-year reservations. See Pricing and billing.

  • Upgrades replace the VM and keep its private IP. An ARM or Bicep deployed cluster upgrades with the Lifecycle swap, never a Terraform targeted apply; see rolling upgrades.
  • Scaling out is the add-node sample above, or adding nodes.
  • Managing DNS moves to the Terraform provider or the API once the cluster is up: provider setup, REST API.
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