Provisions one Azure NetApp Files capacity pool (
azurerm_netapp_pool) β the billing and performance boundary that volumes carve their quota out of. Targetshashicorp/azurerm ~> 4.0.
- ποΈ Provisions one capacity pool β where NetApp Files capacity is actually bought β as a keystone resource named
this. - π°
size_in_tbis the billed quantity, not the sum of what the volumes inside consume. An oversized pool costs the same empty as full. - π Documents which choices are durable:
service_levelandencryption_typeare force-new, andcool_access_enabledis effectively one-way. - β
Validates the three enums, the 1β2048 size range, and the
Flexible+Manualpairing thatcustom_throughput_mibpsrequires β all at plan. β οΈ Explains why the schema's minimum size misleads: one volume onBasicnetwork features raises the pool's floor from 2 TiB to 4 TiB.- π€ Emits the cost and posture facts β
size_in_tb,service_level,encryption_type,cool_access_enabledβ so a review reads them from state.
π‘ Why it matters: Almost nothing about a pool can be changed in place. The service level is force-new, double encryption cannot be retrofitted, and cool access cannot be switched back off without rebuilding the pool and migrating every volume in it. So the useful thing a module can do here is not to offer more knobs β it is to be clear about which decisions you are making permanently, and which one you are being billed for.
If this module saves you time, please consider supporting its continued development:
- β Star the repository on GitHub.
- π€ Connect on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
flowchart LR
rg["terraform-azurerm-resource-group"]
kv["terraform-azurerm-key-vault"]
vnet["terraform-azurerm-virtual-network: subnet delegated to Microsoft.NetApp/volumes"]
acct["terraform-azurerm-netapp-account"]
enc["terraform-azurerm-netapp-account-encryption: who holds the key"]
pool["terraform-azurerm-netapp-pool: the BILLED capacity"]
vol["terraform-azurerm-netapp-volume: what clients mount"]
snap["terraform-azurerm-netapp-snapshot: one on-demand, in the pool"]
spol["terraform-azurerm-netapp-snapshot-policy: the schedule"]
vault["terraform-azurerm-netapp-backup-vault: the destination"]
bpol["terraform-azurerm-netapp-backup-policy: the retention"]
vghana["terraform-azurerm-netapp-volume-group-sap-hana"]
vgora["terraform-azurerm-netapp-volume-group-oracle"]
rg -->|"resource_group_name, location"| acct
kv -->|"encryption_key URI"| enc
acct -->|"id, BY ID"| enc
acct -->|"account_name, BY NAME"| pool
acct -->|"account_name, BY NAME"| vault
acct -->|"account_name, BY NAME"| bpol
acct -->|"account_name, BY NAME"| spol
acct -->|"account_name, BY NAME"| vghana
acct -->|"account_name, BY NAME"| vgora
pool -->|"pool_name, BY NAME"| vol
pool -->|"capacity_pool_id, BY ID"| vghana
pool -->|"capacity_pool_id, BY ID"| vgora
vnet -->|"subnet_id, must be delegated"| vol
vol -->|"name plus pool_name, BY NAME"| snap
spol -->|"snapshot_policy_id, BY ID"| vol
vault -->|"backup_vault_id, BY ID"| vol
bpol -->|"backup_policy_id, BY ID"| vol
classDef me fill:#0078D4,stroke:#004578,color:#fff;
classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
class acct keystone;
class enc,pool,vol,snap,spol,vault,bpol,vghana,vgora me;
class rg,kv,vnet sib;
flowchart TB
addr["name, resource_group_name, account_name, location: ALL force-new"]
sl["service_level: force-new. Standard, Premium, Ultra, Flexible"]
size["size_in_tb 1-2048: THE BILLED QUANTITY, updates in place"]
floor["practical minimum is higher: a Basic-network volume raises the floor 2 to 4 TiB"]
qos["qos_type Auto or Manual"]
ct["custom_throughput_mibps: only Flexible plus Manual, min 128"]
enc["encryption_type Single or Double: force-new, so Double cannot be retrofitted"]
cool["cool_access_enabled: enabling is in place, DISABLING forces a new pool"]
this["terraform-azurerm-netapp-pool"]
pool["azurerm_netapp_pool.this"]
out["outputs: id, name for volumes, size_in_tb, service_level, encryption_type, cool_access_enabled"]
vols["volumes carve quota from this pool and reference it BY NAME"]
addr -->|"identity"| this
sl -->|"durable performance choice"| this
size -->|"cost"| this
floor -->|"why the schema minimum misleads"| size
qos -->|"throughput distribution"| this
ct -->|"validated pairing"| qos
enc -->|"encryption at rest"| this
cool -->|"one-way door"| this
this -->|"creates"| pool
pool -->|"emits"| out
out -->|"pool_name"| vols
classDef me fill:#0078D4,stroke:#004578,color:#fff;
classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
class this me;
class pool keystone;
class addr,sl,size,floor,qos,ct,enc,cool,out,vols sib;
Resource inventory
| Resource | Count | Role |
|---|---|---|
azurerm_netapp_pool.this |
1 | The keystone capacity pool, with its optional timeouts block. |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None in this module β the caller configures provider "azurerm" { features {} }, auth, and subscription. |
Schema notes that bite (verified against the live provider schema):
- Force-new:
name,resource_group_name,account_name,location,service_level, andencryption_type. cool_access_enabledis asymmetric. Enabling it is an in-place update; disabling it forces a new capacity pool β so a pool switched on for an experiment cannot be switched back without a rebuild and a volume migration.size_in_tbis the billed quantity. Billing follows provisioned size, not the sum of volume usage, so an oversized pool costs the same empty as full. It updates in place, so start close to the requirement and grow.- The schema accepts
size_in_tb = 1, but the practical minimum is higher: a 2 TiB pool requires every volume to useStandardnetwork features, and one volume onBasicraises the floor to 4 TiB. A value the schema allows can still be rejected at apply. - The maximum is governed by regional quota, which is a support request rather than a configuration change.
custom_throughput_mibpsis valid only whenservice_levelisFlexibleandqos_typeisManual, with a minimum of 128.- On a
ManualQoS pool, a volume that sets nothroughput_in_mibpsgets none β throughput is not distributed automatically. - A pool cannot be deleted while it holds volumes, so a failing delete is usually reporting a volume the configuration does not know about.
encryption_type = "Double"cannot be retrofitted β it is force-new, so adding it later means rebuilding the pool and migrating every volume.- The account is addressed by name, not by Resource ID β the pattern across the pool, volume, snapshot, and backup resources in this family.
Contributoron the resource group holding the NetApp account, or a custom role coveringMicrosoft.NetApp/netAppAccounts/capacityPools/*.- Read access on the NetApp account, so the pool can be created within it.
- No data-plane or Key Vault permission is needed here; the pool holds no credentials and no data.
- An existing NetApp account, in the same region as this pool.
- Sufficient regional capacity quota for
size_in_tb. The provider accepts a value the subscription's quota does not allow, and the service rejects it at apply. - For a 2 TiB pool: every volume in it must use
Standardnetwork features. - For
encryption_type = "Double": a region and service level that support double encryption at rest. - The caller configures the
provider "azurerm" { features {} }block, auth, and subscription.
terraform-azurerm-netapp-pool/
βββ providers.tf # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
βββ variables.tf # validated enums, size range, the Flexible+Manual pairing, tags/timeouts tail
βββ main.tf # keystone azurerm_netapp_pool.this
βββ outputs.tf # id, name for volumes, cost and posture values
βββ README.md # this document
βββ SCOPE.md # cross-module contract
βββ LICENSE # MIT
βββ .gitignore # canonical library ignore set
provider "azurerm" {
features {}
}
module "anf_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-pool.git?ref=v1.0.0"
name = "pool-prod-01"
resource_group_name = module.anf_rg.name
account_name = module.anf_account.name # the NAME, not the id
location = module.anf_rg.location
service_level = "Premium" # force-new β a durable choice
size_in_tb = 4 # THE BILLED QUANTITY
tags = { workload = "records", cost_owner = "platform-storage" }
}π°
size_in_tbis what you pay for regardless of volume usage. It updates in place, so starting at the requirement and growing is cheaper than provisioning headroom.
βΉοΈ The caller owns the provider, its authentication, and the mandatory
features {}block. This module never declares them.
Consumes
| Input | Type | Source module |
|---|---|---|
resource_group_name |
string |
terraform-azurerm-resource-group (name) |
account_name |
string |
terraform-azurerm-netapp-account (name) |
location |
string |
caller / terraform-azurerm-netapp-account (location) |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Capacity pool Resource ID (first) | diagnostics, RBAC |
name |
Pool name | terraform-azurerm-netapp-volume (pool_name) and terraform-azurerm-netapp-snapshot (pool_name) β both reference the pool by name, not by ID |
account_name / resource_group_name / location |
Addressing | composition wiring; a volume must share the pool's region |
service_level |
The pool's performance tier | migration planning β force-new here, and a volume changing service level must move to a pool of the target level |
size_in_tb |
Provisioned size | cost review β the billed quantity |
qos_type |
Auto or Manual |
volume configuration β Manual means each volume sets its own throughput |
custom_throughput_mibps |
Pool throughput. Reads back as 0 when unset, never null β use has_custom_throughput to branch |
capacity review |
encryption_type |
Single or Double |
governance review β force-new, so this is the posture for the pool's lifetime |
cool_access_enabled |
Whether cool-access volumes are allowed | review β effectively one-way |
Values these examples reference but do not create are declared inputs:
variable "subnet_id" {
description = "subnet id of an existing resource these examples reference."
type = string
}1 Β· The minimal pool
module "anf_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-pool.git?ref=v1.0.0"
name = "pool-prod-01"
resource_group_name = module.anf_rg.name
account_name = module.anf_account.name
location = module.anf_rg.location
service_level = "Premium"
size_in_tb = 4
}βΉοΈ There is no meaningful "empty call" here β
service_levelandsize_in_tbare both required, and both are decisions nobody else can make for you. What you get by default isqos_type = "Auto",encryption_type = "Single", and cool access off.
2 Β· Why the size minimum is not 1
size_in_tb = 2 # requires EVERY volume in the pool to use Standard network featuressize_in_tb = 4 # the floor once any volume uses Basic network features
β οΈ The schema accepts1, and the validation here allows it, because the real constraint lives in the service and depends on the volumes β which this module cannot see. If any volume in the pool usesnetwork_features = "Basic", the minimum is 4 TiB. A pool sized 2 with a Basic volume fails at apply, not at plan.
3 Β· Service level is a durable choice
service_level = "Ultra" # force-new
β οΈ Changing this forces a new capacity pool, which means every volume in it has to be migrated first. And migrating a volume between service levels has its own severe consequence β the volume's Resource ID changes and its dependants are silently dropped from state (documented onterraform-azurerm-netapp-volume). Pick against a measured throughput requirement, not a guess.
4 Β· Manual QoS, and what it obliges
qos_type = "Manual"# Then every volume must set its own throughput:
module "anf_volume" {
# ...
throughput_in_mibps = 128
}
β οΈ On aManualpool, throughput is not distributed automatically β a volume that sets nothing gets nothing.Manualis the right choice when one volume needs guaranteed throughput regardless of its size, but it makes the sum of assignments something you have to manage against the pool's total.
5 Β· The Flexible tier and custom throughput
service_level = "Flexible"
qos_type = "Manual"
custom_throughput_mibps = 256service_level = "Premium" # β
qos_type = "Manual"
custom_throughput_mibps = 256Error: Invalid value for variable
custom_throughput_mibps is only valid when service_level is "Flexible" AND qos_type is
"Manual".
π‘
Flexibleexists precisely to decouple throughput from provisioned capacity, socustom_throughput_mibpsonly means anything in that combination. Both the pairing and the 128 minimum are checked at plan; the provider would report them at apply.
6 Β· Double encryption is a creation-time decision
encryption_type = "Double" # force-new β cannot be added laterπ Data at rest is encrypted either way;
Doubleadds a second independent layer. This module does not default to it despite the stronger posture, because it is limited by region and service level β a default would make the empty call fail where it is unavailable, with no recovery short of rebuilding the pool and migrating its volumes. Where the data class warrants it, set it at creation.
7 Β· Cool access is a one-way door
cool_access_enabled = true # enabling: in-place. DISABLING: forces a new pool.
β οΈ This asymmetry is the trap. Turning it on to try tiering is a cheap in-place update; turning it back off is a pool rebuild and a volume migration. Enable it only when cool access is actually wanted, and note that cool-access volumes are incompatible with large volumes β so a pool intended for both cannot serve both.
8 Β· Enum validation at plan
service_level = "premium" # β lowercaseError: Invalid value for variable
service_level must be one of: Standard, Premium, Ultra, Flexible.
βΉοΈ Capitalized, and note
Flexibleis a real member β it is easy to assume the set is only the three performance tiers.qos_type(Auto/Manual) andencryption_type(Single/Double) are checked the same way.
9 Β· Size range checked, quota not
size_in_tb = 4096 # βError: Invalid value for variable
size_in_tb must be between 1 and 2048. Note the service minimum is higher than 1 in
practice: a 2 TiB pool requires every volume to use Standard network features, and a
volume on Basic raises the minimum to 4 TiB.
β οΈ The 1β2048 range is checkable; your subscription's regional quota is not. A value inside the range can still be rejected at apply, and raising the ceiling is a support request rather than a configuration change.
10 Β· Growing a pool
size_in_tb = 8 # was 4 β updates in placeπ° Growth is in-place and immediate, which is exactly why provisioning headroom "just in case" is the wrong instinct: you pay for the headroom from day one, and you could have added it in seconds when it was needed. Shrinking is also permitted, but only down to the space the volumes actually occupy.
11 Β· Wiring the pool into a volume
module "anf_volume" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-volume.git?ref=v1.0.0"
name = "vol-records"
resource_group_name = module.anf_rg.name
account_name = module.anf_account.name
pool_name = module.anf_pool.name # BY NAME, from the output
location = module.anf_pool.location
service_level = module.anf_pool.service_level # inherit the pool's tier
storage_quota_in_gb = 1
subnet_id = var.subnet_id
volume_path = "volume-path"
}π‘ Volumes reference the pool by name, and that is a plain string with no Terraform dependency β so wire it from the output rather than typing it, and a rename becomes a plan diff instead of a failed apply. Taking
service_levelfrom the pool's output keeps the two consistent by construction.
12 Β· Several pools from a keyed map
locals {
pools = {
"pool-prod-premium" = { level = "Premium", size = 8 }
"pool-prod-standard" = { level = "Standard", size = 4 }
}
}
module "pools" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-pool.git?ref=v1.0.0"
for_each = local.pools
name = each.key
resource_group_name = module.anf_rg.name
account_name = module.anf_account.name
location = module.anf_rg.location
service_level = each.value.level
size_in_tb = each.value.size
}π‘ One pool per service level is the usual arrangement, precisely because
service_levelis force-new on the pool β it is how a volume gets to change tier at all, by moving between pools that already exist.
13 Β· The cost review this module is designed for
output "anf_capacity_cost_basis" {
description = "Provisioned TiB per pool β the billed quantity, regardless of volume usage."
value = {
for k, m in module.pools : k => {
provisioned_tb = m.size_in_tb
tier = m.service_level
encryption = m.encryption_type
cool_access = m.cool_access_enabled
}
}
}π°
size_in_tbandservice_leveltogether are the cost basis;encryption_typeandcool_access_enabledare the two irreversible choices. All four are emitted so this table can be built from state rather than by reading configuration.
14 Β· Deleting a pool
Error: deleting NetApp Pool: pool contains volumes
β οΈ A pool cannot be deleted while it holds volumes. In practice a failing delete is reporting a volume this configuration does not know about β often one created by a volume group, or one left behind by a migration that re-addressed it out of state (see the volume module). Enumerate the pool's volumes before assuming the error is spurious.
15 Β· ποΈ End-to-end composition
provider "azurerm" {
features {}
}
# 1 Β· The resource group.
module "anf_rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-anf-eastus2"
location = "eastus2"
}
# 2 Β· The NetApp account β the regional parent everything else is named against.
module "anf_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account.git?ref=v1.0.0"
name = "anf-prod-eastus2"
resource_group_name = module.anf_rg.name
location = module.anf_rg.location
}
# 3 Β· The delegated subnet the volumes will land in.
module "anf_vnet" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-network.git?ref=v1.0.0"
name = "vnet-anf-eastus2"
resource_group_name = module.anf_rg.name
location = module.anf_rg.location
address_space = ["10.40.0.0/16"]
subnets = {
anf = {
address_prefixes = ["10.40.1.0/24"]
delegations = [{
name = "netapp"
service_delegation = {
name = "Microsoft.NetApp/volumes"
actions = ["Microsoft.Network/networkinterfaces/*", "Microsoft.Network/virtualNetworks/subnets/join/action"]
}
}]
}
}
}
# 4 Β· The capacity pool β this module. Sized at the requirement, not padded.
module "anf_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-pool.git?ref=v1.0.0"
name = "pool-prod-01"
resource_group_name = module.anf_rg.name
account_name = module.anf_account.name
location = module.anf_rg.location
service_level = "Premium" # force-new
size_in_tb = 4 # billed; grow in place when needed
# Left at defaults deliberately: qos_type Auto, encryption_type Single, cool access off.
# encryption_type and cool_access_enabled are both effectively permanent β see examples 6 and 7.
tags = { workload = "records", cost_owner = "platform-storage" }
}
# 5 Β· A volume drawing quota from the pool, referencing it BY NAME.
module "anf_volume" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-volume.git?ref=v1.0.0"
name = "vol-records"
resource_group_name = module.anf_rg.name
account_name = module.anf_account.name
pool_name = module.anf_pool.name
location = module.anf_pool.location
service_level = module.anf_pool.service_level
storage_quota_in_gb = 1024
volume_path = "records"
subnet_id = module.anf_vnet.subnet_ids["anf"]
# The volume exports to nobody until a rule says otherwise.
export_policy_rule = {
app_tier = {
rule_index = 1
allowed_clients = ["10.40.2.0/24"]
protocol = ["NFSv3"]
unix_read_write = true
# root_access_enabled defaults to false.
}
}
}
output "pool_cost_basis" {
description = "What is actually billed, and the two choices that cannot be undone."
value = {
provisioned_tb = module.anf_pool.size_in_tb
tier = module.anf_pool.service_level
encryption = module.anf_pool.encryption_type
cool_access = module.anf_pool.cool_access_enabled
}
}π‘ The ordering in this composition is the family's real dependency chain: the account is named by the pool, the pool is named by the volume, and the delegated subnet in step 3 is what the volume joins. Note step 4 leaves
encryption_typeandcool_access_enabledat their defaults deliberately β both are effectively permanent, so they deserve a decision rather than a drift. Output names on sibling modules are illustrative; match them to the versions you pin.
Required: name, resource_group_name, account_name, location, service_level, size_in_tb.
Performance: qos_type (defaults Auto), custom_throughput_mibps.
Durable choices: encryption_type (defaults Single), cool_access_enabled (defaults false).
Universal tail: tags, timeouts.
Full object() schemas
variable "name" {
# force-new. Volumes and snapshots reference the pool BY THIS NAME as a plain string with
# no Terraform dependency β wire callers from the `name` output.
type = string
}
variable "resource_group_name" { type = string } # force-new
variable "account_name" { type = string } # force-new; the NAME, not the Resource ID
variable "location" { type = string } # force-new; match the account's region
variable "service_level" {
# Standard | Premium | Ultra | Flexible (validated).
# β οΈ FORCE-NEW β changing it means migrating every volume in the pool first.
# Flexible is the only tier where custom_throughput_mibps applies.
type = string
}
variable "size_in_tb" {
# 1-2048 (validated). π° THE BILLED QUANTITY β billing follows provisioned size, not volume
# usage, so an oversized pool costs the same empty as full. Updates in place, so grow rather
# than pad.
# β οΈ The practical minimum is higher than 1: a 2 TiB pool needs EVERY volume on Standard
# network features; one volume on Basic raises the floor to 4 TiB. The ceiling is regional
# quota, which is a support request.
type = number
}
variable "qos_type" {
# Auto (default) distributes throughput by quota | Manual requires each volume to set its own
# throughput_in_mibps β and a volume that sets none gets none. Validated.
type = string
default = "Auto"
}
variable "custom_throughput_mibps" {
# Min 128. Valid ONLY when service_level = "Flexible" AND qos_type = "Manual" β both checks
# live on this variable and read the other two one-directionally, because Terraform rejects
# validations that reference each other.
type = number
default = null
}
variable "encryption_type" {
# Single (default) | Double (validated). β οΈ FORCE-NEW.
# NOT defaulted to Double despite the stronger posture: it is region- and service-level
# limited, so a default would fail the empty call where unavailable β with no recovery short
# of rebuilding the pool and migrating its volumes. Set it at CREATION where warranted.
type = string
default = "Single"
}
variable "cool_access_enabled" {
# β οΈ ASYMMETRIC: enabling is in-place, DISABLING forces a new pool. Effectively one-way.
# Also incompatible with large volumes.
type = bool
default = false
}
variable "tags" { type = map(string), default = {} }
variable "timeouts" { type = object({ create = optional(string), read = optional(string), update = optional(string), delete = optional(string) }), default = null }| Output | Description | Kind |
|---|---|---|
id |
The Azure Resource ID of the capacity pool, of the form /subscriptions/.../resourceGroups/.../providers/Microsoft.NetApp/netAppAccounts//capacityPools/ | Passthrough |
name |
The capacity pool name | Passthrough |
account_name |
The NetApp account this pool belongs to | Passthrough |
resource_group_name |
The resource group holding the NetApp account | Passthrough |
location |
The region the pool resides in, normalized by the provider (an input of "East US" is emitted as "eastus") | Passthrough |
account_id |
The Azure Resource ID of the parent NetApp account, derived by trimming the capacityPools segment from this pool's ID | Passthrough |
arm_resource_type |
The ARM resource type this module creates | Derived |
tags |
The tags applied to the capacity pool | Passthrough |
tag_count |
The number of tag pairs on the pool, against an Azure ceiling of 50 per resource | Passthrough |
size_in_tb |
Provisioned size in TiB | Passthrough |
size_in_bytes |
The provisioned size in bytes, the unit the Azure API actually receives - the provider multiplies size_in_tb by 1024 four times before sending it | Passthrough |
billed_on_provisioned_size_not_consumption |
Always true, and it is the single most expensive fact about this resource | Constant |
pools_and_volumes_never_auto_grow |
Always true | Constant |
can_be_shrunk_down_to_allocated_volume_capacity |
Always true, and it is the reason over-provisioning is recoverable rather than permanent | Constant |
minimum_size_tib_all_volumes_standard_network_features |
The smallest pool Azure will accept when EVERY volume in it uses Standard network features | Passthrough |
minimum_size_tib_if_any_volume_uses_basic_network_features |
The smallest pool Azure will accept once ANY volume in it uses Basic network features - four times the Standard floor | Passthrough |
max_size_tib |
The largest single capacity pool Azure NetApp Files supports | Passthrough |
regional_capacity_quota_tib |
The default regional capacity quota per subscription, in TiB, from the Azure NetApp Files resource-limits table | Passthrough |
max_volumes_per_pool |
The Azure default ceiling on volumes inside one capacity pool, adjustable by support request | Passthrough |
max_pools_per_account |
The Azure default ceiling on capacity pools under one NetApp account, adjustable by support request | Passthrough |
service_level |
The pool's performance tier | Passthrough |
qos_type |
How throughput is assigned to volumes: Auto distributes it in proportion to each volume's quota, Manual requires each volume to set its own throughput_in_mibps and leaves an unassigned volume with none | Passthrough |
custom_throughput_mibps |
The pool's custom throughput in MiB/s | Passthrough |
has_custom_throughput |
Whether a custom throughput value is actually in force on this pool | Derived |
max_custom_throughput_mibps |
The largest custom throughput this pool could carry at its current size - Microsoft's documented maximum of 5 x 128 MiB/s/TiB x the pool size in TiB | Passthrough |
throughput_ceiling_mibps |
The total throughput available to be shared across this pool's volumes, computed from the documented per-TiB allocation of the chosen service level: 16 MiB/s per TiB on Standard, 64 on Premium, 128 on Ultra | Passthrough |
flexible_throughput_decrease_requires_cooldown_hours |
How long a Flexible pool must wait before its throughput can be REDUCED, in hours | Passthrough |
encryption_type |
Encryption at rest: Single or Double | Passthrough |
double_encryption_requested_on_flexible_pool |
True when encryption_type is Double and service_level is Flexible - a combination Microsoft documents as unsupported, only single encryption being available on the Flexible tier | Passthrough |
cool_access_enabled |
Whether the pool may hold cool-access volumes | Passthrough |
cool_access_cannot_be_disabled |
Always true, and the asymmetry is invisible in the schema, where cool_access_enabled looks like an ordinary updatable boolean | Constant |
force_new_arguments |
The arguments that destroy and recreate this pool rather than updating it | Derived |
service_level_change_requires_moving_the_volumes |
Always true | Constant |
flexible_service_level_cannot_be_converted |
Always true, and it is stricter than the general force-new rule | Constant |
destroy_fails_while_volumes_remain |
Always true | Constant |
volumes_reference_this_pool_by_name |
Always true, and it is why the name output above matters more than it looks | Constant |
cannot_be_moved_between_accounts |
Always true | Constant |
timeout_defaults |
The provider's built-in timeouts for this resource, which appear nowhere in the schema | Derived |
effective_timeouts |
The timeouts actually in force: the caller's values where supplied, the provider's defaults otherwise | Derived |
No secret is emitted; this resource carries none.
- The pool is where capacity is bought, and that frames everything else.
size_in_tbis billed on provisioned size rather than volume consumption, so headroom is a standing cost rather than prudence β and since it grows in place in seconds, there is no operational reason to pad it.size_in_tbandservice_levelare emitted together because they are the cost basis. - Three choices here are effectively permanent, and the module's job is to say which.
service_levelandencryption_typeare force-new;cool_access_enabledis force-new in one direction only. That asymmetry is the subtlest of the three: enabling cool access looks like an ordinary toggle and reads as reversible, but reversing it costs a pool rebuild and a volume migration. encryption_typedefaults toSingle, and that is a deliberate departure from defaulting to the hardened value. Double encryption is limited by region and service level, so defaulting it would make the empty call fail where it is unavailable β and because it is force-new, the wrong choice cannot be corrected without rebuilding the pool and migrating its volumes. A default that sometimes fails irreversibly is worse than a documented decision.- The schema's minimum size misleads, and the reason is that the constraint lives elsewhere. The real floor depends on the network features of the volumes in the pool, which this module cannot see: 2 TiB requires every volume on
Standard, and one volume onBasicraises it to 4 TiB. The validation allows what the schema allows and the documentation carries the rest, because a stricter check would reject legitimate configurations. - The account and this pool are addressed by name; volumes address this pool by name too. Those are plain strings with no Terraform dependency, which is why
nameis emitted with an explicit note β wiring from the output is what makes a rename a plan diff instead of a failed apply. ManualQoS shifts an obligation onto every volume. Throughput stops being distributed automatically, so a volume that sets nothroughput_in_mibpsgets none.qos_typeis emitted so a composition can tell whether that obligation is in force.- One pool per service level is the normal arrangement, and it follows from
service_levelbeing force-new: moving a volume between tiers means moving it to a pool that already exists at the target tier. - A pool cannot be deleted while it holds volumes, and the most confusing case is a volume the configuration has lost track of β including one dropped from state by a volume migration (see
terraform-azurerm-netapp-volume). features {}dependence. The module carries noprovider {}block. If it appears not to initialize in isolation, the cause is a missing caller-sideprovider "azurerm" { features {} }.
| Concern | Secure default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Encryption at rest | always on; encryption_type = "Single" |
"Double" β at creation only, region/tier permitting |
| Cool access | cool_access_enabled = false |
set true (effectively one-way) |
| Throughput model | qos_type = "Auto" β no per-volume obligation |
"Manual" (each volume must then set its own) |
| Cost visibility | size_in_tb and service_level emitted |
β (no opt-out) |
| Permanence visibility | encryption_type and cool_access_enabled emitted |
β (no opt-out) |
| Enum and range correctness | three enums, the size range, and the Flexible+Manual pairing validated at plan | β (no opt-out) |
| Caller wiring | name emitted so a rename is a plan diff |
type the pool name into volumes as a string |
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the module with
?ref=v1.0.0β never a branch. - This library is plan-only during authoring; a human runs
terraform plan/applyfrom CI against real credentials. - Before applying: confirm regional quota covers
size_in_tb, and decideencryption_typeandcool_access_enablednow β both are effectively permanent. - Treat a
service_levelchange as a planned migration of every volume in the pool, not an edit. - Before destroying: enumerate the pool's volumes. A pool holding volumes cannot be deleted.
terraform validateproves the configuration is internally consistent and type-correct against the pinned provider schema, and exercises everyvalidation {}block: theservice_level,qos_type, andencryption_typeenums; the 1β2048size_in_tbrange; the 128 minimum oncustom_throughput_mibps; and itsFlexible+Manualpairing.terraform fmt -checkenforces canonical formatting.- Neither command calls Azure. Only
terraform plan(run by a human, from CI) exercises the ARM API β the module ships without any cloud apply. - What only apply exercises: whether regional quota covers
size_in_tb, whether the region and tier supportencryption_type = "Double", and whether the account exists in the named region. - What nothing exercises: whether
size_in_tbclears the real minimum, since that depends on the network features of volumes this module cannot see β a 2 TiB pool with oneBasicvolume fails at apply.
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
id = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-anf-eastus2/providers/Microsoft.NetApp/netAppAccounts/anf-prod-eastus2/capacityPools/pool-prod-01"
name = "pool-prod-01"
account_name = "anf-prod-eastus2"
resource_group_name = "rg-anf-eastus2"
location = "eastus2"
service_level = "Premium"
size_in_tb = 4
qos_type = "Auto"
custom_throughput_mibps = 0
encryption_type = "Single"
cool_access_enabled = false
| Symptom | Cause | Fix |
|---|---|---|
Provider configuration not present / features error |
No caller-side provider "azurerm" { features {} }. |
Add the provider block with features {} in the root module. |
Plan error: service_level must be one of |
Wrong casing, or a value outside the set. | Use Standard, Premium, Ultra, or Flexible. |
Plan error: custom_throughput_mibps is only valid when... |
Set without Flexible + Manual. |
Use that pairing, or drop the field. |
| Apply fails: insufficient quota | Regional capacity quota is below size_in_tb. |
Request more quota; it is not a configuration change. |
| Apply fails: pool size below minimum | A volume uses Basic network features, raising the floor to 4 TiB. |
Raise size_in_tb, or move the volumes to Standard. |
| Apply fails: double encryption unsupported | The region or service level does not offer it. | Use Single, or choose a supporting region β and note it cannot be changed later. |
| Volumes get no throughput | qos_type = "Manual" and the volumes set no throughput_in_mibps. |
Assign throughput per volume, or switch the pool to Auto. |
| Cool access cannot be turned off | Disabling cool_access_enabled is force-new. |
Expected β reversal is a pool rebuild plus a volume migration. |
| Plan wants to replace the pool after a small edit | service_level, encryption_type, and the addressing fields are all force-new. |
Expected; migrate volumes first. |
| Delete fails: pool contains volumes | A volume exists that this configuration may not know about. | Enumerate and remove the volumes β including any dropped from state by a migration. |
| A volume cannot find its pool | The pool name was typed rather than wired, and has since changed. | Wire pool_name from this module's name output. |
- azurerm provider β
azurerm_netapp_pool - Set up a capacity pool for Azure NetApp Files
- Service levels for Azure NetApp Files
- Manage cool access in Azure NetApp Files
- Azure NetApp Files double encryption at rest
- Sibling modules:
terraform-azurerm-netapp-account,terraform-azurerm-netapp-volume,terraform-azurerm-netapp-snapshot,terraform-azurerm-netapp-backup-vault,terraform-azurerm-netapp-backup-policy,terraform-azurerm-virtual-network,terraform-azurerm-monitor-diagnostic-setting,terraform-azurerm-role-assignments. - This module's
SCOPE.md.
π "Infrastructure as Code should be standardized, consistent, and secure."