Provisioning Everpure Cloud Azure Native Resources with Terraform

Share on:

This post walks through a complete Terraform example that provisions an Everpure Cloud Azure Native storage pool, volume group, and volume, then automatically mounts it to an Azure VM over iSCSI.


What is Everpure Cloud Azure Native?

Everpure Cloud Azure Native (EC Azure Native) is a fully managed block storage service jointly developed by Everpure and Microsoft, available directly through the Azure Marketplace. It runs on the same Purity operating environment found in FlashArray, delivered as a native Azure integration.

Unlike traditional deployments, EC Azure Native surfaces native Azure resources under the PureStorage.Block resource provider. You provision, manage, and monitor storage through standard Azure APIs, the Azure portal, ARM/Bicep, or Terraform, with no separate management plane. Performance scales independently from capacity, storage is billed on actual consumed capacity rather than provisioned size, and the service supports thin provisioning and instant volume resizing without downtime.

There are two primary use cases both Generally Available today:

  • Everpure Cloud Azure Native for Azure VMware Solution (AVS): official external storage provider that decouples storage from AVS compute nodes
  • Everpure Cloud Azure Native for Azure VM: delivers block storage to Azure VM workloads via iSCSI

The architecture is straightforward:

  • Reservation - The top-level container tied to your Azure subscription which handles all of your billing for the service.
  • Storage Pool: tied to your Azure VNet via subnet injection, pinned to a specific availability zone, and contains your data.
  • Volume Group: a logical grouping equivalent to a host or workload within the pool, where you define bandwidth and IOPS limits
  • Volume: the block device itself, provisioned at a specific size in bytes, inside a volume group

Once the volume exists, you can mount the volume group to a virtual machine using an Azure VM extension that handles iSCSI discovery, login, and best practices automatically, with no manual initiator configuration required.


Prerequisites

Before running this code, make sure you have the following in place:

  • An active Everpure Cloud Reservation in your Azure subscription
  • An existing Vnet and subnet that has been delegated to the service, dedicated to storage pool injection
  • An Azure VM (Linux or Windows) that will consume the volume
  • Terraform installed locally, or a Terraform Cloud workspace
  • The azure/azapi and hashicorp/azurerm providers configured

Note: The PureStorage.Block resource provider must be registered in your subscription before Terraform can manage these resources. You can register it in the Azure portal under Subscription > Resource Providers, or via the Azure CLI:

az provider register --namespace PureStorage.Block

Project Structure

The example is split across several focused files to keep things readable:

ec-native-example/
├── backend.tf          # Terraform Cloud workspace config
├── providers.tf        # azapi + azurerm provider requirements
├── locals.tf           # API versions and resource group ID helper
├── variables.tf        # All input variables
├── terraform.auto.tfvars  # Variable values (not committed in prod)
├── pool.tf             # Storage pool resource
├── volume_group.tf     # Volume group resource
├── volume.tf           # Volume resource
├── vm.tf               # Data source for the target VM
└── vm_mount.tf         # iSCSI discovery and VM extension mount

Configuring the Providers

This example uses two providers. The azurerm provider handles the VM data source and the VM extension. The azapi provider handles all of the Everpure Cloud Azure Native resources since they are not yet surfaced in the azurerm provider.

terraform {
  required_providers {
    azapi = {
      source = "azure/azapi"
    }
    azurerm = {
      source = "hashicorp/azurerm"
    }
  }
}

provider "azurerm" {
  features {}
  subscription_id = var.subscription_id
}

provider "azapi" {
  subscription_id = var.subscription_id
}

A local helper in locals.tf pre-computes the resource group ID and pins the API versions so they are easy to update in one place:

locals {
  api_version      = "2026-01-01-preview"
  pool_api_version = "2024-11-01-preview"
  rg_id            = "/subscriptions/${var.subscription_id}/resourceGroups/${var.resource_group}"
}

Provisioning the Storage Pool

The storage pool is the foundation of everything that follows. There are a few required parameters worth calling out:

  • reservationResourceId — the full resource ID of your EC reservation
  • vnetInjection — the subnet and VNet IDs the pool will inject into
  • availabilityZone — zone pinning is required
  • provisionedBandwidthMbPerSec — the bandwidth allocated to the pool

The timeouts block is important here. Storage pool create and delete operations can take up to 90 minutes, so Terraform needs to be told to wait:

resource "azapi_resource" "storage_pool" {
  type                      = "PureStorage.Block/storagePools@${local.pool_api_version}"
  name                      = var.pool_name
  location                  = var.location
  parent_id                 = local.rg_id
  tags                      = var.tags
  schema_validation_enabled = true

  body = {
    properties = {
      availabilityZone             = var.zone
      provisionedBandwidthMbPerSec = var.bandwidth
      reservationResourceId        = var.reservation_id
      vnetInjection = {
        subnetId = var.subnet_id
        vnetId   = var.vnet_id
      }
    }
  }

  timeouts {
    create = "90m"
    update = "90m"
    delete = "90m"
    read   = "5m"
  }
}

