Provisions one Azure NetApp Files snapshot policy (
azurerm_netapp_snapshot_policy) β the schedule that takes snapshots and ages them out. Targetshashicorp/azurerm ~> 4.0.
- ποΈ Provisions one snapshot policy β hourly, daily, weekly and monthly schedules β as a keystone resource named
this. - β Rejects a policy with no schedule at all. All four schedule blocks are optional to the provider, so one that takes nothing is otherwise accepted and leaves its volumes unprotected while looking configured.
β οΈ There is no per-volume pause here. Unlike a backup policy,enabled = falsestops snapshots for every volume referencing this policy, and the only way to exempt one is to detach it.- π States the trap plainly: schedules run in UTC, because there is no time-zone field on this resource.
- π° Emits
total_snapshots_retained, because snapshots pin capacity in the parent pool as the volume diverges β the counts are a capacity commitment, not just a recovery window. - π§ This is the resource for a rolling series;
terraform-azurerm-netapp-snapshottakes one on-demand snapshot and is a poor substitute.
π‘ Why it matters: Two things about this resource behave differently from the backup policy it superficially resembles. It has no per-volume off switch, so the one boolean here reaches every volume that uses it. And its retention counts are not free β snapshots live in the volume's own capacity pool and grow as data diverges, so an hourly schedule with a high count is a standing capacity commitment against a resource this module cannot see.
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: force-new"]
rolling["use THIS for a rolling series; netapp-snapshot takes ONE on-demand snapshot"]
hourly["hourly_schedule: minute plus snapshots_to_keep"]
daily["daily_schedule: hour, minute, snapshots_to_keep"]
weekly["weekly_schedule: days_of_week full English names"]
monthly["monthly_schedule: days_of_month 1-31; a day of 29 to 31 does not fire every month"]
optional["ALL FOUR blocks are optional, so a policy with NO schedule is accepted and takes nothing"]
guard["at-least-one validated at plan, on the monthly variable reading the other three"]
utc["schedules run in UTC: there is NO time-zone field on this resource"]
enabled["enabled is provider-REQUIRED with no default; this module supplies true"]
nopause["there is NO per-volume pause, unlike a backup policy: false stops EVERY volume using it"]
capacity["snapshots pin POOL capacity as the volume diverges"]
this["terraform-azurerm-netapp-snapshot-policy"]
pol["azurerm_netapp_snapshot_policy.this"]
out["outputs: id BY ID, enabled, schedule_summary, total_snapshots_retained"]
addr -->|"identity"| this
rolling -->|"when to reach for it"| this
hourly -->|"one schedule"| optional
daily -->|"one schedule"| optional
weekly -->|"one schedule"| optional
monthly -->|"one schedule"| optional
optional -->|"the gap to close"| guard
guard -->|"enforced"| this
utc -->|"schedule correctness"| this
enabled -->|"protective default"| this
nopause -->|"blast radius"| enabled
capacity -->|"why the counts matter"| this
this -->|"creates"| pol
pol -->|"emits"| out
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 pol keystone;
class addr,rolling,hourly,daily,weekly,monthly,optional,guard,utc,enabled,nopause,capacity,out sib;
Resource inventory
| Resource | Count | Role |
|---|---|---|
azurerm_netapp_snapshot_policy.this |
1 | The keystone policy, with its hourly_schedule, daily_schedule, weekly_schedule, monthly_schedule, and timeouts blocks. |
| 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, andlocation. The schedules andenabledupdate in place. - All four schedule blocks are optional, so the provider accepts a policy with no schedule whatsoever β it takes no snapshots and nothing reports the gap. This module rejects it at plan.
- Within each schedule block every field is required; there are no partial schedules.
enabledis marked required by the provider with no default of its own. This module suppliestrue.enabled = falsestops snapshots for every volume referencing the policy, and there is no per-volume pause β unlike a backup policy, whose volumes each carry apolicy_enabledswitch. The only way to exempt one volume is to detach the policy from it.- Schedules run in UTC. There is no time-zone field, so a schedule written in local business hours drifts relative to it twice a year.
- A
monthly_scheduleday of 29, 30, or 31 does not fire in every month. - Snapshots consume capacity in the parent pool, growing as the live volume diverges.
- A volume references the policy by Resource ID, so renaming replaces it and any volume still pointing at the old ID stops having snapshots taken.
- π΄ Destroying this policy DETACHES it from every volume in the account, and succeeds. The provider catches the service's "SnapshotPolicy is used" error, enumerates every volume referencing the policy, clears each one's
SnapshotPolicyId, and retries the delete. Volumes this configuration does not manage lose their scheduled snapshots silently β nothing fails and nothing warns. - The account is addressed by name, but volumes consume this policy by Resource ID β the family mixes both conventions.
Contributoron the resource group holding the NetApp account, or a custom role coveringMicrosoft.NetApp/netAppAccounts/snapshotPolicies/*.- Read access on the NetApp account, so the policy can be created within it.
- No data-plane or Key Vault permission is needed here.
β οΈ Note the operational consequence:enabled = falsestops snapshots for every volume that references this policy, and there is no per-volume pause. Write access here reaches every workload using it. Scope the role accordingly.
- An existing NetApp account, in the same region as this policy and as the volumes that will use it.
- Headroom in the capacity pools holding those volumes β
total_snapshots_retainedis the upper bound on what this policy can pin per volume. - A stated recovery objective. The schedules are deliberately left at
nullrather than invented, because a requirement should supply them. - The caller configures the
provider "azurerm" { features {} }block, auth, and subscription.
terraform-azurerm-netapp-snapshot-policy/
βββ providers.tf # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
βββ variables.tf # four schedule blocks with an at-least-one check, enabled, tags/timeouts tail
βββ main.tf # keystone azurerm_netapp_snapshot_policy.this; four dynamic schedule blocks
βββ outputs.tf # id (consumed by ID), enabled, schedule_summary, total_snapshots_retained
βββ README.md # this document
βββ SCOPE.md # cross-module contract
βββ LICENSE # MIT
βββ .gitignore # canonical library ignore set
provider "azurerm" {
features {}
}
module "anf_snapshot_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-snapshot-policy.git?ref=v1.0.0"
name = "sp-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
# At least one schedule is required. All times are UTC.
daily_schedule = {
hour = 2 # 02:00 UTC β convert deliberately
minute = 0
snapshots_to_keep = 7
}
weekly_schedule = {
days_of_week = ["Sunday"]
hour = 3
minute = 0
snapshots_to_keep = 4
}
# enabled defaults to true.
tags = { recovery_objective = "7-daily-4-weekly" }
}π° That keeps up to 11 snapshots per volume, each pinning pool capacity as the volume diverges. See example 6.
βΉοΈ 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 |
Snapshot policy Resource ID (first) | terraform-azurerm-netapp-volume (data_protection_snapshot_policy.snapshot_policy_id) and both volume-group modules β by ID, not by name |
name |
Policy name | operational review |
account_name / resource_group_name |
Addressing | composition wiring |
location |
The policy's region | composition check β a volume and its policy must share a region |
enabled |
Whether the policy is taking snapshots | operational review β a disabled policy is indistinguishable from a working one from the volume's side, and there is no per-volume pause |
schedule_summary |
Which schedules are set and how many each keeps | recovery-objective review |
total_snapshots_retained |
Sum of all snapshots_to_keep |
capacity review β the upper bound on pool capacity this policy can pin per volume |
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
}The examples below reference existing resources by ID or name rather than creating them; this module owns only its own resource. Those references are declared inputs:
variable "anf_vnet_subnet_ids" {
description = "subnet ids of an existing anf vnet that these examples reference but do not create."
type = map(string)
}1 Β· A layered schedule
module "anf_snapshot_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-snapshot-policy.git?ref=v1.0.0"
name = "sp-prod-01"
resource_group_name = module.anf_rg.name
account_name = module.anf_account.name
location = module.anf_rg.location
daily_schedule = { hour = 2, minute = 0, snapshots_to_keep = 7 }
weekly_schedule = { days_of_week = ["Sunday"], hour = 3, minute = 0, snapshots_to_keep = 4 }
monthly_schedule = { days_of_month = [1], hour = 4, minute = 0, snapshots_to_keep = 6 }
}π‘ Layering is the point of having four blocks: a week of dailies for recent mistakes, a month of weeklies, half a year of monthlies β 17 snapshots for six months of coverage, rather than 180 dailies.
2 Β· The empty policy the provider accepts
module "bad_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-snapshot-policy.git?ref=v1.0.0"
name = "sp-prod-01"
resource_group_name = module.anf_rg.name
account_name = module.anf_account.name
location = module.anf_rg.location
# β no schedule at all
}Error: Invalid value for variable
Set at least one of hourly_schedule, daily_schedule, weekly_schedule, or
monthly_schedule β every schedule block is optional to the provider, so a policy with none
is accepted, takes no snapshots, and leaves the volumes that reference it unprotected
while appearing configured.
β οΈ This module's most valuable check. Without it the policy applies, volumes attach it, protection looks configured, and no snapshot is ever taken. Nothing downstream reports the gap.
3 Β· Schedules are UTC β there is no time-zone field
daily_schedule = {
hour = 2 # 02:00 UTC, NOT local
minute = 0
snapshots_to_keep = 7
}
β οΈ Unlike the batch-configuration resource elsewhere in this library, there is notime_zoneargument here at all. A "2 a.m. quiet period" written ashour = 2runs at 21:00 or 22:00 US Eastern depending on the season β and the offset shifts twice a year. Convert to UTC deliberately and record the intent intags.
4 Β· The blast radius of `enabled = false`
enabled = false # β οΈ stops snapshots for EVERY volume referencing this policy
β οΈ And there is no per-volume alternative. A backup policy lets a volume opt out withpolicy_enabled = falsein its own block; a snapshot policy has no equivalent. To exempt one volume you must detach the policy from that volume or point it at a different one β see example 5.
5 Β· Exempting a single volume
# Volume A keeps the policy:
module "vol_a" {
data_protection_snapshot_policy = { snapshot_policy_id = module.anf_snapshot_policy.id }
}# Volume B is exempted by simply not attaching one:
module "vol_b" {
# data_protection_snapshot_policy omitted
}π‘ Detachment is the only per-volume lever. If several volumes need different cadences, create several policies (example 10) rather than toggling one policy's
enabled.
6 Β· Retention counts are pool capacity
output "capacity_commitment" {
value = module.anf_snapshot_policy.total_snapshots_retained
}# On the pool: leave headroom accordingly.
size_in_tb = 8 # the volumes need 4π° A snapshot starts nearly free and grows as the live volume diverges from it, pinning blocks that cannot be reclaimed.
total_snapshots_retainedis the upper bound on how many this policy holds per volume β and the pool, which this module cannot see, is what pays for them.
7 Β· Hourly is the expensive one
hourly_schedule = { minute = 0, snapshots_to_keep = 24 }
β οΈ Twenty-four hourly snapshots on a busy volume is a substantial standing capacity commitment β each one pins the blocks that changed since the last. Legitimate for a workload where an hour of loss is unacceptable, and worth pricing against the pool before adopting it as a default.
8 Β· Weekday names are full English words
weekly_schedule = { days_of_week = ["Monday", "Thursday"], hour = 3, minute = 0, snapshots_to_keep = 8 }days_of_week = ["Mon"] # βError: weekly_schedule.days_of_week values must be full English weekday names: Monday,
Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday.
βΉοΈ No abbreviations and no locale variants. An empty set is rejected too β it never fires.
9 Β· A monthly day that does not exist every month
monthly_schedule = { days_of_month = [30], hour = 4, minute = 0, snapshots_to_keep = 12 } # β οΈmonthly_schedule = { days_of_month = [28], hour = 4, minute = 0, snapshots_to_keep = 12 } # β
always fires
β οΈ Day 31 fires in seven months of the year; day 29 or 30 skips February most years. If a monthly snapshot must always be taken, use a day of 28 or lower. The provider accepts only 1-30, so day 31 cannot be scheduled at all; whether the day exists in a given month is not something a check can fix.
10 Β· One policy per cadence
locals {
policies = {
"sp-standard" = { daily = 7, weekly = 4 }
"sp-critical" = { daily = 14, weekly = 8 }
}
}
module "snapshot_policies" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-snapshot-policy.git?ref=v1.0.0"
for_each = local.policies
name = each.key
resource_group_name = module.anf_rg.name
account_name = module.anf_account.name
location = module.anf_rg.location
daily_schedule = { hour = 2, minute = 0, snapshots_to_keep = each.value.daily }
weekly_schedule = { days_of_week = ["Sunday"], hour = 3, minute = 0, snapshots_to_keep = each.value.weekly }
}π‘ Because there is no per-volume pause and no per-volume override, several policies is how you get several cadences. Naming them after the objective rather than the workload keeps that legible, since one policy serves many volumes and cannot tell you which.
11 Β· Attaching a policy to a volume
module "anf_volume" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-volume.git?ref=v1.0.0"
account_name = "account-example"
location = "eastus2"
name = "anf-volume-example"
pool_name = "pool-example"
resource_group_name = "rg-example"
service_level = "Premium"
storage_quota_in_gb = 100
subnet_id = var.subnet_id
volume_path = "volume-path"
data_protection_snapshot_policy = {
snapshot_policy_id = module.anf_snapshot_policy.id # BY ID
}
}
β οΈ Valid on a primary volume only β attaching a snapshot policy to a replication destination raises an error. βΉοΈ Note the ID form creates a real Terraform dependency, so ordering is implicit and a rename shows as a plan diff on the volume.
12 Β· Volume groups take the same policy
module "anf_vg_hana" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-volume-group-sap-hana.git?ref=v1.0.0"
account_name = "account-example"
application_identifier = "application-identifier"
group_description = "group-description"
location = "eastus2"
name = "anf-vg-hana-example"
resource_group_name = "rg-example"
volume = [
{
volume_spec_name = "data"
# ...
data_protection_snapshot_policy = { snapshot_policy_id = module.anf_snapshot_policy.id }
},
# ...
]
}π‘ Each volume inside a volume group can attach a snapshot policy individually, so one policy can serve both standalone volumes and volume-group members β which is another reason the policy is account-anchored rather than volume-owned.
13 Β· Reviewing the estate
output "snapshot_posture" {
description = "A false `enabled` means no snapshots at all; a large total is a pool commitment."
value = {
for k, m in module.snapshot_policies : k => {
enabled = m.enabled
schedules = m.schedule_summary
pinned = m.total_snapshots_retained
}
}
}π‘ Three failure modes visible in one table: a disabled policy, a thin schedule, and an oversized retention total. All three are emitted deliberately, because none of them is readable from the resource's own attributes.
14 Β· Deleting a policy in use
Error: deleting Snapshot Policy: policy is in use by one or more volumes
β οΈ A policy cannot be deleted while a volume references it β which is a helpful guard, and also means a teardown must detach it from every volume first. Note the guard does not extend to renaming: a rename replaces the policy, and volumes pointing at the old ID quietly stop having snapshots taken.
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.
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 pool β sized with headroom BECAUSE the snapshot policy in step 4 pins capacity.
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 = 8 # the volume needs 4; the rest is snapshot divergence
}
# 4 Β· The snapshot policy β this module. Layered, UTC, enabled by default.
module "anf_snapshot_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-snapshot-policy.git?ref=v1.0.0"
name = "sp-prod-01"
resource_group_name = module.anf_rg.name
account_name = module.anf_account.name
location = module.anf_rg.location
# All times UTC β there is no time-zone field on this resource.
daily_schedule = { hour = 2, minute = 0, snapshots_to_keep = 7 }
weekly_schedule = { days_of_week = ["Sunday"], hour = 3, minute = 0, snapshots_to_keep = 4 }
monthly_schedule = { days_of_month = [28], hour = 4, minute = 0, snapshots_to_keep = 6 } # 28, so it always fires
# enabled defaults to true. Do NOT use it to pause one volume β detach instead.
tags = {
recovery_objective = "7-daily-4-weekly-6-monthly"
schedule_timezone = "UTC (02:00 UTC = 21:00/22:00 US Eastern)"
}
}
# 5 Β· The volume, attaching the policy BY ID.
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 = 4096
volume_path = "records"
subnet_id = var.anf_vnet_subnet_ids["anf"]
export_policy_rule = {
app_tier = {
rule_index = 1
allowed_clients = ["10.40.2.0/24"]
protocol = ["NFSv3"]
unix_read_write = true
}
}
# Primary volumes only β attaching this to a replication destination is an error.
data_protection_snapshot_policy = {
snapshot_policy_id = module.anf_snapshot_policy.id
}
}
# 6 Β· Off-pool retention, because snapshots share the pool's fate.
module "anf_vault" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-backup-vault.git?ref=v1.0.0"
name = "bv-prod-01"
resource_group_name = module.anf_rg.name
account_name = module.anf_account.name
location = module.anf_rg.location
}
output "snapshot_posture" {
description = "Whether snapshots are running, and what they commit the pool to."
value = {
enabled = module.anf_snapshot_policy.enabled
pinned = module.anf_snapshot_policy.total_snapshots_retained
}
}π‘ Two deliberate choices here. The pool in step 3 is oversized because step 4 pins capacity β snapshots are not free and the pool is what pays. And step 4's monthly day is
28rather than1or31, so it fires in every month; a day of 31 would silently skip five months a year. Step 6 exists because a snapshot shares its pool's fate and is not a backup. Output names on sibling modules are illustrative; match them to the versions you pin.
Required addressing (all four force-new): name, resource_group_name, account_name, location.
Schedules β at least one required: hourly_schedule, daily_schedule, weekly_schedule, monthly_schedule.
Enablement: enabled (defaults true; provider-required with no default of its own).
Universal tail: tags, timeouts.
Full object() schemas
variable "name" {
# force-new. Volumes reference the policy by RESOURCE ID, so a rename replaces it and any
# volume still pointing at the old ID stops having snapshots taken.
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
# --- Schedules. ALL FOUR are optional to the provider, so a policy with NONE is accepted and
# takes nothing. The at-least-one check lives on monthly_schedule, reading the other three
# one-directionally, because Terraform rejects validations that reference each other.
# β οΈ ALL TIMES ARE UTC β there is no time-zone field on this resource.
# Within each block EVERY field is required; there are no partial schedules.
variable "hourly_schedule" {
# π° The shortest interval and the most expensive: snapshots pin POOL capacity as the volume
# diverges. Validated: minute 0-59, snapshots_to_keep > 0.
type = object({ minute = number, snapshots_to_keep = number })
default = null
}
variable "daily_schedule" {
# Validated: hour 0-23, minute 0-59, snapshots_to_keep > 0.
type = object({ hour = number, minute = number, snapshots_to_keep = number })
default = null
}
variable "weekly_schedule" {
# days_of_week takes FULL ENGLISH weekday names ("Monday", not "Mon") and must be non-empty.
type = object({ days_of_week = set(string), hour = number, minute = number, snapshots_to_keep = number })
default = null
}
variable "monthly_schedule" {
# days_of_month 1-31, non-empty. β οΈ A day of 29-31 does not fire in every month β use 28 or
# lower if a monthly snapshot must always be taken.
# THE AT-LEAST-ONE CHECK LIVES HERE.
type = object({ days_of_month = set(number), hour = number, minute = number, snapshots_to_keep = number })
default = null
}
variable "enabled" {
# Provider-REQUIRED with no default of its own; this module supplies true so the protective
# value is what you get.
# β οΈ false stops snapshots for EVERY volume referencing this policy, and there is NO per-volume
# pause (unlike a backup policy's policy_enabled). Detach the policy from a volume instead.
type = bool
default = true
}
variable "tags" {
# A good place for the recovery objective the schedules implement β and for the local-time
# intent behind the UTC hours, since the resource records neither.
type = map(string)
default = {}
}
variable "timeouts" {
# Destroying this policy detaches it from EVERY volume in the account and succeeds --
# including volumes this configuration does not manage.
type = object({ create = optional(string), read = optional(string), update = optional(string), delete = optional(string) })
default = null
}| Output | Description | Kind |
|---|---|---|
id |
Passthrough | |
name |
The snapshot policy name | Passthrough |
account_name |
The NetApp account this policy belongs to | Passthrough |
resource_group_name |
The resource group holding the NetApp account | Passthrough |
location |
The policy's region, 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 snapshotPolicies segment from this policy's ID | Passthrough |
arm_resource_type |
The ARM resource type this module creates | Derived |
tags |
The tags applied to the snapshot policy | Passthrough |
tag_count |
The number of tag pairs on the policy, against an Azure ceiling of 50 per resource | Passthrough |
enabled |
Whether the policy takes snapshots | Passthrough |
configured_schedules |
Which of the four schedule cadences are configured, as a sorted list drawn from hourly, daily, weekly and monthly | Passthrough |
schedule_count |
How many of the four schedule cadences are configured, 0 to 4 | Passthrough |
takes_no_snapshots |
True when this policy is enabled but has no schedule at all, or has schedules that all retain zero snapshots | Derived |
schedules_keeping_nothing |
Names of any configured schedules whose snapshots_to_keep is zero | Passthrough |
total_snapshots_retained |
The sum of snapshots_to_keep across every configured schedule - the number of snapshots this policy alone holds on each volume that attaches it | Passthrough |
max_snapshots_per_volume |
Microsoft's hard ceiling on snapshots held by one volume, counting scheduled and on-demand alike | Passthrough |
retention_exceeds_volume_snapshot_limit |
True when this policy's schedules together demand more than the 255 snapshots a single volume can hold | Passthrough |
snapshots_per_day |
How many snapshots this policy creates per day on each attached volume, averaging the weekly and monthly cadences over a 7-day week and a 30-day month | Passthrough |
schedule_summary |
Derived | |
monthly_days_that_skip_february |
Days of the month in the monthly schedule that do not exist in every month - 29 and 30 | Derived |
schedules_run_in_utc |
Always true | Constant |
destroy_detaches_this_policy_from_every_volume |
Always true, and it is the most consequential fact about this resource | Constant |
removing_a_schedule_block_forces_a_new_policy |
Always true, and it is asymmetric in a way the schema does not show | Constant |
volumes_are_not_re_attached_after_replacement |
Always true, and it is the reason the two facts above matter together | Constant |
disabling_stops_snapshots_for_every_attached_volume |
Always true | Constant |
cannot_be_attached_to_a_replication_destination |
Always true | Constant |
snapshots_consume_parent_pool_capacity |
Always true | Constant |
policy_carries_no_recovery_objective |
Always true, and it is why the tags on this resource are worth using | Constant |
force_new_arguments |
The arguments that destroy and recreate this policy rather than updating it | Derived |
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.
- This resource looks like the backup policy and differs in the way that matters. A backup policy lets a volume opt out with
policy_enabled = falsein its own block; a snapshot policy has no per-volume pause at all. So the singleenabledboolean here reaches every volume referencing the policy, and the only way to exempt one is to detach it. The module documents that asymmetry explicitly, because a reader who knows the backup policy will reasonably assume symmetry. - The at-least-one-schedule check closes a silent gap. All four blocks are optional to the provider, so a policy with none applies cleanly, gets attached by volumes, and takes nothing β protection appears configured and does not exist. The check lives on
monthly_scheduleand reads the other three one-directionally, because Terraform rejects validations that reference each other. enabledis provider-required with no default, which is unusual; this module suppliestrueso the protective value is what an empty call produces. It is also emitted, since a disabled policy is indistinguishable from a working one when viewed from the volume.- Retention counts are capacity, not just recovery. Snapshots live in the volume's parent capacity pool and grow as data diverges, pinning blocks that cannot be reclaimed.
total_snapshots_retainedis emitted as the upper bound on that commitment per volume β a fact no single field expresses, and one the module cannot check because the pool is not an input. - There is no time-zone field, so everything is UTC. That is worth stating flatly rather than glossing, because a sibling resource in this library (
netappbatch scheduling aside, the Logic Apps batch configuration) does have one, and the inconsistency invites a wrong assumption. The module suggests recording local-time intent intags, since the resource cannot. - A monthly day of 29β31 does not fire every month. The provider accepts only 1-30, so day 31 cannot be scheduled at all; calendar reality is not something a check can repair, so it is documented with the practical advice to use 28 or lower.
- The policy is account-anchored, and deliberately so. Because each volume β including each volume inside a volume group β attaches a policy individually, one policy can serve standalone volumes and volume-group members alike. Owning it from a volume would make that impossible.
- Several cadences means several policies. With no per-volume pause and no per-volume override, that is the only mechanism, which is why the module suggests naming policies after the objective rather than the workload.
- Deletion is guarded but renaming is not. A policy in use cannot be deleted; a rename replaces it and leaves volumes pointing at an ID that no longer exists, with no error.
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) |
|---|---|---|
| Policy that takes nothing | at-least-one schedule enforced at plan | β (no opt-out) |
| Zero-count schedule | every snapshots_to_keep validated > 0 |
β (no opt-out) |
| Snapshots running | enabled = true |
set false (stops every volume using it) |
| Disabled-policy visibility | enabled emitted |
β (no opt-out) |
| Capacity commitment | total_snapshots_retained emitted |
β (no opt-out) |
| Invented cadence | schedules left null β the requirement supplies them |
β (structural) |
| Per-volume exemption | documented: detach the policy from the volume | use enabled (far wider) |
| Schedule correctness | ranges and weekday names validated at plan | β (no opt-out) |
| Time-zone intent | documented: UTC only; record local intent in tags |
leave it unrecorded |
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. - Convert schedule hours to UTC deliberately, and record the local-time intent in
tags. - Check the parent pools have headroom for
total_snapshots_retainedper volume before applying. - Treat a plan that flips
enabledto false as stopping snapshots across every volume using the policy β there is no per-volume pause. - To retire a policy, detach it from every volume first; deletion is blocked while it is in use.
terraform validateproves the configuration is internally consistent and type-correct against the pinned provider schema, and exercises everyvalidation {}block: the at-least-one-schedule check, the hour and minute ranges on all four schedules, the positive retention counts, full English weekday names, non-empty day sets, and the 1β31 day-of-month range.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 the named account exists in the stated region.
- What nothing exercises: whether any volume attaches this policy, whether the parent pools have capacity for the retention totals, and whether the UTC hours correspond to the quiet period someone intended. The first two are why
total_snapshots_retainedis emitted; the third is why the module asks for the intent intags.
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/snapshotPolicies/sp-prod-01"
name = "sp-prod-01"
account_name = "anf-prod-eastus2"
resource_group_name = "rg-anf-eastus2"
location = "eastus2"
enabled = true
schedule_summary = {
"daily" = 7
"hourly" = null
"monthly" = 6
"weekly" = 4
}
total_snapshots_retained = 17
| 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: Set at least one of hourly_/daily_/weekly_/monthly_ |
No schedule configured. The provider would have accepted it. | Add a schedule against a stated recovery objective. |
| No snapshots anywhere, configuration looks complete | enabled = false. |
Check the enabled output; re-enable, and detach the policy if only one volume should be exempt. |
| Snapshots stopped for more volumes than intended | The policy's enabled was used to pause one volume. There is no per-volume pause. |
Re-enable, and detach the policy from the specific volume instead. |
| Snapshots fire at the wrong local time | Schedules are UTC; there is no time-zone field. | Convert to UTC; record the local intent in tags. |
| A monthly snapshot is sometimes missing | days_of_month uses 29, 30, or 31. |
Use 28 or lower. |
Plan error: days_of_week values must be full English weekday names |
An abbreviation such as Mon. |
Use Monday. |
| Plan error: must be greater than 0 | A snapshots_to_keep of zero. |
Supply a positive count. |
| The pool ran out of space | Snapshot divergence pinned capacity. | Grow the pool, or reduce snapshots_to_keep; check total_snapshots_retained. |
| A volume stopped getting snapshots after a rename | The policy was replaced, so its Resource ID changed. | Wire snapshot_policy_id from this module's id output. |
| Attaching to a volume raises an error | The volume is a replication destination; snapshot policies attach to primaries only. | Attach it to the primary volume instead. |
| Delete fails: policy in use | A volume still references it. | Detach it from every volume first. |
- azurerm provider β
azurerm_netapp_snapshot_policy - Manage snapshot policies in Azure NetApp Files
- How Azure NetApp Files snapshots work
- Understand Azure NetApp Files backup
- Sibling modules:
terraform-azurerm-netapp-account,terraform-azurerm-netapp-pool,terraform-azurerm-netapp-volume,terraform-azurerm-netapp-snapshot,terraform-azurerm-netapp-backup-vault,terraform-azurerm-netapp-backup-policy,terraform-azurerm-netapp-volume-group-sap-hana,terraform-azurerm-netapp-volume-group-oracle. - This module's
SCOPE.md.
π "Infrastructure as Code should be standardized, consistent, and secure."