Skip to content

Azure Blob Storage Backend Example

This example demonstrates using Azure Blob Storage for Terraform state management. The backend "azurerm" block in main.tf points Terraform at a storage account and container that you create before the first terraform init -upgrade:

terraform {
  backend "azurerm" {
    resource_group_name  = "tfstate-rg"
    storage_account_name = "tfstatequmulo"
    container_name       = "tfstate"
    key                  = "qumulo/prod.terraform.tfstate"
  }
}

Prerequisites

  • Azure subscription with Contributor permissions
  • Storage account and container for state (see below)
  • Existing virtual network and subnet
  • Terraform >= 1.0

Creating the Backend Storage

Before running terraform init -upgrade, create the storage account and container:

az group create -n tfstate-rg -l eastus2
az storage account create -n tfstatequmulo -g tfstate-rg -l eastus2 --sku Standard_LRS
az storage container create -n tfstate --account-name tfstatequmulo

Usage

  1. Edit main.tf:
  2. Update the backend "azurerm" block with your storage details
  3. Update provider and resource values (look for <-- Replace comments)

  4. Initialize and deploy:

    terraform init -upgrade
    terraform plan
    terraform apply
    

Backend Configuration Options

Optional attributes for the backend "azurerm" block:

  • use_azuread_auth: Use Azure AD instead of access keys
  • subscription_id: Specify if different from provider
  • use_oidc: For GitHub Actions or other OIDC providers

Best Practices

  • Enable soft delete on the storage account for state recovery
  • Use a dedicated storage account for Terraform state
  • Enable versioning for state history
  • Restrict access using Azure RBAC

Outputs

  • cluster_name: The name of your cluster
  • endpoint_ips: IP addresses for client connections
  • endpoints: Pre-formatted connection strings for web UI, API, NFS, and SMB

Full Configuration

The complete main.tf:

# Example: Using Azure Blob Storage as Terraform State Backend
# This example shows how to configure remote state with Azure Blob Storage.
# The resource group for the Qumulo cluster is automatically created if it doesn't exist.
# Edit the values below directly, then run: terraform init -upgrade && terraform apply
#
# PREREQUISITES:
# The storage account and container must exist before running terraform init -upgrade.
# Create them with:
#   az group create -n tfstate-rg -l eastus2
#   az storage account create -n tfstatequmulo -g tfstate-rg -l eastus2 --sku Standard_LRS
#   az storage container create -n tfstate --account-name tfstatequmulo

terraform {
  required_version = ">= 1.0"

  # Azure Blob Storage backend configuration
  backend "azurerm" {
    resource_group_name  = "tfstate-rg"                    # <-- Resource group containing storage account
    storage_account_name = "tfstatequmulo"                 # <-- Storage account name (must be globally unique)
    container_name       = "tfstate"                       # <-- Container for state files
    key                  = "qumulo/prod.terraform.tfstate" # <-- Path within container

    # Optional: Use Azure AD authentication instead of access keys
    # use_azuread_auth = true

    # Optional: Specify subscription if different from provider
    # subscription_id = "your-subscription-id"

    # Optional: For GitHub Actions or other OIDC providers
    # use_oidc = true
  }

  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 (state stored in Azure Blob)
# 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"

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

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

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

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

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

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

  # Storage class backing the cluster's persistent data.
  # STANDARD: Plain hot storage. Required when storage_replication_type is LRS.
  # INTELLIGENT_TIERING: Smart tiering (the default on Qumulo Core 7.8.4 and newer),
  #   which Azure only permits on zone-redundant storage (ZRS).
  # Immutable after creation.
  storage_class = "STANDARD"

  # 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"
    ManagedBy   = "Terraform"
  }

  # 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 "endpoint_ips" {
  description = "Endpoint IP addresses for client connections"
  value       = qumulo_filesystem_azure.cluster.endpoint_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"
  }
}