Manage a hardened Azure Recovery Services vault together with its VM and file-share backup policies as one unit β geo-redundant, soft-delete on, immutable, and private by default β on
hashicorp/azurerm ~> 4.0.
- π‘οΈ Creates one
azurerm_recovery_services_vaulthardened by default: soft delete on, public network access off, geo-redundant storage, immutability set to Unlocked, and a system-assigned managed identity. - πΎ Manages
azurerm_backup_policy_vmandazurerm_backup_policy_file_shareas keyed maps, so a schedule/retention change to one policy never re-indexes the rest. - ποΈ Full grandfather-father-son retention: daily, weekly, monthly, and yearly rules, plus V2 enhanced hourly VM policies and hourly file-share schedules.
- π Customer-managed-key encryption and Azure Monitor alert wiring are exposed but off until you supply them.
π‘ Why it matters: the vault is the last line of defense for a regulated workload's data. A vault that is soft-delete protected, immutable, geo-redundant, and private on the empty call means the recoverable posture is the default β every relaxation is a deliberate, reviewable opt-out, and the irreversible
Lockedimmutability state is never reached by accident.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- β Star this repository to help others discover this Terraform module.
- π€ Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
flowchart TD
RG["terraform-azurerm-resource-group"]
RSV["terraform-azurerm-recovery-services-vault"]
VAULT["azurerm_recovery_services_vault"]
POL["backup policies (for_each)"]
KV["terraform-azurerm-key-vault (CMK key id)"]
TARGET["protected VMs / file shares"]
DIAG["terraform-azurerm-monitor-diagnostic-setting"]
RG -->|"resource_group_name + location"| RSV
RSV --> VAULT
VAULT --> POL
KV -->|"key id for CMK encryption"| RSV
POL -->|"policy id protects"| TARGET
RSV -->|"id"| DIAG
classDef this fill:#0078D4,color:#ffffff,stroke:#004578,stroke-width:2px;
classDef key fill:#004578,color:#ffffff,stroke:#004578;
class RSV this;
class VAULT key;
flowchart LR
I1["name / resource_group_name / location / sku"]
I2["storage_mode_type GeoRedundant<br/>soft-delete on, public access off<br/>immutability Unlocked"]
I3["identity SystemAssigned / encryption CMK"]
I4["backup_policy_vm / backup_policy_file_share (maps)"]
RSV["azurerm_recovery_services_vault.this"]
PVM["azurerm_backup_policy_vm.this (for_each)"]
PFS["azurerm_backup_policy_file_share.this (for_each)"]
O1["id / name"]
O2["identity_principal_id"]
O3["backup_policy_vm_ids / backup_policy_file_share_ids"]
I1 --> RSV
I2 --> RSV
I3 --> RSV
I4 --> PVM
I4 --> PFS
RSV --> PVM
RSV --> PFS
RSV --> O1
RSV --> O2
PVM --> O3
PFS --> O3
classDef this fill:#0078D4,color:#ffffff,stroke:#004578,stroke-width:2px;
classDef key fill:#004578,color:#ffffff,stroke:#004578;
class RSV key;
class PVM this;
class PFS this;
Resource inventory
| Resource | Cardinality | Role |
|---|---|---|
azurerm_recovery_services_vault.this |
1 (keystone) | The hardened vault. |
azurerm_backup_policy_vm.this |
0..N (for_each) |
VM backup schedules and retention. |
azurerm_backup_policy_file_share.this |
0..N (for_each) |
Azure File Share backup schedules and retention. |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
| Provider | hashicorp/azurerm ~> 4.0 |
| Provider block | None in this module β the caller configures provider "azurerm" { features {} }, auth, and subscription. |
Schema notes that bite
name,resource_group_name, andlocationare immutable β changing any of them forces replacement of the vault (and, transitively, the policies that reference it).classic_vmware_replication_enabledis force-new too.immutability = "Locked"is irreversible. Once locked, the vault can never return toUnlocked/Disabled, and backup data cannot be shortened or deleted before its retention elapses. The module defaults to the reversibleUnlockedstate; typeLockedonly after a reviewed decision.cross_region_restore_enabled = truerequiresstorage_mode_type = "GeoRedundant"; the module enforces this at plan time. Reducing redundancy (for exampleGeoRedundantβLocallyRedundant) is rejected once protected items exist.storage_mode_typecan generally only be changed before backup items are registered in the vault; change it afterward and the service will reject the update.- Backup-policy
name,resource_group_name, andrecovery_vault_nameare effectively immutable β changing them replaces the policy. Hourly VM schedules requirepolicy_type = "V2"(the enhanced policy). azurerm_backup_policy_vmandazurerm_backup_policy_file_sharedo not support tags β only the vault carries thetagsvariable.
- Backup Contributor on the target resource group (create/update the vault and its backup policies), or Recovery Services Contributor where broader vault management is required. A custom role covering
Microsoft.RecoveryServices/vaults/*andMicrosoft.RecoveryServices/vaults/backupPolicies/*at the resource-group scope is the least-privilege equivalent. - For customer-managed-key encryption, the vault's identity needs Key Vault crypto access (for example Key Vault Crypto Service Encryption User) at the key's scope β granted with the role-assignments module, not here.
- An existing resource group in a supported US Azure region.
- The
Microsoft.RecoveryServicesresource provider registered on the subscription. - For customer-managed-key encryption, an existing Key Vault key (and an identity that can unwrap it).
- For private data-plane access, a subnet plus a private endpoint and the appropriate private DNS zone.
- The caller configures the
provider "azurerm" { features {} }block, auth, and subscription; the module declares none of these.
terraform-azurerm-recovery-services-vault/
βββ providers.tf # required_version + azurerm ~> 4.0; no provider block
βββ variables.tf # name, rg, location, sku, hardening toggles, identity/encryption/monitoring, policy maps
βββ main.tf # azurerm_recovery_services_vault.this + for_each VM / file-share backup policies
βββ outputs.tf # id, name, identity_principal_id, identity_tenant_id, policy id maps
βββ README.md # this document
βββ SCOPE.md # cross-module contract
βββ LICENSE # MIT
βββ .gitignore # canonical Terraform ignore set
provider "azurerm" {
features {}
}
module "rsv" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-recovery-services-vault.git?ref=v1.0.0"
name = "rsv-platform-prod-eus2"
resource_group_name = "rg-backup-prod-eastus2"
location = "eastus2"
}βΉοΈ The empty call is soft-delete protected, geo-redundant, private, immutable (Unlocked), with a system-assigned identity. Pin the module by tag (
?ref=v1.0.0), never a branch.
Consumes
| Input | Type | From |
|---|---|---|
resource_group_name |
string |
terraform-azurerm-resource-group (name) |
location |
string |
caller / resource group (location) |
encryption.key_id |
string |
terraform-azurerm-key-vault (key_versionless_ids) |
identity.identity_ids |
list(string) |
terraform-azurerm-user-assigned-identity (id) |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Vault Resource ID | backup protection, recovery-services-vault-resource-guard-association, terraform-azurerm-site-recovery-services-vault-hyperv-site, diagnostics, private endpoints, role assignments |
name |
Vault name | terraform-azurerm-site-recovery-fabric (as recovery_vault_name) |
identity_principal_id |
System-assigned identity principal ID | Key Vault crypto role assignment (CMK) |
backup_policy_vm_ids |
Map key β VM policy ID | azurerm_backup_protected_vm |
backup_policy_file_share_ids |
Map key β file-share policy ID | azurerm_backup_protected_file_share |
1 Β· Minimal (hardened)
module "rsv" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-recovery-services-vault.git?ref=v1.0.0"
name = "rsv-min-eus2"
resource_group_name = "rg-backup-eastus2"
location = "eastus2"
}π Soft delete on, geo-redundant, public access off, immutability Unlocked, system-assigned identity β all by default.
2 Β· VM backup policy (daily)
module "rsv" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-recovery-services-vault.git?ref=v1.0.0"
name = "rsv-vm-eus2"
resource_group_name = "rg-backup-eastus2"
location = "eastus2"
backup_policy_vm = {
daily = {
backup = { frequency = "Daily", time = "23:00" }
retention_daily = { count = 30 }
}
}
}βΉοΈ
timeis"HH:mm"in the policy'stimezone(defaultUTC). Daily retention is required for a valid VM policy.
3 Β· VM policy with GFS retention (weekly / monthly / yearly)
backup_policy_vm = {
gold = {
timezone = "Eastern Standard Time"
backup = { frequency = "Daily", time = "23:00" }
retention_daily = { count = 14 }
retention_weekly = { count = 12, weekdays = ["Sunday"] }
retention_monthly = { count = 12, weekdays = ["Sunday"], weeks = ["First"] }
retention_yearly = { count = 7, weekdays = ["Sunday"], weeks = ["First"], months = ["January"] }
}
}π‘ Grandfather-father-son: short daily rotation plus long-horizon weekly/monthly/yearly restore points.
4 Β· Enhanced V2 VM policy (hourly)
backup_policy_vm = {
enhanced = {
policy_type = "V2"
instant_restore_retention_days = 7
backup = {
frequency = "Hourly"
time = "08:00"
hour_interval = 4
hour_duration = 12
}
retention_daily = { count = 30 }
}
}
β οΈ Hourly schedules require the enhanced policy (policy_type = "V2");hour_durationmust be a multiple ofhour_interval.
5 Β· File-share backup policy (daily)
backup_policy_file_share = {
shares = {
backup = { frequency = "Daily", time = "22:00" }
retention_daily = { count = 30 }
}
}βΉοΈ File-share policies always require
retention_daily; the module types it as required.
6 Β· File-share policy (hourly)
backup_policy_file_share = {
hourly = {
backup = {
frequency = "Hourly"
hourly = { interval = 4, start_time = "08:00", window_duration = 12 }
}
retention_daily = { count = 30 }
}
}7 Β· Geo-redundant with cross-region restore
module "rsv" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-recovery-services-vault.git?ref=v1.0.0"
name = "rsv-grs-eus2"
resource_group_name = "rg-backup-eastus2"
location = "eastus2"
storage_mode_type = "GeoRedundant"
cross_region_restore_enabled = true
}π Cross-region restore is only valid with
GeoRedundant(the default). The module rejects the combination otherwise at plan time.
8 Β· Immutability β Locked (irreversible)
module "rsv" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-recovery-services-vault.git?ref=v1.0.0"
name = "rsv-locked-eus2"
resource_group_name = "rg-backup-eastus2"
location = "eastus2"
immutability = "Locked"
}
β οΈ Lockedcannot be undone. The vault can never leave the immutable state and retention can never be shortened. Use only after a reviewed, deliberate decision β the defaultUnlockedgives the same protection but stays reversible.
9 Β· Customer-managed-key encryption
module "rsv" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-recovery-services-vault.git?ref=v1.0.0"
name = "rsv-cmk-eus2"
resource_group_name = "rg-backup-eastus2"
location = "eastus2"
identity = { type = "SystemAssigned" }
encryption = {
key_id = var.cmk_key_versionless_id # from a Key Vault key output
use_system_assigned_identity = true
}
}π Provide the key by reference (a Key Vault key output), never inline material.
infrastructure_encryption_enableddefaults to true. The vault's identity must hold crypto access on the key.
10 Β· User-assigned identity
module "rsv" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-recovery-services-vault.git?ref=v1.0.0"
name = "rsv-uai-eus2"
resource_group_name = "rg-backup-eastus2"
location = "eastus2"
identity = {
type = "UserAssigned"
identity_ids = [var.user_assigned_identity_id]
}
}11 Β· Azure Monitor alerts
module "rsv" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-recovery-services-vault.git?ref=v1.0.0"
name = "rsv-mon-eus2"
resource_group_name = "rg-backup-eastus2"
location = "eastus2"
monitoring = {
alerts_for_all_job_failures_enabled = true
alerts_for_critical_operation_failures_enabled = true
}
}12 Β· Public network access (opt-in)
module "rsv" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-recovery-services-vault.git?ref=v1.0.0"
name = "rsv-public-eus2"
resource_group_name = "rg-backup-eastus2"
location = "eastus2"
public_network_access_enabled = true
}
β οΈ Opening public access removes the private guardrail β use only with justification and prefer a private endpoint instead.
13 Β· for_each β a vault per environment
module "rsv" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-recovery-services-vault.git?ref=v1.0.0"
for_each = toset(["dev", "prod"])
name = "rsv-${each.key}-eus2"
resource_group_name = "rg-backup-eastus2"
location = "eastus2"
backup_policy_vm = {
daily = {
backup = { frequency = "Daily", time = "23:00" }
retention_daily = { count = each.key == "prod" ? 30 : 7 }
}
}
}14 Β· Compose diagnostics and RBAC
module "rsv" { # ... }
module "rsv_diag" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-monitor-diagnostic-setting.git?ref=v1.0.0"
name = "diag-rsv"
target_resource_id = module.rsv.id
log_analytics_workspace_id = var.law_id
}
module "rsv_roles" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"
scope = module.rsv.id
role_assignments = {
backup-operator = {
role_definition_name = "Backup Operator"
principal_id = var.ops_principal_id
}
}
}π‘ Diagnostics and RBAC are deliberately sibling concerns, wired to the vault's
idβ this module stays composable.
15 Β· ποΈ End-to-end composition
A resource group, a Key Vault key for CMK, a hardened vault encrypted with that key, and a daily VM backup policy.
provider "azurerm" {
features {}
}
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-backup-prod-eastus2"
location = "eastus2"
}
module "kv" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault.git?ref=v1.0.0"
name = "kv-backup-prod-eus2"
resource_group_name = module.rg.name
location = module.rg.location
tenant_id = var.tenant_id
keys = {
rsv-cmk = { key_type = "RSA", key_size = 3072, key_opts = ["wrapKey", "unwrapKey"] }
}
}
module "rsv" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-recovery-services-vault.git?ref=v1.0.0"
name = "rsv-plat-prod-eus2"
resource_group_name = module.rg.name
location = module.rg.location
identity = { type = "SystemAssigned" }
encryption = {
key_id = module.kv.key_versionless_ids["rsv-cmk"]
use_system_assigned_identity = true
}
backup_policy_vm = {
daily = {
backup = { frequency = "Daily", time = "23:00" }
retention_daily = { count = 30 }
retention_weekly = { count = 12, weekdays = ["Sunday"] }
}
}
}
# module.rsv.identity_principal_id is granted "Key Vault Crypto Service Encryption User" on the key,
# and module.rsv.backup_policy_vm_ids["daily"] feeds azurerm_backup_protected_vm to protect a VM.π‘ The vault's system-assigned identity unwraps the Key Vault key for CMK encryption, and the daily policy's ID drives VM protection β hardened and least-privilege, end to end.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string |
β | β | Vault name (2β50). Immutable. |
resource_group_name |
string |
β | β | Containing resource group. Immutable. |
location |
string |
β | β | Azure region. Immutable. |
sku |
string |
β | "Standard" |
Standard / RS0. |
storage_mode_type |
string |
β | "GeoRedundant" |
GeoRedundant / LocallyRedundant / ZoneRedundant. |
cross_region_restore_enabled |
bool |
β | false |
Requires GeoRedundant. |
soft_delete_enabled |
bool |
β | true |
Soft-delete protection. |
public_network_access_enabled |
bool |
β | false |
Public reachability. |
immutability |
string |
β | "Unlocked" |
Disabled / Unlocked / Locked (Locked is irreversible). |
classic_vmware_replication_enabled |
bool |
β | false |
Classic VMware replication. Force-new. |
identity |
object |
β | { type = "SystemAssigned" } |
Managed identity; null for none. |
encryption |
object |
β | null |
CMK encryption (key + identity). |
monitoring |
object |
β | null |
Azure Monitor alert settings. |
backup_policy_vm |
map(object) |
β | {} |
VM backup policies. |
backup_policy_file_share |
map(object) |
β | {} |
File-share backup policies. |
tags |
map(string) |
β | {} |
Tags (vault only; policies do not support tags). |
timeouts |
object |
β | null |
Optional timeouts. |
Full variable schemas (children)
backup_policy_vm = map(object({
name = optional(string) # defaults to map key
timezone = optional(string, "UTC")
policy_type = optional(string, "V1") # V1 | V2 (validated)
instant_restore_retention_days = optional(number)
consistency_type = optional(string) # ShutdownFileConsistency | FileConsistency (V2)
backup = object({
frequency = string # Daily | Weekly | Hourly (validated)
time = string # "HH:mm"
hour_interval = optional(number)
hour_duration = optional(number)
weekdays = optional(set(string))
})
instant_restore_resource_group = optional(object({ prefix = string, suffix = optional(string) }))
retention_daily = optional(object({ count = number }))
retention_weekly = optional(object({ count = number, weekdays = set(string) }))
retention_monthly = optional(object({ count = number, days = optional(set(number)), include_last_days = optional(bool), weekdays = optional(set(string)), weeks = optional(set(string)) }))
retention_yearly = optional(object({ count = number, months = set(string), days = optional(set(number)), include_last_days = optional(bool), weekdays = optional(set(string)), weeks = optional(set(string)) }))
tiering_policy = optional(object({ archived_restore_point = object({ mode = string, duration = optional(number), duration_type = optional(string) }) }))
})) # this resource does not support tags
backup_policy_file_share = map(object({
name = optional(string) # defaults to map key
timezone = optional(string, "UTC")
backup_tier = optional(string)
snapshot_retention_in_days = optional(number)
backup = object({
frequency = string # Daily | Hourly (validated)
time = optional(string)
hourly = optional(object({ interval = number, start_time = string, window_duration = number }))
})
retention_daily = object({ count = number }) # required
retention_weekly = optional(object({ count = number, weekdays = set(string) }))
retention_monthly = optional(object({ count = number, days = optional(set(number)), include_last_days = optional(bool), weekdays = optional(set(string)), weeks = optional(set(string)) }))
retention_yearly = optional(object({ count = number, months = set(string), days = optional(set(number)), include_last_days = optional(bool), weekdays = optional(set(string)), weeks = optional(set(string)) }))
})) # this resource does not support tags| Output | Description | Notes |
|---|---|---|
id |
Vault Resource ID | Emitted first. |
name |
Vault name | β |
location |
Azure region, in the canonical form Azure uses. | Read from the resource, not var.location. |
identity_principal_id |
System-assigned identity principal ID | null when no system-assigned identity. |
identity_tenant_id |
System-assigned identity tenant ID | null when no system-assigned identity. |
backup_policy_vm_ids |
Map key β VM policy ID | Feeds azurerm_backup_protected_vm. |
backup_policy_file_share_ids |
Map key β file-share policy ID | Feeds azurerm_backup_protected_file_share. |
- Hardened empty call. Soft delete, geo-redundant storage, no public access, immutability
Unlocked, and a system-assigned identity are the defaults; each is a documented opt-out. - Immutability is a one-way door when Locked. The module defaults to
Unlocked, which delivers immutability protection while remaining reversible.Lockedis exposed but requires the caller to type it, because it can never be undone. - Children reference the keystone by name.
azurerm_backup_policy_vmandazurerm_backup_policy_file_sharesetrecovery_vault_name = azurerm_recovery_services_vault.this.name, so the vault is always created first β nodepends_onis needed. Children usefor_eachover keyed maps, so a key rename never re-creates an unrelated policy. - Policies do not carry tags. The universal
tagsvariable applies to the vault only; the backup-policy resources have notagsargument, confirmed against the live provider schema. - CMK is caller-supplied. Encryption is off (Microsoft-managed keys) unless an
encryptionobject with a Key Vaultkey_idis provided; the key is passed by reference and the vault's identity unwraps it. features {}dependence. Noprovider {}block here; the caller configuresprovider "azurerm" { features {} }. Vault soft-delete and purge behavior on destroy is governed by the caller'sfeaturesconfiguration.
| Concern | Secure default (empty call) | Opt-out |
|---|---|---|
| Soft delete | soft_delete_enabled = true |
set false |
| Public network access | public_network_access_enabled = false |
set true |
| Storage redundancy | storage_mode_type = "GeoRedundant" |
LocallyRedundant / ZoneRedundant |
| Immutability | immutability = "Unlocked" (reversible) |
Disabled, or Locked (irreversible) |
| Identity | system-assigned identity |
set identity = null |
| Encryption | Microsoft-managed keys | supply encryption for CMK |
cd terraform-azurerm-recovery-services-vault
terraform init -backend=false
terraform validate
terraform fmt -check
Remove-Item -Recurse -Force .terraform -ErrorAction SilentlyContinuePin the module by tag (
?ref=v1.0.0), never a branch. Plan-only during authoring; a human runsplan/applyfrom CI.
The offline proof gate β terraform init -backend=false, terraform validate, terraform fmt -check β proves the configuration is type-correct against the pinned azurerm ~> 4.0 schema (including the sku, storage_mode_type, immutability, and backup-frequency validations, plus the cross-region-restore/GeoRedundant guard) and canonically formatted, with no cloud calls. What it does not exercise: global name uniqueness, whether the deploying identity holds Backup Contributor, service-side retention rule combinations, and CMK key access β those surface only under terraform plan/apply against real credentials from CI.
$ terraform output
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-backup-prod-eastus2/providers/Microsoft.RecoveryServices/vaults/rsv-plat-prod-eus2"
name = "rsv-plat-prod-eus2"
identity_principal_id = "11111111-2222-3333-4444-555555555555"
backup_policy_vm_ids = {
"daily" = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-backup-prod-eastus2/providers/Microsoft.RecoveryServices/vaults/rsv-plat-prod-eus2/backupPolicies/daily"
}
backup_policy_file_share_ids = {}| Symptom | Cause | Fix |
|---|---|---|
cross_region_restore_enabled requires storage_mode_type = "GeoRedundant" |
Cross-region restore set with non-geo redundancy | Set storage_mode_type = "GeoRedundant" or disable cross-region restore. |
| Cannot reduce storage redundancy | Protected items already exist in the vault | Redundancy is fixed once items are registered; plan it before onboarding backups. |
| Cannot revert immutability | immutability was set to Locked |
Locked is irreversible by design; a new vault is the only path back. |
| Hourly VM schedule rejected | policy_type is V1 |
Set policy_type = "V2" for hourly (enhanced) VM backups. |
| VM policy apply error on retention | Missing retention_daily |
Provide retention_daily; it is required for a valid VM policy. |
| Vault will not delete | Soft delete / registered backup items | Stop protection and clear soft-deleted items, or wait out the window, before destroy. |
| Tag argument rejected on a policy | Backup policies do not support tags | Apply tags on the vault only. |
- Provider resources:
azurerm_recovery_services_vault,azurerm_backup_policy_vm,azurerm_backup_policy_file_share - Sibling modules:
terraform-azurerm-resource-group,terraform-azurerm-key-vault,terraform-azurerm-user-assigned-identity,terraform-azurerm-monitor-diagnostic-setting,terraform-azurerm-role-assignments,terraform-azurerm-private-endpoint - This module's cross-module contract:
SCOPE.md
π "Infrastructure as Code should be standardized, consistent, and secure."