Creating the Volume Group and Volume

With the pool deployed, create a volume group inside it. The volume group is where you set performance guardrails: bandwidth in MB/s and an IOPS limit.

resource "azapi_resource" "volume_group" {
  type                      = "PureStorage.Block/storagePools/volumeGroups@${local.api_version}"
  name                      = var.vg_name
  location                  = var.location
  parent_id                 = azapi_resource.storage_pool.id
  tags                      = var.tags
  schema_validation_enabled = false

  body = {
    properties = {
      performanceParameters = {
        bandwidthLimitMbPerSec = var.bandwidth
        iopsLimit              = var.vg_iops_limit
      }
    }
  }

  response_export_values = ["*"]
  depends_on             = [azapi_resource.storage_pool]
}

Next, create the volume inside the group. The size is specified in bytes (107374182400 bytes equals 100 GiB):

resource "azapi_resource" "volume" {
  type                      = "PureStorage.Block/storagePools/volumeGroups/volumes@${local.api_version}"
  name                      = var.vol_name
  parent_id                 = azapi_resource.volume_group.id
  schema_validation_enabled = false

  body = {
    properties = {
      provisionedSize = var.vol_size_bytes
    }
  }

  response_export_values = ["*"]
  depends_on             = [azapi_resource.volume_group]
}

Mounting the Volume to a VM

Rather than logging into each VM and configuring iSCSI manually, we use the listConnectionParameters action on the volume group to retrieve the iSCSI target IPs and IQNs, then pass them directly to a VM extension.

The extension is published by Pure Storage and handles both Linux and Windows with a single os_type variable:

data "azapi_resource_action" "conn_params" {
  type        = "PureStorage.Block/storagePools/volumeGroups@${local.api_version}"
  resource_id = azapi_resource.volume_group.id
  action      = "listConnectionParameters"
  method      = "POST"

  response_export_values = ["iscsi.endpoints"]
}

locals {
  iscsi_endpoints = data.azapi_resource_action.conn_params.output.iscsi.endpoints
  iscsi_ip_ct0    = local.iscsi_endpoints[0].ip
  iscsi_iqn_ct0   = local.iscsi_endpoints[0].iqn
  iscsi_ip_ct1    = local.iscsi_endpoints[1].ip
  iscsi_iqn_ct1   = local.iscsi_endpoints[1].iqn

  ext_name = var.os_type == "windows" ? "MountIscsiWindows" : "MountIscsiLinux"

  iscsi_settings_linux = {
    targets = [
      { ip = local.iscsi_ip_ct0, port = 3260, iqn = local.iscsi_iqn_ct0 },
      { ip = local.iscsi_ip_ct1, port = 3260, iqn = local.iscsi_iqn_ct1 },
    ]
    replaceTargets = false
    sessionCount   = var.iscsi_sessions
    reboot         = null
  }

  iscsi_settings_windows = merge(local.iscsi_settings_linux, { reboot = true })
}

resource "azurerm_virtual_machine_extension" "iscsi_mount" {
  name                 = local.ext_name
  virtual_machine_id   = data.azurerm_virtual_machine.vm.id
  publisher            = "PureStorage.Extensions"
  type                 = local.ext_name
  type_handler_version = "1.2"

  settings = jsonencode(
    var.os_type == "windows" ? local.iscsi_settings_windows : local.iscsi_settings_linux
  )

  tags = var.tags
}

A couple of things worth noting here:

  • The extension establishes two iSCSI sessions by default (sessionCount = 2), one per controller, for throughput and path redundancy
  • Windows VMs have reboot = true set because the iSCSI initiator service requires a restart to pick up new targets
  • The replaceTargets = false flag tells the extension to append to existing iSCSI configuration rather than overwrite it

Deploying the Configuration

Once you have your terraform.auto.tfvars populated with your subscription, reservation, VNet, and VM details, deploying is straightforward. Initialize and apply:

terraform init
terraform plan
terraform apply

After a successful apply, Terraform will output the iSCSI target details for reference:

Outputs:

storage_pool_id  = "/subscriptions/.../storagePools/my-pool"
volume_group_id  = "/subscriptions/.../volumeGroups/my-vg"
volume_id        = "/subscriptions/.../volumes/my-vol"

iscsi_targets = {
  ct0_ip  = "10.x.x.x"
  ct0_iqn = "iqn.2021-09.com.purestorage:..."
  ct1_ip  = "10.x.x.x"
  ct1_iqn = "iqn.2021-09.com.purestorage:..."
}

To verify the iSCSI session is active on a Linux VM, run the following command after the extension completes:

iscsiadm -m session

On Windows, you can verify via PowerShell:

Get-IscsiSession

Wrapping Up

If you have questions or run into anything, drop a comment below.

comments powered by Disqus

See Also