Add Recovery Services Vault module with VM backup policies and outputs

This commit is contained in:
2026-03-01 21:17:57 +01:00
parent 1883aaa38d
commit fae7623813
5 changed files with 489 additions and 1 deletions

115
README.md
View File

@@ -1,3 +1,116 @@
# Azure Recovery Services Vault Terraform Module
# Azure Recovery Services Vault Module
Creates a Recovery Services Vault and can optionally configure VM backup policies and VM protection.
## Usage scenarios
The recovery services vault may be used to protect the following Azure workloads:
- **Azure Virtual Machines**: Policy-based backup and restore for IaaS VMs.
- **SQL Server in Azure VMs**: Workload-aware database backup for SQL running inside Azure VMs.
- **SAP HANA in Azure VMs**: Workload-aware backup for SAP HANA databases running in Azure VMs.
- **Azure Files**: Share-level backup and restore for Azure file shares.
- **MARS agent workloads**: File/folder and system-state backup from supported Windows servers/clients.
- **MABS / DPM-protected workloads**: Backup streams managed through Azure Backup Server or System Center DPM.
## Storage modes
`LocallyRedundant` stores backup data redundantly within a single region.
`ZoneRedundant` stores backup data across availability zones in the same region.
`GeoRedundant` replicates backup data to a paired region and enables cross-region restore when `cross_region_restore_enabled` is set to `true`.
## Protecting Resources
This module can protect Recovery Services Vault workloads. Supported resource types in module status are listed below.
Implemented:
- Azure Virtual Machines (`azurerm_backup_policy_vm`, `azurerm_backup_protected_vm`)
Not implemented yet:
- SQL Server in Azure VMs (`azurerm_backup_policy_vm_workload` + protected workload resources)
- SAP HANA in Azure VMs (`azurerm_backup_policy_vm_workload` + protected workload resources)
### Azure Virtual Machines
Use `vm_backup_policies` to define one or more VM backup policy profiles, and `protected_vms` to map each VM to a selected policy via `backup_policy_key`.
For each protected VM, you can optionally set:
- `include_disk_luns` to include only selected data disks
- `exclude_disk_luns` to exclude selected data disks
- `protection_state` to control protection state (`Protected`, `BackupsSuspended`, `ProtectionStopped`)
## Module Inputs, Outputs, and Examples
### Variables
- `rg_name`: The name of the resource group where the Recovery Services Vault will be created.
- `location`: The Azure region where the Recovery Services Vault will be created.
- `base_name`: Optional base name used to generate a unique vault name when `name` is not set.
- `name`: Optional explicit vault name. If omitted, the module generates a deterministic name from `base_name`.
- `sku`: Vault SKU. Allowed values: `Standard`, `RS0`.
- `storage_mode_type`: Backup storage redundancy type. Allowed values: `GeoRedundant`, `LocallyRedundant`, `ZoneRedundant`.
- `cross_region_restore_enabled`: Enables cross-region restore. Can only be set to `true` when `storage_mode_type = "GeoRedundant"`.
- `soft_delete_enabled`: Enables soft delete in the Recovery Services Vault.
- `public_network_access_enabled`: Enables public network access to the vault.
- `immutability`: Immutability state. Allowed values: `Disabled`, `Locked`, `Unlocked`.
- `identity`: Optional managed identity configuration object:
- `type`: Identity type. Allowed values: `SystemAssigned`, `UserAssigned`, `SystemAssigned, UserAssigned`.
- `identity_ids`: Optional list of user-assigned identity IDs (required when `type` includes `UserAssigned`).
- `tags`: A map of tags to apply to the vault.
- `vm_backup_policies`: Map of VM backup policy definitions.
- `protected_vms`: Map of VMs to protect, including policy mapping via `backup_policy_key`.
### Outputs
- `recovery_services_vault_id`: The ID of the created Recovery Services Vault.
- `recovery_services_vault_name`: The name of the created Recovery Services Vault.
- `recovery_services_vault_identity_principal_id`: Principal ID of the assigned managed identity, if configured.
- `vm_backup_policy_ids`: Map of VM backup policy IDs keyed by policy key.
- `protected_vm_backup_ids`: Map of protected VM backup item IDs keyed by protected VM key.
### Example Usage
```hcl
module "recovery_services_vault" {
source = "./modules/recovery-services-vault"
rg_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
base_name = "rsv"
storage_mode_type = "LocallyRedundant"
vm_backup_policies = {
default = {
backup = {
frequency = "Daily"
time = "23:00"
}
retention_daily = {
count = 30
}
}
}
protected_vms = {
app = {
source_vm_id = azurerm_linux_virtual_machine.app.id
backup_policy_key = "default"
}
}
}
```
## References
- [Recovery Services vaults overview](https://learn.microsoft.com/azure/backup/backup-azure-recovery-services-vault-overview)
- [Back up Azure VMs in a Recovery Services vault](https://learn.microsoft.com/azure/backup/backup-azure-arm-vms-prepare)
- [Azure Backup FAQ: vault support matrix](https://learn.microsoft.com/azure/backup/backup-azure-backup-faq#what-are-the-various-vaults-supported-for-backup-and-restore)

146
main.tf Normal file
View File

@@ -0,0 +1,146 @@
data "azurerm_client_config" "current" {}
locals {
recovery_services_vault_name = (
var.name != null &&
trimspace(var.name) != "" ?
var.name :
"${coalesce(var.base_name, "")}${substr(md5("${data.azurerm_client_config.current.subscription_id}/${var.rg_name}/${coalesce(var.base_name, "")}"), 0, 6)}"
)
default_vm_backup_policies = {
default = {
name = "${local.recovery_services_vault_name}-vm-policy"
policy_type = "V2"
timezone = "UTC"
instant_restore_retention_days = 5
backup = {
frequency = "Daily"
time = "23:00"
}
retention_daily = {
count = 30
}
retention_weekly = null
retention_monthly = null
retention_yearly = null
}
}
effective_vm_backup_policies = length(var.vm_backup_policies) > 0 ? {
for key, policy in var.vm_backup_policies : key => {
name = coalesce(try(policy.name, null), "${local.recovery_services_vault_name}-${key}-vm-policy")
policy_type = coalesce(try(policy.policy_type, null), "V2")
timezone = coalesce(try(policy.timezone, null), "UTC")
instant_restore_retention_days = try(policy.instant_restore_retention_days, null)
backup = policy.backup
retention_daily = try(policy.retention_daily, null)
retention_weekly = try(policy.retention_weekly, null)
retention_monthly = try(policy.retention_monthly, null)
retention_yearly = try(policy.retention_yearly, null)
}
} : (length(var.protected_vms) > 0 ? local.default_vm_backup_policies : {})
default_vm_backup_policy_key = contains(keys(local.effective_vm_backup_policies), "default") ? "default" : (
length(keys(local.effective_vm_backup_policies)) > 0 ? sort(keys(local.effective_vm_backup_policies))[0] : null
)
}
resource "azurerm_recovery_services_vault" "this" {
name = local.recovery_services_vault_name
location = var.location
resource_group_name = var.rg_name
sku = var.sku
storage_mode_type = var.storage_mode_type
cross_region_restore_enabled = var.cross_region_restore_enabled
soft_delete_enabled = var.soft_delete_enabled
public_network_access_enabled = var.public_network_access_enabled
immutability = var.immutability
dynamic "identity" {
for_each = var.identity == null ? [] : [var.identity]
content {
type = identity.value.type
identity_ids = try(identity.value.identity_ids, null)
}
}
tags = var.tags
}
resource "azurerm_backup_policy_vm" "this" {
for_each = local.effective_vm_backup_policies
name = each.value.name
resource_group_name = var.rg_name
recovery_vault_name = azurerm_recovery_services_vault.this.name
policy_type = each.value.policy_type
timezone = each.value.timezone
instant_restore_retention_days = each.value.instant_restore_retention_days
backup {
frequency = each.value.backup.frequency
time = each.value.backup.time
hour_interval = try(each.value.backup.hour_interval, null)
hour_duration = try(each.value.backup.hour_duration, null)
weekdays = try(each.value.backup.weekdays, null)
}
dynamic "retention_daily" {
for_each = try(each.value.retention_daily, null) == null ? [] : [each.value.retention_daily]
content {
count = retention_daily.value.count
}
}
dynamic "retention_weekly" {
for_each = try(each.value.retention_weekly, null) == null ? [] : [each.value.retention_weekly]
content {
count = retention_weekly.value.count
weekdays = retention_weekly.value.weekdays
}
}
dynamic "retention_monthly" {
for_each = try(each.value.retention_monthly, null) == null ? [] : [each.value.retention_monthly]
content {
count = retention_monthly.value.count
weekdays = try(retention_monthly.value.weekdays, null)
weeks = try(retention_monthly.value.weeks, null)
days = try(retention_monthly.value.days, null)
include_last_days = try(retention_monthly.value.include_last_days, null)
}
}
dynamic "retention_yearly" {
for_each = try(each.value.retention_yearly, null) == null ? [] : [each.value.retention_yearly]
content {
count = retention_yearly.value.count
months = retention_yearly.value.months
weekdays = try(retention_yearly.value.weekdays, null)
weeks = try(retention_yearly.value.weeks, null)
days = try(retention_yearly.value.days, null)
include_last_days = try(retention_yearly.value.include_last_days, null)
}
}
}
resource "azurerm_backup_protected_vm" "this" {
for_each = var.protected_vms
resource_group_name = var.rg_name
recovery_vault_name = azurerm_recovery_services_vault.this.name
source_vm_id = each.value.source_vm_id
backup_policy_id = azurerm_backup_policy_vm.this[coalesce(try(each.value.backup_policy_key, null), local.default_vm_backup_policy_key)].id
include_disk_luns = try(each.value.include_disk_luns, null)
exclude_disk_luns = try(each.value.exclude_disk_luns, null)
protection_state = try(each.value.protection_state, null)
}

25
outputs.tf Normal file
View File

@@ -0,0 +1,25 @@
output "recovery_services_vault_id" {
value = azurerm_recovery_services_vault.this.id
}
output "recovery_services_vault_name" {
value = azurerm_recovery_services_vault.this.name
}
output "recovery_services_vault_identity_principal_id" {
value = try(azurerm_recovery_services_vault.this.identity[0].principal_id, null)
}
output "vm_backup_policy_ids" {
value = {
for key, policy in azurerm_backup_policy_vm.this :
key => policy.id
}
}
output "protected_vm_backup_ids" {
value = {
for key, item in azurerm_backup_protected_vm.this :
key => item.id
}
}

194
variables.tf Normal file
View File

@@ -0,0 +1,194 @@
variable "rg_name" {
type = string
}
variable "location" {
type = string
}
variable "base_name" {
type = string
default = null
}
variable "name" {
type = string
default = null
validation {
condition = (
(var.name != null && trimspace(var.name) != "") ||
(var.base_name != null && trimspace(var.base_name) != "")
)
error_message = "Provide name or base_name with a non-empty value."
}
}
variable "sku" {
type = string
default = "Standard"
validation {
condition = contains(["Standard", "RS0"], var.sku)
error_message = "sku must be one of 'Standard' or 'RS0'."
}
}
variable "storage_mode_type" {
type = string
default = "LocallyRedundant"
validation {
condition = contains(["GeoRedundant", "LocallyRedundant", "ZoneRedundant"], var.storage_mode_type)
error_message = "storage_mode_type must be one of 'GeoRedundant', 'LocallyRedundant', or 'ZoneRedundant'."
}
}
variable "cross_region_restore_enabled" {
type = bool
default = false
validation {
condition = var.cross_region_restore_enabled == false || var.storage_mode_type == "GeoRedundant"
error_message = "cross_region_restore_enabled can only be true when storage_mode_type is 'GeoRedundant'."
}
}
variable "soft_delete_enabled" {
type = bool
default = true
}
variable "public_network_access_enabled" {
type = bool
default = true
}
variable "immutability" {
type = string
default = "Disabled"
validation {
condition = contains(["Disabled", "Locked", "Unlocked"], var.immutability)
error_message = "immutability must be one of 'Disabled', 'Locked', or 'Unlocked'."
}
}
variable "identity" {
type = object({
type = string
identity_ids = optional(list(string))
})
default = null
validation {
condition = (
var.identity == null ||
contains([
"SystemAssigned",
"UserAssigned",
"SystemAssigned, UserAssigned",
], var.identity.type)
)
error_message = "identity.type must be one of 'SystemAssigned', 'UserAssigned', or 'SystemAssigned, UserAssigned'."
}
validation {
condition = (
var.identity == null ||
var.identity.type == "SystemAssigned" ||
length(try(var.identity.identity_ids, [])) > 0
)
error_message = "identity.identity_ids must be provided when identity.type includes 'UserAssigned'."
}
}
variable "tags" {
type = map(string)
default = {}
}
variable "vm_backup_policies" {
type = map(object({
name = optional(string)
policy_type = optional(string)
timezone = optional(string)
instant_restore_retention_days = optional(number)
backup = object({
frequency = string
time = string
hour_interval = optional(number)
hour_duration = optional(number)
weekdays = optional(list(string))
})
retention_daily = optional(object({
count = number
}))
retention_weekly = optional(object({
count = number
weekdays = list(string)
}))
retention_monthly = optional(object({
count = number
weekdays = optional(list(string))
weeks = optional(list(string))
days = optional(list(number))
include_last_days = optional(bool)
}))
retention_yearly = optional(object({
count = number
months = list(string)
weekdays = optional(list(string))
weeks = optional(list(string))
days = optional(list(number))
include_last_days = optional(bool)
}))
}))
default = {}
validation {
condition = alltrue([
for policy in values(var.vm_backup_policies) : contains(["V1", "V2"], coalesce(try(policy.policy_type, null), "V2"))
])
error_message = "Each vm_backup_policies[*].policy_type must be 'V1' or 'V2' when set."
}
}
variable "protected_vms" {
type = map(object({
source_vm_id = string
backup_policy_key = optional(string)
include_disk_luns = optional(list(number))
exclude_disk_luns = optional(list(number))
protection_state = optional(string)
}))
default = {}
validation {
condition = alltrue([
for vm in values(var.protected_vms) : (
try(vm.backup_policy_key, null) == null ||
contains(
keys(length(var.vm_backup_policies) > 0 ? var.vm_backup_policies : { default = {} }),
vm.backup_policy_key
)
)
])
error_message = "Each protected_vms[*].backup_policy_key must exist in vm_backup_policies."
}
validation {
condition = alltrue([
for vm in values(var.protected_vms) : (
try(vm.protection_state, null) == null ||
contains(["Protected", "BackupsSuspended", "ProtectionStopped"], vm.protection_state)
)
])
error_message = "Each protected_vms[*].protection_state must be one of 'Protected', 'BackupsSuspended', or 'ProtectionStopped' when set."
}
}

10
versions.tf Normal file
View File

@@ -0,0 +1,10 @@
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 4.0.0, < 5.0.0"
}
}
required_version = ">= 1.0.0"
}