Skip to content

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

☁️ Azure NetApp Backup Vault Terraform Module

Provisions one Azure NetApp Files backup vault (azurerm_netapp_backup_vault) — the destination volume backups are written to. Targets hashicorp/azurerm ~> 4.0.

Terraform Provider Module Type Resources


🧩 Overview

  • 🗃️ Provisions one backup vault — the destination NetApp Files volume backups land in — as a keystone resource named this.
  • 🧭 Documents the resource's absent surface as carefully as its present one: there is no redundancy, retention, or immutability setting here.
  • 🔗 Retention lives on the backup policy; a vault with no policy pointed at it holds nothing. The two are separate resources on purpose.
  • ⚠️ Destroying the vault FAILS while it still holds backups — unless the caller opts in with features { netapp { delete_backups_on_backup_vault_destroy = true } }, in which case every backup is deleted irreversibly. There is no soft-delete and no recycle bin either way.
  • 🔄 A replacement is not a migration: rename or move the vault and existing backups do not follow.
  • 📤 Emits id (volumes consume the vault by ID, unlike the account and pool which go by name) and location, so a composition can check the same-region requirement nothing else enforces.

💡 Why it matters: This is a deliberately small module because the resource is small — and knowing that is the point. People come to a "backup vault" looking for redundancy options, a retention lock, or an immutability window, and none of those exist on this resource. Retention is the policy's job; everything else is service-managed. The most useful thing this README does is stop you hunting for a control that isn't there.

❤️ Support this project

If this module saves you time, please consider supporting its continued development:


🗺️ Where this fits in the family

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;
Loading

🧬 What this module builds

flowchart TB
  addr["name, resource_group_name, account_name, location: force-new, i.e. everything except tags"]
  norepl["a replacement is NOT a migration: existing backups do not follow"]
  nosurface["NO redundancy, retention, or immutability configuration exists on this resource"]
  retention["retention lives on the backup POLICY, not here"]
  members["the vault does not know its members: a VOLUME opts in by naming vault id plus policy id"]
  del["destroy FAILS while backups remain, unless the caller enables the provider opt-in"]
  this["terraform-azurerm-netapp-backup-vault"]
  vault["azurerm_netapp_backup_vault.this"]
  out["outputs: id consumed BY ID, name, location for the same-region check"]

  addr -->|"identity"| this
  norepl -->|"cost of a rename"| addr
  nosurface -->|"the useful absence"| this
  retention -->|"separate module"| nosurface
  members -->|"membership is one-way"| this
  del -->|"write access is destroy access"| this
  this -->|"creates"| vault
  vault -->|"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 vault keystone;
  class addr,norepl,nosurface,retention,members,del,out sib;
Loading

Resource inventory

Resource Count Role
azurerm_netapp_backup_vault.this 1 The keystone backup vault, with its optional timeouts block.

✅ Provider / Versions

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, and location — every field except tags.
  • A replacement is not a migration. Renaming or moving the vault creates a new one; existing backups do not follow, and the old vault's contents go with it.
  • Destroying the vault fails while it still holds backups. The provider refuses with "cannot delete backups from backup vault due to missing DeleteBackupsOnBackupVaultDestroy feature set as true". The caller can opt in with features { netapp { delete_backups_on_backup_vault_destroy = true } }, which then deletes every backup irreversibly. There is no soft-delete or retention lock either way. This is a caller-side features {} setting, so it is not — and cannot be — a module variable.
  • The resource has no redundancy, retention, or immutability configuration. Retention belongs to the backup policy; everything else is service-managed.
  • A vault and the volumes backing up to it should be in the same region, but the relationship is expressed only inside each volume's configuration — nothing validates it.
  • The vault does not know its members. A volume opts in by naming this vault's id alongside a policy id, so there is no way to enumerate a vault's volumes from the vault.
  • The account is addressed by name, but volumes consume this vault by Resource ID — the family mixes both conventions.

