Provisioning Everpure Cloud Azure Native Resources with Terraform
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/azapiandhashicorp/azurermproviders configured
Note: The
PureStorage.Blockresource 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 reservationvnetInjection— the subnet and VNet IDs the pool will inject intoavailabilityZone— zone pinning is requiredprovisionedBandwidthMbPerSec— 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 = trueset because the iSCSI initiator service requires a restart to pick up new targets - The
replaceTargets = falseflag 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 DisqusSee Also
- Everpure Cloud Dedicated on Azure - Quick Launch
- Using the Everpure Cloud Dedicated Terraform Provider for Azure
- Everpure Cloud Dedicated Use Cases for Microsoft Azure - Terraform Edition
- Deploying a Windows Azure VM with Hashicorp Terraform to Microsoft Azure
- Unlocking Performance Insights: Using resxtop to Monitor Your Azure VMware Solution Environment