Azure Custom Images Example¶
This example demonstrates all VM image configuration options for Qumulo clusters on Azure. It covers:
- the default Ubuntu 22.04 LTS images
- custom managed images for hardened or pre-configured VMs
- Azure Marketplace images (e.g., RHEL for compliance)
- mixed configurations that use different image sources for cluster nodes and the provisioner VM
Prerequisites¶
- Azure subscription with Contributor permissions. Enabling
deletion_protectionadditionally requiresMicrosoft.Authorization/locks/*. Owner, User Access Administrator, or a custom role carries that permission; Contributor alone cannot manage management locks. - Existing virtual network and subnet
- (Optional) Custom managed images or Azure Marketplace image access
- Terraform >= 1.0
Subnet Service Endpoints Required
Your Virtual Network subnet must have the Microsoft.KeyVault and Microsoft.Storage service endpoints enabled before deploying a cluster. To verify, go to the Azure Portal, navigate to Virtual networks > your VNet > Settings > Service endpoints, and confirm both endpoints are added for the target subnet. Deployment will fail without these service endpoints.
Usage¶
-
Edit
main.tfwith your values (look for<-- Replacecomments) -
Uncomment your preferred image configuration option
-
Initialize and deploy:
Image Configuration Options¶
Node image changes replace the cluster
Changing cluster node images triggers cluster replacement. Provisioner image changes do NOT trigger replacement.
Default (No Configuration)¶
By default, Qumulo uses Ubuntu 22.04 LTS for both cluster nodes and the provisioner VM.
Option 1: Custom Managed Images¶
Use your own Azure managed images for hardened or pre-configured VMs:
custom_image_id = "/subscriptions/.../images/cluster-image"
provisioner_custom_image_id = "/subscriptions/.../images/provisioner-image"
Option 2: Azure Marketplace Images¶
Use images from the Azure Marketplace (e.g., RHEL for compliance):
Common marketplace images:
| Image | publisher |
offer |
sku |
version |
|---|---|---|---|---|
| Ubuntu 22.04 LTS (default) | Canonical |
0001-com-ubuntu-server-jammy |
22_04-lts-gen2 |
latest |
| RHEL 8 (for compliance) | RedHat |
RHEL |
8-lvm-gen2 |
latest |
| RHEL 9 | RedHat |
RHEL |
9-lvm-gen2 |
latest |
Option 3: Mixed Configuration¶
Use different image sources for cluster nodes vs provisioner. The provisioner VM has its own
attributes: provisioner_custom_image_id and provisioner_marketplace_image.
Outputs¶
cluster_name: The name of your clustercluster_uuid: UUID of the Qumulo clusterdeployment_unique_name: Unique deployment identifierendpoint_ips: IP addresses for client connections (floating IPs if configured, otherwise primary IPs)primary_ips: Per-node primary IPsendpoints: Pre-formatted connection strings for web UI, API, NFS, and SMB
Full Configuration¶
# Example: Azure Qumulo Cluster with Custom Images
# This example demonstrates all VM image configuration options.
# The resource group is automatically created if it doesn't exist.
# Edit the values below directly, then run: terraform init -upgrade && terraform apply
terraform {
required_version = ">= 1.0"
required_providers {
qumulo = {
source = "qumulo-terraform-registry.s3.us-east-1.amazonaws.com/qumulo/qumulo"
version = "~> 1.0"
}
}
}
provider "qumulo" {
azure {
subscription_id = "your-subscription-id" # <-- Replace with your Azure subscription ID
environment = "public" # "public" or "usgovernment"
}
}
variable "ssh_public_key_path" {
description = "Path to SSH public key file for cluster node access"
type = string
default = "~/.ssh/id_rsa.pub"
}
# Qumulo cluster with custom image configuration
# Note: The resource group is automatically created if it doesn't exist.
# If it exists, the provider will use it (location must match).
resource "qumulo_filesystem_azure" "cluster" {
provider = qumulo
# =============================================================================
# All attributes (alphabetical order)
# Uncomment any attribute to use it. Required attributes are not commented.
# =============================================================================
# Admin password for cluster access.
# 8-128 characters, must include 3+ of: uppercase, lowercase, digit, special character.
# Immutable after creation - use Qumulo UI or CLI to change.
admin_password = "YourSecurePassword123!"
# CIDR blocks allowed to access the cluster (required, at least one).
# Example: ["10.0.0.0/8", "172.16.0.0/12"]
allow_cidrs = ["0.0.0.0/0"]
# Availability zones for node distribution.
# Controls both node placement and VM disk storage type:
# - With zones: Uses PremiumV2_LRS disks (better price/performance).
# - Without zones: Uses Premium_LRS disks (required for zoneless regions).
# Omit for single-zone or zoneless regions (e.g., northcentralus).
# Example: ["1", "2", "3"] for multi-AZ high availability.
availability_zones = ["1", "2", "3"]
# # User-assigned managed identity for cluster nodes.
# # When provided, RBAC roles must be pre-configured (not auto-created).
# # Format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{name}
# cluster_node_identity_id = "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.ManagedIdentity/userAssignedIdentities/xxx"
# Cluster storage product type.
# HOT: Optimized for frequently accessed data (default).
# COLD: Optimized for archival/infrequently accessed data.
# Immutable after creation.
cluster_product_type = "HOT"
# # Qumulo software version (optional, defaults to latest).
# # Immutable after creation - use Qumulo UI or CLI to upgrade.
# cluster_version = "7.5.0"
# =============================================================================
# Image Configuration Options (choose ONE approach)
# By default, Qumulo uses Ubuntu 22.04 LTS for cluster nodes and provisioner VM.
# WARNING: Changing cluster node images triggers cluster replacement!
# Provisioner image changes do NOT trigger replacement.
# =============================================================================
# Option 1: Custom managed image ID for cluster nodes.
# Use for hardened or pre-configured VMs from Azure Compute Gallery or Managed Images.
# Get the image ID from Azure Portal > Images > Properties > Resource ID
# # Create private endpoints in the cluster subnet.
# # Set the matching private_link_appconfig_dns_zone_id,
# # private_link_keyvault_dns_zone_id, or private_link_storage_dns_zone_id to also
# # register them in an Azure Private DNS zone. Without a zone, read the computed
# # appconfig_private_endpoint / keyvault_private_endpoint /
# # storage_private_endpoints outputs and create the records yourself.
# # create_keyvault_private_endpoint conflicts with key_vault_id.
# # Each endpoint carries a per-hour Azure cost.
# create_appconfig_private_endpoint = false
# create_keyvault_private_endpoint = false
# create_storage_private_endpoint = false
# custom_image_id = "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/images/xxx"
# Option 2: Azure Marketplace image for cluster nodes.
# Use standard enterprise images from the Azure Marketplace.
# marketplace_image = [{
# publisher = "Canonical"
# offer = "0001-com-ubuntu-server-jammy"
# sku = "22_04-lts-gen2"
# version = "latest"
# }]
# =============================================================================
# Common Marketplace Image Examples:
#
# Ubuntu 22.04 LTS (default):
# publisher = "Canonical"
# offer = "0001-com-ubuntu-server-jammy"
# sku = "22_04-lts-gen2"
# version = "latest"
#
# RHEL 8 (for compliance):
# publisher = "RedHat"
# offer = "RHEL"
# sku = "8-lvm-gen2"
# version = "latest"
#
# RHEL 9:
# publisher = "RedHat"
# offer = "RHEL"
# sku = "9-lvm-gen2"
# version = "latest"
# =============================================================================
# # Disable App Configuration public network access.
# # When true, requires private_link_appconfig_dns_zone_id.
# disable_appconfig_public_network_access = false
# # Disable Key Vault public network access.
# # When true, requires private_link_keyvault_dns_zone_id.
# disable_keyvault_public_network_access = false
# # Floating IP addresses for client access.
# # Use for DNS round-robin or custom load balancing.
# # When set, endpoint_ips output returns these instead of node IPs.
# # Number of floating IPs for client access. The provider picks the
# # addresses from the cluster subnet and reports them in floating_ips.
# floating_ip_count = 3
# # Customer-managed Key Vault resource ID.
# # When provided, the provider uses this vault instead of creating one.
# # Conflicts with disable_keyvault_public_network_access and private_link_keyvault_dns_zone_id.
# # Immutable after creation. You own vault lifecycle, network, and access policies.
# # The vault must permit access from the cluster subnet - the nodes read their
# # storage SAS tokens from it. See "Customer-Managed Key Vault" in azure-production.md.
# # Format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/{name}
# key_vault_id = "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.KeyVault/vaults/xxx"
# Azure region for deployment.
location = "eastus2" # <-- Azure region
# qfsd cluster name, shown in the Qumulo UI (2-15 chars, case preserved).
cluster_name = "qumuloimg"
# Prefix for the Azure resources this cluster creates (2-15 lowercase chars).
deployment_name = "qumuloimg"
# # Custom naming templates for Azure resources.
# # vm_name must contain exactly one {node_id} placeholder.
# # storage_account must contain exactly one {index} or {index:N} placeholder.
# # {index} is the unpadded index. {index:N} zero-pads the index to width N, so
# # {index:2} yields "01", "02", ... ({index:02} is equivalent -- the leading 0 is redundant).
# # Changing either forces replacement of the affected resource.
# naming = [{
# vm_name = "myapp-{node_id}"
# storage_account = "myappstor{index:02}"
# }]
# # Network management mode.
# # "host_managed" (default) for clusters created by this provider.
# # "qumulo_managed" only for clusters originally created by azure-terraform-cnq.
# # Immutable after creation. Mixing modes across nodes is prohibited.
# networking_mode = "host_managed"
# # Qumulo Nexus registration key for remote support.
# # Obtain from https://nexus.qumulo.com/user/registration-key
# # Only applied during initial cluster creation.
# nexus_registration_key = "your-nexus-key"
# Number of nodes in the cluster.
# Valid values: 1 (single node), or 3-24 (4 nodes: single-zone only).
# Note: 2 is not a valid node count.
node_count = 3
# # Enable ICMP ingress (ping) in network security group.
# # Useful for network diagnostics.
# nsg_allow_ingress_icmp = false
# # Resource group containing persistent storage accounts and KeyVault.
# # Required when migrating from the legacy azure-terraform-cnq module, where persistent
# # storage lived in a separate resource group. Defaults to resource_group_name when omitted.
# persistent_storage_resource_group = "rg-qumulo-persistent"
# # Private DNS zone ID for App Configuration private endpoint.
# # Required when disable_appconfig_public_network_access is true.
# # Requires create_appconfig_private_endpoint = true.
# # Format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/privateDnsZones/privatelink.azconfig.io
# private_link_appconfig_dns_zone_id = "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Network/privateDnsZones/privatelink.azconfig.io"
# # Private DNS zone ID for Key Vault private endpoint.
# # Required when disable_keyvault_public_network_access is true.
# # Requires create_keyvault_private_endpoint = true.
# # Format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/privateDnsZones/privatelink.vaultcore.azure.net
# private_link_keyvault_dns_zone_id = "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Network/privateDnsZones/privatelink.vaultcore.azure.net"
# # Private DNS zone ID for the storage private endpoints.
# # Requires create_storage_private_endpoint = true.
# # Format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/privateDnsZones/privatelink.blob.core.windows.net
# private_link_storage_dns_zone_id = "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Network/privateDnsZones/privatelink.blob.core.windows.net"
# # Custom managed image ID for provisioner VM.
# # Does NOT trigger cluster replacement when changed.
# provisioner_custom_image_id = "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/images/xxx"
# # User-assigned managed identity for provisioner VM.
# # When provided, RBAC roles must be pre-configured.
# provisioner_identity_id = "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.ManagedIdentity/userAssignedIdentities/xxx"
# # Azure Marketplace image for provisioner VM.
# # Does NOT trigger cluster replacement when changed.
# provisioner_marketplace_image = [{
# publisher = "Canonical"
# offer = "0001-com-ubuntu-server-jammy"
# sku = "22_04-lts-gen2"
# version = "latest"
# }]
# # VM size for the provisioner instance.
# # Used during deployment operations only.
# provisioner_vm_type = "Standard_B2s"
# Azure resource group name.
# The resource group is automatically created if it doesn't exist.
# If it exists, the provider will use it (location must match).
resource_group_name = "rg-qumulo-images" # <-- Name your resource group
# Soft capacity limit in TB (50-10000).
# Can be increased to add storage, but cannot be decreased.
soft_capacity_limit_tb = 1000
# SSH public key for cluster node access.
# Point to your public key file (e.g., ~/.ssh/id_rsa.pub).
ssh_public_key = file(var.ssh_public_key_path)
# Azure storage replication type for storage accounts only (does not affect VM disks).
# LRS: Locally redundant (3 copies in single datacenter).
# ZRS: Zone redundant (3 copies across availability zones) - RECOMMENDED for multi-AZ clusters.
# Note: VM disk redundancy is controlled by availability_zones (see above).
# Immutable after creation.
storage_replication_type = "ZRS"
# Full Azure resource ID of the subnet for cluster deployment.
# The subnet must already exist before deploying the cluster.
# Format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnet}/subnets/{subnet}
subnet_id = "/subscriptions/your-subscription-id/resourceGroups/rg-network/providers/Microsoft.Network/virtualNetworks/vnet-main/subnets/subnet-qumulo"
# Tags to apply to all Azure resources (including the resource group if created).
tags = {
Environment = "Development"
ManagedBy = "Terraform"
ImageType = "Default" # Change to "Custom", "Marketplace", or "Mixed" as appropriate
}
# Azure VM size for cluster nodes.
# Only L-series storage-optimized VMs are supported.
# Examples: Standard_L8s_v3, Standard_L16s_v3, Standard_L32s_v3
vm_type = "Standard_L8s_v3"
deletion_protection = true # recommended: guard the cluster's VMs and storage accounts
timeouts {
create = "90m"
delete = "30m"
}
}
output "cluster_name" {
description = "Name of the Qumulo cluster"
value = qumulo_filesystem_azure.cluster.cluster_name
}
output "cluster_uuid" {
description = "UUID of the Qumulo cluster"
value = qumulo_filesystem_azure.cluster.cluster_uuid
}
output "deployment_unique_name" {
description = "Unique deployment identifier"
value = qumulo_filesystem_azure.cluster.deployment_unique_name
}
output "endpoint_ips" {
description = "Client-facing IPs. Floating IPs if configured, otherwise primary IPs."
value = qumulo_filesystem_azure.cluster.endpoint_ips
}
output "primary_ips" {
description = "Per-node primary IPs. Use these directly when no floating IPs are configured, or for per-node access."
value = qumulo_filesystem_azure.cluster.primary_ips
}
output "endpoints" {
description = "Connection endpoints for various protocols"
value = {
web_ui = "https://${try(qumulo_filesystem_azure.cluster.endpoint_ips[0], "pending")}"
api = "https://${try(qumulo_filesystem_azure.cluster.endpoint_ips[0], "pending")}:8000"
nfs = "${try(qumulo_filesystem_azure.cluster.endpoint_ips[0], "pending")}:/"
smb = "\\\\${try(qumulo_filesystem_azure.cluster.endpoint_ips[0], "pending")}\\share"
}
}