🔑 Required Azure RBAC Roles / Permissions

  • Contributor on the resource group holding the NetApp account, or a custom role covering Microsoft.NetApp/netAppAccounts/backupVaults/*.
  • Read access on the NetApp account, so the vault can be created within it.
  • No data-plane or Key Vault permission is needed here.

⚠️ Note the operational consequence: a caller who enables features { netapp { delete_backups_on_backup_vault_destroy = true } } turns destroying this vault into irreversible deletion of every backup in it, so write access here plus that provider flag is destroy access over the recovery copies. Without the flag the destroy simply fails. Scope the role accordingly.

Azure Prerequisites

  • An existing NetApp account, in the same region as this vault and as the volumes whose backups land here.
  • The caller configures the provider "azurerm" { features {} } block, auth, and subscription.

📁 Module Structure

terraform-azurerm-netapp-backup-vault/
├── providers.tf   # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
├── variables.tf   # four force-new addressing fields; tags/timeouts tail; nothing else exists
├── main.tf        # keystone azurerm_netapp_backup_vault.this
├── outputs.tf     # id (consumed by ID), name, account/rg, and location for the region check
├── README.md      # this document
├── SCOPE.md       # cross-module contract
├── LICENSE        # MIT
└── .gitignore     # canonical library ignore set

⚙️ Quick Start

provider "azurerm" {
  features {}
}

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 # the NAME, not the id
  location            = module.anf_rg.location

  tags = { workload = "records", retention_owner = "platform-storage" }
}

ℹ️ That is the whole resource. A vault on its own backs nothing up — pair it with a backup policy and attach both to a volume (example 3).

ℹ️ The caller owns the provider, its authentication, and the mandatory features {} block. This module never declares them.


🔌 Cross-Module Contract

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 Backup vault Resource ID (first) terraform-azurerm-netapp-volume (data_protection_backup_policy.backup_vault_id) — by ID, not by name
name Backup vault name operational review
account_name / resource_group_name Addressing composition wiring
location The vault's region composition check — a volume and its vault should share a region, and nothing enforces that

📚 Example Library

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 · The whole module
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
}

ℹ️ Four addressing fields and tags. There is genuinely nothing else — see example 2 for what people expect to find and don't.

2 · The controls that do not exist here
# ❌ None of these are arguments on this resource.
# redundancy         = "GeoRedundant"
# retention_days     = 30
# immutability       = "Locked"
# soft_delete        = true
# public_access      = false

⚠️ Worth stating plainly: there is no redundancy setting, no retention, no immutability lock, and no soft-delete on a NetApp Files backup vault. Retention is the backup policy's job (example 3); the rest is service-managed. If you are hardening a backup destination and looking for a lock, it is not on this resource.

3 · A vault only does something when paired with a policy
module "anf_backup_policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-backup-policy.git?ref=v1.0.0"

  name                = "bp-prod-01"
  resource_group_name = module.anf_rg.name
  account_name        = module.anf_account.name
  location            = module.anf_rg.location

  daily_backups_to_keep   = 7
  weekly_backups_to_keep  = 5
  monthly_backups_to_keep = 12
}
# The VOLUME is what joins them:
module "anf_volume" {
  # ...
  data_protection_backup_policy = {
    backup_vault_id  = module.anf_vault.id
    backup_policy_id = module.anf_backup_policy.id
  }
}

💡 The vault is the destination, the policy is the retention, and the volume is what opts into both. A vault with no policy pointed at it holds nothing, and neither resource knows about the other — the pairing exists only inside each volume's configuration.

4 · Volumes consume the vault by ID, not by name
backup_vault_id  = module.anf_vault.id  # ✅ by Resource ID
backup_policy_id = module.anf_backup_policy.id

ℹ️ Note the inconsistency in this family and plan for it: the account and pool are addressed by name, while the vault and policy are consumed by Resource ID. The ID form creates a real Terraform dependency, which is the better of the two — a rename shows up as a plan diff rather than a run-time failure.

5 · A rename is not a migration
name = "bv-prod-02" # was bv-prod-01 — forces replacement

⚠️ Every field except tags is force-new, so this creates a new, empty vault — and the old one is destroyed along with the backups it held. There is no move operation and no way to re-point existing backups. Treat the vault's name as permanent for the life of the data in it.

6 · Destroying a vault that still holds backups
# Removing the module destroys the vault AND its contents. No soft-delete,
# no recycle bin, no grace period.

⚠️ This is the single most consequential fact about the resource. A terraform destroy over a NetApp environment takes the recovery copies with it, and there is nothing to recover them from. Where that matters, keep the vault in a separate state from the volumes it protects.

7 · The same-region requirement nothing enforces
output "region_check" {
  description = "A volume and its vault should share a region; nothing validates it."
  value = {
    vault  = module.anf_vault.location
    volume = module.anf_volume.location
  }
}

⚠️ location is emitted specifically so a composition can compare it. The vault and the volumes backing up to it should be in the same region, but the relationship lives inside each volume's configuration — neither Terraform nor this module can check it.

8 · One vault, many policies, many volumes
module "anf_vault" { source = "...netapp-backup-vault..." } # one destination

module "policy_short" {
  source = "...netapp-backup-policy..."
  daily_backups_to_keep = 7
}

module "policy_long" {
  source = "...netapp-backup-policy..."
  daily_backups_to_keep   = 7
  monthly_backups_to_keep = 84
}

💡 The relationship is genuinely many-to-many, which is why the vault and policy are separate modules. One vault serves every retention scheme; you do not need a vault per policy, and creating one would just fragment the destination for no benefit.

9 · Tags carry what the resource cannot
tags = {
  workload        = "records"
  retention_owner = "platform-storage"
  data_class      = "member-records"
  destroy_review  = "required"
}

💡 tags is the only descriptive surface on this resource, and the only field that is not force-new. Since the vault cannot tell you which volumes back up to it, recording the workload here is the sole trace a later reader gets.

10 · Several vaults from a keyed map
locals {
  vaults = { "bv-prod-01" = "records", "bv-nonprod-01" = "sandbox" }
}

module "vaults" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-backup-vault.git?ref=v1.0.0"
  for_each = local.vaults

  name                = each.key
  resource_group_name = module.anf_rg.name
  account_name        = module.anf_account.name
  location            = module.anf_rg.location

  tags = { workload = each.value }
}

💡 Separate vaults per environment is a reasonable blast-radius decision — since deleting a vault destroys its contents, keeping production's recovery copies out of a sandbox teardown's path is worth the extra resource.

11 · Generous delete timeouts, used carefully
timeouts = {
  create = "30m"
  delete = "2h" # a vault holding many backups takes time to remove
}

⚠️ Raise delete when tearing down an environment with substantial backup history — but read that as a warning, not just a tuning tip: a long delete is the service removing recovery copies. Be certain they are expendable first.

12 · Reviewing the backup estate
output "backup_destinations" {
  description = "Vaults and their regions. Membership is not readable from the vault side."
  value = {
    for k, m in module.vaults : k => { id = m.id, region = m.location }
  }
}

ℹ️ Note what this table cannot show: which volumes actually back up to each vault. That knowledge lives only in each volume's data_protection_backup_policy block, so a complete picture has to be assembled from the volume side.

13 · Separating the vault's lifecycle from the volumes'
# Vault + policy in their own root module / state:
module "anf_vault"  { source = "...netapp-backup-vault..." }
module "anf_policy" { source = "...netapp-backup-policy..." }
# Volumes elsewhere, consuming the IDs via remote state or variables:
data_protection_backup_policy = {
  backup_vault_id  = var.backup_vault_id
  backup_policy_id = var.backup_policy_id
}

🔒 Because destroying the vault destroys the recovery copies, keeping it out of the same state as the volumes it protects means a volume teardown cannot take the backups with it. A deliberate split, not an accident of layout.

14 · Write access is destroy access
Microsoft.NetApp/netAppAccounts/backupVaults/write
Microsoft.NetApp/netAppAccounts/backupVaults/delete

🔒 A role that can create a vault through this module can delete one, and deleting it destroys the backups inside. There is no immutability lock on this resource to fall back on, so the boundary is the RBAC scope — grant it on the resource group holding the NetApp account rather than broadly.

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 — addressed by NAME by everything below.
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 capacity 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
}

# 4 · The backup vault — this module. The DESTINATION only; no retention here.
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 # must match the volumes' region

  tags = {
    workload        = "records"
    retention_owner = "platform-storage"
    destroy_review  = "required" # deleting this destroys the recovery copies
  }
}

# 5 · The retention policy — a separate resource, deliberately.
module "anf_backup_policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-backup-policy.git?ref=v1.0.0"

  name                = "bp-prod-01"
  resource_group_name = module.anf_rg.name
  account_name        = module.anf_account.name
  location            = module.anf_rg.location

  daily_backups_to_keep   = 7
  weekly_backups_to_keep  = 5
  monthly_backups_to_keep = 12
}

# 6 · The VOLUME is what joins the vault and the policy — both BY RESOURCE 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 = 1024
  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
    }
  }

  data_protection_backup_policy = {
    backup_vault_id  = module.anf_vault.id
    backup_policy_id = module.anf_backup_policy.id
  }
}

output "backup_wiring" {
  description = "The region check nothing enforces, plus the two IDs the volume joined."
  value = {
    vault_region  = module.anf_vault.location
    volume_region = module.anf_volume.location
    vault_id      = module.anf_vault.id
    policy_id     = module.anf_backup_policy.id
  }
}

💡 The shape of this composition is the design: three independent resources (vault, policy, volume) and the volume is the only one that knows about the other two. That is why the vault cannot list its members, why the same-region requirement is unenforceable, and why the vault and policy are separate modules rather than one. Output names on sibling modules are illustrative; match them to the versions you pin.


📥 Inputs

Required (all four force-new): name, resource_group_name, account_name, location.

Universal tail: tags — the only field that is not force-new — and timeouts.

Full object() schemas
variable "name" {
  # force-new. A volume references the vault by RESOURCE ID rather than by this name, but a
  # rename still replaces the vault — and a replacement is NOT a migration: existing backups do
  # not follow.
  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 and the volumes

variable "tags" {
  # The ONLY non-force-new field, and the only descriptive surface on this resource. Since the
  # vault cannot tell you which volumes back up to it, recording the workload here is the sole
  # trace a later reader gets.
  type    = map(string)
  default = {}
}

variable "timeouts" {
  # ⚠️ Destroying a backup vault FAILS while backups remain unless the caller enables the provider
  # opt-in, which then deletes them all irreversibly. Allow a generous `delete` when
  #    tearing down an environment — and be certain the backups are expendable first.
  type = object({
    create = optional(string)
    read   = optional(string)
    update = optional(string)
    delete = optional(string)
  })
  default = null
}

# NOTE what does not exist here: no redundancy setting, no retention, no immutability lock, no
# soft-delete. Retention belongs to terraform-azurerm-netapp-backup-policy; the rest is
# service-managed.

🧾 Outputs

Output Description Kind
id The Azure Resource ID of the backup vault Passthrough
name The backup vault name Passthrough
account_name The NetApp account this backup vault belongs to Passthrough
resource_group_name The resource group holding the NetApp account Passthrough
location The region the backup vault 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 vault segment from this vault's ID Passthrough
arm_resource_type The ARM resource type this module creates Derived
tags The tags applied to the vault Passthrough
tag_count The number of tag pairs on the vault, against an Azure ceiling of 50 per resource Passthrough
destroy_fails_while_backups_remain Always true under the provider's DEFAULT configuration Constant
provider_feature_that_forces_destroy The name of the provider feature a caller must set to true, inside provider azurerm { features { netapp { Derived
destroy_deletes_every_backup_when_that_feature_is_enabled Always true, and it is irreversible Constant
destroy_vault_retry_attempts How many times the provider retries the whole vault delete when a backup appears in the vault mid-teardown - a real race, because a backup started just before its volume was deleted can take time to show up Passthrough
destroy_backup_delete_retry_attempts How many times the provider retries deleting each individual backup, at 30-second intervals, while Azure reports that a backup transfer is still in progress Passthrough
only_tags_are_updatable 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
is_prerequisite_for_any_backup Always true Constant
backups_survive_deletion_of_their_volume Always true, and it is the opposite of the intuition most teardown scripts encode Constant
restore_is_same_region_only Always true Constant
backup_data_placement_is_service_managed Always true, and it explains why this resource has no configuration Constant
backup_data_encryption_at_rest How the backup data is encrypted at rest, for a control questionnaire Derived
backup_data_encryption_in_transit How backup data reaches the vault, for a control questionnaire Derived
one_vault_per_account_is_recommended Always true Constant
max_manual_backups_per_volume_per_day The Azure ceiling on on-demand backups of a single volume within a day, which is NOT adjustable by support request Passthrough
max_combined_backup_retention_count The Azure ceiling on backups retained per volume, counting the hourly, daily, weekly and monthly retention settings of a backup policy TOGETHER Passthrough
max_protected_volume_size_tib The largest volume, in TiB, that Azure NetApp Files backup can protect Passthrough
max_backup_enabled_volumes_per_subscription The Azure ceiling on how many volumes in a subscription can have backup enabled, which is NOT adjustable by support request Passthrough

No secret is emitted; this resource carries none.

🧠 Architecture Notes

  • The module is small because the resource is, and documenting the absence is the main contribution. There is no redundancy setting, no retention, no immutability lock, and no soft-delete on a NetApp Files backup vault. Retention belongs to the policy; everything else is service-managed. A reader hardening a backup destination will go looking for a lock, and the fastest help is to say it isn't here.
  • Deleting the vault destroys the backups it holds, with no grace period and no immutability to fall back on. That single fact drives two pieces of guidance: tag the vault so a destroy review is prompted, and consider keeping it in a different state from the volumes it protects so a volume teardown cannot take the recovery copies with it.
  • A replacement is not a migration. Every field except tags is force-new, so a rename creates a new empty vault and destroys the old one's contents. The vault's name is effectively permanent for the life of the data in it — which is a stronger constraint than "force-new" usually implies.
  • The vault does not know its members. A volume opts in by naming this vault's id alongside a policy id, so membership is readable only from the volume side. Any inventory of "what backs up where" has to be assembled from volumes, not vaults.
  • This family mixes two addressing conventions, and this module sits on the better side of it: the account and pool are consumed by name (plain strings, no Terraform dependency), while the vault and policy are consumed by Resource ID (real references, so a rename surfaces as a plan diff).
  • The same-region requirement is unenforceable from here. The vault and its volumes should share a region, but the relationship lives inside each volume's configuration — so location is emitted specifically to let a composition compare the two.
  • The vault and the policy are separate modules on the ownership test. They have independent lifecycles and a genuinely many-to-many relationship with volumes: one vault serves every retention scheme. Combining them would force a redundant vault per policy.
  • No validation {} blocks, because there is no closed value set and no cross-field rule — every input is a free-form addressing string. Where a resource has nothing checkable, this suite says so rather than inventing a check.
  • features {} dependence. The module carries no provider {} block. If it appears not to initialize in isolation, the cause is a missing caller-side provider "azurerm" { features {} }.

🧱 Design Principles

Concern Secure default (empty call) Opt-out (caller must type it)
Absent controls documented: no redundancy, retention, immutability, or soft-delete exists here — (structural)
Destroy blast radius destroy FAILS while backups remain; irreversible deletion only when the caller sets the provider opt-in — (mitigate with state separation and RBAC scope)
Rename safety documented: a replacement is not a migration rename anyway (destroys the contents)
Retention delegated to the backup policy module — (structural)
Region consistency location emitted for the check nothing enforces — (no opt-out)
Referencing id emitted; volumes consume by ID, creating a real dependency — (no opt-out)
Provenance tags documented as the only descriptive surface leave it empty

🚀 Runbook

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 / apply from CI against real credentials.
  • Treat any plan that proposes replacing or destroying this vault as data loss — the backups go with it, and nothing recovers them.
  • Confirm the vault's region matches the volumes that will back up to it; nothing checks this.
  • Consider keeping the vault in a separate state from the volumes it protects.

🧪 Testing

  • terraform validate proves the configuration is internally consistent and type-correct against the pinned provider schema. This module has no validation {} blocks — every input is a free-form addressing string with no closed value set.
  • terraform fmt -check enforces 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 actually backs up to this vault, whether a policy is paired with it, and whether the vault's region matches its volumes'. All three live in the volume's configuration, so a vault that is correct in isolation can still be part of a backup arrangement that does nothing.

💬 Example Output

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/backupVaults/bv-prod-01"
name                = "bv-prod-01"
account_name        = "anf-prod-eastus2"
resource_group_name = "rg-anf-eastus2"
location            = "eastus2"

🔍 Troubleshooting

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.
Looking for a redundancy or immutability setting It does not exist on this resource. Retention is the backup policy; the rest is service-managed.
No backups appear in the vault No policy is paired with it, or no volume opted in. Create a backup policy and attach both IDs in the volume's data_protection_backup_policy.
Backups vanished after a rename Every field except tags is force-new; a replacement is not a migration. Restore from elsewhere if you can, and treat the vault name as permanent.
A terraform destroy of the vault fails The provider refuses while backups remain, unless the caller has enabled the delete_backups_on_backup_vault_destroy provider feature. Delete the backups deliberately first, or enable the flag knowing it deletes every backup irreversibly.
Backups vanished after a terraform destroy The caller had that provider feature enabled, so the destroy cascaded. There is no soft-delete. Keep the vault in a separate state from the volumes it protects, and leave the flag off unless you mean it.
Backups fail with a region error The vault and the volume are in different regions. Align them; nothing validates this.
Cannot determine which volumes use this vault The vault does not know its members. Enumerate from the volume side — the relationship lives there.
Delete times out The vault holds substantial backup history. Raise the delete timeout — and confirm the backups are expendable.
Apply fails: account not found account_name does not resolve, or the region differs. Wire it from the account module's name output.

🔗 Related Docs


💙 "Infrastructure as Code should be standardized, consistent, and secure."