Skip to content

Production Azure Qumulo Cluster Example

This example demonstrates a production-ready configuration with high availability, enhanced security, and enterprise features.

Prerequisites

  • Azure subscription with Contributor permissions
  • Enabling deletion_protection additionally requires Microsoft.Authorization/locks/*, carried by Owner, User Access Administrator, or a custom role (Contributor alone cannot manage management locks)
  • Setting floating_ip_count additionally requires Microsoft.Network/virtualNetworks/checkIpAddressAvailability/read on the virtual network or any parent scope, used to find free subnet addresses. Reader, Network Contributor, and Contributor all carry it; a narrow cross-resource-group grant of only subnets/read plus subnets/join/action does not. The deploying credential needs it even when cluster_node_identity_id or provisioner_identity_id is set, which skips the permission pre-check
  • Existing virtual network and subnet, sized for one address per node, one for the provisioner VM, one per floating IP, and the 5 Azure reserves in every subnet
  • SSH public key for node 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

  1. Edit main.tf with your values (look for <-- Replace comments)

  2. Initialize and deploy:

    terraform init -upgrade
    terraform plan
    terraform apply
    

Features

This example includes:

  • Multi-zone deployment for high availability
  • Zone-redundant storage (ZRS)
  • SSH key authentication for node access
  • CIDR-based access restrictions
  • Production-appropriate tagging

Optional Features

Uncomment and configure in main.tf:

  • Private Endpoints: Secure access to Azure services via private network
  • Custom Managed Identities: Use pre-configured identities with RBAC
  • Custom VM Images: Use hardened or enterprise images
  • Floating IPs: Additional IPs for load balancing

Outputs

  • cluster_name: Name of the Qumulo cluster
  • cluster_uuid: UUID of the Qumulo cluster
  • deployment_unique_name: Unique deployment identifier
  • endpoint_ips: Client-facing IPs. Floating IPs if configured, otherwise primary IPs.
  • primary_ips: Per-node primary IPs. Use these directly when no floating IPs are configured, or for per-node access.
  • endpoints: Connection endpoints for various protocols (web UI, API, NFS, SMB)

VM Type Selection

Only L-series storage-optimized VMs are supported:

# L-Series v3 (Intel)
vm_type = "Standard_L8s_v3"   # 8 vCPUs, 64 GB RAM, 1.92 TB NVMe
vm_type = "Standard_L16s_v3"  # 16 vCPUs, 128 GB RAM, 3.84 TB NVMe
vm_type = "Standard_L32s_v3"  # 32 vCPUs, 256 GB RAM, 7.68 TB NVMe
vm_type = "Standard_L48s_v3"  # 48 vCPUs, 384 GB RAM, 11.52 TB NVMe
vm_type = "Standard_L64s_v3"  # 64 vCPUs, 512 GB RAM, 15.36 TB NVMe
vm_type = "Standard_L80s_v3"  # 80 vCPUs, 640 GB RAM, 19.20 TB NVMe

# L-Series v3 (AMD)
vm_type = "Standard_L8as_v3"  # 8 vCPUs, 64 GB RAM, 1.92 TB NVMe
vm_type = "Standard_L16as_v3" # 16 vCPUs, 128 GB RAM, 3.84 TB NVMe
vm_type = "Standard_L32as_v3" # 32 vCPUs, 256 GB RAM, 7.68 TB NVMe
vm_type = "Standard_L48as_v3" # 48 vCPUs, 384 GB RAM, 11.52 TB NVMe
vm_type = "Standard_L64as_v3" # 64 vCPUs, 512 GB RAM, 15.36 TB NVMe
vm_type = "Standard_L80as_v3" # 80 vCPUs, 640 GB RAM, 19.20 TB NVMe

# L-Series v4 (Intel)
vm_type = "Standard_L8s_v4"   # 8 vCPUs, 64 GB RAM, 1.92 TB NVMe
vm_type = "Standard_L16s_v4"  # 16 vCPUs, 128 GB RAM, 3.84 TB NVMe
vm_type = "Standard_L32s_v4"  # 32 vCPUs, 256 GB RAM, 7.68 TB NVMe
vm_type = "Standard_L48s_v4"  # 48 vCPUs, 384 GB RAM, 11.52 TB NVMe
vm_type = "Standard_L64s_v4"  # 64 vCPUs, 512 GB RAM, 15.36 TB NVMe
vm_type = "Standard_L80s_v4"  # 80 vCPUs, 640 GB RAM, 19.20 TB NVMe

# L-Series v4 (AMD)
vm_type = "Standard_L8as_v4"  # 8 vCPUs, 64 GB RAM, 1.92 TB NVMe
vm_type = "Standard_L16as_v4" # 16 vCPUs, 128 GB RAM, 3.84 TB NVMe
vm_type = "Standard_L32as_v4" # 32 vCPUs, 256 GB RAM, 7.68 TB NVMe
vm_type = "Standard_L48as_v4" # 48 vCPUs, 384 GB RAM, 11.52 TB NVMe
vm_type = "Standard_L64as_v4" # 64 vCPUs, 512 GB RAM, 15.36 TB NVMe
vm_type = "Standard_L80as_v4" # 80 vCPUs, 640 GB RAM, 19.20 TB NVMe

# L-Series v2 (Legacy)
vm_type = "Standard_L8s_v2"   # 8 vCPUs, 64 GB RAM, 1.92 TB NVMe
vm_type = "Standard_L16s_v2"  # 16 vCPUs, 128 GB RAM, 3.84 TB NVMe
vm_type = "Standard_L32s_v2"  # 32 vCPUs, 256 GB RAM, 7.68 TB NVMe
vm_type = "Standard_L48s_v2"  # 48 vCPUs, 384 GB RAM, 11.52 TB NVMe
vm_type = "Standard_L64s_v2"  # 64 vCPUs, 512 GB RAM, 15.36 TB NVMe
vm_type = "Standard_L80s_v2"  # 80 vCPUs, 640 GB RAM, 19.20 TB NVMe

Write-cache disk layout

Each cluster node gets 4 key-value disks plus a write-cache tier sized to the VM. The provider takes one write-cache disk per two vCPUs, never fewer than 4 and never more than 24, drops disks that the VM's published uncached disk bandwidth or IOPS could not drive, and then provisions each remaining disk to reach 90% of that cap. Premium SSD v2 bills provisioned IOPS and throughput above 3000 IOPS / 125 MB/s, so the per-disk figures below are what a node costs beyond the free floor. In zoneless regions the cluster uses Premium SSD instead: write-cache disks are 256 GiB P15 and the key-value disks are P15.

Existing nodes keep the disks they already have, so a cluster created by an earlier release can have a mix of disk counts per node. Scaling that cluster up adds nodes built with the layout above, alongside older nodes that still have 4 write-cache disks, and the mix persists until every node has been rebuilt, for example by a replacement.

VM size Write-cache disks Per-disk MB/s Per-disk IOPS DKV disks Total data disks
Standard_L8s_v3 4 125 3000 4 8
Standard_L16s_v3 7 129 3292 4 11
Standard_L32s_v3 10 130 4608 4 14
Standard_L48s_v3 15 132 4608 4 19
Standard_L64s_v3 20 131 3600 4 24
Standard_L80s_v3 24 135 3000 4 28
Standard_L8as_v3 4 125 3000 4 8
Standard_L16as_v3 4 144 5760 4 8
Standard_L32as_v3 9 128 5120 4 13
Standard_L48as_v3 13 133 5317 4 17
Standard_L64as_v3 15 128 4800 4 19
Standard_L80as_v3 16 132 4500 4 20
Standard_L8s_v2 4 125 3000 4 8
Standard_L16s_v2 4 126 3600 4 8
Standard_L32s_v2 8 126 3600 4 12
Standard_L48s_v2 12 126 3600 4 16
Standard_L64s_v2 16 126 3600 4 20
Standard_L80s_v2 17 130 4236 4 21
Standard_L2s_v4 4 125 3000 4 8
Standard_L8s_v4 4 159 3000 4 8
Standard_L16s_v4 7 182 3292 4 11
Standard_L32s_v4 15 170 3072 4 19
Standard_L48s_v4 23 166 3006 4 27
Standard_L64s_v4 24 212 3840 4 28
Standard_L80s_v4 24 265 4800 4 28
Standard_L96s_v4 24 318 5760 4 28
Standard_L8as_v4 4 135 3420 4 8
Standard_L16as_v4 8 135 3420 4 12
Standard_L32as_v4 16 135 3240 4 20
Standard_L48as_v4 24 135 3240 4 28
Standard_L64as_v4 24 180 4320 4 28
Standard_L80as_v4 24 225 5400 4 28
Standard_L96as_v4 24 270 6480 4 28
Standard_L8aos_v4 4 225 3960 4 8
Standard_L16aos_v4 8 225 3960 4 12
Standard_L32aos_v4 16 225 3960 4 20

Custom Managed Identity

Use pre-existing user-assigned managed identities for organizations with centralized RBAC management:

data "azurerm_user_assigned_identity" "cluster" {
  name                = "qumulo-cluster-identity"
  resource_group_name = "rg-identities"
}

data "azurerm_user_assigned_identity" "provisioner" {
  name                = "qumulo-provisioner-identity"
  resource_group_name = "rg-identities"
}

resource "qumulo_filesystem_azure" "custom_identity" {
  # ... required fields ...

  cluster_node_identity_id = data.azurerm_user_assigned_identity.cluster.id
  provisioner_identity_id  = data.azurerm_user_assigned_identity.provisioner.id
  deletion_protection      = true # recommended: guard the cluster's VMs and storage accounts
}

Required roles

The cluster node identity runs on the node VMs. The provisioner identity runs on the short-lived provisioner VM that configures the cluster. Each needs its own roles.

Grant every role at the scope of the deployment's resource group, before the first terraform apply. The Key Vault, App Configuration store, and storage accounts do not exist until the apply creates them, and they inherit role assignments from the resource group.

cluster_node_identity_id:

  • Key Vault Secrets User
  • Storage Blob Data Contributor
  • Reader

provisioner_identity_id:

  • Key Vault Secrets User
  • App Configuration Data Owner
  • Virtual Machine Contributor
SCOPE=/subscriptions/<subscription-id>/resourceGroups/<deployment-resource-group>

for role in "Key Vault Secrets User" "Storage Blob Data Contributor" "Reader"; do
  az role assignment create --assignee-object-id <cluster-node-identity-principal-id> \
    --assignee-principal-type ServicePrincipal --role "$role" --scope "$SCOPE"
done

for role in "Key Vault Secrets User" "App Configuration Data Owner" "Virtual Machine Contributor"; do
  az role assignment create --assignee-object-id <provisioner-identity-principal-id> \
    --assignee-principal-type ServicePrincipal --role "$role" --scope "$SCOPE"
done

One identity can serve both attributes. Grant it every role from both lists.

If persistent_storage_resource_group is set, the storage accounts are created there instead, so grant Storage Blob Data Contributor for the cluster node identity on that resource group.

Cross-subscription identities are supported. The subscription is automatically extracted from the identity resource ID.

Customer-Managed Key Vault

key_vault_id points the deployment at a vault you own, and the provider never changes that vault's network configuration. The vault must permit network access from the cluster subnet. The nodes read their storage SAS tokens from it while forming the first quorum and for the life of the cluster, so this is a standing requirement, not a create-time one.

Terraform writing the SAS token definitions successfully is not evidence the nodes can reach the vault: those writes come from wherever Terraform runs, and the nodes egress from the cluster subnet.

If the vault's firewall is set to deny by default (networkAcls.defaultAction = "Deny"), grant the cluster subnet access one of two ways:

# Service endpoint plus a matching virtual network rule
az network vnet subnet update -g <network-rg> --vnet-name <vnet> -n <cluster-subnet> \
    --service-endpoints Microsoft.KeyVault
az keyvault network-rule add -g <vault-rg> -n <vault-name> --subnet <cluster-subnet-id>

Or place a Key Vault private endpoint in the cluster subnet, linked to a privatelink.vaultcore.azure.net private DNS zone. The provider's create_keyvault_private_endpoint attribute is rejected together with key_vault_id, because the provider only builds private endpoints for vaults it creates. Build the endpoint for your own vault outside Terraform.

Allowing an egress IP address works only when that address is pinned by a NAT gateway. Azure's default outbound SNAT address can change, which breaks the cluster's access to its storage later. "Allow trusted Microsoft services" does not cover this traffic: the caller is a VM managed identity, not a trusted service.

When the nodes cannot reach the vault, cluster creation fails with a storage URI validation error that names the vault and the blocked client address:

Error 400: object_storage_uri_validate_error: Timed out validating uri https://<account>.blob.core.windows.net
  (http_forbidden_error: /secrets/<account>-SasTokenDefinition ... "code":"ForbiddenByFirewall")

Private Endpoints

The provider can place a private endpoint in the cluster subnet for the deployment's App Configuration store, the Key Vault it creates, and every one of its storage accounts. Each service has its own switch:

create_appconfig_private_endpoint = true
create_keyvault_private_endpoint  = true
create_storage_private_endpoint   = true

There are two ways to run this, and which one you pick decides whether public network access can be disabled in the same apply.

With Azure Private DNS

Set the matching create_appconfig_private_endpoint, create_keyvault_private_endpoint, or create_storage_private_endpoint switch and the matching zone ID. The endpoint registers in the zone during create, so the provisioner can resolve it minutes later, and public network access may be disabled in the same apply:

create_appconfig_private_endpoint       = true
private_link_appconfig_dns_zone_id      = "/subscriptions/.../privateDnsZones/privatelink.azconfig.io"
disable_appconfig_public_network_access = true

create_keyvault_private_endpoint        = true
private_link_keyvault_dns_zone_id       = "/subscriptions/.../privateDnsZones/privatelink.vaultcore.azure.net"
disable_keyvault_public_network_access  = true

create_storage_private_endpoint         = true
private_link_storage_dns_zone_id        = "/subscriptions/.../privateDnsZones/privatelink.blob.core.windows.net"

Each zone must exist and be linked to the cluster VNet before the apply. The provider checks both and fails the plan with the zone and VNet named if either is missing.

With your own DNS

Set only the create_appconfig_private_endpoint, create_keyvault_private_endpoint, and create_storage_private_endpoint switches. The endpoint's private IP does not exist until the apply creates it, so no DNS record can exist while the provisioner boots, which means public network access must stay enabled during create. After the apply, read the endpoint details out of the computed outputs, create your records, and disable public access on the Azure resources out of band:

output "storage_endpoints" {
  value = qumulo_filesystem_azure.cluster.storage_private_endpoints
}

Check for an existing Azure Private DNS zone before choosing this path. This path only works when no Azure Private DNS zone for the service (privatelink.azconfig.io, privatelink.vaultcore.azure.net, or privatelink.blob.core.windows.net) is already linked to the cluster VNet. If one is, Azure DNS answers queries from inside the VNet using that zone, and the zone has no record for the endpoint you just created, so it returns NXDOMAIN for the resource as soon as the endpoint exists. The cluster cannot reach its Key Vault or storage during creation, and the apply fails. If a zone is already linked, use the "With Azure Private DNS" path above and pass the matching private_link_appconfig_dns_zone_id, private_link_keyvault_dns_zone_id, or private_link_storage_dns_zone_id instead.

To check, run az network private-dns link vnet list --zone-name <zone> --resource-group <rg> for each of the three zone names against every resource group you can see, since the zone can live in a subscription other than the VNet's. A broader check across subscriptions is an Azure Resource Graph query on type microsoft.network/privatednszones/virtualnetworklinks filtered to links whose virtual network ID matches the cluster VNet.

appconfig_private_endpoint and keyvault_private_endpoint are single objects, and storage_private_endpoints is a map keyed by storage account name. Every record carries five fields:

Field Purpose
fqdn The name to point your DNS record at.
ip_address The private IP the record should resolve to.
endpoint_name The Azure private endpoint resource's name.
resource_group The resource group the private endpoint was created in.
target_resource_id The ARM ID of the account, vault, or App Configuration store the endpoint fronts.

target_resource_id is the resource to PATCH once you have confirmed the private endpoint is serving traffic. Point a network-posture change (for example, disabling public network access) at that ID. The provider writes network posture only at creation and never reads it back, so disabling public access yourself causes no drift on later plans.

On a capacity increase, the whole storage_private_endpoints map is unknown at plan time. Terraform cannot know the keys of accounts that do not exist yet, so a downstream for_each over the map needs -target on the cluster first, or a second apply.

Remove your DNS records before turning a switch off. Setting create_appconfig_private_endpoint, create_keyvault_private_endpoint, or create_storage_private_endpoint back to false deletes that service's endpoints. A record still pointing at a deleted endpoint answers with a dead address. Removing a private_link_appconfig_dns_zone_id, private_link_keyvault_dns_zone_id, or private_link_storage_dns_zone_id value on its own only deletes that endpoint's DNS zone group, leaving the endpoint itself in place.

Storage traffic path

Today the nodes reach blob storage over the subnet's Microsoft.Storage service endpoint. Once your DNS points a storage account's FQDN at its private endpoint IP, that traffic moves to Private Link. Both paths work, and neither needs a configuration change, but if you watch service-endpoint metrics, expect them to go quiet at that point.

Cost

Each private endpoint carries a per-hour Azure charge, and create_storage_private_endpoint creates one endpoint per storage account. A cluster with 30 accounts gets 30 endpoints.

Day-2 Operations

The provider supports in-place cluster modifications with a single terraform apply:

Scaling Nodes

Change node_count to scale the cluster up or down. The provider handles everything automatically:

  • Scale-up: New nodes are added to the cluster
  • Scale-down: Two-phase removal (remove from quorum, then delete VMs)

Changing VM Types

Change vm_type to upgrade to a larger VM size. This triggers an automatic cluster replacement:

  1. Creates new nodes with the new VM type
  2. Adds new nodes to cluster membership
  3. Removes old nodes from membership
  4. Deletes old VMs

Converting to Multi-AZ

Add or change availability_zones to convert between single-AZ and multi-AZ deployments. This also triggers an automatic cluster replacement.

Full Configuration

# Example: Production Azure Qumulo Cluster
# This example shows a production-ready configuration with HA, monitoring, and security.
# 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"
}

# Production Qumulo cluster with full features
# 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.
  # Empty list means no IP-based restrictions (use with caution).
  # Example: ["10.0.0.0/8", "172.16.0.0/12"]
  allow_cidrs = ["10.0.0.0/8", "172.16.0.0/12"]

  # 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"

  # # Custom managed image ID for cluster nodes.
  # # Use for hardened or pre-configured VMs.
  # # WARNING: Changes trigger cluster replacement!
  # # Format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/images/{name}
  # custom_image_id = "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/images/xxx"

  # # 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

  # # Azure Marketplace image for cluster nodes.
  # # Alternative to custom_image_id for standard enterprise images.
  # # WARNING: Changes trigger cluster replacement!
  # marketplace_image = [{
  #   publisher = "Canonical"
  #   offer     = "0001-com-ubuntu-server-jammy"
  #   sku       = "22_04-lts-gen2"
  #   version   = "latest"
  # }]

  # qfsd cluster name, shown in the Qumulo UI (2-15 chars, case preserved).
  cluster_name    = "qumuloprod"

  # Prefix for the Azure resources this cluster creates (2-15 lowercase chars).
  deployment_name = "qumuloprod"

  # # 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"

  # 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 = 5

  # # 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.
  # # 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.
  # # 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"

  # # 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-prod" # <-- 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        = "Production"
    Department         = "IT"
    CostCenter         = "Storage"
    DataClassification = "Confidential"
    ManagedBy          = "Terraform"
    BackupPolicy       = "Daily"
    SLA                = "99.99"
  }

  # 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_L16s_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"
  }
}